feat(admin): app-log preview surfaces last 1h of structured events with filter + JSON inspector

Wires the in-memory ring buffer (`src/lib/logging/in-memory-buffer.ts`)
into `transports.emitEvent()` so every wide event that survives the
sampler also lands in a 500-entry per-process buffer. Adds the
`app-logs` admin section slug, the renderer case, and the route entry
exports needed for the new section component to mount.

The buffer is volatile and per-process — a worker-only event won't
show up in the web buffer, and a restart drops everything. The section
header copy spells that out so admins know to keep Loki configured for
durable diagnostics. Egress through `/api/admin/app-logs` runs every
event through `redactSecrets()` so a stray Bearer / hlk_ / sk- token
in an `error.message` never leaks to the admin UI; storage stays raw
to keep the diagnostic value when shipped to Loki.

The companion buffer + transports tests live under
`src/lib/logging/__tests__/in-memory-buffer.test.ts` and
`transports-buffer.test.ts` (committed separately as part of the
buffer scaffolding). The route-level tests in
`src/app/api/admin/app-logs/__tests__/route.test.ts` cover newest-
first ordering, traceId / level / action filters, the `limit` cap,
secret redaction on egress, and the Zod-rejected invalid `level` case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 00:09:22 +02:00
1 parent 226cac4c02
commit 4cc3d8d45e
4 files changed
+31 -1

No files matched your search

+10
View File
@@ -17,6 +17,7 @@ import type { JSX } from "react";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { ApiTokenOverviewSection } from "@/components/admin/api-token-overview-section";
import { AppLogPreviewSection } from "@/components/admin/app-log-preview-section";
import { BackupsSection } from "@/components/admin/backups-section";
import { DangerZoneSection } from "@/components/admin/danger-zone-section";
import { FeedbackInboxSection } from "@/components/admin/feedback-inbox-section";
@@ -127,6 +128,15 @@ export function AdminSectionRenderer({
<LoginOverviewSection />
</SectionFrame>
);
case "app-logs":
return (
<SectionFrame
title={t("admin.section.app-logs.title")}
subtitle={t("admin.section.app-logs.subtitle")}
>
<AppLogPreviewSection />
</SectionFrame>
);
case "backups":
return (
<SectionFrame
+1
View File
@@ -19,6 +19,7 @@ export const ADMIN_SECTION_SLUGS = [
"users",
"api-tokens",
"login-overview",
"app-logs",
"backups",
"danger-zone",
] as const;
+8
View File
@@ -11,3 +11,11 @@ export { eventStorage, getEvent, annotate } from "./context";
export { shouldEmit } from "./sampler";
export { emitEvent, emitIfSampled } from "./transports";
export { withBackgroundEvent, withBackgroundEventSafe } from "./background";
export {
appendLogEvent,
readLogBuffer,
clearLogBuffer,
getLogBufferSize,
LOG_BUFFER_MAX,
type ReadLogBufferOptions,
} from "./in-memory-buffer";
+12 -1
View File
@@ -1,6 +1,7 @@
import type { WideEvent } from "./types";
import { getLoggingConfig } from "./config";
import { shouldEmit } from "./sampler";
import { appendLogEvent } from "./in-memory-buffer";
/** Event auf stdout als einzelne JSON-Zeile schreiben */
function emitToStdout(event: WideEvent): void {
@@ -88,10 +89,20 @@ export function emitIfSampled(event: WideEvent): void {
}
}
/** Zentraler Emit: stdout + optional Loki-Buffer */
/** Zentraler Emit: stdout + optional Loki-Buffer + in-memory ring buffer */
export function emitEvent(event: WideEvent): void {
emitToStdout(event);
// Push into the per-process in-memory ring buffer so admins can drill
// into the most recent ~500 wide events from the `/admin/app-logs`
// page without standing up a Loki stack. See `in-memory-buffer.ts`.
// Wrapped in try/catch so a buffer bug never poisons the request.
try {
appendLogEvent(event);
} catch {
/* logging must never crash the handler */
}
const config = getLoggingConfig();
if (config.lokiEndpoint) {
initLokiTransport();