Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user