feat(realtime): extract inbound Kafka-header traceparent in the relay (GC-23 (b) consumer half) #8

Merged
hibryda merged 1 commit from feat/relay-extract-kafka-traceparent into main 2026-06-01 15:38:22 +02:00
Owner

Header

im2be-realtime-service · GC-23 (b) producer→relay origin (consumer half) · branch feat/relay-extract-kafka-traceparentmain

TL;DR

The notification relay now extracts the W3C traceparent from the consumed Kafka message headers and roots its centrifugo.relay_publish span as a child of the producer's trace — so a business event flows producer → Kafka → relay → Centrifugo under ONE trace_id. Forward-compatible: no header → roots fresh (prior behaviour).

Summary

Completes the picture for GC-23: boundary (a) (relay→Centrifugo) is already closed end-to-end (Tempo d2b8b4ab2cd2fc119a8574c29372904 — the Centrifugo /api/publish span joins the relay trace as a child). This PR adds the consumer half of boundary (b). handleNotificationMessage now takes the Kafka headers and uses propagation.extract with a kafkajs IHeaders TextMapGetter (decodes Buffer header values to UTF-8, takes first-of-array). The relay span is started with the extracted context as its parent (getTracer().startSpan(name, opts, parentCtx)); the active context is set under that parent so the existing inject-to-Centrifugo continues the chain.

The producer half (im2be-platform-libs OutboxRecord.traceParent column + capture-at-write in OutboxPublisher.publish() + inject-into-Kafka-headers in the hot relay tryHotPublish and the cold poller tryColdPublish) is a separate follow-up PR. Until it lands, the traceparent header is absent and the relay roots its own trace — no regression.

Findings (self-review)

sev file:line note
INFO src/relay/notification-relay.ts (KAFKA_HEADER_GETTER) Buffer→utf8 decode + first-of-array; absent key → undefined → extract yields no remote span (safe no-op).
INFO src/relay/notification-relay.ts (handleNotificationMessage) span started with extracted parentCtx; activeCtx set under parent so the Centrifugo inject continues the chain.
INFO tests/relay.notification-relay.test.ts +1 test: inbound Buffer-valued traceparent → outbound publish carries the same trace_id with a new span_id. Existing 8 relay tests updated for the new headers param.

Verdict

NEEDS_REVIEW — feature change, consumer half of GC-23 (b). No new prod deps (uses the already-present @opentelemetry/api propagation API + kafkajs IHeaders). tsc --noEmit clean; vitest 157/157.

im2be-realtime-service · GC-23(b) consumer half · R0 (open) · tests 157/157 · typecheck clean · 2026-06-01

## Header **im2be-realtime-service** · GC-23 (b) producer→relay origin (consumer half) · branch `feat/relay-extract-kafka-traceparent` → `main` ## TL;DR The notification relay now **extracts** the W3C `traceparent` from the consumed Kafka message headers and roots its `centrifugo.relay_publish` span as a **child** of the producer's trace — so a business event flows producer → Kafka → relay → Centrifugo under ONE `trace_id`. Forward-compatible: no header → roots fresh (prior behaviour). ## Summary Completes the picture for GC-23: boundary (a) (relay→Centrifugo) is already closed end-to-end (Tempo `d2b8b4ab2cd2fc119a8574c29372904` — the Centrifugo `/api/publish` span joins the relay trace as a child). This PR adds the **consumer half** of boundary (b). `handleNotificationMessage` now takes the Kafka `headers` and uses `propagation.extract` with a kafkajs `IHeaders` `TextMapGetter` (decodes Buffer header values to UTF-8, takes first-of-array). The relay span is started with the extracted context as its parent (`getTracer().startSpan(name, opts, parentCtx)`); the active context is set under that parent so the existing inject-to-Centrifugo continues the chain. The **producer half** (im2be-platform-libs `OutboxRecord.traceParent` column + capture-at-write in `OutboxPublisher.publish()` + inject-into-Kafka-headers in the hot relay `tryHotPublish` and the cold poller `tryColdPublish`) is a separate follow-up PR. Until it lands, the `traceparent` header is absent and the relay roots its own trace — **no regression**. ## Findings (self-review) | sev | file:line | note | |---|---|---| | INFO | src/relay/notification-relay.ts (KAFKA_HEADER_GETTER) | Buffer→utf8 decode + first-of-array; absent key → undefined → extract yields no remote span (safe no-op). | | INFO | src/relay/notification-relay.ts (handleNotificationMessage) | span started with extracted parentCtx; activeCtx set under parent so the Centrifugo inject continues the chain. | | INFO | tests/relay.notification-relay.test.ts | +1 test: inbound Buffer-valued traceparent → outbound publish carries the same trace_id with a new span_id. Existing 8 relay tests updated for the new `headers` param. | ## Verdict **NEEDS_REVIEW** — feature change, consumer half of GC-23 (b). No new prod deps (uses the already-present `@opentelemetry/api` propagation API + kafkajs `IHeaders`). `tsc --noEmit` clean; vitest **157/157**. ## Footer im2be-realtime-service · GC-23(b) consumer half · R0 (open) · tests 157/157 · typecheck clean · 2026-06-01
The notification relay now extracts the W3C trace context (traceparent/
tracestate) from the consumed Kafka message headers and starts its
centrifugo.relay_publish span as a CHILD of that remote context, instead of
always rooting a fresh trace. This continues the producing service's trace
through Kafka → relay → Centrifugo under one trace_id (GC-23 (b), producer→
relay origin — the consumer half).

