Initial Commit
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user