mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
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>
115 lines
3.2 KiB
TypeScript
115 lines
3.2 KiB
TypeScript
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 {
|
|
const config = getLoggingConfig();
|
|
const json = config.prettyPrint
|
|
? JSON.stringify(event, null, 2)
|
|
: JSON.stringify(event);
|
|
process.stdout.write(json + "\n");
|
|
}
|
|
|
|
// Loki Push API Buffer
|
|
const LOKI_MAX_BUFFER_SIZE = 1000;
|
|
let lokiBuffer: WideEvent[] = [];
|
|
let lokiFlushTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
function initLokiTransport(): void {
|
|
const config = getLoggingConfig();
|
|
if (!config.lokiEndpoint || lokiFlushTimer) return;
|
|
|
|
lokiFlushTimer = setInterval(() => {
|
|
flushLokiBuffer().catch((err) => {
|
|
process.stderr.write(`[logging] Loki flush error: ${err}\n`);
|
|
});
|
|
}, 5000);
|
|
|
|
if (lokiFlushTimer.unref) lokiFlushTimer.unref();
|
|
}
|
|
|
|
async function flushLokiBuffer(): Promise<void> {
|
|
if (lokiBuffer.length === 0) return;
|
|
const config = getLoggingConfig();
|
|
if (!config.lokiEndpoint) return;
|
|
|
|
const batch = lokiBuffer;
|
|
lokiBuffer = [];
|
|
|
|
const streams = [
|
|
{
|
|
stream: {
|
|
service: "healthlog",
|
|
environment: batch[0]?.environment || "production",
|
|
},
|
|
values: batch.map((event) => [
|
|
// Loki erwartet Nanosekunden-Timestamp als String
|
|
new Date(event.timestamp).getTime() + "000000",
|
|
JSON.stringify(event),
|
|
]),
|
|
},
|
|
];
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
};
|
|
if (config.lokiUsername && config.lokiPassword) {
|
|
headers["Authorization"] =
|
|
"Basic " +
|
|
Buffer.from(`${config.lokiUsername}:${config.lokiPassword}`).toString(
|
|
"base64",
|
|
);
|
|
}
|
|
|
|
try {
|
|
await fetch(`${config.lokiEndpoint}/loki/api/v1/push`, {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({ streams }),
|
|
signal: AbortSignal.timeout(10_000),
|
|
});
|
|
} catch {
|
|
// Events gehen verloren — akzeptabel fuer Logging
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Event emittieren falls Sampling-Kriterien erfuellt.
|
|
* Zentraler Einstiegspunkt — entfernt Stack Traces falls konfiguriert.
|
|
*/
|
|
export function emitIfSampled(event: WideEvent): void {
|
|
const config = getLoggingConfig();
|
|
if (!config.includeStackTrace && event.error?.stack) {
|
|
delete event.error.stack;
|
|
}
|
|
if (shouldEmit(event)) {
|
|
emitEvent(event);
|
|
}
|
|
}
|
|
|
|
/** 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();
|
|
if (lokiBuffer.length >= LOKI_MAX_BUFFER_SIZE) {
|
|
lokiBuffer.shift();
|
|
}
|
|
lokiBuffer.push(event);
|
|
}
|
|
}
|