import type { Meter, MeterProvider, Counter as OtelCounter, Histogram as OtelHistogram, } from "@opentelemetry/api"; import { metrics } from "@opentelemetry/api"; /** * Discriminator carried on every counter and histogram emission so * operators can split timeseries by ABI variant without re-reading the * package's source. */ export type WasmAbiVariantAttribute = "direct-i32-i64" | "wasi-stdio"; /** * Counter names emitted by the WASM-tier host bridge on production paths. * Pinned at the module level so a future regression that drops one of * them surfaces as a typecheck error rather than a silent metric gap. */ export const SAND_WASM_BRIDGE_COUNTER_NAMES = [ "plugin.wasm_bridge.handler_invocations_total", "plugin.wasm_bridge.handler_traps_total", "plugin.wasm_bridge.marshal_failures_total", "plugin.wasm_bridge.lifecycle_failures_total", ] as const; export type SandboxWasmBridgeCounterName = (typeof SAND_WASM_BRIDGE_COUNTER_NAMES)[number]; export const SAND_WASM_BRIDGE_HISTOGRAM_NAMES = [ "plugin.wasm_bridge.handler_duration_ms", ] as const; export type SandboxWasmBridgeHistogramName = (typeof SAND_WASM_BRIDGE_HISTOGRAM_NAMES)[number]; export interface SandboxWasmBridgeInstrumentsConfig { readonly pluginName: string; readonly meterProvider?: MeterProvider; /** * ABI variant the bridge is running. Pinned at instrument-factory * time because every counter/histogram emission threads it through * as an attribute. Defaults to `"direct-i32-i64"` for back-compat * with the shipped contract. */ readonly abiVariant?: WasmAbiVariantAttribute; } /** * Frozen bundle of every counter + histogram the host bridge emits on a * production path. Acquired once at bridge construction so the first * `.add()` / `.record()` call after that point is allocation-free. */ export interface SandboxWasmBridgeInstruments { readonly meter: Meter; readonly counters: Readonly< Record >; readonly histograms: Readonly< Record >; /** Record one handler invocation with its outcome. */ recordInvocation( route: string, outcome: "ok" | "trap" | "marshal_failure" ): void; /** Record one handler duration sample. */ recordDuration(route: string, durationMs: number): void; /** Record a lifecycle-phase failure. */ recordLifecycleFailure(phase: "start" | "dispose"): void; /** ABI variant pinned on every emission. */ readonly abiVariant: WasmAbiVariantAttribute; } /** Build the bridge instrument registry for a given plugin. */ export function createSandboxWasmBridgeInstruments( config: SandboxWasmBridgeInstrumentsConfig ): SandboxWasmBridgeInstruments { const provider = config.meterProvider ?? metrics.getMeterProvider(); const meter = provider.getMeter( `@suaveplan/api-sandbox-wasm-host-bridge/${config.pluginName}` ); const abiVariant: WasmAbiVariantAttribute = config.abiVariant ?? "direct-i32-i64"; const counters = Object.create(null) as Record< SandboxWasmBridgeCounterName, OtelCounter >; for (const name of SAND_WASM_BRIDGE_COUNTER_NAMES) { counters[name] = meter.createCounter(name); } const histograms = Object.create(null) as Record< SandboxWasmBridgeHistogramName, OtelHistogram >; for (const name of SAND_WASM_BRIDGE_HISTOGRAM_NAMES) { histograms[name] = meter.createHistogram(name); } const frozenCounters = Object.freeze(counters); const frozenHistograms = Object.freeze(histograms); return Object.freeze({ meter, counters: frozenCounters, histograms: frozenHistograms, abiVariant, recordInvocation( route: string, outcome: "ok" | "trap" | "marshal_failure" ): void { frozenCounters["plugin.wasm_bridge.handler_invocations_total"].add( 1, { plugin: config.pluginName, route, outcome, abi_variant: abiVariant, } ); if (outcome === "trap") { frozenCounters["plugin.wasm_bridge.handler_traps_total"].add( 1, { plugin: config.pluginName, route, abi_variant: abiVariant, } ); } else if (outcome === "marshal_failure") { frozenCounters["plugin.wasm_bridge.marshal_failures_total"].add( 1, { plugin: config.pluginName, route, abi_variant: abiVariant, } ); } }, recordDuration(route: string, durationMs: number): void { frozenHistograms["plugin.wasm_bridge.handler_duration_ms"].record( durationMs, { plugin: config.pluginName, route, abi_variant: abiVariant, } ); }, recordLifecycleFailure(phase: "start" | "dispose"): void { frozenCounters["plugin.wasm_bridge.lifecycle_failures_total"].add( 1, { plugin: config.pluginName, phase, abi_variant: abiVariant, } ); }, }); }