Files
HealthLog/src/lib/logging/transports.ts
T
Marc-André BombeckandClaude Opus 4.7 8528c3b4fe chore(release): v1.5.6
Pure-history medication detail page, legacy step consolidation, and the
finish of the outbound-fetch migration.

- /medications/[id] becomes a read-only history surface; every setting
  moves into a new advanced-settings sheet, and the edit trigger becomes
  a two-option picker. The list page is unchanged.
- A pg-boss job folds pre-v1.5.0 granular step rows into one canonical
  daily total per user per day and soft-deletes the originals; migration
  0087 adds a partial index for the converging discovery scan.
- Every remaining server-side fetch routes through safeFetch (Withings,
  Codex OAuth + SSE, bug-report, Umami, GlitchTip, Loki); a new
  healthlog/safe-fetch-required ESLint rule guards the boundary.
- Fixes: intake-edit dialog seeded from real values, consolidation skips
  a unique-constraint day instead of stranding it, avatar upload reads
  the body once under a bounded cap, egress lint rejects absolute URLs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 22:07:10 +02:00

121 lines
3.3 KiB
TypeScript

import type { WideEvent } from "./types";
import { getLoggingConfig } from "./config";
import { shouldEmit } from "./sampler";
import { appendLogEvent } from "./in-memory-buffer";
import { safeFetch } from "@/lib/safe-fetch";
/** 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 safeFetch(
`${config.lokiEndpoint}/loki/api/v1/push`,
{
method: "POST",
headers,
body: JSON.stringify({ streams }),
},
// Operator-configured Loki endpoint — pin the connect-time IP
// against DNS rebinding.
{ timeoutMs: 10_000, requirePublicHost: true },
);
} 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);
}
}