From bcd239592df4b427b0a43e6c3cdcde7fba41f0aa Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Thu, 15 Oct 2026 16:55:12 -0400 Subject: [PATCH 01/10] [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 02/10] [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 03/10] [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 04/10] [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 05/10] [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 06/10] [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 07/10] [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 08/10] [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 09/10] [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); From ef35d9c0d0d44ec4c2db2be50b8e6d3e87f7f563 Mon Sep 17 00:00:00 2001 From: "E. Lewis" Date: Tue, 27 Oct 2026 13:24:36 -0400 Subject: [PATCH 10/10] [SVLS-8245] Warn when dd-java-agent is listed before the compat layer The JVM runs -javaagent premains in command-line order. A tracer listed first can read its settings before the compat layer sets the pipe system properties, and then keeps TCP and UDP. The gate now notes the order, and the compat layer logs a warning when the tracer would get pipes but comes first. --- .../com/datadog/ServerlessCompatAgent.java | 6 ++ .../java/com/datadog/TracerVersionGate.java | 68 ++++++++++++------- .../com/datadog/TracerVersionGateTest.java | 24 +++++++ 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/datadog/ServerlessCompatAgent.java b/src/main/java/com/datadog/ServerlessCompatAgent.java index ad106ba..1772c0c 100644 --- a/src/main/java/com/datadog/ServerlessCompatAgent.java +++ b/src/main/java/com/datadog/ServerlessCompatAgent.java @@ -138,6 +138,12 @@ static NamedPipeNames configureNamedPipes(TracerVersionGate gate, Map jvmArguments) { 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); + return new TracerVersionGate(false, "invalid minimum dd-java-agent version " + minimumVersion, false); } + String jarPath = null; + Manifest tracerManifest = null; + boolean tracerListedBeforeCompat = false; for (String argument : jvmArguments) { if (!argument.startsWith(JAVAAGENT_PREFIX)) { continue; } - String jarPath = javaagentJarPath(argument); - Manifest manifest = readManifest(jarPath); + String agentJarPath = javaagentJarPath(argument); + Manifest manifest = readManifest(agentJarPath); if (manifest == null) { continue; } - Attributes attributes = manifest.getMainAttributes(); - if (!TRACER_PREMAIN_CLASS.equals(attributes.getValue("Premain-Class"))) { - continue; + String premainClass = manifest.getMainAttributes().getValue("Premain-Class"); + if (COMPAT_PREMAIN_CLASS.equals(premainClass) && tracerManifest != null) { + tracerListedBeforeCompat = true; + } else if (TRACER_PREMAIN_CLASS.equals(premainClass) && tracerManifest == null) { + jarPath = agentJarPath; + tracerManifest = manifest; } + } - 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 + ")"); + if (tracerManifest == null) { + return new TracerVersionGate(false, + "no readable dd-java-agent found among -javaagent options (Premain-Class " + TRACER_PREMAIN_CLASS + ")", + false); } - return new TracerVersionGate(false, - "no readable dd-java-agent found among -javaagent options (Premain-Class " + TRACER_PREMAIN_CLASS + ")"); + String version = tracerManifest.getMainAttributes().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", + tracerListedBeforeCompat); + } + int[] tracer = parseVersion(version); + if (tracer == null) { + return new TracerVersionGate(false, + "unable to read dd-java-agent version '" + version + "' at " + jarPath, tracerListedBeforeCompat); + } + 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", + tracerListedBeforeCompat); + } + return new TracerVersionGate(true, "dd-java-agent " + version + " at " + jarPath + + " supports named pipes (minimum " + minimumVersion + ")", tracerListedBeforeCompat); } // The JVM treats everything after the first '=' as agent options. diff --git a/src/test/java/com/datadog/TracerVersionGateTest.java b/src/test/java/com/datadog/TracerVersionGateTest.java index 9af6fff..8dd656b 100644 --- a/src/test/java/com/datadog/TracerVersionGateTest.java +++ b/src/test/java/com/datadog/TracerVersionGateTest.java @@ -105,6 +105,30 @@ void check_findsTracerByPremainClassNotFileName() throws IOException { assertTrue(gate.passed, gate.reason); } + @Test + void check_detectsTracerListedBeforeCompat() throws IOException { + Path compat = writeAgentJar("compat.jar", TracerVersionGate.COMPAT_PREMAIN_CLASS, "1.0.0"); + Path tracer = writeTracerJar("1.68.0"); + + TracerVersionGate gate = TracerVersionGate.check( + Arrays.asList(javaagent(tracer), "-Xmx512m", javaagent(compat)), MINIMUM); + + assertTrue(gate.passed, gate.reason); + assertTrue(gate.tracerListedBeforeCompat); + } + + @Test + void check_acceptsCompatListedBeforeTracer() throws IOException { + Path compat = writeAgentJar("compat.jar", TracerVersionGate.COMPAT_PREMAIN_CLASS, "1.0.0"); + Path tracer = writeTracerJar("1.68.0"); + + TracerVersionGate gate = TracerVersionGate.check( + Arrays.asList(javaagent(compat), javaagent(tracer)), MINIMUM); + + assertTrue(gate.passed, gate.reason); + assertFalse(gate.tracerListedBeforeCompat); + } + @Test void check_ignoresAgentOptions() throws IOException { Path tracer = writeTracerJar("1.68.0");