import type { MetricPoint } from "./metrics.ts"; import type { LogRecord } from "./logging.ts"; import type { MetricExporter } from "./server.ts"; import type { SpanExporter, SpanRecord, TraceContext } from "./trace.ts"; import { reportObservabilityFailure, type ObservabilityDiagnostic } from "./diagnostics.ts"; export type FrameworkSpanKind = "database" | "cache" | "queue" | "realtime" | "server-action" | "application"; export interface OperationTracer { span( kind: FrameworkSpanKind, name: string, operation: () => T | Promise, attributes?: Record, ): Promise; } const randomHex = (bytes: number) => Array.from(crypto.getRandomValues(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, "0"), ).join(""); export function createOperationTracer( options: { exporter?: SpanExporter; context?: () => Partial; now?: () => number; onExportError?: (error: unknown) => void; diagnostic?: ObservabilityDiagnostic; } = {}, ): OperationTracer { const now = options.now ?? Date.now; return { async span(kind, name, operation, attributes = {}) { const context = options.context?.() ?? {}; const started = now(); let status: SpanRecord["status"] = "ok"; let failure: unknown; try { return await operation(); } catch (error) { status = "error"; failure = error; throw error; } finally { const ended = now(); const record: SpanRecord = { name, traceId: context.traceId ?? randomHex(16), spanId: randomHex(8), ...(context.spanId ? { parentSpanId: context.spanId } : {}), sampled: context.sampled ?? true, startTime: started, endTime: ended, durationMs: ended - started, status, attributes: { "wrnexus.span.kind": kind, ...attributes }, ...(failure ? { error: { name: failure instanceof Error ? failure.name : "Error", message: failure instanceof Error ? failure.message : String(failure), }, } : {}), }; try { await options.exporter?.export([record]); } catch (error) { if (options.onExportError) options.onExportError(error); else reportObservabilityFailure("operation span export failed", error, options.diagnostic); } } }, }; } const safeMetricName = (name: string) => name.replace(/[^a-zA-Z0-9_:]/g, "_"); const labels = (point: MetricPoint) => { const entries = Object.entries(point.labels); return entries.length ? `{${entries.map(([key, value]) => `${safeMetricName(key)}=${JSON.stringify(String(value))}`).join(",")}}` : ""; }; export function renderPrometheus(points: readonly MetricPoint[]): string { return `${points.map((point) => `${safeMetricName(point.name)}${labels(point)} ${point.type === "histogram" ? (point.sum ?? 0) : point.value} ${point.timestamp}`).join("\n")}\n`; } export function createPrometheusPushExporter( endpoint: string, options: { fetch?: typeof fetch; headers?: HeadersInit } = {}, ): MetricExporter { return { async export(points) { const response = await (options.fetch ?? fetch)(endpoint, { method: "POST", headers: { "content-type": "text/plain; version=0.0.4", ...Object.fromEntries(new Headers(options.headers)), }, body: renderPrometheus(points), }); if (!response.ok) throw new Error(`Prometheus export failed with ${response.status}.`); }, }; } export function createZipkinExporter( endpoint: string, options: { fetch?: typeof fetch; serviceName?: string } = {}, ): SpanExporter { return { async export(spans) { const body = spans.map((span) => ({ traceId: span.traceId, id: span.spanId, parentId: span.parentSpanId, name: span.name, timestamp: Math.trunc(span.startTime * 1000), duration: Math.trunc(span.durationMs * 1000), localEndpoint: { serviceName: options.serviceName ?? "wrnexus" }, tags: Object.fromEntries( Object.entries(span.attributes).map(([key, value]) => [key, String(value)]), ), ...(span.error ? { error: span.error.message } : {}), })); const response = await (options.fetch ?? fetch)(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) throw new Error(`Zipkin export failed with ${response.status}.`); }, }; } /** Jaeger accepts Zipkin v2 JSON at its compatibility endpoint. */ export const createJaegerExporter = createZipkinExporter; export interface LogExporter { export(records: readonly LogRecord[]): void | Promise; } export function createOtlpLogExporter( endpoint: string, options: { fetch?: typeof fetch; headers?: HeadersInit } = {}, ): LogExporter { return { async export(records) { const response = await (options.fetch ?? fetch)(endpoint, { method: "POST", headers: { "content-type": "application/json", ...Object.fromEntries(new Headers(options.headers)), }, body: JSON.stringify({ resourceLogs: [ { scopeLogs: [ { scope: { name: "@wrnexus/observability" }, logRecords: records.map((record) => ({ timeUnixNano: String(BigInt(new Date(record.timestamp).getTime()) * 1_000_000n), severityText: record.level.toUpperCase(), body: { stringValue: record.message }, attributes: Object.entries(record.attributes).map(([key, value]) => ({ key, value: { stringValue: String(value) }, })), })), }, ], }, ], }), }); if (!response.ok) throw new Error(`OTLP log export failed with ${response.status}.`); }, }; } export interface ErrorReporter { capture(error: unknown, context?: Record): Promise; } export function createSentryCompatibleReporter( endpoint: string, options: { fetch?: typeof fetch; publicKey?: string } = {}, ): ErrorReporter { return { async capture(error, context = {}) { const eventId = randomHex(16); const failure = error instanceof Error ? error : new Error(String(error)); const response = await (options.fetch ?? fetch)(endpoint, { method: "POST", headers: { "content-type": "application/json", ...(options.publicKey ? { "x-sentry-auth": `Sentry sentry_key=${options.publicKey}, sentry_version=7` } : {}), }, body: JSON.stringify({ event_id: eventId, timestamp: new Date().toISOString(), platform: "javascript", level: "error", exception: { values: [{ type: failure.name, value: failure.message, stacktrace: failure.stack }], }, contexts: context, }), }); if (!response.ok) throw new Error(`Error report failed with ${response.status}.`); return eventId; }, }; } export function createPerformanceProfiler( options: { now?: () => number; onProfile?: (profile: { name: string; durationMs: number; attributes: Record; }) => void; } = {}, ) { const now = options.now ?? performance.now.bind(performance); return async ( name: string, operation: () => T | Promise, attributes: Record = {}, ): Promise => { const started = now(); try { return await operation(); } finally { options.onProfile?.({ name, durationMs: now() - started, attributes }); } }; }