first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
# `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.