/* Minimal OTLP/HTTP JSON push client for metrics (RFC-0337). Zero-dependency, Workers-compatible, fire-and-forget. Do not import node: modules — must be Workers-compatible. Do not retry — one flush = at most one HTTP request. Do not throw from flush — always resolve with delivery status. RFC-0337: initial implementation. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import { buildResourceAttributes, type WarpgogolResourceInput } from "./conventions.ts"; import { findMetricSpec, isLabelKeyForbidden, isMetricNameValid } from "./metric-registry.ts"; import { encodeOtlpMetrics, nowUnixNano } from "./otlp-json.ts"; import { convertAccumulatedToOtlp, type AccumulatedPoint } from "./otlp-converter.ts"; import type { OtlpTransport } from "./otlp-transport.ts"; import { createOtlpHttpTransport } from "./otlp-transport.ts"; import type { MetricsPusherEnv } from "./env-resolver.ts"; import { resolvePusherEnv, detectEnvironment } from "./env-resolver.ts"; export type { MetricsPusherEnv } from "./env-resolver.ts"; export interface MetricsPusher { counterAdd(name: string, value: number, labels?: Record): void; gaugeSet(name: string, value: number, labels?: Record): void; histogramRecord(name: string, value: number, labels?: Record): void; flush(): Promise<{ delivered: boolean; reason?: string }>; } const SCOPE_NAME = "@warpgogol/werkstatt-shared/observability"; const SCOPE_VERSION = "1"; const DEFAULT_TIMEOUT_MS = 2000; export function createMetricsPusher( resource: WarpgogolResourceInput, env?: MetricsPusherEnv, options?: { timeoutMs?: number }, ): MetricsPusher | null { const resolved = resolvePusherEnv(env); if (!resolved.endpoint || !resolved.token) { return null; } const endpoint = resolved.endpoint; const token = resolved.token; const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS; const environment = resource.environment ?? detectEnvironment(); const layer = resource.layer; const resourceInput: WarpgogolResourceInput = { ...resource, environment, layer, }; const points: AccumulatedPoint[] = []; const startTimeNano = nowUnixNano(); let _droppedCount = 0; const isStrict = environment === "development" || environment === "ci"; function validateMetric(name: string, labels: Record): void { if (!isMetricNameValid(name)) { const msg = `[observability] metric name "${name}" does not match the naming grammar`; if (isStrict) throw new Error(msg); _droppedCount++; return; } const spec = findMetricSpec(name); if (!spec) { const msg = `[observability] metric name "${name}" is not declared in WARPGOGOL_METRIC_REGISTRY`; if (isStrict) throw new Error(msg); _droppedCount++; return; } for (const key of Object.keys(labels)) { if (isLabelKeyForbidden(key)) { const msg = `[observability] label key "${key}" is forbidden for metric "${name}"`; if (isStrict) throw new Error(msg); _droppedCount++; return; } if (!spec.labelKeys.includes(key)) { const msg = `[observability] label key "${key}" is not declared for metric "${name}"`; if (isStrict) throw new Error(msg); _droppedCount++; return; } } } function addPoint( name: string, value: number, labels: Record, kind: AccumulatedPoint["kind"], ): void { try { validateMetric(name, labels); } catch { return; } points.push({ name, labels, value, kind }); } const transport: OtlpTransport = createOtlpHttpTransport({ endpoint, token, timeoutMs }); const pusher: MetricsPusher = { counterAdd(name, value, labels = {}) { addPoint(name, value, labels, "counter"); }, gaugeSet(name, value, labels = {}) { addPoint(name, value, labels, "gauge"); }, histogramRecord(name, value, labels = {}) { addPoint(name, value, labels, "histogram"); }, async flush() { if (points.length === 0) { return { delivered: true }; } const resourceAttrs = buildResourceAttributes(resourceInput); const otlpPoints = convertAccumulatedToOtlp(points, startTimeNano); if (otlpPoints.length === 0) { return { delivered: true }; } const body = encodeOtlpMetrics(resourceAttrs, otlpPoints, SCOPE_NAME, SCOPE_VERSION); return transport.send(JSON.stringify(body)); }, }; return pusher; }