Skip to content

Commit 468d566

Browse files
khanayan123devflow.devflow-routing-intake
andauthored
Add stable session ID headers to telemetry requests (#10914)
Add stable session ID headers to telemetry requests Implements the Stable Service Instance Identifier RFC for Java. - Add DD-Session-ID header (= runtime_id) to all telemetry requests - Add DD-Root-Session-ID header when process is a child (inherited from parent) - Read _DD_ROOT_JAVA_SESSION_ID from environment at init time - Auto-propagate _DD_ROOT_JAVA_SESSION_ID to child processes via ProcessBuilder instrumentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Consolidate session ID propagation into existing ProcessImplStartAdvice Remove separate ProcessBuilderSessionId instrumentation files and fold env var injection into the existing ProcessImplStartAdvice. Adds _DD_ROOT_JAVA_SESSION_ID to the child process environment map directly in the ProcessImpl.start() hook. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Add DD-Root-Session-ID assertion to telemetry header tests Verify DD-Session-ID equals runtime ID and DD-Root-Session-ID is absent when rootSessionId == runtimeId (non-child process), or present with the correct value when they differ. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Fix config-inversion lint and advice bytecode issues - Use ConfigHelper.env() instead of EnvironmentVariables.get() for _DD_ROOT_JAVA_SESSION_ID to satisfy config-inversion-linter. The underscore prefix bypasses supported-config validation (internal var). - Remove readOnly=false from @Advice.Argument(1) — we mutate the map via put(), not reassign the reference. readOnly=false generates unnecessary bytecode that breaks bootstrap class instrumentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Address PR feedback: include rootSessionId in logs, respect runtimeIdEnabled, fix test - Add rootSessionId to Config.toString() for tracer log visibility - Gate getRootSessionId() behind runtimeIdEnabled like getRuntimeId() - Force environment map initialization in test to fix test_inst failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Remove unnecessary comment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent aebdcee commit 468d566

File tree

5 files changed

+62
-2
lines changed

5 files changed

+62
-2
lines changed

dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/main/java/datadog/trace/instrumentation/java/lang/ProcessImplStartAdvice.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
package datadog.trace.instrumentation.java.lang;
22

3+
import datadog.trace.api.Config;
34
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
45
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
56
import datadog.trace.bootstrap.instrumentation.api.java.lang.ProcessImplInstrumentationHelpers;
7+
import java.util.Map;
68
import net.bytebuddy.asm.Advice;
79

810
class ProcessImplStartAdvice {
911
@Advice.OnMethodEnter(suppress = Throwable.class)
10-
public static AgentSpan beforeStart(@Advice.Argument(0) final String[] command) {
12+
public static AgentSpan beforeStart(
13+
@Advice.Argument(0) final String[] command,
14+
@Advice.Argument(1) final Map<String, String> environment) {
15+
String rootSessionId = Config.get().getRootSessionId();
16+
if (rootSessionId != null && environment != null) {
17+
environment.put("_DD_ROOT_JAVA_SESSION_ID", rootSessionId);
18+
}
19+
1120
if (!ProcessImplInstrumentationHelpers.ONLINE) {
1221
return null;
1322
}

dd-java-agent/instrumentation/java/java-lang/java-lang-1.8/src/test/groovy/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentationSpecification.groovy

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package datadog.trace.instrumentation.java.lang
33
import datadog.trace.agent.test.InstrumentationSpecification
44
import datadog.trace.agent.test.asserts.SpanAssert
55
import datadog.trace.agent.test.utils.TraceUtils
6+
import datadog.trace.api.Config
67
import datadog.trace.bootstrap.ActiveSubsystems
78

89
import java.util.concurrent.TimeUnit
@@ -331,4 +332,18 @@ class ProcessImplInstrumentationSpecification extends InstrumentationSpecificati
331332
['/does/not/exist/cmd', '--pass', 'abc', '--token=def'] | '["/does/not/exist/cmd","--pass","?","--token=?"]'
332333
['/does/not/exist/md5', '-s', 'pony'] | '["/does/not/exist/md5","?","?"]'
333334
}
335+
336+
void 'child process inherits root session ID'() {
337+
when:
338+
def command = ['/bin/sh', '-c', 'echo $_DD_ROOT_JAVA_SESSION_ID']
339+
def builder = new ProcessBuilder(command)
340+
builder.environment()
341+
Process p = builder.start()
342+
def output = p.inputStream.text.trim()
343+
def terminated = p.waitFor(5, TimeUnit.SECONDS)
344+
345+
then:
346+
terminated
347+
output == Config.get().getRootSessionId()
348+
}
334349
}

internal-api/src/main/java/datadog/trace/api/Config.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,12 @@ public class Config {
792792
*/
793793
static class RuntimeIdHolder {
794794
static final String runtimeId = RandomUtils.randomUUID().toString();
795+
static final String rootSessionId = initRootSessionId();
796+
797+
private static String initRootSessionId() {
798+
String inherited = ConfigHelper.env("_DD_ROOT_JAVA_SESSION_ID");
799+
return inherited != null ? inherited : runtimeId;
800+
}
795801
}
796802

797803
static class HostNameHolder {
@@ -3123,6 +3129,10 @@ public String getRuntimeId() {
31233129
return runtimeIdEnabled ? RuntimeIdHolder.runtimeId : "";
31243130
}
31253131

3132+
public String getRootSessionId() {
3133+
return runtimeIdEnabled ? RuntimeIdHolder.rootSessionId : "";
3134+
}
3135+
31263136
public Long getProcessId() {
31273137
return PidHelper.getPidAsLong();
31283138
}
@@ -5863,6 +5873,9 @@ public String toString() {
58635873
+ ", runtimeId='"
58645874
+ getRuntimeId()
58655875
+ '\''
5876+
+ ", rootSessionId='"
5877+
+ getRootSessionId()
5878+
+ '\''
58665879
+ ", runtimeVersion='"
58675880
+ runtimeVersion
58685881
+ ", apiKey="

telemetry/src/main/java/datadog/telemetry/TelemetryRequest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ public Request.Builder httpRequest() {
8080
builder.addHeader("DD-Telemetry-Debug-Enabled", "true");
8181
}
8282

83+
Config config = Config.get();
84+
String sessionId = config.getRuntimeId();
85+
if (sessionId != null && !sessionId.isEmpty()) {
86+
builder.addHeader("DD-Session-ID", sessionId);
87+
}
88+
String rootSessionId = config.getRootSessionId();
89+
if (rootSessionId != null && !rootSessionId.equals(sessionId)) {
90+
builder.addHeader("DD-Root-Session-ID", rootSessionId);
91+
}
92+
8393
return builder;
8494
}
8595

telemetry/src/test/groovy/datadog/telemetry/TestTelemetryRouter.groovy

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import datadog.telemetry.api.DistributionSeries
77
import datadog.telemetry.api.LogMessage
88
import datadog.telemetry.api.Metric
99
import datadog.telemetry.api.RequestType
10+
import datadog.trace.api.Config
1011
import datadog.trace.api.ConfigSetting
1112
import datadog.trace.api.telemetry.Endpoint
1213
import datadog.trace.api.telemetry.ProductChange
@@ -84,7 +85,8 @@ class TestTelemetryRouter extends TelemetryRouter {
8485
'DD-Client-Library-Language',
8586
'DD-Client-Library-Version',
8687
'DD-Telemetry-API-Version',
87-
'DD-Telemetry-Request-Type'
88+
'DD-Telemetry-Request-Type',
89+
'DD-Session-ID'
8890
])
8991
assert this.request.header('Content-Type') == 'application/json; charset=utf-8'
9092
assert this.request.header('Content-Length').toInteger() > 0
@@ -94,6 +96,17 @@ class TestTelemetryRouter extends TelemetryRouter {
9496
assert this.request.header('DD-Telemetry-Request-Type') == requestType.toString()
9597
def entityId = this.request.header('Datadog-Entity-ID')
9698
assert entityId == null || entityId.startsWith("in-") || entityId.startsWith("cin-")
99+
def sessionId = this.request.header('DD-Session-ID')
100+
assert sessionId =~ /[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}/
101+
assert sessionId == Config.get().getRuntimeId()
102+
// DD-Root-Session-ID should only be present when inherited from a parent process
103+
// (i.e., when rootSessionId != runtimeId). In normal test context, they're equal.
104+
def rootSessionId = this.request.header('DD-Root-Session-ID')
105+
if (Config.get().getRootSessionId() == Config.get().getRuntimeId()) {
106+
assert rootSessionId == null
107+
} else {
108+
assert rootSessionId == Config.get().getRootSessionId()
109+
}
97110
return this
98111
}
99112

0 commit comments

Comments
 (0)