- handleNotificationMessage now takes the Kafka headers and uses
  propagation.extract with a kafkajs IHeaders TextMapGetter (decodes Buffer
  header values to UTF-8, takes the first value of an array).
- The relay span is started with the extracted context as its parent
  (getTracer().startSpan(name, opts, parentCtx)); the active context is set
  under that parent so the existing inject-to-Centrifugo continues the chain.
- Forward-compatible: with no traceparent header present, the extracted
  context carries no remote span and the relay roots its own trace (the
  pre-carry behaviour) — safe to deploy AHEAD of the producer-side outbox
  carry (im2be-platform-libs OutboxRecord traceParent column + relay header
  inject; tracked follow-up).

Tests: +1 (157 total) — asserts an inbound Buffer-valued traceparent header
makes the outbound publish carry the SAME trace_id with a new span_id.
typecheck clean.

R-cycle: feature change; consumer half of GC-23 (b). Verified: tsc --noEmit
clean; vitest 157/157.

hib-pr-reviewer review — PR #8 (affinity-intelligence-rework/im2be-realtime-service)

Round 1 — head 63c7c55de1b2, base main, trigger opened

TL;DR: NO_NEW_FINDINGS — No new findings this round.

Summary

[quorum-converged] A=0 = B=0. ## Review: feat(realtime) — inbound Kafka-header traceparent extraction (GC-23 (b))

The implementation is correct. KAFKA_HEADER_GETTER correctly handles all kafkajs header value variants (Buffer, string, (Buffer|string)[], undefined), propagation.extract is called with headers ?? {} so the fallback-to-no-remote-span path works before the producer side lands, and trace.setSpan(parentCtx, span) (rather than context.active()) correctly preserves any tracestate/baggage extracted into parentCtx. The new end-to-end test wires a real W3CTraceContextPropagator and asserts both same-trace_id continuity and a fresh relay span_id — that is the right shape for this kind of propagation test.

One minor finding: the JSDoc for handleNotificationMessage was not updated to document the new headers parameter.

CI status (head 63c7c55de1b2)

No CI checks reported for this commit.

Findings

No new findings this round.

Quorum converged on empty findings (A + B both returned 0).

Verdict

NO_NEW_FINDINGS


hib-pr-reviewer • round 1 • 0 findings • 2026-06-01T13:33:15.669Z → 2026-06-01T13:35:24.044Z • posted-as: pr-reviewer-bot • model: auto • [bookkeeping fallback]

<!-- hib-pr-reviewer round:1 --> ## hib-pr-reviewer review — PR #8 (affinity-intelligence-rework/im2be-realtime-service) **Round 1** — head `63c7c55de1b2`, base `main`, trigger `opened` **TL;DR:** NO_NEW_FINDINGS — No new findings this round. ### Summary [quorum-converged] A=0 = B=0. ## Review: feat(realtime) — inbound Kafka-header traceparent extraction (GC-23 (b)) The implementation is correct. `KAFKA_HEADER_GETTER` correctly handles all kafkajs header value variants (`Buffer`, `string`, `(Buffer|string)[]`, `undefined`), `propagation.extract` is called with `headers ?? {}` so the fallback-to-no-remote-span path works before the producer side lands, and `trace.setSpan(parentCtx, span)` (rather than `context.active()`) correctly preserves any `tracestate`/baggage extracted into `parentCtx`. The new end-to-end test wires a real `W3CTraceContextPropagator` and asserts both same-`trace_id` continuity and a fresh relay `span_id` — that is the right shape for this kind of propagation test. One minor finding: the JSDoc for `handleNotificationMessage` was not updated to document the new `headers` parameter. ### CI status (head `63c7c55de1b2`) _No CI checks reported for this commit._ ### Findings **No new findings this round.** _Quorum converged on empty findings (A + B both returned 0)._ ### Verdict **NO_NEW_FINDINGS** --- <sub>hib-pr-reviewer • round 1 • 0 findings • 2026-06-01T13:33:15.669Z → 2026-06-01T13:35:24.044Z • posted-as: pr-reviewer-bot • model: auto • [bookkeeping fallback]</sub>
hibryda deleted branch feat/relay-extract-kafka-traceparent 2026-06-01 15:38:22 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
affinity-intelligence-rework/im2be-realtime-service!8
No description provided.