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:
InMemorySpanExporter— testing-only span exporter (legacysrc/exporters/memory/memory.tsL1-L51, 51 L). Every package today ad-hocs its own when it needs to assert on emitted spans.ConsoleSpanExporter— pretty-printed development exporter (legacysrc/exporters/console/console.tsL1-L85, 85 L).id-generator—generateTraceId()/generateSpanId()/isValidTraceId()/isValidSpanId()per W3C Trace Context (legacysrc/utils/id-generator.tsL1-L74, 74 L).W3CBaggagePropagator— header-based baggage extract/inject (legacysrc/propagation/baggage/baggage.tsL78-L173, 96 L of class plus parser/serializer helpers).- Deterministic
id-generator-deterministic— alternate generator backed by an injectableRngfrom@suaveplan/types. Genesis-original; required because the legacy generator falls back toMath.random()whencrypto.getRandomValuesis 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-metricsare already pulled via theobservabilitycatalog 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 recordsSuaveplanErrorinstances as span events through the OTel adapter that ships there). No port required. - Precedent:
bitpacker-crypto/SUPERSEDED.mddocuments 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— implementsSpanExporterfrom@opentelemetry/sdk-trace-base; methodsexport(spans, resultCallback),getFinishedSpans(),getSpansByName(name),reset(),shutdown(),forceFlush();spanCountgetter;shutdown()is idempotent and subsequentexport()calls returnExportResultCode.FAILED. - ADDED:
ConsoleSpanExporter— implementsSpanExporter; constructor acceptsConsoleSpanExporterOptions { logFn?: (line: string) => void; pretty?: boolean }; defaultlogFnisconsole.log, defaultprettyistrue; 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_IDconstants — 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 injectableRngfrom@suaveplan/types. Used in tests where deterministic trace IDs are required. - ADDED:
W3CBaggagePropagator— implementsTextMapPropagatorfrom@opentelemetry/api; methodsextract(context, carrier, getter),inject(context, carrier, setter),fields()returning["baggage"]. URI-encodes keys/values per W3C Baggage spec; honours theMAX_BAGGAGE_ENTRIES(180) /MAX_BAGGAGE_KEY_LENGTH(4096) /MAX_BAGGAGE_VALUE_LENGTH(8192) limits. - ADDED:
BAGGAGE_HEADERconstant — 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 byInMemorySpanExporter/ConsoleSpanExporterwhen called aftershutdown(). - ADDED: error code
OTEL.INVALID_TRACE_ID/OTEL.INVALID_SPAN_ID— thrown by deterministic generator when the suppliedRngproduces an all-zero ID and exhausts retry attempts.
Out of scope
- Custom
Tracer/TracerProvider/Span/WritableSpanclasses — covered by@opentelemetry/api+@opentelemetry/sdk-trace-node, both already in theobservabilitycatalog. Re-implementing duplicates maintained code; consumers should import from@opentelemetry/apidirectly. Position documented in## Position on the legacy substrateabove. - Custom
Meter/MeterProvider/Counter/Gauge/Histogramclasses — covered by@opentelemetry/api+@opentelemetry/sdk-metrics. Same rationale as above. Context/ContextManager/setSpanInContext/getSpanFromContext— covered by@opentelemetry/api'scontextnamespace andtrace.setSpan/trace.getSpan.error-bridge(installSuaveplanErrorInterceptor) — already implemented by@suaveplan/loggervia its OTel span-event adapter. Not re-introduced here. Migration: existing consumers should depend on@suaveplan/loggerdirectly.W3CTraceContextPropagator— the legacy class wrapstraceparent/tracestateparsing. Genesis already exposesbuildTraceParent,parseTraceParent,injectTraceContext,extractTraceContextfrom./propagation(live insrc/core/propagation/propagation.ts). The wrapper class would duplicate this surface. If aTextMapPropagator-shaped binding is required later, it can be added as a tiny adapter; not in this change.Baggageclass as a value object — the legacyBaggageis an immutableMap<string, BaggageEntry>wrapper.@opentelemetry/apialready exposespropagation.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— coversInMemorySpanExporter(collect, query-by-name, reset, shutdown idempotency, post-shutdown export rejection, forceFlush)packages/monitor/opentelemetry/src/exporters/memory/memory.spec-conformance.test.ts— pins theSpanExportercontract from@opentelemetry/sdk-trace-basepackages/monitor/opentelemetry/src/exporters/console/console.test.ts— coversConsoleSpanExporter(default logFn, custom logFn, pretty / compact, post-shutdown export rejection)packages/monitor/opentelemetry/src/exporters/console/console.spec-conformance.test.ts— pins SpanExporter contractpackages/monitor/opentelemetry/src/id-generator/id-generator.test.ts— coversgenerateTraceId/generateSpanIdlength, charset, non-zero invariant;isValidTraceId/isValidSpanIdaccept/reject grammarpackages/monitor/opentelemetry/src/id-generator/id-generator.spec-conformance.test.ts— runsfast-checkround-trip property (∀ generated id,isValidId(id)is true)packages/monitor/opentelemetry/src/id-generator/deterministic.test.ts— coverscreateDeterministicIdGeneratorwith seededRngpackages/monitor/opentelemetry/src/id-generator/deterministic.spec-conformance.test.ts— pins behaviour when suppliedRngproduces all-zero output (throwsOTEL.INVALID_TRACE_ID/OTEL.INVALID_SPAN_IDafter retry budget)packages/monitor/opentelemetry/src/baggage/baggage.test.ts— coversW3CBaggagePropagatorextract/inject round-trip,serializeBaggage/parseBaggageround-trip, URI-encoding for special characterspackages/monitor/opentelemetry/src/baggage/baggage.spec-conformance.test.ts— pins theMAX_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/opentelemetryre-exportstrace,metrics,context,propagationfrom@opentelemetry/apifor 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/Histogramfrom@opentelemetry/apidirectly. The api package is already pulled via theobservabilitycatalog and is@suaveplan/opentelemetry's peer.
For consumers expecting installSuaveplanErrorInterceptor from the legacy error-bridge:
- Old behavior: legacy
installSuaveplanErrorInterceptor()returned an uninstaller and wiredSuaveplanErrorinstances onto active spans as span events. - New behavior:
@suaveplan/loggerships 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/loggerand use itserror()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 ofInMemorySpanExporter; 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 adoptinjectTraceContext(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.