Files
skills/docs/gaming/games/controller/src/telemetry/telemetry.md
2026-05-29 08:20:56 +01:00

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 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

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 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.