Fix trace ID collision in DDEntryPoint.continue()#1270
Merged
kubukoz merged 5 commits intotypelevel:mainfrom Feb 4, 2026
Merged
Fix trace ID collision in DDEntryPoint.continue()#1270kubukoz merged 5 commits intotypelevel:mainfrom
kubukoz merged 5 commits intotypelevel:mainfrom
Conversation
Add ignoreActiveSpan() to the continue() method to prevent the Datadog tracer from inheriting thread-local active spans. In fiber-based runtimes (cats-effect, ZIO), fibers don't have a 1:1 mapping to threads. Without ignoreActiveSpan(), when continue() is called with an empty or invalid kernel, the tracer falls back to the thread's currently active span, causing unrelated requests to share trace IDs. The root() method already correctly uses ignoreActiveSpan(), but continue() was missing it. This fix ensures both methods behave consistently and prevent trace ID collisions under high concurrency.
added 3 commits
January 30, 2026 13:48
Add ignoreActiveSpan() to the continue() method to prevent the Datadog tracer from inheriting thread-local active spans. In fiber-based runtimes (cats-effect, ZIO), fibers don't have a 1:1 mapping to threads. Without ignoreActiveSpan(), when continue() is called with an empty or invalid kernel, the tracer falls back to the thread's currently active span, causing unrelated requests to share trace IDs. The root() method already correctly uses ignoreActiveSpan(), but continue() was missing it. This fix ensures both methods behave consistently and prevent trace ID collisions under high concurrency. Added test that verifies: - Without fix: ~13 unique trace IDs for 100 concurrent requests - With fix: 100 unique trace IDs (each request gets its own trace)
…com/brianmowen/natchez into fix/ddentrypoint-ignore-active-span
kubukoz
approved these changes
Feb 2, 2026
Member
kubukoz
left a comment
There was a problem hiding this comment.
That looks good, just one nitpick for the tests.
| } | ||
| .use { ep => | ||
| for { | ||
| traceIds <- AtomicCell[IO].of(List.empty[Option[String]]) |
Member
There was a problem hiding this comment.
question: would a Ref suffice here? It doesn't looks like we're using evalModify or others, here nor in the other test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When using
natchez-datadogwith cats-effect in a high-concurrency scenario, multiple unrelated requests can end up with the same trace ID. This causes their spans to appear as siblings in Datadog, making distributed tracing unreliable.Root Cause
In
DDEntryPoint.scala, theroot()method correctly usesignoreActiveSpan():However,
continue()does not useignoreActiveSpan():The Datadog tracer uses thread-local storage for the "active span". When
continue()is called:tracer.extract()returnsnullasChildOf(null)is a no-op for setting the parent from headersignoreActiveSpan(), the tracer falls back to using whatever span is currently "active" on that threadWhy
continueOrElseRootDoesn't HelpThe fallback to
root()only happens if the span itself is null. Butcontinue()always returns a valid span - it just has the wrong parent. The null check doesn't trigger for missing/invalid headers.Reproduction
We created a minimal reproduction that demonstrates the bug:
Results:
Proposed Fix
Add
ignoreActiveSpan()tocontinue()and only set the parent from the extractedspanContextif it's not null:Environment
Impact
This bug affects any application using
natchez-datadogwith: