fix(internal/injector): ArgumentThatImplements always fails for context.Context#798
Conversation
…ntext.Context` Go's type checker assigns each loaded package a unique object. If the same package is loaded twice through different means, you get two distinct objects for what is logically the same type. `types.Implements` uses pointer equality to compare types across method signatures, so it sees them as different and returns `false`. `ResolveInterfaceTypeByName` loads context.Context one way; the instrumented code is type-checked another way. The two `context` packages end up as different objects, `types.Implements` fails, and `ArgumentThatImplements` never matches. Fix by falling back to comparing method names and package paths instead of relying on pointer equality.
|
Additional context: BackgroundThe Reproduction ScenarioThe repro project used to replicate the issue described in the JIRA ticket implements a type CustomContext struct {
context.Context
UserID string
}
//dd:span
func DoThingWithCtx(ctx context.Context) error { return nil }
//dd:span
func DoThingWithCustomCtx(ctx *CustomContext) error { return nil }
// Creates span1 explicitly, wraps it in a CustomContext
span1, nativeCtx := tracer.StartSpanFromContext(context.Background(), "theOperation")
customCtx := &CustomContext{nativeCtx, "user-id"}
DoThingWithCtx(customCtx) // SPAN 0 — should be child of span1
DoThingWithCustomCtx(customCtx) // SPAN 1 — should be child of span1
// Creates span2 with context.Background() — should be a root span
span2, _ := tracer.StartSpanFromContext(context.Background(), "theOperation2") // SPAN 4
// customCtx still carries span1 — both calls should still be children of span1
DoThingWithCustomCtx(customCtx) // SPAN 2 — should be child of span1
DoThingWithCtx(customCtx) // SPAN 3 — should be child of span1Observed output (before fixes): How each bug manifests: SPANs 2 & 3 — After Additionally, SPAN 1 appearing correct is coincidental: without Bug 3 fixed, SPAN 4 — FixesBug —
|
| // signatures (e.g., time.Time in context.Context.Deadline). | ||
| // types.LookupFieldOrMethod compares by package path, not pointer, so it | ||
| // correctly finds matching methods across importers. | ||
| for i := 0; i < iface.NumMethods(); i++ { |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #798 +/- ##
==========================================
+ Coverage 65.72% 69.62% +3.90%
==========================================
Files 113 116 +3
Lines 7926 6900 -1026
==========================================
- Hits 5209 4804 -405
+ Misses 2192 1545 -647
- Partials 525 551 +26
🚀 New features to boost your workflow:
|
…ntext.Context` (#798) Go's type checker assigns each loaded package a unique object. If the same package is loaded twice through different means, you get two distinct objects for what is logically the same type. `types.Implements` uses pointer equality to compare types across method signatures, so it sees them as different and returns `false`. `ResolveInterfaceTypeByName` loads context.Context one way; the instrumented code is type-checked another way. The two `context` packages end up as different objects, `types.Implements` fails, and `ArgumentThatImplements` never matches. Fix by falling back to comparing method names and package paths instead of relying on pointer equality.
### What does this PR do? This PR fixes three independent bugs where the wrong span was used as the parent. Relates to DataDog/orchestrion#798. ### Motivation Orchestrion instruments Go code by injecting span creation logic and using goroutine-local storage (GLS) as a side-channel to propagate the active span through call sites that don't have a `context.Context` in their signature. Spans are stored both in the `context.Context` chain (explicit) and in the GLS stack (implicit). `glsContext` wraps a `context.Context` and intercepts `.Value()` calls to consult both sources. #### Reproduction Scenario The repro project used to replicate the issue described in the JIRA ticket implements a `CustomContext` type that embeds `context.Context` and adds a `UserID` field, mimicking how frameworks like Fiber wrap request contexts: ```go type CustomContext struct { context.Context UserID string } //dd:span func DoThingWithCtx(ctx context.Context) error { return nil } //dd:span func DoThingWithCustomCtx(ctx *CustomContext) error { return nil } ``` `theOperation` (excluded from instrumentation with `//orchestrion:ignore`) does the following: ```go // Creates span1 explicitly, wraps it in a CustomContext span1, nativeCtx := tracer.StartSpanFromContext(context.Background(), "theOperation") customCtx := &CustomContext{nativeCtx, "user-id"} DoThingWithCtx(customCtx) // SPAN 0 — should be child of span1 DoThingWithCustomCtx(customCtx) // SPAN 1 — should be child of span1 // Creates span2 with context.Background() — should be a root span span2, _ := tracer.StartSpanFromContext(context.Background(), "theOperation2") // SPAN 4 // customCtx still carries span1 — both calls should still be children of span1 DoThingWithCustomCtx(customCtx) // SPAN 2 — should be child of span1 DoThingWithCtx(customCtx) // SPAN 3 — should be child of span1 ``` **Observed output (before fixes):** ``` SPAN 0 [ DoThingWithCtx ] parent=span1 ✓ correct SPAN 1 [ DoThingWithCustomCtx ] parent=span1 ✓ correct (coincidence — GLS has span1 at this point) SPAN 2 [ DoThingWithCustomCtx ] parent=span2 ✗ wrong (should be span1) SPAN 3 [ DoThingWithCtx ] parent=span2 ✗ wrong (should be span1) SPAN 4 [ theOperation2 ] parent=span1 ✗ wrong (should be root) SPAN 5 [ theOperation ] parent=0 ✓ correct ``` **How each bug manifests:** **SPANs 2 & 3** — After `span2` is created, GLS holds `span2` as the active span. The second calls to both functions receive `customCtx` which carries `span1` in its context chain, but GLS takes priority, so `span2` becomes the parent. This is **Bug 2** (GLS priority). Additionally, **SPAN 1** appearing correct is coincidental: without **Bug 3** fixed, `DoThingWithCustomCtx` can't extract a context from `*CustomContext` and falls back to `context.TODO()`. At that point GLS holds `span1`, which happens to be the right answer. After `span2` is created, the same fallback to GLS explains why SPAN 2 is wrong: `context.TODO()` has no span, GLS returns `span2`. **SPAN 4** — `theOperation2` is created with `context.Background()`. Since Background has no values, the GLS fallback kicks in and returns the active `span1`, making it appear as a child of span1 instead of a root span. This is **Bug 1** (`context.Background()` sentinel). --- #### Fixes ##### Bug 1 — `context.Background()` inherits active GLS span **Symptom**: A span started with `context.Background()` (explicit intent: no parent) would incorrectly inherit the active GLS span as its parent. **Root cause**: `SpanFromContext(context.Background())` would call `WrapContext(context.Background())`, producing a `glsContext` wrapping an empty context. When `.Value(activeSpanKey)` is called on it, the context chain returns `nil` (Background has no values), which triggers the GLS fallback — returning whatever span happens to be active. The span ends up with an unintended parent. **Fix**: Early-return `(nil, false)` in `SpanFromContext` when `ctx == context.Background()`. `context.Background()` is the conventional "no context" sentinel; a caller using it is explicitly opting out of context-based trace propagation. --- ##### Bug 2 — GLS overrides an explicitly-propagated context **Symptom**: When a function received an explicit `context.Context` carrying span X, but GLS held span Y (from an outer scope), span Y was used as the parent instead of X. **Root cause**: `glsContext.Value` consulted GLS **first**, then fell back to the context chain. Any value present in GLS would silently override the explicitly-propagated one. **Fix**: Reverse the lookup order in `glsContext.Value`: check the context chain first; fall back to GLS only if the chain returns `nil`. GLS is designed for implicit propagation through *un-instrumented* call sites that lack a `context.Context` — it must not override contexts that were explicitly propagated. --- ##### Bug 3 — `*CustomContext` not recognized as a context source in `//dd:span` **Symptom**: Functions annotated with `//dd:span` whose first context-like argument was a custom type implementing `context.Context` (e.g. `*fiber.Ctx`) would fall back to `context.TODO()`, breaking trace propagation. **Root cause**: The `//dd:span` injection template only looked for an argument of the **exact** type `context.Context`. If the function's parameter was a custom type that satisfied the interface, it was ignored, and the template fell back to `context.TODO()`. **Fix**: Added an `ArgumentThatImplements "context.Context"` lookup path. When found, the argument is assigned with an explicit interface type declaration (`var ctx context.Context = $impl`) so that the subsequent reassignment from `StartSpanFromContext` (which returns `context.Context`) compiles correctly against the concrete type. ### Reviewer's Checklist - [x] Changed code has unit tests for its functionality at or near 100% coverage. - [x] New code is free of linting errors. You can check this by running `make lint` locally. - [x] New code doesn't break existing tests. You can check this by running `make test` locally. - [x] Add an appropriate team label so this PR gets put in the right place for the release notes. - [x] All generated files are up to date. You can check this by running `make generate` locally. Unsure? Have a question? Request a review! Co-authored-by: kakkoyun <[email protected]> Co-authored-by: dario.castane <[email protected]>
This PR fixes three independent bugs where the wrong span was used as the parent. Relates to DataDog/orchestrion#798. Orchestrion instruments Go code by injecting span creation logic and using goroutine-local storage (GLS) as a side-channel to propagate the active span through call sites that don't have a `context.Context` in their signature. Spans are stored both in the `context.Context` chain (explicit) and in the GLS stack (implicit). `glsContext` wraps a `context.Context` and intercepts `.Value()` calls to consult both sources. The repro project used to replicate the issue described in the JIRA ticket implements a `CustomContext` type that embeds `context.Context` and adds a `UserID` field, mimicking how frameworks like Fiber wrap request contexts: ```go type CustomContext struct { context.Context UserID string } //dd:span func DoThingWithCtx(ctx context.Context) error { return nil } //dd:span func DoThingWithCustomCtx(ctx *CustomContext) error { return nil } ``` `theOperation` (excluded from instrumentation with `//orchestrion:ignore`) does the following: ```go // Creates span1 explicitly, wraps it in a CustomContext span1, nativeCtx := tracer.StartSpanFromContext(context.Background(), "theOperation") customCtx := &CustomContext{nativeCtx, "user-id"} DoThingWithCtx(customCtx) // SPAN 0 — should be child of span1 DoThingWithCustomCtx(customCtx) // SPAN 1 — should be child of span1 // Creates span2 with context.Background() — should be a root span span2, _ := tracer.StartSpanFromContext(context.Background(), "theOperation2") // SPAN 4 // customCtx still carries span1 — both calls should still be children of span1 DoThingWithCustomCtx(customCtx) // SPAN 2 — should be child of span1 DoThingWithCtx(customCtx) // SPAN 3 — should be child of span1 ``` **Observed output (before fixes):** ``` SPAN 0 [ DoThingWithCtx ] parent=span1 ✓ correct SPAN 1 [ DoThingWithCustomCtx ] parent=span1 ✓ correct (coincidence — GLS has span1 at this point) SPAN 2 [ DoThingWithCustomCtx ] parent=span2 ✗ wrong (should be span1) SPAN 3 [ DoThingWithCtx ] parent=span2 ✗ wrong (should be span1) SPAN 4 [ theOperation2 ] parent=span1 ✗ wrong (should be root) SPAN 5 [ theOperation ] parent=0 ✓ correct ``` **How each bug manifests:** **SPANs 2 & 3** — After `span2` is created, GLS holds `span2` as the active span. The second calls to both functions receive `customCtx` which carries `span1` in its context chain, but GLS takes priority, so `span2` becomes the parent. This is **Bug 2** (GLS priority). Additionally, **SPAN 1** appearing correct is coincidental: without **Bug 3** fixed, `DoThingWithCustomCtx` can't extract a context from `*CustomContext` and falls back to `context.TODO()`. At that point GLS holds `span1`, which happens to be the right answer. After `span2` is created, the same fallback to GLS explains why SPAN 2 is wrong: `context.TODO()` has no span, GLS returns `span2`. **SPAN 4** — `theOperation2` is created with `context.Background()`. Since Background has no values, the GLS fallback kicks in and returns the active `span1`, making it appear as a child of span1 instead of a root span. This is **Bug 1** (`context.Background()` sentinel). --- **Symptom**: A span started with `context.Background()` (explicit intent: no parent) would incorrectly inherit the active GLS span as its parent. **Root cause**: `SpanFromContext(context.Background())` would call `WrapContext(context.Background())`, producing a `glsContext` wrapping an empty context. When `.Value(activeSpanKey)` is called on it, the context chain returns `nil` (Background has no values), which triggers the GLS fallback — returning whatever span happens to be active. The span ends up with an unintended parent. **Fix**: Early-return `(nil, false)` in `SpanFromContext` when `ctx == context.Background()`. `context.Background()` is the conventional "no context" sentinel; a caller using it is explicitly opting out of context-based trace propagation. --- **Symptom**: When a function received an explicit `context.Context` carrying span X, but GLS held span Y (from an outer scope), span Y was used as the parent instead of X. **Root cause**: `glsContext.Value` consulted GLS **first**, then fell back to the context chain. Any value present in GLS would silently override the explicitly-propagated one. **Fix**: Reverse the lookup order in `glsContext.Value`: check the context chain first; fall back to GLS only if the chain returns `nil`. GLS is designed for implicit propagation through *un-instrumented* call sites that lack a `context.Context` — it must not override contexts that were explicitly propagated. --- **Symptom**: Functions annotated with `//dd:span` whose first context-like argument was a custom type implementing `context.Context` (e.g. `*fiber.Ctx`) would fall back to `context.TODO()`, breaking trace propagation. **Root cause**: The `//dd:span` injection template only looked for an argument of the **exact** type `context.Context`. If the function's parameter was a custom type that satisfied the interface, it was ignored, and the template fell back to `context.TODO()`. **Fix**: Added an `ArgumentThatImplements "context.Context"` lookup path. When found, the argument is assigned with an explicit interface type declaration (`var ctx context.Context = $impl`) so that the subsequent reassignment from `StartSpanFromContext` (which returns `context.Context`) compiles correctly against the concrete type. - [x] Changed code has unit tests for its functionality at or near 100% coverage. - [x] New code is free of linting errors. You can check this by running `make lint` locally. - [x] New code doesn't break existing tests. You can check this by running `make test` locally. - [x] Add an appropriate team label so this PR gets put in the right place for the release notes. - [x] All generated files are up to date. You can check this by running `make generate` locally. Unsure? Have a question? Request a review! Co-authored-by: kakkoyun <[email protected]> Co-authored-by: dario.castane <[email protected]>
Go's type checker assigns each loaded package a unique object. If the same package is loaded twice through different means, you get two distinct objects for what is logically the same type.
types.Implementsuses pointer equality to compare types across method signatures, so it sees them as different and returnsfalse.ResolveInterfaceTypeByNameloads context.Context one way; the instrumented code is type-checked another way. The twocontextpackages end up as different objects,types.Implementsfails, andArgumentThatImplementsnever matches.Fix by falling back to comparing method names and package paths instead of relying on pointer equality.