3.1 KiB
telemetry — OpenTelemetry instruments
(no submodules)
Purpose
Owns the OpenTelemetry instruments emitted by @suaveplan/controller:
two counters tracking input and state-change throughput per device, and
one histogram for the pattern engine's match-attempt duration. Both
counters are exercised by BaseController via the recordInput and
recordStateChange helpers; the histogram is reserved for the pattern
matcher landing in Chunk B/C.
Features
- Lazy-bound meter — instruments resolve against the globally registered
MeterProvideron first import and fall back to a no-op meter when none is registered. Production callers pay zero cost for OTel. controller_inputs_totalcounter withdevice+inputattributes for fine-grained per-input throughput.controller_state_changes_totalcounter with thedeviceattribute.controller_pattern_match_duration_mshistogram (unitms) for the pattern engine.- Typed attribute bundles (
InputCounterAttributes,StateCounterAttributes) prevent string-typo bugs at the call site.
Usage: Basic
import { recordInput, recordStateChange } from "@suaveplan/controller";
recordInput({ device: "keyboard", input: "up" });
recordStateChange({ device: "keyboard" });
Usage: Advanced
import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import { recordInput } from "@suaveplan/controller";
const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
const reader = new PeriodicExportingMetricReader({
exporter,
exportIntervalMillis: 60_000,
});
const provider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(provider);
recordInput({ device: "gamepad", input: "a" });
await provider.forceFlush();
const points = exporter.getMetrics();
The same pattern is exercised in the package's polyfill setup; see
tests/setup/polyfills.ts for the canonical wire-up used by the
in-memory MeterProvider assertions.
API Reference
inputsCounter: Counter— OTel countercontroller_inputs_total.stateChangesCounter: Counter—controller_state_changes_total.patternMatchDurationHistogram: Histogram—controller_pattern_match_duration_ms.recordInput({ device, input }): void— single-call helper.recordStateChange({ device }): void— single-call helper.recordPatternMatchDuration(durationMs): void— histogram helper.
Implementation Notes
The instruments are module-scoped so the meter is resolved exactly once
per process. Tests inject their MeterProvider via
metrics.setGlobalMeterProvider BEFORE the source module is imported —
the package's tests/setup/polyfills.ts runs as a setup file and
guarantees the binding order. Switching to a deferred-resolve pattern
would require every adapter to plumb a meter object through its
constructor; the current shape keeps the device-adapter APIs free of
observability boilerplate while still allowing override via OTel's
setGlobalMeterProvider contract.