13 KiB
Capability Spec Delta: opentelemetry (opentelemetry-completion)
Package: @suaveplan/opentelemetry
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions. (Seeopenspec/AGENTS.mdSection 13.)
ADDED Requirements
Requirement: In-memory span exporter for testing
The package SHALL expose an InMemorySpanExporter class that satisfies the SpanExporter interface from @opentelemetry/sdk-trace-base, collects every span passed to export(), and exposes getFinishedSpans(), getSpansByName(name), reset(), shutdown(), forceFlush(), and a spanCount getter for test assertions. After shutdown(), subsequent calls to export() SHALL return ExportResultCode.FAILED with an OpenTelemetryError(OTEL.EXPORT_FAILED, ...) in the error field, and shutdown() SHALL be idempotent.
Scenario: collected spans are returned by getFinishedSpans
- WHEN three spans are passed to
export()and the result callback is invoked withExportResultCode.SUCCESS - THEN
getFinishedSpans()returns a fresh array containing those three spans in insertion order, andspanCount === 3
Scenario: getSpansByName filters on span name
- WHEN five spans named
"a","b","a","c","a"are exported - THEN
getSpansByName("a")returns exactly the three"a"spans
Scenario: reset clears collected spans without shutting down
- WHEN spans are exported, then
reset()is called - THEN
getFinishedSpans()returns[],spanCount === 0, AND a subsequentexport()succeeds and accumulates new spans
Scenario: post-shutdown export is rejected with OTEL.EXPORT_FAILED
- WHEN
shutdown()has been called andexport()is invoked again - THEN the result callback receives
{ code: ExportResultCode.FAILED, error: <OpenTelemetryError with code OTEL.EXPORT_FAILED> }
Scenario: shutdown is idempotent
- WHEN
shutdown()is called twice - THEN both calls resolve without throwing AND no double-shutdown side effect (e.g. duplicate clearing) is observable
Verified by: packages/monitor/opentelemetry/src/exporters/memory/memory.test.ts::"collects exported spans and surfaces them via getFinishedSpans" AND packages/monitor/opentelemetry/src/exporters/memory/memory.test.ts::"filters spans by name via getSpansByName" AND packages/monitor/opentelemetry/src/exporters/memory/memory.test.ts::"reset clears spans without shutting down" AND packages/monitor/opentelemetry/src/exporters/memory/memory.test.ts::"rejects export after shutdown with OTEL.EXPORT_FAILED" AND packages/monitor/opentelemetry/src/exporters/memory/memory.test.ts::"shutdown is idempotent" AND packages/monitor/opentelemetry/src/exporters/memory/memory.spec-conformance.test.ts::"satisfies the SpanExporter contract from @opentelemetry/sdk-trace-base"
Requirement: Console span exporter for development
The package SHALL expose a ConsoleSpanExporter class that satisfies the same SpanExporter contract and pretty-prints every span to a configurable log function. ConsoleSpanExporterOptions SHALL accept an optional logFn: (line: string) => void (default console.log) and an optional pretty: boolean (default true). Pretty mode produces indented JSON; compact mode produces single-line JSON. Each rendered span SHALL include traceId, spanId, parentSpanId, name, kind (decoded to label), status (code + message), ISO startTime / endTime, duration, attributes, events, links, and resource.attributes. After shutdown(), subsequent export() calls SHALL return ExportResultCode.FAILED with OpenTelemetryError(OTEL.EXPORT_FAILED, ...).
Scenario: default logFn is console.log
- WHEN a
ConsoleSpanExporteris constructed with no options - THEN
export()writes viaconsole.log(verified by spying on the global)
Scenario: custom logFn captures rendered output
- WHEN
new ConsoleSpanExporter({ logFn: capture })is constructed and a span is exported - THEN
capturereceives one call per exported span with a JSON string
Scenario: pretty mode indents output
- WHEN
pretty: true(default) and a span is exported - THEN the captured string contains
"\n"(multi-line indented JSON)
Scenario: compact mode emits single-line JSON
- WHEN
pretty: falseand a span is exported - THEN the captured string contains no
"\n"(single-line JSON)
Scenario: post-shutdown export rejected
- WHEN
shutdown()has been called andexport()is invoked again - THEN the result callback receives
{ code: ExportResultCode.FAILED, error: <OpenTelemetryError with code OTEL.EXPORT_FAILED> }
Verified by: packages/monitor/opentelemetry/src/exporters/console/console.test.ts::"defaults logFn to console.log" AND packages/monitor/opentelemetry/src/exporters/console/console.test.ts::"routes spans to the supplied logFn" AND packages/monitor/opentelemetry/src/exporters/console/console.test.ts::"emits indented JSON in pretty mode" AND packages/monitor/opentelemetry/src/exporters/console/console.test.ts::"emits single-line JSON in compact mode" AND packages/monitor/opentelemetry/src/exporters/console/console.test.ts::"rejects export after shutdown with OTEL.EXPORT_FAILED" AND packages/monitor/opentelemetry/src/exporters/console/console.spec-conformance.test.ts::"satisfies the SpanExporter contract from @opentelemetry/sdk-trace-base"
Requirement: W3C-compliant trace and span ID generator
The package SHALL expose generateTraceId() returning a 32-character lower-hex string, generateSpanId() returning a 16-character lower-hex string, isValidTraceId(id) and isValidSpanId(id) predicates, plus the constants TRACE_ID_HEX_LENGTH (32), SPAN_ID_HEX_LENGTH (16), INVALID_TRACE_ID (32 zero chars) and INVALID_SPAN_ID (16 zero chars). Generation SHALL use crypto.getRandomValues exclusively — no Math.random() fallback (genesis determinism rule). If the generated value equals the all-zero invalid value, the generator SHALL re-roll, capped at 8 attempts; on exhaustion it SHALL throw OpenTelemetryError(OTEL.INVALID_TRACE_ID, ...) (or OTEL.INVALID_SPAN_ID).
Scenario: generateTraceId produces a valid 32-char lower-hex string
- WHEN
generateTraceId()is called - THEN the returned string has length 32, matches
/^[0-9a-f]{32}$/, andisValidTraceId(returned) === true
Scenario: generateSpanId produces a valid 16-char lower-hex string
- WHEN
generateSpanId()is called - THEN the returned string has length 16, matches
/^[0-9a-f]{16}$/, andisValidSpanId(returned) === true
Scenario: isValidTraceId rejects the all-zero string
- WHEN
isValidTraceId(INVALID_TRACE_ID)is called - THEN the result is
false
Scenario: isValidTraceId rejects mixed-case input
- WHEN
isValidTraceId("ABCDEF0000000000000000000000ABCD")is called - THEN the result is
false(lower-hex required)
Scenario: isValidSpanId rejects wrong-length input
- WHEN
isValidSpanId("123")is called - THEN the result is
false
Scenario: round-trip property holds for 1000 samples
- WHEN
fast-checkgenerates 1000 trace IDs viagenerateTraceId() - THEN every one passes
isValidTraceIdand is unique with high probability (no collisions in the 1000 sample)
Verified by: packages/monitor/opentelemetry/src/id-generator/id-generator.test.ts::"generateTraceId returns a 32-char lower-hex non-zero string" AND packages/monitor/opentelemetry/src/id-generator/id-generator.test.ts::"generateSpanId returns a 16-char lower-hex non-zero string" AND packages/monitor/opentelemetry/src/id-generator/id-generator.test.ts::"isValidTraceId rejects the all-zero invalid value" AND packages/monitor/opentelemetry/src/id-generator/id-generator.test.ts::"isValidTraceId rejects upper-case hex" AND packages/monitor/opentelemetry/src/id-generator/id-generator.test.ts::"isValidSpanId rejects wrong-length input" AND packages/monitor/opentelemetry/src/id-generator/id-generator.spec-conformance.test.ts::"every generated trace id passes isValidTraceId (1000-sample property)"
Requirement: Deterministic id-generator backed by an injectable Rng
The package SHALL expose createDeterministicIdGenerator(rng: Rng) returning { generateTraceId, generateSpanId }. Both methods SHALL fill the ID by reading bytes from rng.bytes(n) (16 for trace, 8 for span) rather than crypto.getRandomValues, so a seeded Rng produces reproducible IDs. The same all-zero retry behaviour applies — capped at 8 attempts, then OTEL.INVALID_TRACE_ID / OTEL.INVALID_SPAN_ID.
Scenario: seeded Rng produces reproducible IDs
- WHEN two
createDeterministicIdGenerator(rng)instances are constructed withRnginstances seeded identically andgenerateTraceId()is called once on each - THEN both return the same 32-char hex string
Scenario: zero-byte Rng exhausts retry budget
- WHEN the supplied
Rng.bytesalways returnsUint8Array.from({ length: n }, () => 0)andgenerateTraceId()is called - THEN after 8 attempts the call throws
OpenTelemetryErrorwith codeOTEL.INVALID_TRACE_IDandcontext.attempts === 8
Verified by: packages/monitor/opentelemetry/src/id-generator/deterministic.test.ts::"seeded Rng produces reproducible trace ids" AND packages/monitor/opentelemetry/src/id-generator/deterministic.test.ts::"seeded Rng produces reproducible span ids" AND packages/monitor/opentelemetry/src/id-generator/deterministic.spec-conformance.test.ts::"throws OTEL.INVALID_TRACE_ID after 8 zero-byte attempts" AND packages/monitor/opentelemetry/src/id-generator/deterministic.spec-conformance.test.ts::"throws OTEL.INVALID_SPAN_ID after 8 zero-byte attempts"
Requirement: W3C Baggage propagator for cross-process header injection
The package SHALL expose W3CBaggagePropagator implementing TextMapPropagator from @opentelemetry/api, plus the helper functions serializeBaggage(baggage) / parseBaggage(header) and the constant BAGGAGE_HEADER (literal "baggage"). The propagator's fields() SHALL return ["baggage"]. inject() SHALL read the active baggage via propagation.getBaggage(context), URI-encode keys and values, and call setter.set(carrier, BAGGAGE_HEADER, value). extract() SHALL read carrier[BAGGAGE_HEADER], parse, and call propagation.setBaggage(context, baggage). The serialiser/parser SHALL honour the constants MAX_BAGGAGE_ENTRIES (180), MAX_BAGGAGE_KEY_LENGTH (4096), MAX_BAGGAGE_VALUE_LENGTH (8192) — entries past these caps are silently dropped (W3C tolerant-parser behaviour, parity with legacy).
Scenario: round-trip extract then inject preserves entries
- WHEN
parseBaggage("k1=v1,k2=v2")is called and the result is re-serialised viaserializeBaggage(...) - THEN the round-tripped string equals
"k1=v1,k2=v2"
Scenario: URI-encoding survives special characters
- WHEN baggage has key
"k 1"and value"v;2,3"and is serialised, then parsed back - THEN the parsed entry has key
"k 1"and value"v;2,3"(the inner;and,are URI-encoded on the wire and decoded on parse; metadata after;is parsed only at the top level)
Scenario: inject writes the baggage header
- WHEN an active context has baggage
{ user: { value: "alice" } }andpropagator.inject(context, carrier, setter)is called - THEN
setter.setwas called once with(carrier, "baggage", "user=alice")
Scenario: extract reads the baggage header
- WHEN
carrier = { baggage: "user=alice" }andpropagator.extract(context, carrier, getter)is called - THEN the returned context has
propagation.getBaggage(returnedContext)?.getEntry("user")?.value === "alice"
Scenario: empty active baggage is a no-op for inject
- WHEN the active context has no baggage and
injectis called - THEN
setter.setis not invoked
Scenario: MAX_BAGGAGE_ENTRIES cap drops the 181st entry
- WHEN a header with 181 entries is parsed
- THEN the resulting baggage has 180 entries (the last one dropped); no error thrown
Scenario: oversized key is dropped
- WHEN a header has an entry whose key length exceeds
MAX_BAGGAGE_KEY_LENGTH - THEN that entry is silently dropped from the parsed result
Scenario: fields returns ["baggage"]
- WHEN
propagator.fields()is called - THEN it returns the array
["baggage"]
Verified by: packages/monitor/opentelemetry/src/baggage/baggage.test.ts::"round-trips simple key=value pairs" AND packages/monitor/opentelemetry/src/baggage/baggage.test.ts::"URI-encodes special characters in keys and values" AND packages/monitor/opentelemetry/src/baggage/baggage.test.ts::"inject writes the baggage header from active context" AND packages/monitor/opentelemetry/src/baggage/baggage.test.ts::"extract reads the baggage header into the returned context" AND packages/monitor/opentelemetry/src/baggage/baggage.test.ts::"inject is a no-op when active baggage is empty" AND packages/monitor/opentelemetry/src/baggage/baggage.spec-conformance.test.ts::"silently drops entries past MAX_BAGGAGE_ENTRIES" AND packages/monitor/opentelemetry/src/baggage/baggage.spec-conformance.test.ts::"silently drops entries with oversized keys" AND packages/monitor/opentelemetry/src/baggage/baggage.spec-conformance.test.ts::"fields returns the canonical baggage header name"