Files
2026-05-27 23:29:53 +01:00

122 lines
15 KiB
Markdown

# OpenTelemetry completion
**Package**: `@suaveplan/opentelemetry`
**Status**: PROPOSED
**Type**: feature
## Why
The 2026-04-19 archive `enterprise-opentelemetry/proposal.md` committed to migrating the legacy `packages/universal/opentelemetry/src` substrate (custom `Tracer` / `TracerProvider` / `Span` / `WritableSpan`, `Meter` / `Counter` / `Gauge` / `Histogram`, `Context` / `ContextManager`, `Baggage` / `W3CBaggagePropagator`, `ConsoleSpanExporter` / `InMemorySpanExporter`, error-bridge, and `id-generator`) into genesis at full parity. That migration shipped only the thin wrappers around `@opentelemetry/sdk-node` / `sdk-trace-node` / `sdk-metrics`. Five capabilities the legacy substrate provided are still missing in genesis:
1. `InMemorySpanExporter` — testing-only span exporter (legacy `src/exporters/memory/memory.ts` L1-L51, 51 L). Every package today ad-hocs its own when it needs to assert on emitted spans.
2. `ConsoleSpanExporter` — pretty-printed development exporter (legacy `src/exporters/console/console.ts` L1-L85, 85 L).
3. `id-generator` — `generateTraceId()` / `generateSpanId()` / `isValidTraceId()` / `isValidSpanId()` per W3C Trace Context (legacy `src/utils/id-generator.ts` L1-L74, 74 L).
4. `W3CBaggagePropagator` — header-based baggage extract/inject (legacy `src/propagation/baggage/baggage.ts` L78-L173, 96 L of class plus parser/serializer helpers).
5. Deterministic `id-generator-deterministic` — alternate generator backed by an injectable `Rng` from `@suaveplan/types`. Genesis-original; required because the legacy generator falls back to `Math.random()` when `crypto.getRandomValues` is missing — a violation of the genesis determinism rule.
Without these, every dependent test file (currently `@suaveplan/events-metrics` and any consumer that wants to assert spans were emitted) reaches into the OTel SDK packages directly, duplicates the in-memory exporter, or ships flaky assertions on real SDK output.
## Position on the legacy substrate
Genesis explicitly **does not** rebuild the custom `Tracer` / `TracerProvider` / `Span` / `Meter` / `Counter` / `Gauge` / `Histogram` classes. The reasoning:
- `@opentelemetry/api`, `@opentelemetry/sdk-node`, `@opentelemetry/sdk-trace-node`, and `@opentelemetry/sdk-metrics` are already pulled via the `observability` catalog and are the de-facto vendor-neutral substrate the legacy package was attempting to recreate. Re-implementing them would duplicate a maintained codebase with zero downstream benefit.
- The legacy `error-bridge` (`src/error-bridge/error-bridge.ts`) is already covered by `@suaveplan/logger` (which records `SuaveplanError` instances as span events through the OTel adapter that ships there). No port required.
- Precedent: `bitpacker-crypto/SUPERSEDED.md` documents the same "we deliberately rejected the substrate-from-scratch port" position; this proposal is the OpenTelemetry analogue of that rejection.
What this change adds is the **runtime-neutral pieces that `@opentelemetry/sdk-*` does not ship under a stable ESM-friendly surface**: the testing in-memory exporter, the pretty-printed console exporter, the W3C ID generator with explicit determinism toggles, and a baggage propagator wrapper sized for cross-process header injection. These five pieces close the verified gap without dragging the rest of the substrate along.
## What changes
- ADDED: `InMemorySpanExporter` — implements `SpanExporter` from `@opentelemetry/sdk-trace-base`; methods `export(spans, resultCallback)`, `getFinishedSpans()`, `getSpansByName(name)`, `reset()`, `shutdown()`, `forceFlush()`; `spanCount` getter; `shutdown()` is idempotent and subsequent `export()` calls return `ExportResultCode.FAILED`.
- ADDED: `ConsoleSpanExporter` — implements `SpanExporter`; constructor accepts `ConsoleSpanExporterOptions { logFn?: (line: string) => void; pretty?: boolean }`; default `logFn` is `console.log`, default `pretty` is `true`; renders span kind, status code, ISO start/end times, duration, attributes, events, links, and resource attributes.
- ADDED: `ConsoleSpanExporterOptions` — interface for the constructor options above.
- ADDED: `generateTraceId()` — returns a 32-char lower-hex W3C trace ID, guaranteed non-zero (`!== "00000000000000000000000000000000"`).
- ADDED: `generateSpanId()` — returns a 16-char lower-hex W3C span ID, guaranteed non-zero.
- ADDED: `isValidTraceId(id)` — boolean predicate for 32-char lower-hex non-zero.
- ADDED: `isValidSpanId(id)` — boolean predicate for 16-char lower-hex non-zero.
- ADDED: `INVALID_TRACE_ID` / `INVALID_SPAN_ID` constants — the all-zero values.
- ADDED: `TRACE_ID_HEX_LENGTH` (32) / `SPAN_ID_HEX_LENGTH` (16) constants.
- ADDED: `createDeterministicIdGenerator(rng)` — factory returning `{ generateTraceId, generateSpanId }` backed by an injectable `Rng` from `@suaveplan/types`. Used in tests where deterministic trace IDs are required.
- ADDED: `W3CBaggagePropagator` — implements `TextMapPropagator` from `@opentelemetry/api`; methods `extract(context, carrier, getter)`, `inject(context, carrier, setter)`, `fields()` returning `["baggage"]`. URI-encodes keys/values per W3C Baggage spec; honours the `MAX_BAGGAGE_ENTRIES` (180) / `MAX_BAGGAGE_KEY_LENGTH` (4096) / `MAX_BAGGAGE_VALUE_LENGTH` (8192) limits.
- ADDED: `BAGGAGE_HEADER` constant — the literal string `"baggage"`.
- ADDED: `serializeBaggage(baggage)` / `parseBaggage(header)` helper functions exposed for direct use without a propagator.
- ADDED: subpath exports — `./exporters`, `./id-generator`, `./baggage`, `./constants`.
- ADDED: error code `OTEL.EXPORT_FAILED` — thrown by `InMemorySpanExporter` / `ConsoleSpanExporter` when called after `shutdown()`.
- ADDED: error code `OTEL.INVALID_TRACE_ID` / `OTEL.INVALID_SPAN_ID` — thrown by deterministic generator when the supplied `Rng` produces an all-zero ID and exhausts retry attempts.
## Out of scope
- **Custom `Tracer` / `TracerProvider` / `Span` / `WritableSpan` classes** — covered by `@opentelemetry/api` + `@opentelemetry/sdk-trace-node`, both already in the `observability` catalog. Re-implementing duplicates maintained code; consumers should import from `@opentelemetry/api` directly. Position documented in `## Position on the legacy substrate` above.
- **Custom `Meter` / `MeterProvider` / `Counter` / `Gauge` / `Histogram` classes** — covered by `@opentelemetry/api` + `@opentelemetry/sdk-metrics`. Same rationale as above.
- **`Context` / `ContextManager` / `setSpanInContext` / `getSpanFromContext`** — covered by `@opentelemetry/api`'s `context` namespace and `trace.setSpan` / `trace.getSpan`.
- **`error-bridge` (`installSuaveplanErrorInterceptor`)** — already implemented by `@suaveplan/logger` via its OTel span-event adapter. Not re-introduced here. Migration: existing consumers should depend on `@suaveplan/logger` directly.
- **`W3CTraceContextPropagator`** — the legacy class wraps `traceparent` / `tracestate` parsing. Genesis already exposes `buildTraceParent`, `parseTraceParent`, `injectTraceContext`, `extractTraceContext` from `./propagation` (live in `src/core/propagation/propagation.ts`). The wrapper class would duplicate this surface. If a `TextMapPropagator`-shaped binding is required later, it can be added as a tiny adapter; not in this change.
- **`Baggage` class as a value object** — the legacy `Baggage` is an immutable `Map<string, BaggageEntry>` wrapper. `@opentelemetry/api` already exposes `propagation.createBaggage` / `propagation.getBaggage` / `propagation.setBaggage`; this change wraps the propagation step (header inject/extract) without re-introducing the class.
- **Confluent / OTLP-grpc / OTLP-http exporter wrappers** — those exporters are first-class npm packages (`@opentelemetry/exporter-trace-otlp-http`, etc.). Out of scope here.
## Source mapping
This change derives from the legacy `packages/universal/opentelemetry/src` tree at `../suaveplan/`. No `enterprise/opentelemetry.md` per-feature audit doc exists; source mapping cites the upstream files directly.
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `InMemorySpanExporter` (export / getFinishedSpans / getSpansByName / reset / shutdown / spanCount) | `packages/universal/opentelemetry/src/exporters/memory/memory.ts` | L1-L51 | Verbatim port; retarget `SpanExporter` import to `@opentelemetry/sdk-trace-base` (legacy used in-package types); `forceFlush()` added per upstream-SDK contract. |
| `ConsoleSpanExporter` + `ConsoleSpanExporterOptions` | `packages/universal/opentelemetry/src/exporters/console/console.ts` | L1-L85 | Verbatim port; same retarget; `pretty: true` default preserved. |
| `generateTraceId` / `generateSpanId` / `isValidTraceId` / `isValidSpanId` | `packages/universal/opentelemetry/src/utils/id-generator.ts` | L36-L74 | Verbatim grammar; drop the `Math.random()` fallback (genesis determinism rule — see below). |
| `INVALID_TRACE_ID` / `INVALID_SPAN_ID` / `TRACE_ID_HEX_LENGTH` / `SPAN_ID_HEX_LENGTH` | `packages/universal/opentelemetry/src/constants/index.ts` | L1-L40 | Verbatim. |
| `W3CBaggagePropagator` (extract / inject / fields) | `packages/universal/opentelemetry/src/propagation/baggage/baggage.ts` | L140-L173 | Re-target the propagator interface to `TextMapPropagator` from `@opentelemetry/api`; preserve URI-encoding semantics. |
| `serializeBaggage` / `parseBaggage` helper functions | `packages/universal/opentelemetry/src/propagation/baggage/baggage.ts` | L91-L134 | Verbatim. |
| `BAGGAGE_HEADER` / `MAX_BAGGAGE_ENTRIES` / `MAX_BAGGAGE_KEY_LENGTH` / `MAX_BAGGAGE_VALUE_LENGTH` | `packages/universal/opentelemetry/src/constants/index.ts` | L41-L60 | Verbatim. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `createDeterministicIdGenerator(rng)` | Genesis-original. Legacy `randomHex` (L14-L30 of `id-generator.ts`) silently falls back to `Math.random()` when `crypto.getRandomValues` is unavailable — a determinism-rule violation per CLAUDE.md and the genesis-wide rule that no module-scope `Math.random()` is permitted. The deterministic factory replaces the fallback with an explicit `Rng` injection point so tests can pin trace IDs without wiring the `crypto` polyfill. |
| `OTEL.EXPORT_FAILED` / `OTEL.INVALID_TRACE_ID` / `OTEL.INVALID_SPAN_ID` error codes | CLAUDE.md §Workflow rule 2 — every thrown error routes through `@suaveplan/error`. Legacy returned plain `ExportResultCode.FAILED { error: new Error("...") }`; genesis wraps that `error` field in `OpenTelemetryError` with a stable code so consumers can `instanceof` discriminate. |
| `forceFlush()` on `InMemorySpanExporter` | The `@opentelemetry/sdk-trace-base` `SpanExporter` interface requires `forceFlush()` even though the legacy in-package interface did not. Without it the exporter does not satisfy the upstream contract and `BatchSpanProcessor` rejects it at runtime. |
## Verification approach
Each spec delta requirement carries a `**Verified by:**` line pointing at concrete `it(...)` titles. The test files that will exist after this change ships:
- `packages/monitor/opentelemetry/src/exporters/memory/memory.test.ts` — covers `InMemorySpanExporter` (collect, query-by-name, reset, shutdown idempotency, post-shutdown export rejection, forceFlush)
- `packages/monitor/opentelemetry/src/exporters/memory/memory.spec-conformance.test.ts` — pins the `SpanExporter` contract from `@opentelemetry/sdk-trace-base`
- `packages/monitor/opentelemetry/src/exporters/console/console.test.ts` — covers `ConsoleSpanExporter` (default logFn, custom logFn, pretty / compact, post-shutdown export rejection)
- `packages/monitor/opentelemetry/src/exporters/console/console.spec-conformance.test.ts` — pins SpanExporter contract
- `packages/monitor/opentelemetry/src/id-generator/id-generator.test.ts` — covers `generateTraceId` / `generateSpanId` length, charset, non-zero invariant; `isValidTraceId` / `isValidSpanId` accept/reject grammar
- `packages/monitor/opentelemetry/src/id-generator/id-generator.spec-conformance.test.ts` — runs `fast-check` round-trip property (∀ generated id, `isValidId(id)` is true)
- `packages/monitor/opentelemetry/src/id-generator/deterministic.test.ts` — covers `createDeterministicIdGenerator` with seeded `Rng`
- `packages/monitor/opentelemetry/src/id-generator/deterministic.spec-conformance.test.ts` — pins behaviour when supplied `Rng` produces all-zero output (throws `OTEL.INVALID_TRACE_ID` / `OTEL.INVALID_SPAN_ID` after retry budget)
- `packages/monitor/opentelemetry/src/baggage/baggage.test.ts` — covers `W3CBaggagePropagator` extract/inject round-trip, `serializeBaggage` / `parseBaggage` round-trip, URI-encoding for special characters
- `packages/monitor/opentelemetry/src/baggage/baggage.spec-conformance.test.ts` — pins the `MAX_BAGGAGE_ENTRIES` / key-length / value-length caps (entries past the cap are ignored, not thrown)
## Migration
This is an additive change. No legacy API is removed.
For consumers expecting the legacy substrate's custom `Tracer` / `Meter` / `Span` classes:
- **Old behavior:** `import { Tracer, Span } from "@suaveplan/opentelemetry"` returned hand-rolled classes implementing the OTel data model.
- **New behavior:** the same surface comes from `@opentelemetry/api`. `@suaveplan/opentelemetry` re-exports `trace`, `metrics`, `context`, `propagation` from `@opentelemetry/api` for convenience.
- **Search command consumers should run:** `grep -rln "from \"@suaveplan/opentelemetry\"" packages/ src/ | xargs grep -l "Tracer\|TracerProvider\|Meter\|Counter\|Gauge\|Histogram\|Span\b"`.
- **Recommended replacement:** import `Tracer` / `Meter` / `Counter` / `Gauge` / `Histogram` from `@opentelemetry/api` directly. The api package is already pulled via the `observability` catalog and is `@suaveplan/opentelemetry`'s peer.
For consumers expecting `installSuaveplanErrorInterceptor` from the legacy `error-bridge`:
- **Old behavior:** legacy `installSuaveplanErrorInterceptor()` returned an uninstaller and wired `SuaveplanError` instances onto active spans as span events.
- **New behavior:** `@suaveplan/logger` ships the OTel span-event adapter; `createLogger().error(err)` records the error onto the active span automatically.
- **Search command:** `grep -rln "installSuaveplanErrorInterceptor\|isSuaveplanErrorInterceptorInstalled" packages/ src/`.
- **Recommended replacement:** depend on `@suaveplan/logger` and use its `error()` method.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/opentelemetry` to enumerate. From `grep -rl "@suaveplan/opentelemetry" /home/ubuntu/web/genesis/packages/ --include="package.json"`:
- `@suaveplan/events-metrics` — [x] tests run / [x] docs reviewed / [ ] no action needed. The events-metrics test suite is the most likely consumer of `InMemorySpanExporter`; the package's existing OTel assertions (currently driven by the upstream SDK directly) should be migrated to use the new exporter so future tests don't have to recreate it. Any places that emit traceparent headers should adopt `injectTraceContext` (already exported); this change does not rename that helper.
For all other monorepo packages: no action — they consume `@opentelemetry/api` directly via the catalog, not through `@suaveplan/opentelemetry`. This change is additive only.