# OpenSpec Change: enterprise-events-metrics - **Change ID**: enterprise-events-metrics - **Package**: `@suaveplan/events-metrics` - **Category**: universal - **Target path**: `universal/events/events-metrics` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/events-metrics.md` --- ## Summary Deliver `@suaveplan/events-metrics` on the genesis branch from scratch at enterprise quality. The package is an opt-in metrics companion for `@suaveplan/events`: it attaches to the event bus via the public interceptor/companion API (never monkey-patching), records dispatch totals, listener error counts, dispatch latency histograms, queue depth gauges, and active subscriber counts. All metric names follow the `suaveplan.events.*` convention with stable attribute keys. Sub-microsecond hot-path overhead is a hard requirement. --- ## Current-state audit (genesis) The package does not exist on genesis yet — fresh build. No prior source, no test results, no coverage data. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/events` (`workspace:^`) | Event bus and interceptor API | | `@suaveplan/opentelemetry` (`workspace:^`) | `MeterProvider` factory, counter/histogram/gauge helpers | | `@suaveplan/error` (`workspace:^`) | Base class for `EventsMetricsError`; `defineCodes` for `EVENTS_METRICS.*` codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` injection interfaces | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | `Meter`, `Counter`, `Histogram`, `ObservableGauge` | | `tslib` (`catalog:build`) | Runtime helpers | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`). --- ## Public API surface ### Setup ```ts // Attaches metrics companion to an existing EventBus function attachEventMetrics(bus: EventBusLike, options?: EventMetricsOptions): EventMetricsHandle interface EventMetricsOptions { meter?: Meter // default: global meter from @opentelemetry/api eventFilter?: (name: string) => boolean // optional: record only matching events clock?: Clock logger?: LoggerLike } interface EventMetricsHandle { detach(): void snapshot(): MetricsSnapshot } ``` ### Metric definitions ```ts // All metric names and attribute keys as stable constants const METRIC_NAMES = { 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", } as const const ATTRIBUTE_KEYS = { EVENT_NAME: "event.name", EVENT_CATEGORY: "event.category", LISTENER_ID: "listener.id", BUS_ID: "bus.id", } as const ``` ### Snapshot ```ts interface MetricsSnapshot { dispatchTotal: Record // keyed by event.name listenerErrorTotal: Record avgDispatchLatencyMs: Record queueDepth: number activeSubscribers: number } ``` ### Error handling ```ts class EventsMetricsError extends SuaveplanError type EventsMetricsErrorCode = | "EVENTS_METRICS_ATTACH_FAILED" | "EVENTS_METRICS_ALREADY_ATTACHED" | "EVENTS_METRICS_DETACHED" function registerEventsMetricsErrorCodes(): void ``` ### Zod schemas ```ts const EventMetricsOptionsSchema: ZodSchema ``` --- ## New additions for genesis ### 1. Companion-API only `attachEventMetrics` uses only the public `EventBus` interceptor/companion surface from `@suaveplan/events`. No access to private fields, no prototype patching. If the bus is replaced or re-created, callers must call `detach()` and `attachEventMetrics` again. ### 2. Low-overhead hot path The dispatch interceptor is a thin closure: one array lookup for the counter and one `performance.now()` call for the histogram. Allocation on the critical path is zero when the event name was previously seen (name-to-attributes map is pre-cached). ### 3. Observable gauge for queue depth and subscribers Queue depth and active subscriber count are reported as OTel `ObservableGauge` (polled at collection time), not recorded on every dispatch, to avoid per-dispatch overhead. ### 4. Property-based tests (fast-check) - Counter monotonicity: `dispatchTotal[name]` never decreases after more dispatches - Error counter accuracy: `listenerErrorTotal` equals the number of listener throws injected - Histogram latency: average latency is within 1ms of a synthetic injected delay ### 5. Benchmark suite (`src/__benchmarks__/`) - Overhead of `attachEventMetrics` on dispatch hot path: compare instrumented vs uninstrumented bus at 1M events/sec - Snapshot retrieval latency at 1k distinct event names --- ## File layout ```text universal/events/events-metrics/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/ index.ts core/ attach-event-metrics/ attach-event-metrics.ts attach-event-metrics.md attach-event-metrics.types.ts attach-event-metrics.test.ts index.ts metrics/ metric-names/ metric-names.ts metric-names.md index.ts snapshot/ snapshot.ts snapshot.md snapshot.test.ts index.ts __benchmarks__/ events-metrics.bench.ts tests/ integration.test.ts property.test.ts ``` --- ## Impact **Downstream packages**: any application wiring `@suaveplan/events` that needs production observability without code changes to event publishers or subscribers. **Workspace dep prerequisites**: `@suaveplan/events`, `@suaveplan/opentelemetry`, `@suaveplan/error`, `@suaveplan/types` must be published to Verdaccio. **Breaking changes**: none (new package).