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

15 KiB

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.