From bcd239592df4b427b0a43e6c3cdcde7fba41f0aa Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:55:12 -0400 Subject: [PATCH 1/9] [SVLS-8245] Add tests for named pipe name rules --- .../java/com/datadog/NamedPipeNamesTest.java | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/test/java/com/datadog/NamedPipeNamesTest.java diff --git a/src/test/java/com/datadog/NamedPipeNamesTest.java b/src/test/java/com/datadog/NamedPipeNamesTest.java new file mode 100644 index 0000000..5523169 --- /dev/null +++ b/src/test/java/com/datadog/NamedPipeNamesTest.java @@ -0,0 +1,176 @@ +package com.datadog; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class NamedPipeNamesTest { + + private static String repeat(char c, int count) { + StringBuilder builder = new StringBuilder(count); + for (int i = 0; i < count; i++) { + builder.append(c); + } + return builder.toString(); + } + + @Test + void choose_windowsPipeNamesWin() { + Map env = new HashMap<>(); + env.put("DD_APM_WINDOWS_PIPE_NAME", "apm_windows"); + env.put("DD_DOGSTATSD_WINDOWS_PIPE_NAME", "dogstatsd_windows"); + + NamedPipeNames names = NamedPipeNames.choose(env, new Properties()); + + assertEquals("apm_windows", names.tracePipeName); + assertEquals("dogstatsd_windows", names.dogstatsdPipeName); + assertTrue(names.warnings.isEmpty()); + } + + @Test + void choose_windowsPipeNamesWinOverCustomerSetNames() { + Map env = new HashMap<>(); + env.put("DD_APM_WINDOWS_PIPE_NAME", "apm_windows"); + env.put("DD_DOGSTATSD_WINDOWS_PIPE_NAME", "dogstatsd_windows"); + env.put("DD_TRACE_PIPE_NAME", "trace_env"); + Properties systemProperties = new Properties(); + systemProperties.setProperty("dd.dogstatsd.pipe.name", "dogstatsd_property"); + + NamedPipeNames names = NamedPipeNames.choose(env, systemProperties); + + assertEquals("apm_windows", names.tracePipeName); + assertEquals("dogstatsd_windows", names.dogstatsdPipeName); + } + + @Test + void choose_usesCustomerSetEnvironmentVariables() { + Map env = new HashMap<>(); + env.put("DD_TRACE_PIPE_NAME", "trace_env"); + env.put("DD_DOGSTATSD_PIPE_NAME", "dogstatsd_env"); + + NamedPipeNames names = NamedPipeNames.choose(env, new Properties()); + + assertEquals("trace_env", names.tracePipeName); + assertEquals("dogstatsd_env", names.dogstatsdPipeName); + assertTrue(names.warnings.isEmpty()); + } + + @Test + void choose_usesCustomerSetJvmOptions() { + Properties systemProperties = new Properties(); + systemProperties.setProperty("dd.trace.pipe.name", "trace_property"); + systemProperties.setProperty("dd.dogstatsd.pipe.name", "dogstatsd_property"); + + NamedPipeNames names = NamedPipeNames.choose(new HashMap<>(), systemProperties); + + assertEquals("trace_property", names.tracePipeName); + assertEquals("dogstatsd_property", names.dogstatsdPipeName); + assertTrue(names.warnings.isEmpty()); + } + + @Test + void choose_jvmOptionsWinOverEnvironmentVariablesLikeTheTracer() { + Map env = new HashMap<>(); + env.put("DD_TRACE_PIPE_NAME", "trace_env"); + env.put("DD_DOGSTATSD_PIPE_NAME", "dogstatsd_env"); + Properties systemProperties = new Properties(); + systemProperties.setProperty("dd.trace.pipe.name", "trace_property"); + systemProperties.setProperty("dd.dogstatsd.pipe.name", "dogstatsd_property"); + + NamedPipeNames names = NamedPipeNames.choose(env, systemProperties); + + assertEquals("trace_property", names.tracePipeName); + assertEquals("dogstatsd_property", names.dogstatsdPipeName); + } + + @Test + void choose_generatesNamesWhenNoneAreSet() { + NamedPipeNames names = NamedPipeNames.choose(new HashMap<>(), new Properties()); + + assertTrue(names.tracePipeName.matches("dd_trace_[0-9a-f]{32}"), names.tracePipeName); + assertTrue(names.dogstatsdPipeName.matches("dd_dogstatsd_[0-9a-f]{32}"), names.dogstatsdPipeName); + assertTrue(names.warnings.isEmpty()); + } + + @Test + void choose_generatesDifferentNamesEachTime() { + NamedPipeNames first = NamedPipeNames.choose(new HashMap<>(), new Properties()); + NamedPipeNames second = NamedPipeNames.choose(new HashMap<>(), new Properties()); + + assertNotEquals(first.tracePipeName, second.tracePipeName); + assertNotEquals(first.dogstatsdPipeName, second.dogstatsdPipeName); + } + + @Test + void choose_treatsEmptyValuesAsUnset() { + Map env = new HashMap<>(); + env.put("DD_APM_WINDOWS_PIPE_NAME", ""); + env.put("DD_TRACE_PIPE_NAME", "trace_env"); + env.put("DD_DOGSTATSD_PIPE_NAME", ""); + + NamedPipeNames names = NamedPipeNames.choose(env, new Properties()); + + assertEquals("trace_env", names.tracePipeName); + assertTrue(names.dogstatsdPipeName.matches("dd_dogstatsd_[0-9a-f]{32}"), names.dogstatsdPipeName); + } + + @Test + void choose_warnsWhenCustomerSetNamesDifferFromWindowsPipeNames() { + Map env = new HashMap<>(); + env.put("DD_APM_WINDOWS_PIPE_NAME", "apm_windows"); + env.put("DD_DOGSTATSD_WINDOWS_PIPE_NAME", "dogstatsd_windows"); + env.put("DD_TRACE_PIPE_NAME", "trace_env"); + Properties systemProperties = new Properties(); + systemProperties.setProperty("dd.dogstatsd.pipe.name", "dogstatsd_property"); + + NamedPipeNames names = NamedPipeNames.choose(env, systemProperties); + + assertEquals(2, names.warnings.size(), names.warnings.toString()); + assertTrue(names.warnings.get(0).contains("DD_TRACE_PIPE_NAME"), names.warnings.get(0)); + assertTrue(names.warnings.get(0).contains("DD_APM_WINDOWS_PIPE_NAME"), names.warnings.get(0)); + assertTrue(names.warnings.get(1).contains("dd.dogstatsd.pipe.name"), names.warnings.get(1)); + assertTrue(names.warnings.get(1).contains("DD_DOGSTATSD_WINDOWS_PIPE_NAME"), names.warnings.get(1)); + } + + @Test + void choose_doesNotWarnWhenCustomerSetNamesMatchWindowsPipeNames() { + Map env = new HashMap<>(); + env.put("DD_APM_WINDOWS_PIPE_NAME", "apm_windows"); + env.put("DD_DOGSTATSD_WINDOWS_PIPE_NAME", "dogstatsd_windows"); + env.put("DD_TRACE_PIPE_NAME", "apm_windows"); + env.put("DD_DOGSTATSD_PIPE_NAME", "dogstatsd_windows"); + + NamedPipeNames names = NamedPipeNames.choose(env, new Properties()); + + assertTrue(names.warnings.isEmpty(), names.warnings.toString()); + } + + @Test + void choose_warnsWhenNamesAreLongerThan256Characters() { + Map env = new HashMap<>(); + env.put("DD_APM_WINDOWS_PIPE_NAME", repeat('a', 257)); + Properties systemProperties = new Properties(); + systemProperties.setProperty("dd.dogstatsd.pipe.name", repeat('b', 300)); + + NamedPipeNames names = NamedPipeNames.choose(env, systemProperties); + + assertEquals(2, names.warnings.size(), names.warnings.toString()); + assertTrue(names.warnings.get(0).contains("DD_APM_WINDOWS_PIPE_NAME"), names.warnings.get(0)); + assertTrue(names.warnings.get(0).contains("257"), names.warnings.get(0)); + assertTrue(names.warnings.get(1).contains("dd.dogstatsd.pipe.name"), names.warnings.get(1)); + assertTrue(names.warnings.get(1).contains("300"), names.warnings.get(1)); + } + + @Test + void choose_doesNotWarnFor256CharacterNames() { + Map env = new HashMap<>(); + env.put("DD_TRACE_PIPE_NAME", repeat('a', 256)); + + NamedPipeNames names = NamedPipeNames.choose(env, new Properties()); + + assertTrue(names.warnings.isEmpty(), names.warnings.toString()); + } +} From bfee86f4b2cca6f0a3a30c072f9d5e07a8919010 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:55:42 -0400 Subject: [PATCH 2/9] [SVLS-8245] Add helper that chooses per-function pipe names --- src/main/java/com/datadog/NamedPipeNames.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/main/java/com/datadog/NamedPipeNames.java diff --git a/src/main/java/com/datadog/NamedPipeNames.java b/src/main/java/com/datadog/NamedPipeNames.java new file mode 100644 index 0000000..1237f79 --- /dev/null +++ b/src/main/java/com/datadog/NamedPipeNames.java @@ -0,0 +1,87 @@ +package com.datadog; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; + +/** + * Chooses the trace and DogStatsD pipe names for one function, following the .NET + * compatibility layer: the binary's *_WINDOWS_PIPE_NAME setting, then a name the customer + * already set for the tracer, then a generated per-function name. + */ +final class NamedPipeNames { + static final int MAX_PIPE_NAME_LENGTH = 256; + + static final String TRACE_WINDOWS_PIPE_NAME_ENV = "DD_APM_WINDOWS_PIPE_NAME"; + static final String TRACE_PIPE_NAME_ENV = "DD_TRACE_PIPE_NAME"; + static final String TRACE_PIPE_NAME_PROPERTY = "dd.trace.pipe.name"; + + static final String DOGSTATSD_WINDOWS_PIPE_NAME_ENV = "DD_DOGSTATSD_WINDOWS_PIPE_NAME"; + static final String DOGSTATSD_PIPE_NAME_ENV = "DD_DOGSTATSD_PIPE_NAME"; + static final String DOGSTATSD_PIPE_NAME_PROPERTY = "dd.dogstatsd.pipe.name"; + + final String tracePipeName; + final String dogstatsdPipeName; + final List warnings; + + private NamedPipeNames(String tracePipeName, String dogstatsdPipeName, List warnings) { + this.tracePipeName = tracePipeName; + this.dogstatsdPipeName = dogstatsdPipeName; + this.warnings = Collections.unmodifiableList(warnings); + } + + static NamedPipeNames choose(Map env, Properties systemProperties) { + List warnings = new ArrayList<>(); + String tracePipeName = choosePipeName(env, systemProperties, TRACE_WINDOWS_PIPE_NAME_ENV, + TRACE_PIPE_NAME_PROPERTY, TRACE_PIPE_NAME_ENV, "dd_trace_", warnings); + String dogstatsdPipeName = choosePipeName(env, systemProperties, DOGSTATSD_WINDOWS_PIPE_NAME_ENV, + DOGSTATSD_PIPE_NAME_PROPERTY, DOGSTATSD_PIPE_NAME_ENV, "dd_dogstatsd_", warnings); + return new NamedPipeNames(tracePipeName, dogstatsdPipeName, warnings); + } + + private static String choosePipeName(Map env, Properties systemProperties, + String windowsPipeNameEnv, String pipeNameProperty, String pipeNameEnv, + String generatedPrefix, List warnings) { + // The tracer reads system properties before environment variables, so a JVM option + // the customer passed is the customer-set name when both are present. + String customerSetName = nonEmpty(systemProperties.getProperty(pipeNameProperty)); + String customerSetSource = pipeNameProperty; + if (customerSetName == null) { + customerSetName = nonEmpty(env.get(pipeNameEnv)); + customerSetSource = pipeNameEnv; + } + + String windowsPipeName = nonEmpty(env.get(windowsPipeNameEnv)); + if (windowsPipeName != null) { + if (customerSetName != null && !customerSetName.equals(windowsPipeName)) { + warnings.add(String.format("%s ('%s') differs from %s ('%s'). Using %s.", + customerSetSource, customerSetName, windowsPipeNameEnv, windowsPipeName, + windowsPipeNameEnv)); + } + warnIfTooLong(windowsPipeNameEnv, windowsPipeName, warnings); + return windowsPipeName; + } + + if (customerSetName != null) { + warnIfTooLong(customerSetSource, customerSetName, warnings); + return customerSetName; + } + + return generatedPrefix + UUID.randomUUID().toString().replace("-", ""); + } + + private static void warnIfTooLong(String source, String pipeName, List warnings) { + if (pipeName.length() > MAX_PIPE_NAME_LENGTH) { + warnings.add(String.format( + "%s is %d characters long. Windows pipe names can be at most %d characters.", + source, pipeName.length(), MAX_PIPE_NAME_LENGTH)); + } + } + + private static String nonEmpty(String value) { + return value == null || value.isEmpty() ? null : value; + } +} From c69b429ad1eb0f9b1510dd6907bbd8a5d101da46 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:56:14 -0400 Subject: [PATCH 3/9] [SVLS-8245] Add tests for the dd-java-agent version gate --- .../com/datadog/TracerVersionGateTest.java | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/test/java/com/datadog/TracerVersionGateTest.java diff --git a/src/test/java/com/datadog/TracerVersionGateTest.java b/src/test/java/com/datadog/TracerVersionGateTest.java new file mode 100644 index 0000000..9af6fff --- /dev/null +++ b/src/test/java/com/datadog/TracerVersionGateTest.java @@ -0,0 +1,173 @@ +package com.datadog; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.jar.Attributes; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class TracerVersionGateTest { + private static final String MINIMUM = "1.68.0"; + private static final String TRACER_PREMAIN_CLASS = "datadog.trace.bootstrap.AgentPreCheck"; + + @TempDir + Path tempDir; + + private Path writeAgentJar(String fileName, String premainClass, String implementationVersion) + throws IOException { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + if (premainClass != null) { + attributes.putValue("Premain-Class", premainClass); + } + if (implementationVersion != null) { + attributes.put(Attributes.Name.IMPLEMENTATION_VERSION, implementationVersion); + } + Path jar = tempDir.resolve(fileName); + try (OutputStream out = Files.newOutputStream(jar); + JarOutputStream jarOut = new JarOutputStream(out, manifest)) { + // A manifest is all the gate reads. + } + return jar; + } + + private Path writeTracerJar(String implementationVersion) throws IOException { + return writeAgentJar("dd-java-agent.jar", TRACER_PREMAIN_CLASS, implementationVersion); + } + + private static String javaagent(Path jar) { + return "-javaagent:" + jar.toAbsolutePath(); + } + + @ParameterizedTest + @ValueSource(strings = {"1.68.0", "1.68.1", "1.69.0", "1.100.0", "2.0.0"}) + void check_passesForMinimumVersionOrLater(String version) throws IOException { + Path tracer = writeTracerJar(version); + + TracerVersionGate gate = TracerVersionGate.check( + Collections.singletonList(javaagent(tracer)), MINIMUM); + + assertTrue(gate.passed, gate.reason); + } + + @ParameterizedTest + @ValueSource(strings = {"1.67.1", "1.67.0", "1.9.9", "0.99.0", "1.67.0-SNAPSHOT"}) + void check_failsForOlderVersions(String version) throws IOException { + Path tracer = writeTracerJar(version); + + TracerVersionGate gate = TracerVersionGate.check( + Collections.singletonList(javaagent(tracer)), MINIMUM); + + assertFalse(gate.passed); + assertTrue(gate.reason.contains(version), gate.reason); + assertTrue(gate.reason.contains(MINIMUM), gate.reason); + } + + @ParameterizedTest + @ValueSource(strings = {"1.68.0-SNAPSHOT", "1.68.0-105-gefa6bd64", "1.68.0-SNAPSHOT-DIRTY"}) + void check_passesForDevBuildsOfMinimumVersion(String version) throws IOException { + Path tracer = writeTracerJar(version); + + TracerVersionGate gate = TracerVersionGate.check( + Collections.singletonList(javaagent(tracer)), MINIMUM); + + assertTrue(gate.passed, gate.reason); + } + + @Test + void check_usesDefaultMinimumVersion() throws IOException { + Path tracer = writeTracerJar(TracerVersionGate.MINIMUM_TRACER_VERSION); + + TracerVersionGate gate = TracerVersionGate.check(Collections.singletonList(javaagent(tracer))); + + assertTrue(gate.passed, gate.reason); + } + + @Test + void check_findsTracerByPremainClassNotFileName() throws IOException { + Path compat = writeAgentJar("dd-java-agent-lookalike.jar", "com.datadog.ServerlessCompatAgent", "9.9.9"); + Path tracer = writeAgentJar("renamed-tracer.jar", TRACER_PREMAIN_CLASS, "1.68.0"); + + TracerVersionGate gate = TracerVersionGate.check( + Arrays.asList("-Xmx512m", javaagent(compat), javaagent(tracer)), MINIMUM); + + assertTrue(gate.passed, gate.reason); + } + + @Test + void check_ignoresAgentOptions() throws IOException { + Path tracer = writeTracerJar("1.68.0"); + + TracerVersionGate gate = TracerVersionGate.check( + Collections.singletonList(javaagent(tracer) + "=some=option"), MINIMUM); + + assertTrue(gate.passed, gate.reason); + } + + @Test + void check_failsWhenNoJavaagentIsPresent() { + TracerVersionGate gate = TracerVersionGate.check(Arrays.asList("-Xmx512m", "-Dfoo=bar"), MINIMUM); + + assertFalse(gate.passed); + assertTrue(gate.reason.contains("dd-java-agent"), gate.reason); + } + + @Test + void check_failsWhenOnlyOtherAgentsArePresent() throws IOException { + Path compat = writeAgentJar("dd-java-agent.jar", "com.datadog.ServerlessCompatAgent", "1.68.0"); + + TracerVersionGate gate = TracerVersionGate.check(Collections.singletonList(javaagent(compat)), MINIMUM); + + assertFalse(gate.passed); + } + + @Test + void check_failsWhenTracerJarDoesNotExist() { + Path missing = tempDir.resolve("dd-java-agent.jar"); + + TracerVersionGate gate = TracerVersionGate.check(Collections.singletonList(javaagent(missing)), MINIMUM); + + assertFalse(gate.passed); + } + + @Test + void check_failsWhenJavaagentIsNotAJar() throws IOException { + Path notAJar = tempDir.resolve("dd-java-agent.jar"); + Files.write(notAJar, "not a jar".getBytes("UTF-8")); + + TracerVersionGate gate = TracerVersionGate.check(Collections.singletonList(javaagent(notAJar)), MINIMUM); + + assertFalse(gate.passed); + } + + @Test + void check_failsWhenTracerVersionIsMissing() throws IOException { + Path tracer = writeTracerJar(null); + + TracerVersionGate gate = TracerVersionGate.check(Collections.singletonList(javaagent(tracer)), MINIMUM); + + assertFalse(gate.passed); + assertTrue(gate.reason.contains("Implementation-Version"), gate.reason); + } + + @ParameterizedTest + @ValueSource(strings = {"unknown", "1.68", "v1.68.0", ""}) + void check_failsWhenTracerVersionIsUnreadable(String version) throws IOException { + Path tracer = writeTracerJar(version); + + TracerVersionGate gate = TracerVersionGate.check(Collections.singletonList(javaagent(tracer)), MINIMUM); + + assertFalse(gate.passed); + } +} From 8134982e3afd09219c43033b443af983484ccc74 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:57:23 -0400 Subject: [PATCH 4/9] [SVLS-8245] Add dd-java-agent version gate for named pipes --- .../java/com/datadog/TracerVersionGate.java | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/main/java/com/datadog/TracerVersionGate.java diff --git a/src/main/java/com/datadog/TracerVersionGate.java b/src/main/java/com/datadog/TracerVersionGate.java new file mode 100644 index 0000000..56f96d0 --- /dev/null +++ b/src/main/java/com/datadog/TracerVersionGate.java @@ -0,0 +1,131 @@ +package com.datadog; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Decides whether the dd-java-agent in this JVM supports named pipes. Older tracers hang + * uploading traces over a pipe and never send their own metrics over one, so pipe names are + * only handed out when the tracer is at least {@link #MINIMUM_TRACER_VERSION}. + */ +final class TracerVersionGate { + static final String TRACER_PREMAIN_CLASS = "datadog.trace.bootstrap.AgentPreCheck"; + + // Placeholder: the next unreleased dd-trace-java version on master. Replace it with the + // first dd-trace-java release that includes the named-pipe transport and StatsD fixes. + static final String MINIMUM_TRACER_VERSION = "1.68.0"; + + private static final String JAVAAGENT_PREFIX = "-javaagent:"; + private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)(\\D.*)?$"); + + final boolean passed; + final String reason; + + private TracerVersionGate(boolean passed, String reason) { + this.passed = passed; + this.reason = reason; + } + + static TracerVersionGate check() { + List jvmArguments; + try { + jvmArguments = ManagementFactory.getRuntimeMXBean().getInputArguments(); + } catch (Exception e) { + return new TracerVersionGate(false, "unable to read JVM arguments to find dd-java-agent: " + e); + } + return check(jvmArguments); + } + + static TracerVersionGate check(List jvmArguments) { + return check(jvmArguments, MINIMUM_TRACER_VERSION); + } + + static TracerVersionGate check(List jvmArguments, String minimumVersion) { + int[] minimum = parseVersion(minimumVersion); + if (minimum == null) { + return new TracerVersionGate(false, "invalid minimum dd-java-agent version " + minimumVersion); + } + + for (String argument : jvmArguments) { + if (!argument.startsWith(JAVAAGENT_PREFIX)) { + continue; + } + String jarPath = javaagentJarPath(argument); + Manifest manifest = readManifest(jarPath); + if (manifest == null) { + continue; + } + Attributes attributes = manifest.getMainAttributes(); + if (!TRACER_PREMAIN_CLASS.equals(attributes.getValue("Premain-Class"))) { + continue; + } + + String version = attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION); + if (version == null || version.isEmpty()) { + return new TracerVersionGate(false, + "dd-java-agent at " + jarPath + " has no Implementation-Version in its manifest"); + } + int[] tracer = parseVersion(version); + if (tracer == null) { + return new TracerVersionGate(false, + "unable to read dd-java-agent version '" + version + "' at " + jarPath); + } + if (compare(tracer, minimum) < 0) { + return new TracerVersionGate(false, "dd-java-agent " + version + " at " + jarPath + + " is older than " + minimumVersion + ", the first version that supports named pipes"); + } + return new TracerVersionGate(true, "dd-java-agent " + version + " at " + jarPath + + " supports named pipes (minimum " + minimumVersion + ")"); + } + + return new TracerVersionGate(false, + "no readable dd-java-agent found among -javaagent options (Premain-Class " + TRACER_PREMAIN_CLASS + ")"); + } + + // The JVM treats everything after the first '=' as agent options. + private static String javaagentJarPath(String argument) { + String value = argument.substring(JAVAAGENT_PREFIX.length()); + int optionsStart = value.indexOf('='); + return optionsStart >= 0 ? value.substring(0, optionsStart) : value; + } + + private static Manifest readManifest(String jarPath) { + try (JarFile jarFile = new JarFile(jarPath, false)) { + return jarFile.getManifest(); + } catch (IOException | RuntimeException e) { + return null; + } + } + + // Reads major.minor.patch and ignores any suffix, so 1.68.0-SNAPSHOT counts as 1.68.0. + private static int[] parseVersion(String version) { + Matcher matcher = VERSION_PATTERN.matcher(version); + if (!matcher.matches()) { + return null; + } + try { + return new int[] { + Integer.parseInt(matcher.group(1)), + Integer.parseInt(matcher.group(2)), + Integer.parseInt(matcher.group(3)) + }; + } catch (NumberFormatException e) { + return null; + } + } + + private static int compare(int[] left, int[] right) { + for (int i = 0; i < left.length; i++) { + if (left[i] != right[i]) { + return Integer.compare(left[i], right[i]); + } + } + return 0; + } +} From 726abc26ac37453f9fb03b17338a172944c56526 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:57:46 -0400 Subject: [PATCH 5/9] [SVLS-8245] Set tracer pipe system properties on Windows Azure Functions --- .../com/datadog/ServerlessCompatAgent.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/com/datadog/ServerlessCompatAgent.java b/src/main/java/com/datadog/ServerlessCompatAgent.java index 97abbae..d36d7af 100644 --- a/src/main/java/com/datadog/ServerlessCompatAgent.java +++ b/src/main/java/com/datadog/ServerlessCompatAgent.java @@ -106,6 +106,34 @@ public static boolean isAzureFlexWithoutDDAzureResourceGroup() { && System.getenv("DD_AZURE_RESOURCE_GROUP") == null; } + /** + * Gives this function its own named pipes when dd-java-agent supports them. The tracer reads + * these system properties before its environment variables, so they must be set before + * dd-java-agent's premain runs. When the gate fails, nothing is set and the tracer keeps + * whatever it inherited: TCP and UDP by default, or the customer's own pipe settings. + * + * @return the chosen pipe names, or null when named pipes are off + */ + static NamedPipeNames configureNamedPipes() { + TracerVersionGate gate = TracerVersionGate.check(); + if (!gate.passed) { + log.info("Named pipes are off, keeping inherited trace and DogStatsD settings: {}", gate.reason); + return null; + } + log.debug("Named pipes are on: {}", gate.reason); + + NamedPipeNames pipeNames = NamedPipeNames.choose(System.getenv(), System.getProperties()); + for (String warning : pipeNames.warnings) { + log.warn(warning); + } + + System.setProperty(NamedPipeNames.TRACE_PIPE_NAME_PROPERTY, pipeNames.tracePipeName); + System.setProperty(NamedPipeNames.DOGSTATSD_PIPE_NAME_PROPERTY, pipeNames.dogstatsdPipeName); + log.info("Configured named pipes - Trace: {}, DogStatsD: {}", + pipeNames.tracePipeName, pipeNames.dogstatsdPipeName); + return pipeNames; + } + public static void premain(String agentArgs, Instrumentation instrumentation) { CloudEnvironment environment = getEnvironment(); log.debug("Environment detected: {}", environment); @@ -169,6 +197,10 @@ public static void premain(String agentArgs, Instrumentation instrumentation) { String packageVersion = getPackageVersion(); log.debug("Found package version {}", packageVersion); + if (environment == CloudEnvironment.AZURE_FUNCTION && isWindows()) { + configureNamedPipes(); + } + ProcessBuilder processBuilder = new ProcessBuilder(executableFile.getAbsolutePath()); processBuilder.environment().put("DD_SERVERLESS_COMPAT_VERSION", packageVersion); processBuilder.inheritIO(); From e30da1c20b36a6f9d32204a8dc94fcafa7d9c63e Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:58:41 -0400 Subject: [PATCH 6/9] [SVLS-8245] Pass pipe names to the binary's environment --- src/main/java/com/datadog/ServerlessCompatAgent.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/datadog/ServerlessCompatAgent.java b/src/main/java/com/datadog/ServerlessCompatAgent.java index d36d7af..7ae512f 100644 --- a/src/main/java/com/datadog/ServerlessCompatAgent.java +++ b/src/main/java/com/datadog/ServerlessCompatAgent.java @@ -197,12 +197,18 @@ public static void premain(String agentArgs, Instrumentation instrumentation) { String packageVersion = getPackageVersion(); log.debug("Found package version {}", packageVersion); + NamedPipeNames pipeNames = null; if (environment == CloudEnvironment.AZURE_FUNCTION && isWindows()) { - configureNamedPipes(); + pipeNames = configureNamedPipes(); } ProcessBuilder processBuilder = new ProcessBuilder(executableFile.getAbsolutePath()); processBuilder.environment().put("DD_SERVERLESS_COMPAT_VERSION", packageVersion); + if (pipeNames != null) { + processBuilder.environment().put(NamedPipeNames.TRACE_WINDOWS_PIPE_NAME_ENV, pipeNames.tracePipeName); + processBuilder.environment().put(NamedPipeNames.DOGSTATSD_WINDOWS_PIPE_NAME_ENV, + pipeNames.dogstatsdPipeName); + } processBuilder.inheritIO(); processBuilder.start(); } catch (Exception e) { From 6641acfccf7e918bf2f6030b8db203d16a933301 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 22 Oct 2026 17:54:20 -0400 Subject: [PATCH 7/9] [SVLS-8245] Ask the binary to keep UDP beside the DogStatsD pipe With a pipe set, the binary listens on UDP only when DD_DOGSTATSD_PORT is set, so it doesn't take 8125 from a neighbor whose binary has no pipe. The .NET and Node layers point their in-process clients at the pipe, but a JVM can't change its own environment after it starts. A DogStatsD client that doesn't read dd.dogstatsd.pipe.name, or can't open the pipe, still sends to UDP 8125, so compat-java sets DD_DOGSTATSD_PORT=8125 for its binary. A port the customer already set wins. --- .../com/datadog/ServerlessCompatAgent.java | 23 ++++++++++++-- .../datadog/ServerlessCompatAgentTest.java | 30 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/datadog/ServerlessCompatAgent.java b/src/main/java/com/datadog/ServerlessCompatAgent.java index 7ae512f..9d8e1b2 100644 --- a/src/main/java/com/datadog/ServerlessCompatAgent.java +++ b/src/main/java/com/datadog/ServerlessCompatAgent.java @@ -43,6 +43,8 @@ private static String mapDdLogLevelToSlf4jLogLevel(String ddLogLevel) { private static final String os = System.getProperty("os.name").toLowerCase(); private static final String binaryPath = System.getenv("DD_SERVERLESS_COMPAT_PATH"); + static final String DOGSTATSD_PORT_ENV = "DD_DOGSTATSD_PORT"; + static final String DEFAULT_DOGSTATSD_PORT = "8125"; public static boolean isWindows() { return os.contains("win"); @@ -134,6 +136,23 @@ static NamedPipeNames configureNamedPipes() { return pipeNames; } + /** + * Gives the binary this function's pipe names and asks it to keep listening on UDP too. + * + *

With a pipe set, the binary listens on UDP only when {@code DD_DOGSTATSD_PORT} is set, + * because a binary without a pipe panics when another binary on the instance takes 8125. + * The .NET and Node compatibility layers don't need UDP: they point their in-process + * DogStatsD clients at the pipe through environment variables. Java can't, because a JVM + * can't change its own environment after it starts. A DogStatsD client that doesn't read + * the {@code dd.dogstatsd.pipe.name} system property, or can't open the pipe, still sends to + * UDP 8125, so this asks the binary to listen there. A port the customer already set wins. + */ + static void addPipeSettings(Map binaryEnvironment, NamedPipeNames pipeNames) { + binaryEnvironment.put(NamedPipeNames.TRACE_WINDOWS_PIPE_NAME_ENV, pipeNames.tracePipeName); + binaryEnvironment.put(NamedPipeNames.DOGSTATSD_WINDOWS_PIPE_NAME_ENV, pipeNames.dogstatsdPipeName); + binaryEnvironment.putIfAbsent(DOGSTATSD_PORT_ENV, DEFAULT_DOGSTATSD_PORT); + } + public static void premain(String agentArgs, Instrumentation instrumentation) { CloudEnvironment environment = getEnvironment(); log.debug("Environment detected: {}", environment); @@ -205,9 +224,7 @@ public static void premain(String agentArgs, Instrumentation instrumentation) { ProcessBuilder processBuilder = new ProcessBuilder(executableFile.getAbsolutePath()); processBuilder.environment().put("DD_SERVERLESS_COMPAT_VERSION", packageVersion); if (pipeNames != null) { - processBuilder.environment().put(NamedPipeNames.TRACE_WINDOWS_PIPE_NAME_ENV, pipeNames.tracePipeName); - processBuilder.environment().put(NamedPipeNames.DOGSTATSD_WINDOWS_PIPE_NAME_ENV, - pipeNames.dogstatsdPipeName); + addPipeSettings(processBuilder.environment(), pipeNames); } processBuilder.inheritIO(); processBuilder.start(); diff --git a/src/test/java/com/datadog/ServerlessCompatAgentTest.java b/src/test/java/com/datadog/ServerlessCompatAgentTest.java index 8f20a84..8a7d7f5 100644 --- a/src/test/java/com/datadog/ServerlessCompatAgentTest.java +++ b/src/test/java/com/datadog/ServerlessCompatAgentTest.java @@ -3,6 +3,7 @@ import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; +import java.util.Properties; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -36,6 +37,35 @@ void testInitLogger(String ddLogLevel, String expectedSlf4jLevel) throws Excepti } } + private static NamedPipeNames pipeNames() { + Map env = new HashMap<>(); + env.put("DD_APM_WINDOWS_PIPE_NAME", "trace_pipe"); + env.put("DD_DOGSTATSD_WINDOWS_PIPE_NAME", "dogstatsd_pipe"); + return NamedPipeNames.choose(env, new Properties()); + } + + @Test + void addPipeSettings_asksBinaryToKeepUdp() { + Map binaryEnvironment = new HashMap<>(); + + ServerlessCompatAgent.addPipeSettings(binaryEnvironment, pipeNames()); + + assertEquals("trace_pipe", binaryEnvironment.get("DD_APM_WINDOWS_PIPE_NAME")); + assertEquals("dogstatsd_pipe", binaryEnvironment.get("DD_DOGSTATSD_WINDOWS_PIPE_NAME")); + // With a pipe, the binary listens on UDP only when the port is set. + assertEquals("8125", binaryEnvironment.get("DD_DOGSTATSD_PORT")); + } + + @Test + void addPipeSettings_keepsCustomerPort() { + Map binaryEnvironment = new HashMap<>(); + binaryEnvironment.put("DD_DOGSTATSD_PORT", "0"); + + ServerlessCompatAgent.addPipeSettings(binaryEnvironment, pipeNames()); + + assertEquals("0", binaryEnvironment.get("DD_DOGSTATSD_PORT")); + } + @Test void getEnvironment_returnsUnknownWhenNoVarsSet() { assertEquals(CloudEnvironment.UNKNOWN, From f5851b202b20d105a8cd06f88b488bca887feb30 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 22 Oct 2026 19:53:27 -0400 Subject: [PATCH 8/9] [SVLS-8245] Give the binary its pipes even when the tracer gate fails On Windows Azure Functions, compat-java now always passes pipe names and DD_DOGSTATSD_PORT to the binary, as the .NET compatibility layer gives its binary pipes whether or not a tracer is present. Only dd-java-agent's system properties still depend on the version gate. With a pipe, a taken UDP 8125 or TCP 8126 makes the binary warn and keep running instead of panicking, so Java apps with an older or missing tracer stop crashing next to a neighbor that owns the port. Their tracer keeps TCP and UDP: dd-java-agent learns a pipe name only from its own configuration, never from the binary. --- .../com/datadog/ServerlessCompatAgent.java | 41 ++++++++++++------- .../datadog/ServerlessCompatAgentTest.java | 20 +++++++++ 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/datadog/ServerlessCompatAgent.java b/src/main/java/com/datadog/ServerlessCompatAgent.java index 9d8e1b2..8ad9856 100644 --- a/src/main/java/com/datadog/ServerlessCompatAgent.java +++ b/src/main/java/com/datadog/ServerlessCompatAgent.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.helpers.NOPLogger; @@ -109,28 +110,40 @@ public static boolean isAzureFlexWithoutDDAzureResourceGroup() { } /** - * Gives this function its own named pipes when dd-java-agent supports them. The tracer reads - * these system properties before its environment variables, so they must be set before - * dd-java-agent's premain runs. When the gate fails, nothing is set and the tracer keeps - * whatever it inherited: TCP and UDP by default, or the customer's own pipe settings. + * Chooses this function's named pipes. The binary always gets them, as the .NET compatibility + * layer gives its binary pipes whether or not a tracer is present, so the binary never + * panics when another binary on the instance owns UDP 8125. * - * @return the chosen pipe names, or null when named pipes are off + *

dd-java-agent gets the pipes, as system properties, only when it is new enough to use + * them. The tracer reads these properties before its environment variables, so they must be + * set before dd-java-agent's premain runs. When the gate fails, nothing is set for the + * tracer, and it keeps whatever it inherited: TCP and UDP by default, or the customer's own + * pipe settings. The tracer only learns a pipe name from its own configuration, never from + * the binary, so the binary's pipes don't reach an older tracer. + * + * @return the chosen pipe names for the binary */ static NamedPipeNames configureNamedPipes() { - TracerVersionGate gate = TracerVersionGate.check(); - if (!gate.passed) { - log.info("Named pipes are off, keeping inherited trace and DogStatsD settings: {}", gate.reason); - return null; - } - log.debug("Named pipes are on: {}", gate.reason); + return configureNamedPipes(TracerVersionGate.check(), System.getenv(), System.getProperties()); + } - NamedPipeNames pipeNames = NamedPipeNames.choose(System.getenv(), System.getProperties()); + static NamedPipeNames configureNamedPipes(TracerVersionGate gate, Map env, + Properties systemProperties) { + NamedPipeNames pipeNames = NamedPipeNames.choose(env, systemProperties); for (String warning : pipeNames.warnings) { log.warn(warning); } - System.setProperty(NamedPipeNames.TRACE_PIPE_NAME_PROPERTY, pipeNames.tracePipeName); - System.setProperty(NamedPipeNames.DOGSTATSD_PIPE_NAME_PROPERTY, pipeNames.dogstatsdPipeName); + if (!gate.passed) { + log.info("Tracer named pipes are off, so dd-java-agent keeps its inherited trace and DogStatsD " + + "settings: {}. The binary still listens on pipes {} and {}, TCP, and UDP.", + gate.reason, pipeNames.tracePipeName, pipeNames.dogstatsdPipeName); + return pipeNames; + } + log.debug("Tracer named pipes are on: {}", gate.reason); + + systemProperties.setProperty(NamedPipeNames.TRACE_PIPE_NAME_PROPERTY, pipeNames.tracePipeName); + systemProperties.setProperty(NamedPipeNames.DOGSTATSD_PIPE_NAME_PROPERTY, pipeNames.dogstatsdPipeName); log.info("Configured named pipes - Trace: {}, DogStatsD: {}", pipeNames.tracePipeName, pipeNames.dogstatsdPipeName); return pipeNames; diff --git a/src/test/java/com/datadog/ServerlessCompatAgentTest.java b/src/test/java/com/datadog/ServerlessCompatAgentTest.java index 8a7d7f5..9af357a 100644 --- a/src/test/java/com/datadog/ServerlessCompatAgentTest.java +++ b/src/test/java/com/datadog/ServerlessCompatAgentTest.java @@ -1,6 +1,7 @@ package com.datadog; import java.lang.reflect.Method; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; @@ -44,6 +45,25 @@ private static NamedPipeNames pipeNames() { return NamedPipeNames.choose(env, new Properties()); } + @Test + void configureNamedPipes_failedGateGivesPipesToBinaryOnly() { + // No -javaagent options, so the gate fails because dd-java-agent is missing. + TracerVersionGate gate = TracerVersionGate.check(Collections.emptyList()); + assertFalse(gate.passed); + Properties systemProperties = new Properties(); + + NamedPipeNames pipeNames = ServerlessCompatAgent.configureNamedPipes(gate, new HashMap<>(), + systemProperties); + + // The binary still gets pipes, but an older tracer must never see them: it would hang + // or throw on a pipe. + assertNotNull(pipeNames); + assertTrue(pipeNames.tracePipeName.startsWith("dd_trace_")); + assertTrue(pipeNames.dogstatsdPipeName.startsWith("dd_dogstatsd_")); + assertNull(systemProperties.getProperty("dd.trace.pipe.name")); + assertNull(systemProperties.getProperty("dd.dogstatsd.pipe.name")); + } + @Test void addPipeSettings_asksBinaryToKeepUdp() { Map binaryEnvironment = new HashMap<>(); From aaab2f1bebda6704aae067ce899e1eff20ace7a0 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 22 Oct 2026 20:16:01 -0400 Subject: [PATCH 9/9] [SVLS-8245] Tighten the named-pipe Javadoc --- .../com/datadog/ServerlessCompatAgent.java | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/datadog/ServerlessCompatAgent.java b/src/main/java/com/datadog/ServerlessCompatAgent.java index 8ad9856..ad106ba 100644 --- a/src/main/java/com/datadog/ServerlessCompatAgent.java +++ b/src/main/java/com/datadog/ServerlessCompatAgent.java @@ -110,16 +110,13 @@ public static boolean isAzureFlexWithoutDDAzureResourceGroup() { } /** - * Chooses this function's named pipes. The binary always gets them, as the .NET compatibility - * layer gives its binary pipes whether or not a tracer is present, so the binary never - * panics when another binary on the instance owns UDP 8125. + * Chooses this function's named pipes. The binary always gets them, so it warns instead of + * panicking when another binary on the instance owns a port. * - *

dd-java-agent gets the pipes, as system properties, only when it is new enough to use - * them. The tracer reads these properties before its environment variables, so they must be - * set before dd-java-agent's premain runs. When the gate fails, nothing is set for the - * tracer, and it keeps whatever it inherited: TCP and UDP by default, or the customer's own - * pipe settings. The tracer only learns a pipe name from its own configuration, never from - * the binary, so the binary's pipes don't reach an older tracer. + *

dd-java-agent gets them, as system properties, only when the gate says it can use pipes; + * the properties must be set before its premain runs. Otherwise the tracer keeps what it + * inherited. It learns a pipe name only from its own configuration, so the binary's pipes + * can't reach an older tracer. * * @return the chosen pipe names for the binary */ @@ -150,15 +147,10 @@ static NamedPipeNames configureNamedPipes(TracerVersionGate gate, MapWith a pipe set, the binary listens on UDP only when {@code DD_DOGSTATSD_PORT} is set, - * because a binary without a pipe panics when another binary on the instance takes 8125. - * The .NET and Node compatibility layers don't need UDP: they point their in-process - * DogStatsD clients at the pipe through environment variables. Java can't, because a JVM - * can't change its own environment after it starts. A DogStatsD client that doesn't read - * the {@code dd.dogstatsd.pipe.name} system property, or can't open the pipe, still sends to - * UDP 8125, so this asks the binary to listen there. A port the customer already set wins. + * Passes the pipe names to the binary and asks it to keep UDP too. With a pipe, the binary + * listens on UDP only when {@code DD_DOGSTATSD_PORT} is set. A JVM can't change its own + * environment, so DogStatsD clients that don't read {@code dd.dogstatsd.pipe.name}, or can't + * open the pipe, still send to UDP 8125. A port the customer set wins. */ static void addPipeSettings(Map binaryEnvironment, NamedPipeNames pipeNames) { binaryEnvironment.put(NamedPipeNames.TRACE_WINDOWS_PIPE_NAME_ENV, pipeNames.tracePipeName);