feat(telemetry): add stable session identifier headers#4574
feat(telemetry): add stable session identifier headers#4574gh-worker-dd-mergequeue-cf854d[bot] merged 15 commits intomainfrom
Conversation
Implements the Stable Service Instance Identifier RFC for Go telemetry. - DD-Session-ID: always present, set to current runtime_id - DD-Root-Session-ID: present only in child processes, inherited via DD_ROOT_GO_SESSION_ID env var (omitted when equal to session_id) - globalconfig: reads DD_ROOT_GO_SESSION_ID and DD_PARENT_GO_SESSION_ID at init, falls back to runtime_id for root processes Co-Authored-By: Claude Opus 4.6 <[email protected]>
Only root and session IDs are required. Parent session ID is optional and not needed for the initial implementation. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
✅ Tests 🎉 All green!❄️ No new flaky tests detected 🎯 Code Coverage (details) 🔗 Commit SHA: 33a27d8 | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback! |
Re-execs the test binary as a subprocess with DD_ROOT_GO_SESSION_ID set to verify the child process correctly inherits the root session ID and generates its own distinct runtime_id. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Provides InjectSessionEnv(cmd) to inject DD_ROOT_GO_SESSION_ID into child process environments. Required by the Stable Session Identifier RFC — Go uses os/exec exclusively (raw fork is unsafe). Users call InjectSessionEnv before cmd.Start()/cmd.Run(). Automatic injection via Orchestrion aspect is a planned follow-up. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Codecov Report❌ Patch coverage is
Additional details and impacted files
🚀 New features to boost your workflow:
|
Fix import ordering (goimports) and use strings.SplitSeq (modernize linter). Co-Authored-By: Claude Opus 4.6 <[email protected]>
BenchmarksBenchmark execution time: 2026-03-20 18:41:34 Comparing candidate commit 33a27d8 in PR branch Found 0 performance improvements and 1 performance regressions! Performance is the same for 214 metrics, 9 unstable metrics.
|
Remove InjectSessionEnv (contrib/os/exec.go) — the SDK now sets DD_ROOT_GO_SESSION_ID in the process environment at init time so child processes spawned via os/exec inherit it automatically without any user-side calls. Also remove from supported_configurations since it is an internal env var, not user-facing config. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
This is an internal propagation env var, not user-facing config, so it uses os.Getenv/Setenv directly instead of internal/env.Get. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Prefix with underscore to signify internal env var. Remove redundant comments from tests — the test names are self-documenting. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Verify DD-Session-ID equals RuntimeID (not just non-empty), and assert DD-Root-Session-ID is absent when rootSessionID == runtimeID or present with the correct value when inherited from a parent process. Co-Authored-By: Claude Opus 4.6 <[email protected]>
mtoffl01
left a comment
There was a problem hiding this comment.
Generally speaking, globalconfig is a legacy package that will be replaced by the new ddtrace/internal/config package. But, you cannot import that package into the telemetry package without hitting a circular import right now. So for now, adding a new field to globalconfig is fine.
| rootID = cfg.runtimeID | ||
| } | ||
| cfg.rootSessionID = rootID | ||
| os.Setenv("_DD_ROOT_GO_SESSION_ID", rootID) //nolint:forbidigo |
There was a problem hiding this comment.
I know this is a pattern in dd-trace-py, and maybe dd-trace-js, but there is no precedence for it in dd-trace-go.
Why do you need to overwrite the env var directly? Couldn't anywhere that wants to query "_DD_ROOT_GO_SESSION_ID" just query globalconfig.RootSessionId()?
There was a problem hiding this comment.
The os.Setenv is needed for child process propagation. globalconfig.RootSessionID() only works within the current process — child processes spawned via os/exec are separate binaries that can't call Go functions from the parent.
By setting _DD_ROOT_GO_SESSION_ID in the process environment at init time, any child spawned with default env inheritance (cmd.Env == nil) automatically receives it. When the child starts dd-trace-go, newConfig() reads it from the environment and knows it's part of the same session tree.
This is the core propagation mechanism described in the RFC — it's how Node.js, Python, and Ruby SDKs handle it too (each via their language's equivalent of env var injection into child processes). Without this, every user spawning child processes would need to manually set the env var, which defeats the purpose.
Happy to discuss if there's a Go-idiomatic alternative that achieves the same automatic propagation.
There was a problem hiding this comment.
Okay. Let's just put it behind a constant (rootSessionIdEnvVar or similar)
There was a problem hiding this comment.
Done — extracted it into a rootSessionIDEnvVar constant at the top of the file. Both the production code and tests now reference the constant instead of the raw string.
| headersAsTags: internal.NewLockMap(map[string]string{}), | ||
| } | ||
|
|
||
| func init() { |
There was a problem hiding this comment.
init() is very unpopular for go. Can you move this over to some helper function, and call it to set the rootSessionID field on the cfg variable?
e.g, line 21+ becomes:
var cfg = &config{
analyticsRate: math.NaN(),
runtimeID: uuid.New().String(),
headersAsTags: internal.NewLockMap(map[string]string{}),
rootSessionID: getRootSessionID()
}
There was a problem hiding this comment.
Done — replaced init() with a newConfig() helper that's called inline in the var cfg declaration.
Address review feedback: move rootSessionID initialization from init() into a newConfig() helper called in the var declaration. Keeps os.Setenv for child process propagation (see PR comment for rationale). Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
| func newConfig() *config { | ||
| runtimeID := uuid.New().String() | ||
| rootSessionID := env.Get(rootSessionIDEnvVar) | ||
| if rootSessionID == "" { | ||
| rootSessionID = runtimeID | ||
| } | ||
| // Set in the process environment so child processes spawned via os/exec | ||
| // with default env inheritance (cmd.Env == nil) automatically receive it. | ||
| os.Setenv(rootSessionIDEnvVar, rootSessionID) //nolint:forbidigo | ||
| return &config{ | ||
| analyticsRate: math.NaN(), | ||
| runtimeID: runtimeID, | ||
| rootSessionID: rootSessionID, | ||
| headersAsTags: internal.NewLockMap(map[string]string{}), | ||
| } |
There was a problem hiding this comment.
Not exactly 😆
newConfig() is fine but was not required by my previous comment. I meant to abstract away the rootSessionID logic into its own helper, like getRootSessionID.
There was a problem hiding this comment.
Good call — extracted the session ID logic into a getRootSessionID(runtimeID) helper. It takes runtimeID as a param since inlining it directly into the struct literal would cause an init cycle (cfg → getRootSessionID → cfg). So newConfig() stays as the thin wrapper that generates the runtimeID and passes it through.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
| } | ||
| // Set in the process environment so child processes spawned via os/exec | ||
| // with default env inheritance (cmd.Env == nil) automatically receive it. | ||
| os.Setenv(rootSessionIDEnvVar, id) //nolint:forbidigo |
There was a problem hiding this comment.
I don't see a rule forbidding os.Setenv, only os.Getenv. Is this comment necessary?
There was a problem hiding this comment.
You're right — only os.Getenv and os.LookupEnv are in the forbidigo rules, not os.Setenv. Removed the nolint directive.
Only os.Getenv and os.LookupEnv are forbidden by the linter. Co-Authored-By: Claude Opus 4.6 <[email protected]>
mtoffl01
left a comment
There was a problem hiding this comment.
Unblocking you with an approval, but overall I think the comments are noisy and some are unnecessary; I'd suggest cutting them down.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
/merge |
|
View all feedbacks in Devflow UI.
This pull request is not mergeable according to GitHub. Common reasons include pending required checks, missing approvals, or merge conflicts — but it could also be blocked by other repository rules or settings.
The expected merge time in
|
Summary
Implements the Stable Service Instance Identifier RFC for Go instrumentation telemetry.
DD-Session-ID: always present on every telemetry request, set to the currentruntime_idDD-Root-Session-ID: present only in child processes, inherited via_DD_ROOT_GO_SESSION_IDenv var. Omitted when equal to session ID — backend infers root = self when absentglobalconfig.init()sets_DD_ROOT_GO_SESSION_IDinos.Environ()so child processes spawned viaos/execinherit it automatically without any user-side callsChanges
internal/globalconfig/globalconfig.go: addsrootSessionIDfield,init()reads/sets_DD_ROOT_GO_SESSION_ID(internal env var, not in supported_configurations),RootSessionID()getterinternal/telemetry/internal/writer.go: addsDD-Session-ID(always) andDD-Root-Session-ID(child processes only) to pre-baked telemetry headersRelated