103 lines
2.7 KiB
TypeScript
103 lines
2.7 KiB
TypeScript
import type { DiagnosticsPort } from "../../application/ports/diagnostics-port.ts";
|
|
import {
|
|
projectDiagnosticRecord,
|
|
safeErrorKind,
|
|
type DiagnosticRecord,
|
|
type DiagnosticRecordInput,
|
|
} from "../../contracts/diagnostics.ts";
|
|
import { projectTelemetryEvent } from "../../contracts/telemetry.ts";
|
|
|
|
export const noOpDiagnostics: DiagnosticsPort = Object.freeze({
|
|
record() {},
|
|
});
|
|
|
|
export function createDiagnosticsAdapter(
|
|
options: Readonly<{
|
|
maxEntries?: number;
|
|
now?: () => number;
|
|
sink?: (record: DiagnosticRecord) => void;
|
|
}> = {},
|
|
) {
|
|
const maxEntries = Math.max(1, options.maxEntries ?? 100);
|
|
const entries: DiagnosticRecord[] = [];
|
|
const droppedReasons = new Map<string, number>();
|
|
|
|
function drop(reason: string) {
|
|
droppedReasons.set(reason, (droppedReasons.get(reason) ?? 0) + 1);
|
|
}
|
|
|
|
function record(input: DiagnosticRecordInput) {
|
|
try {
|
|
const projected = projectDiagnosticRecord(input, options.now);
|
|
if (!projected.success) {
|
|
drop(projected.reason);
|
|
return;
|
|
}
|
|
if (entries.length >= maxEntries) {
|
|
entries.shift();
|
|
drop("queue-full");
|
|
}
|
|
entries.push(projected.record);
|
|
try {
|
|
options.sink?.(projected.record);
|
|
} catch {
|
|
drop("sink-failure");
|
|
}
|
|
} catch {
|
|
drop("serialization-failure");
|
|
}
|
|
}
|
|
|
|
return Object.freeze({
|
|
record,
|
|
entries: () => structuredClone(entries) as readonly DiagnosticRecord[],
|
|
dropped: () => Object.freeze(Object.fromEntries(droppedReasons)),
|
|
});
|
|
}
|
|
|
|
type BootSafeContext = Readonly<{
|
|
kind?: string;
|
|
buildId?: string;
|
|
configSchemaVersion?: string;
|
|
supportReference?: string;
|
|
}>;
|
|
|
|
let lastBootEvidence:
|
|
| Readonly<{
|
|
diagnostic: DiagnosticRecord | null;
|
|
telemetry: Readonly<Record<string, unknown>> | null;
|
|
}>
|
|
| undefined;
|
|
|
|
export function recordBootFailure(
|
|
error: unknown,
|
|
safe: BootSafeContext,
|
|
now: () => number = Date.now,
|
|
) {
|
|
const errorKind =
|
|
typeof safe.kind === "string" ? safe.kind : safeErrorKind(error);
|
|
const attributes = {
|
|
error_kind: errorKind,
|
|
build_id: safe.buildId ?? "unknown",
|
|
config_schema_version: safe.configSchemaVersion ?? "unknown",
|
|
};
|
|
const diagnostic = projectDiagnosticRecord(
|
|
{
|
|
level: "error",
|
|
eventId: "app.boot.failed",
|
|
context: attributes,
|
|
},
|
|
now,
|
|
);
|
|
const telemetry = projectTelemetryEvent("app.boot.failed", attributes, now);
|
|
lastBootEvidence = Object.freeze({
|
|
diagnostic: diagnostic.success ? diagnostic.record : null,
|
|
telemetry: telemetry.success ? telemetry.event : null,
|
|
});
|
|
return lastBootEvidence;
|
|
}
|
|
|
|
export function getLastBootEvidence() {
|
|
return lastBootEvidence ? structuredClone(lastBootEvidence) : undefined;
|
|
}
|