162 lines
5.6 KiB
TypeScript
162 lines
5.6 KiB
TypeScript
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<SandboxWasmBridgeCounterName, OtelCounter>
|
|
>;
|
|
readonly histograms: Readonly<
|
|
Record<SandboxWasmBridgeHistogramName, OtelHistogram>
|
|
>;
|
|
/** 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,
|
|
}
|
|
);
|
|
},
|
|
});
|
|
}
|