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,32 @@
# @suaveplan/events-metrics - OTel metrics companion for @suaveplan/events: dispatch counters, latency histograms, queue-depth gauges, and active-subscriber counts via interceptor API.
## Summary
OTel metrics companion for `@suaveplan/events`. Attaches via the public interceptor API to record dispatch totals, listener error counts, and latency histograms per event name.
<!-- begin:auto-docs -->
## Documentation tree
- [attach-event-metrics](./src/core/attach-event-metrics/attach-event-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [attach-event-metrics](./src/core/attach-event-metrics/attach-event-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [metric-names](./src/metrics/metric-names/metric-names.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [metric-names](./src/metrics/metric-names/metric-names.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [attach-event-metrics](./src/core/attach-event-metrics/attach-event-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metric-names](./src/metrics/metric-names/metric-names.md) — <!-- begin:auto-docs --> _(no submodules)_- [metric-names](./src/metrics/metric-names/metric-names.md) — <!-- begin:auto-docs --> _(no submodules)_- [attach-event-metrics](./src/core/attach-event-metrics/attach-event-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metric-names](./src/metrics/metric-names/metric-names.md) — <!-- begin:auto-docs --> _(no submodules)_- [metric-names](./src/metrics/metric-names/metric-names.md) — <!-- begin:auto-docs --> _(no submodules)_- [attach-event-metrics](./src/core/attach-event-metrics/attach-event-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metric-names](./src/metrics/metric-names/metric-names.md) — <!-- begin:auto-docs --> _(no submodules)_- [metric-names](./src/metrics/metric-names/metric-names.md) — <!-- begin:auto-docs --> _(no submodules)_- [attach-event-metrics](./src/core/attach-event-metrics/attach-event-metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metric-names](./src/metrics/metric-names/metric-names.md) — <!-- begin:auto-docs --> _(no submodules)_ - [metric-names](./src/metrics/metric-names/metric-names.md) — <!-- begin:auto-docs --> _(no submodules)_
## Quick Start
```ts
import { EventBus } from "@suaveplan/events";
import { attachEventMetrics } from "@suaveplan/events-metrics";
const bus = new EventBus();
const handle = attachEventMetrics(bus);
await bus.emitAsync("user.created", { id: "u-1" });
console.log(handle.snapshot().dispatchTotal["user.created"]); // 1
handle.detach();
```
## Modules
- [`src/core/attach-event-metrics/attach-event-metrics`](src/core/attach-event-metrics/attach-event-metrics.md) — `attachEventMetrics`, `EventMetricsHandle`, `MetricsSnapshot`
- [`src/metrics/metric-names/metric-names`](src/metrics/metric-names/metric-names.md) — `METRIC_NAMES`, `ATTRIBUTE_KEYS`

View File

@@ -0,0 +1,98 @@
# attach-event-metrics
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Attaches OTel metrics instrumentation to an `EventBus` via the public interceptor API from `@suaveplan/events`. It records event dispatch totals, listener error counts, and latency histograms per event name without modifying the bus or its listeners. The attachment is transparent to publishers and subscribers — they do not need to know that metrics are being collected.
The implementation prioritises zero per-dispatch allocation on the hot path for previously seen event names by pre-caching the OTel counter/histogram references and using object property access rather than Map lookups.
## Features
- `attachEventMetrics(bus, options?)` — installs a metrics interceptor and returns a `EventMetricsHandle`
- Dispatch total counter (`suaveplan.events.dispatch.total`) per event name
- Listener error counter (`suaveplan.events.listener.error.total`) per event name
- Dispatch latency histogram (`suaveplan.events.dispatch.latency_ms`) per event name
- `EventMetricsHandle.snapshot()` returns in-process totals without needing an OTel exporter
- `EventMetricsHandle.detach()` removes the interceptor
- Injectable `meter`, `clock`, `logger` for testability
- `eventFilter` option to limit recording to a subset of event names
## Usage: Basic Example
```ts
import { EventBus } from "@suaveplan/events";
import { attachEventMetrics } from "@suaveplan/events-metrics";
const bus = new EventBus<{ "user.created": { id: string } }>();
const handle = attachEventMetrics(bus);
bus.on("user.created", (payload) => console.log(payload));
await bus.emitAsync("user.created", { id: "u-1" });
const snap = handle.snapshot();
console.log(snap.dispatchTotal["user.created"]); // 1
handle.detach();
```
## Usage: Advanced Example
```ts
import { EventBus } from "@suaveplan/events";
import { attachEventMetrics } from "@suaveplan/events-metrics";
import { metrics } from "@opentelemetry/api";
const bus = new EventBus<{ click: { x: number }; submit: { formId: string } }>();
const handle = attachEventMetrics(bus, {
meter: metrics.getMeter("my-app"),
eventFilter: (name) => name.startsWith("click"),
clock: { now: () => performance.now() },
});
// Only "click" events will be instrumented; "submit" events pass through uninstrumented
await bus.emitAsync("click", { x: 42 });
await bus.emitAsync("submit", { formId: "f-1" });
const snap = handle.snapshot();
console.log(snap.dispatchTotal["click"]); // 1
console.log(snap.dispatchTotal["submit"]); // undefined (filtered out)
handle.detach();
```
## API Reference
### `attachEventMetrics<TMap>(bus, options?): EventMetricsHandle`
Installs a metrics interceptor on `bus`.
| Param | Type | Description |
|---|---|---|
| `bus` | `EventBus<TMap>` | The event bus to instrument |
| `options` | `EventMetricsOptions?` | Meter, filter, clock, logger |
**Returns**: `EventMetricsHandle`
### `EventMetricsHandle`
| Method | Returns | Description |
|---|---|---|
| `detach()` | `void` | Remove the interceptor |
| `snapshot()` | `MetricsSnapshot` | In-process metric values |
### `MetricsSnapshot`
| Field | Type | Description |
|---|---|---|
| `dispatchTotal` | `Record<string, number>` | Total dispatches per event name |
| `listenerErrorTotal` | `Record<string, number>` | Total listener errors per event name |
| `avgDispatchLatencyMs` | `Record<string, number>` | Average latency per event name |
| `queueDepth` | `number` | Current queue depth (0 for synchronous bus) |
| `activeSubscribers` | `number` | Active subscriber count |
## Implementation Notes
The metrics interceptor uses `bus.use()` which is the `@suaveplan/events` public interceptor API. It wraps `next()` with a timer, increments counters, and records the histogram. OTel instruments are lazily created on first use to avoid startup cost when the bus is never dispatched. The `queueDepth` and `activeSubscribers` fields in `snapshot()` are reported as 0 because the synchronous `EventBus` does not expose a queue depth API; applications using `BoundedQueue` would need a separate gauge observer.

View File

@@ -0,0 +1,61 @@
# metric-names
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Exports the stable constant objects `METRIC_NAMES` and `ATTRIBUTE_KEYS` that define every OTel metric name and span attribute key used across `@suaveplan/events-metrics`. Centralising these names prevents typos, enables IDE completion, and makes it easy to audit the full observability surface of the package.
These constants are the single source of truth consumed by both the instrumentation code and the tests that assert metric names are correct.
## Features
- `METRIC_NAMES` — all five OTel metric names: dispatch total, listener error total, dispatch latency histogram, queue depth gauge, active subscribers gauge
- `ATTRIBUTE_KEYS` — all four OTel attribute keys: event name, event category, listener ID, bus ID
- Both objects use `as const` for full literal-type narrowing
## Usage: Basic Example
```ts
import { METRIC_NAMES, ATTRIBUTE_KEYS } from "@suaveplan/events-metrics";
console.log(METRIC_NAMES.DISPATCH_TOTAL); // "suaveplan.events.dispatch.total"
console.log(ATTRIBUTE_KEYS.EVENT_NAME); // "event.name"
```
## Usage: Advanced Example
```ts
import { METRIC_NAMES, ATTRIBUTE_KEYS } from "@suaveplan/events-metrics";
import { metrics } from "@opentelemetry/api";
const meter = metrics.getMeter("my-app");
const counter = meter.createCounter(METRIC_NAMES.DISPATCH_TOTAL);
counter.add(1, { [ATTRIBUTE_KEYS.EVENT_NAME]: "user.created" });
```
## API Reference
### `METRIC_NAMES`
| Key | Value |
|---|---|
| `DISPATCH_TOTAL` | `"suaveplan.events.dispatch.total"` |
| `LISTENER_ERROR_TOTAL` | `"suaveplan.events.listener.error.total"` |
| `DISPATCH_LATENCY_MS` | `"suaveplan.events.dispatch.latency_ms"` |
| `QUEUE_DEPTH` | `"suaveplan.events.queue.depth"` |
| `ACTIVE_SUBSCRIBERS` | `"suaveplan.events.subscribers.active"` |
### `ATTRIBUTE_KEYS`
| Key | Value |
|---|---|
| `EVENT_NAME` | `"event.name"` |
| `EVENT_CATEGORY` | `"event.category"` |
| `LISTENER_ID` | `"listener.id"` |
| `BUS_ID` | `"bus.id"` |
## Implementation Notes
The constants are defined in `src/constants/index.ts` and re-exported from this module to follow the package structure convention. Consumers should import from `@suaveplan/events-metrics` (the root barrel) rather than the internal path to benefit from tree-shaking and future path changes.