Signal Forge ADR-002: SpanLink for async RabbitMQ propagation (not parent-child)
Status: Accepted
Decision: The notification-svc consumer span uses a Link to the order-api producer span
context, not a parent-child relationship. The same consumer also NACKs poison messages to a
dead-letter queue
— see the instrumentation reference for the full
producer/consumer code walkthrough.
Rationale:
- OTel semantic conventions for messaging specify parent-child for synchronous in-process consumption, SpanLink for asynchronous cross-process consumption.
- Messages may be redelivered after NACK; each redelivery produces a separate consumer span. With parent-child, multiple consumer spans would all claim the same producer span as parent, creating an invalid trace tree. With SpanLink, each consumer span links to the producer span independently.
- In Jaeger, SpanLinks render as dashed arrows — visually distinct from synchronous parent-child chains — making the async boundary immediately visible.
Alternative considered: Parent-child — rejected because it misrepresents the async relationship and breaks under retry scenarios.
Local graph
Linked from 5 notes
Guide: .NET Instrumentation
Step-by-step: instrument an ASP.NET Core / gRPC .NET 8 service with OpenTelemetry — SDK wiring, custom spans and metrics, and RabbitMQ producer-side async trace propagation via the outbox pattern.
Guide: Python Instrumentation
Step-by-step: instrument a Python FastAPI service with OpenTelemetry — SDK wiring, custom metrics, and RabbitMQ consumer-side async trace propagation via manual context extraction and SpanLink.
Signal Forge ADR-008: Dead Letter Queue for poison message handling
Routes messages that exceed retry limits or are NACKed without requeue to a RabbitMQ DLQ, preventing infinite retry loops from starving the consumer.
Architecture Overview
Signal Forge's topology, service communication, trace propagation, and per-signal pipeline flow across local and Grafana Cloud deployment modes.
Replication Guides: Instrumenting Your Own Project
Step-by-step, copy-paste guides for replicating SignalForge's OpenTelemetry instrumentation pattern in a new .NET/Python/Angular/RabbitMQ/K8s project.
Related notes
Signal Forge ADR-005: Separate collector configmaps per deployment mode
Keeps cloud and local Alloy collector configs in separate files rather than one conditional configmap, so each mode's exporters stay explicit and uncoupled.
Signal Forge ADR-003: Span metrics generated before tail sampling
Places the spanmetrics connector ahead of tail_sampling so RED metrics reflect all traffic instead of only the ~25% of traces that survive sampling.
Architecture Overview
Signal Forge's topology, service communication, trace propagation, and per-signal pipeline flow across local and Grafana Cloud deployment modes.
Signal Forge ADR-008: Dead Letter Queue for poison message handling
Routes messages that exceed retry limits or are NACKed without requeue to a RabbitMQ DLQ, preventing infinite retry loops from starving the consumer.