89 lines
3.1 KiB
Markdown
89 lines
3.1 KiB
Markdown
# `telemetry` — OpenTelemetry instruments
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
|
|
## 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.
|