mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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>
121 lines
3.3 KiB
TypeScript
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);
|
|
}
|
|
}
|