Files
suaveplan/openspec/archive/2026-04-24-enterprise-events-metrics/proposal.md
2026-05-27 23:29:53 +01:00

5.8 KiB

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

// 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

// 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

interface MetricsSnapshot {
  dispatchTotal: Record<string, number>      // keyed by event.name
  listenerErrorTotal: Record<string, number>
  avgDispatchLatencyMs: Record<string, number>
  queueDepth: number
  activeSubscribers: number
}

Error handling

class EventsMetricsError extends SuaveplanError<EventsMetricsErrorCode>

type EventsMetricsErrorCode =
  | "EVENTS_METRICS_ATTACH_FAILED"
  | "EVENTS_METRICS_ALREADY_ATTACHED"
  | "EVENTS_METRICS_DETACHED"

function registerEventsMetricsErrorCodes(): void

Zod schemas

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

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).