Initial Commit
This commit is contained in:
193
openspec/archive/2026-04-24-enterprise-events-otel/proposal.md
Normal file
193
openspec/archive/2026-04-24-enterprise-events-otel/proposal.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user