# `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 `MeterProvider` on first import and fall back to a no-op meter when none is registered. Production callers pay zero cost for OTel. - `controller_inputs_total` counter with `device` + `input` attributes for fine-grained per-input throughput. - `controller_state_changes_total` counter with the `device` attribute. - `controller_pattern_match_duration_ms` histogram (unit `ms`) for the pattern engine. - Typed attribute bundles (`InputCounterAttributes`, `StateCounterAttributes`) prevent string-typo bugs at the call site. ## Usage: Basic ```ts import { recordInput, recordStateChange } from "@suaveplan/controller"; recordInput({ device: "keyboard", input: "up" }); recordStateChange({ device: "keyboard" }); ``` ## Usage: Advanced ```ts 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 counter `controller_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.