[Bug][Backend][Observability] #575 was fixed in ONE publisher — BookController (5 publish sites) and 3 others still ship RabbitMQ messages with no traceparent, so user-initiated flows appear in Jaeger as disconnected orphan traces #597
Reference in New Issue
Block a user
Filed by: QA Team — referencing #575, which is CLOSED. The fix is correct but was applied to one publisher only. The same defect is live in several others, including the user-facing book pipeline.
Summary
#575 ("ScheduledTaskPublisher does not propagate W3C trace context on publish — violates AGENTS.md non-negotiable #1") was fixed and closed. I verified the fix in
origin/master—ScheduledTaskPublishernow correctly starts a producer Activity and callsActivityHelper.InjectTraceContext(properties, activity). That part is genuinely done.But the bug was treated as a one-file bug. It is a class bug. Auditing every
BasicPublishAsynccall site against current master, several origin publishers still construct a freshBasicPropertieswith no trace injection at all.Confirmed trace-blind origin publishers
These files contain zero references to
ActivityHelperand do not copy incoming headers — so the message leaves with notraceparent:SpikerSoft.Api/Domain/Books/BookController.csSpikerSoft.Business/Domain/Calendar/Services/CalendarNotificationService.csSpikerSoft.EventHandlers.SecurityScanner/Services/SecurityScanService.csSpikerSoft.EventHandlers.UploadCoordinator/Services/StalenessDetectorService.csAlso flagged and worth a look (they reference
ActivityHelpersomewhere but the specific publish appears uninstrumented — needs an owner's eye rather than my grep):SpikerSoft.EventHandlers.Scheduler/Services/NotificationEventPublisher.cs:91,LeaseManagerService.cs:388/797,LessonRegradeWorkerHostedService.cs:403,LessonRegradeClient.cs:102.The worst offender is user-facing
BookController.cspublishes five times and referencesActivityHelperzero times. Verbatim from line ~655:No
traceparent. Compare the correct pattern, fromBookManagementConsumer.cs:212— the downstream consumer of this very flow:So the worker does it right; the API that kicks the work off does not.
Why this matters more than "a missing header"
Consumers call
ActivityHelper.StartActivityFromRabbitMQMessage(...), which extracts the parent fromtraceparent. With notraceparent, that call starts a brand-new ROOT trace. The result is not a slightly-degraded trace — it is two completely disconnected traces with no link between them.Concretely: a user uploads a book → the API's HTTP span ends there. The image-description / metadata / book-management work appears in Jaeger as an unrelated orphan trace with no path back to the request or the user that caused it. Correlation IDs are set, so you can grep your way across, but the distributed trace — the thing Jaeger exists for — is severed at exactly the boundary that matters.
This is consistent with what I see in Jaeger today: the API's traces are almost entirely internal RabbitMQ/SignalR plumbing, and real user-initiated flows do not connect end-to-end.
This is the same AGENTS.md non-negotiable #1 violation that #575 was raised for:
What is NOT broken (checked, so nobody re-chases these)
RabbitMQRetryHelper— I initially suspected the shared retry/DLQ ladder (26 consumers depend on it) of dropping trace context, because its comment says "Preserve correlation and trace context" while only copyingCorrelationId/MessageId. It is fine. Both the retry path (line 135) and the DLQ path (line ~217) copy the incoming headers wholesale vianew Dictionary<string, object?>(eventArgs.BasicProperties.Headers ?? ...), sotraceparentsurvives. No action needed.ScheduledTaskPublisher— #575's fix is present and correct in master.autoAck: truesites (LessonRegradeClient,RabbitMqMessageBusPublisher) are on Direct-Reply-To pseudo-queues, where RabbitMQ requiresnoAck. Correct as written — not a repeat of #573.Fix
Apply #575's pattern to each origin publisher: start a producer
Activity, thenActivityHelper.InjectTraceContext(properties, activity)before publishing.Suggest a guard so this class cannot regress again — the same reasoning as #596. A CI check, or better, funnel all publishing through a single helper that injects unconditionally, so it is impossible to publish without trace context rather than merely discouraged.
Related
.AddServicepositional-arg bug) inEventHandlers.Infrastructure. Same subsystem.Fix up in spikersoft-backend PR #296 (open, awaiting merge). Good ticket — and your instinct to "funnel all publishing through a single helper... so it is impossible to publish without trace context rather than merely discouraged" was the right call. That's what shipped.
Your audit was right, and it undercounted. Checking every
BasicPublishAsyncagainst master: 10 trace-blind origin publishers, and 4 of the 5 you flagged as "needs an owner's eye rather than my grep" are genuinely broken (LeaseManagerService:797is fine — the one you weren't sure about).Two things the audit couldn't see, which changed the shape of the fix:
Hand-calling
ActivityHelper.InjectTraceContext(properties)— the obvious fix — propagates nothing from a timer loop. Its first act isactivity ??= Activity.Current; if (activity == null) return;.StalenessDetectorServiceandNotificationEventPublisherpublish from timer loops with no ambient activity, so "apply #575's pattern" to those would have injected nothing while looking completely correct at the call site — and passed any test that asserts inject was called.Two publishers are already "fixed" that way and are silently propagating nothing right now.
SharedGpuLeaseService(3 sites) andRabbitMqGpuHeartbeatPublishercallInjectTraceContext(props)with no activity argument, from background paths. They read as instrumented in any grep. They emit notraceparent. They were not on anyone's list, including mine, until I stopped counting calls and started asserting headers.So:
PublishTracedAsyncis now the only way to publish. It starts a producer span onTraceSources.MessagePublishingfirst, so there is always context to inject — and that source is registered by the API and every worker, because anActivitySourcewith no listener returns null fromStartActivityand puts you right back to injecting nothing. Fourth sighting of that exact silent no-op (#458, #575, #588), so it's asserted, not trusted. All 35 publish sites migrated;PublishersUseTheTracedHelperTestsfails the build on a rawBasicPublishAsyncoutside a justified allow-list.RPC reply legs deliberately excluded (and annotated): the caller's span is still open and correlates by
CorrelationId, and no reply consumer readstraceparent, so injecting one would propagate to nobody. Including them would have made the allow-list meaningless.Tests assert a
traceparentactually reaches the wire, including the no-ambient-activity case — never that inject was called, since that's the assertion that goes green on the bug itself. 9,759 tests pass. AGENTS.md #1 and the telemetry rule now flag the old advice as the trap.Leaving open until #296 merges.
Resolved in spikersoft-backend PR #296 (merged to
master).The fix went wider than the ticket, because the ticket's premise was incomplete in two ways:
ActivityHelper.InjectTraceContext(properties)at each site looks correct but is a silent no-op from a background/timer path:InjectTraceContextdoesactivity ??= Activity.Current; if (activity == null) return;. Timer loops have no ambient activity, so the call injects nothing and still returns cleanly.SharedGpuLeaseService(3 sites) andRabbitMqGpuHeartbeatPublisherwere already calling it that way and propagating nothing in production.So instead of patching call sites, publishing now goes through a single choke point:
IChannel.PublishTracedAsync(...)inSpikerSoft.Common/Messaging/RabbitMqPublishExtensions.cs. It starts a Producer activity from a registeredActivitySource(TraceSources.MessagePublishing, registered in both the API and worker infrastructure — an unregistered source returnsnullfromStartActivity, which is this same bug's fourth sighting after #458/#575/#588) and injects the resulting context. All 35 sites migrated; the signature mirrorsBasicPublishAsyncso it was a pure rename.Guard tests:
PublishTracedAsyncTestsassertstraceparentactually lands in the headers (includingPublish_FromATimerLoopWithNoAmbientActivity_StillCarriesTraceContext, which fails against the old hand-call approach), andPublishersUseTheTracedHelperTestsis a source scan that fails the build if a rawBasicPublishAsyncreappears outside the allow-list (helper + 3 RPC reply legs, which correctly must not start a new trace).Closing.