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

194 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OpenSpec Change: enterprise-events-otel
- **Change ID**: enterprise-events-otel
- **Package**: `@suaveplan/events-otel`
- **Category**: universal
- **Target path**: `universal/events/events-otel`
- **Status**: PROPOSED
- **Date**: 2026-04-24
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/events-otel.md`
---
## Summary
Deliver `@suaveplan/events-otel` on the genesis branch from scratch at enterprise quality. The package is an opt-in OpenTelemetry tracing companion for `@suaveplan/events`: it wraps each dispatch in a span, propagates W3C Trace Context across synchronous and async subscriber invocations, records event-level attributes on each span, and correctly marks listener exceptions as span errors. Attaches via the public `@suaveplan/events` interceptor API. Sampling-aware: honours the parent sampling decision and supports rule-based head sampling.
---
## 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/error` (`workspace:^`) | Base class for `EventsOtelError`; `defineCodes` for `EVENTS_OTEL.*` codes |
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` injection interfaces |
| `zod` (`catalog:forms`) | Zod schemas for all public option types |
| `@opentelemetry/api` (`catalog:observability`) | `Tracer`, `Span`, `SpanStatusCode`, W3C context propagation |
| `tslib` (`catalog:build`) | Runtime helpers |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check` (all `catalog:testing`).
---
## Public API surface
### Setup
```ts
// Attaches OTel tracing companion to an existing EventBus
function attachEventTracing(bus: EventBusLike, options?: EventTracingOptions): EventTracingHandle
interface EventTracingOptions {
tracer?: Tracer // default: global tracer from @opentelemetry/api
propagator?: TextMapPropagator // default: W3CTraceContextPropagator
eventFilter?: (name: string) => boolean
samplingRule?: SamplingRule
clock?: Clock
logger?: LoggerLike
}
interface EventTracingHandle {
detach(): void
}
```
### Span attribute convention
```ts
// All span attribute keys as stable constants
const SPAN_ATTRIBUTES = {
EVENT_NAME: "suaveplan.event.name",
EVENT_ID: "suaveplan.event.id",
EVENT_CATEGORY: "suaveplan.event.category",
LISTENER_ID: "suaveplan.listener.id",
BUS_ID: "suaveplan.bus.id",
} as const
// Span names
const SPAN_NAMES = {
DISPATCH: "events.dispatch",
LISTENER: "events.listener",
} as const
```
### Sampling rule
```ts
type SamplingRule =
| { type: "always" }
| { type: "never" }
| { type: "rate"; rate: number } // 0.0–1.0
| { type: "event-name"; names: string[] } // sample only named events
```
### Trace context envelope (for transport interop)
```ts
// Serialize current trace context into a plain object for transport
function serializeTraceContext(): Record<string, string>
// Restore trace context from a serialized envelope
function deserializeTraceContext(headers: Record<string, string>): Con```text
### Error handling
```ts
class EventsOtelError extends SuaveplanError<EventsOtelErrorCode>
type EventsOtelErrorCode =
| "EVENTS_OTEL_ATTACH_FAILED"
| "EVENTS_OTEL_ALREADY_ATTACHED"
| "EVENTS_OTEL_PROPAGATION_FAILED"
function registerEventsOtelErrorCodes(): void
```
### Zod schemas
```ts
const EventTracingOptionsSchema: ZodSchema
const SamplingRuleSchema: ZodSchema
```
---
## New additions for genesis
### 1. W3C Trace Context propagation across async subscribers
The interceptor captures the active `Context` at dispatch time. Each listener invocation runs with that context restored as the active context, so any nested OTel operations inside listeners are correctly parented to the dispatch span.
### 2. Listener exception → span error
If a listener throws, the dispatch span's status is set to `SpanStatusCode.ERROR` with the exception recorded via `span.recordException(err)`. The error is re-thrown after recording.
### 3. Sampling-aware dispatch
When `samplingRule` is `"rate"` or `"event-name"`, spans are only created for the matching subset. The companion checks the parent sampling decision first; if the parent is sampled, the rule is bypassed to maintain trace completeness.
### 4. `serializeTraceContext` / `deserializeTraceContext`
These helpers allow `@suaveplan/events-transport` to inject and extract trace context from transport envelopes, enabling cross-context trace propagation without a direct package dependency on OTel internals.
### 5. Property-based tests (fast-check)
- Parent-child relationship: every listener span is a child of its dispatch span
- Context propagation under reentrancy: nested dispatches inside listeners produce correct span trees
- Sampling rate accuracy: at sampling rate 0.5, sampled span count is within 5% of 50% over 10k dispatches
---
## File layout
```text
universal/events/events-otel/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts
core/
attach-event-tracing/
attach-event-tracing.ts
attach-event-tracing.md
attach-event-tracing.types.ts
attach-event-tracing.test.ts
index.ts
propagation/
trace-context/
trace-context.ts
trace-context.md
trace-context.test.ts
index.ts
sampling/
sampling-rule/
sampling-rule.ts
sampling-rule.md
sampling-rule.types.ts
sampling-rule.test.ts
index.ts
tests/
integration.test.ts
property.test.ts
```
---
## Impact
**Downstream packages**: `@suaveplan/events-transport` uses `serializeTraceContext`/`deserializeTraceContext` for cross-context propagation. Any application using `@suaveplan/events` with production tracing can add this companion without code changes to publishers or subscribers.
**Workspace dep prerequisites**: `@suaveplan/events`, `@suaveplan/error`, `@suaveplan/types` must be published to Verdaccio.
**Breaking changes**: none (new package).