From 5f0a764a7ead5a0ce5069c3ac0fc6f0cd80bb39e Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 27 Nov 2025 11:04:28 +0100 Subject: [PATCH 1/5] fix: Only enable client side stats if the host agent is at least 7.65.0 --- .../communication/ddagent/AgentVersion.java | 62 +++++++++++++++++++ .../ddagent/DDAgentFeaturesDiscovery.java | 3 +- .../DDAgentFeaturesDiscoveryTest.groovy | 61 ++++++++++++++++++ .../agent-info-with-client-dropping.json | 2 +- .../metrics/MetricsReliabilityTest.groovy | 2 +- 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 communication/src/main/java/datadog/communication/ddagent/AgentVersion.java diff --git a/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java b/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java new file mode 100644 index 00000000000..035fed49122 --- /dev/null +++ b/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java @@ -0,0 +1,62 @@ +package datadog.communication.ddagent; + +public class AgentVersion { + + /** + * Checks if the given version string represents a version that is at least the specified major, + * minor, and patch version. + * + * @param version the version string to check (e.g., "7.65.0") + * @param minMajor minimum major version + * @param minMinor minimum minor version + * @param minPatch minimum patch version + * @return true if version is at least the specified minimum, false otherwise (including when + * version is null or unparseable) + */ + public static boolean isVersionAtLeast(String version, int minMajor, int minMinor, int minPatch) { + if (version == null || version.isEmpty()) { + return false; + } + + try { + // Parse version string in format "major.minor.patch" (e.g., "7.65.0") + int majorDot = version.indexOf('.'); + if (majorDot == -1) { + return false; + } + + int major = Integer.parseInt(version.substring(0, majorDot)); + + if (major > minMajor) { + return true; + } else if (major < minMajor) { + return false; + } + + // major == minMajor + int minorDot = version.indexOf('.', majorDot + 1); + if (minorDot == -1) { + return false; + } + + int minor = Integer.parseInt(version.substring(majorDot + 1, minorDot)); + if (minor > minMinor) { + return true; + } else if (minor < minMinor) { + return false; + } + + // major == minMajor && minor == minMinor + // Find end of patch version (may have suffix like "-rc.1") + int patchEnd = minorDot + 1; + while (patchEnd < version.length() && Character.isDigit(version.charAt(patchEnd))) { + patchEnd++; + } + + int patch = Integer.parseInt(version.substring(minorDot + 1, patchEnd)); + return patch >= minPatch; + } catch (NumberFormatException | IndexOutOfBoundsException e) { + return false; + } + } +} diff --git a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java index 840a11c0b4c..3791462622a 100644 --- a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java +++ b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java @@ -358,7 +358,8 @@ private static void discoverStatsDPort(final Map info) { public boolean supportsMetrics() { return metricsEnabled && null != discoveryState.metricsEndpoint - && discoveryState.supportsDropping; + && discoveryState.supportsDropping + && AgentVersion.isVersionAtLeast(discoveryState.version, 7, 65, 0); } public boolean supportsDebugger() { diff --git a/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy b/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy index 5724633e524..ca662d6fd52 100644 --- a/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy +++ b/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy @@ -498,6 +498,67 @@ class DDAgentFeaturesDiscoveryTest extends DDSpecification { ) } + def "test metrics disabled for agent version below 7.65"() { + setup: + OkHttpClient client = Mock(OkHttpClient) + DDAgentFeaturesDiscovery features = new DDAgentFeaturesDiscovery(client, monitoring, agentUrl, true, true) + + when: "agent version is below 7.65" + features.discover() + + then: + 1 * client.newCall(_) >> { Request request -> + def response = """ + { + "version": "${version}", + "endpoints": ["/v0.5/traces", "/v0.6/stats"], + "client_drop_p0s": true, + "config": {} + } + """ + infoResponse(request, response) + } + features.getMetricsEndpoint() == V6_METRICS_ENDPOINT + features.supportsDropping() == true + features.supportsMetrics() == expected + + where: + version | expected + "7.64.0" | false + "7.64.9" | false + "7.64.9-rc.1" | false + "7.65.0" | true + "7.65.1" | true + "7.70.0" | true + "8.0.0" | true + } + + def "test metrics disabled for agent with unparseable version"() { + setup: + OkHttpClient client = Mock(OkHttpClient) + DDAgentFeaturesDiscovery features = new DDAgentFeaturesDiscovery(client, monitoring, agentUrl, true, true) + + when: "agent version is unparseable" + features.discover() + + then: + 1 * client.newCall(_) >> { Request request -> + def response = """ + { + "version": "${version}", + "endpoints": ["/v0.5/traces", "/v0.6/stats"], + "client_drop_p0s": true, + "config": {} + } + """ + infoResponse(request, response) + } + !features.supportsMetrics() + + where: + version << ["invalid", "7", "7.65", "", null] + } + def "should send container id as header on the info request and parse the hash in the response"() { setup: OkHttpClient client = Mock(OkHttpClient) diff --git a/communication/src/test/resources/agent-features/agent-info-with-client-dropping.json b/communication/src/test/resources/agent-features/agent-info-with-client-dropping.json index 29baa64c04a..18467a1170b 100644 --- a/communication/src/test/resources/agent-features/agent-info-with-client-dropping.json +++ b/communication/src/test/resources/agent-features/agent-info-with-client-dropping.json @@ -1,5 +1,5 @@ { - "version": "0.99.0", + "version": "7.65.0", "git_commit": "fab047e10", "build_date": "2020-12-04 15:57:06.74187 +0200 EET m=+0.029001792", "endpoints": [ diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/MetricsReliabilityTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/MetricsReliabilityTest.groovy index 17dba326ff3..777b5e1436f 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/MetricsReliabilityTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/MetricsReliabilityTest.groovy @@ -35,7 +35,7 @@ class MetricsReliabilityTest extends DDCoreSpecification { httpServer { handlers { get("/info") { - final def res = '{"endpoints":[' + (state.agentMetricsAvailable ? '"/v0.6/stats", ' : '') + '"/v0.4/traces"], "client_drop_p0s" : true}' + final def res = '{"version":"7.65.0","endpoints":[' + (state.agentMetricsAvailable ? '"/v0.6/stats", ' : '') + '"/v0.4/traces"], "client_drop_p0s" : true}' state.hash = Strings.sha256(res) response.send(res) state.latch.countDown() From c08baa10c64793ddab35e298daacd1d28fb9f54f Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 27 Nov 2025 14:55:24 +0100 Subject: [PATCH 2/5] fix: PR review, moved version check outside hotpath --- .../communication/ddagent/DDAgentFeaturesDiscovery.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java index 3791462622a..1a7fefd2b38 100644 --- a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java +++ b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java @@ -87,6 +87,7 @@ private static class State { String dataStreamsEndpoint; boolean supportsLongRunning; boolean supportsDropping; + boolean supportsClientSideStats; String state; String configEndpoint; String debuggerLogEndpoint; @@ -306,6 +307,8 @@ private boolean processInfoResponse(State newState, String response) { Boolean.TRUE.equals(map.getOrDefault("long_running_spans", false)); if (metricsEnabled) { + newState.supportsClientSideStats = + AgentVersion.isVersionAtLeast(newState.version, 7, 65, 0); Object canDrop = map.get("client_drop_p0s"); newState.supportsDropping = null != canDrop @@ -359,7 +362,7 @@ public boolean supportsMetrics() { return metricsEnabled && null != discoveryState.metricsEndpoint && discoveryState.supportsDropping - && AgentVersion.isVersionAtLeast(discoveryState.version, 7, 65, 0); + && discoveryState.supportsClientSideStats; } public boolean supportsDebugger() { From 110d9c9ef6cc1bd55a563b46e91f0282f49ff82f Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 5 Dec 2025 18:03:48 +0100 Subject: [PATCH 3/5] fix: Use the revert check isVersionAtLeast -> isVersionBelow --- .../communication/ddagent/AgentVersion.java | 47 ++++++++------ .../ddagent/DDAgentFeaturesDiscovery.java | 2 +- .../ddagent/AgentVersionTest.java | 64 +++++++++++++++++++ 3 files changed, 94 insertions(+), 19 deletions(-) create mode 100644 communication/src/test/java/datadog/communication/ddagent/AgentVersionTest.java diff --git a/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java b/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java index 035fed49122..2e033d1100a 100644 --- a/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java +++ b/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java @@ -1,52 +1,58 @@ package datadog.communication.ddagent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class AgentVersion { + private static final Logger log = LoggerFactory.getLogger(AgentVersion.class); + /** - * Checks if the given version string represents a version that is at least the specified major, + * Checks if the given version string represents a version that is below the specified major, * minor, and patch version. * - * @param version the version string to check (e.g., "7.65.0") - * @param minMajor minimum major version - * @param minMinor minimum minor version - * @param minPatch minimum patch version - * @return true if version is at least the specified minimum, false otherwise (including when + * @param version the version string to check (e.g., "7.64.0") + * @param maxMajor maximum major version (exclusive) + * @param maxMinor maximum minor version (exclusive) + * @param maxPatch maximum patch version (exclusive) + * @return true if version is below the specified maximum, false otherwise (including when * version is null or unparseable) */ - public static boolean isVersionAtLeast(String version, int minMajor, int minMinor, int minPatch) { + public static boolean isVersionBelow(String version, int maxMajor, int maxMinor, int maxPatch) { if (version == null || version.isEmpty()) { - return false; + return true; } try { // Parse version string in format "major.minor.patch" (e.g., "7.65.0") + // Assumes the 'version' is below if it can't be parsed. int majorDot = version.indexOf('.'); if (majorDot == -1) { - return false; + return true; } int major = Integer.parseInt(version.substring(0, majorDot)); - if (major > minMajor) { + if (major < maxMajor) { return true; - } else if (major < minMajor) { + } else if (major > maxMajor) { return false; } - // major == minMajor + // major == maxMajor int minorDot = version.indexOf('.', majorDot + 1); if (minorDot == -1) { - return false; + return true; } int minor = Integer.parseInt(version.substring(majorDot + 1, minorDot)); - if (minor > minMinor) { + if (minor < maxMinor) { return true; - } else if (minor < minMinor) { + } else if (minor > maxMinor) { return false; } - // major == minMajor && minor == minMinor + // major == maxMajor && minor == maxMinor // Find end of patch version (may have suffix like "-rc.1") int patchEnd = minorDot + 1; while (patchEnd < version.length() && Character.isDigit(version.charAt(patchEnd))) { @@ -54,9 +60,14 @@ public static boolean isVersionAtLeast(String version, int minMajor, int minMino } int patch = Integer.parseInt(version.substring(minorDot + 1, patchEnd)); - return patch >= minPatch; + if (patch != maxPatch) { + return patch < maxPatch; + } else { + // If there's a suffix (like "-rc.1"), consider it below the non-suffixed version + return patchEnd < version.length(); + } } catch (NumberFormatException | IndexOutOfBoundsException e) { - return false; + return true; } } } diff --git a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java index 1a7fefd2b38..d6498cb521f 100644 --- a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java +++ b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java @@ -308,7 +308,7 @@ private boolean processInfoResponse(State newState, String response) { if (metricsEnabled) { newState.supportsClientSideStats = - AgentVersion.isVersionAtLeast(newState.version, 7, 65, 0); + !AgentVersion.isVersionBelow(newState.version, 7, 65, 0); Object canDrop = map.get("client_drop_p0s"); newState.supportsDropping = null != canDrop diff --git a/communication/src/test/java/datadog/communication/ddagent/AgentVersionTest.java b/communication/src/test/java/datadog/communication/ddagent/AgentVersionTest.java new file mode 100644 index 00000000000..d9a1cd0d966 --- /dev/null +++ b/communication/src/test/java/datadog/communication/ddagent/AgentVersionTest.java @@ -0,0 +1,64 @@ +package datadog.communication.ddagent; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class AgentVersionTest { + + @Test + void testIsVersionBelow_VersionBelowThreshold() { + assertTrue(AgentVersion.isVersionBelow("7.64.0", 7, 65, 0)); + assertTrue(AgentVersion.isVersionBelow("7.64.9", 7, 65, 0)); + assertTrue(AgentVersion.isVersionBelow("6.99.99", 7, 65, 0)); + assertTrue(AgentVersion.isVersionBelow("7.0.0", 7, 65, 0)); + } + + @Test + void testIsVersionBelow_VersionEqualToThreshold() { + assertFalse(AgentVersion.isVersionBelow("7.65.0", 7, 65, 0)); + } + + @Test + void testIsVersionBelow_VersionAboveThreshold() { + assertFalse(AgentVersion.isVersionBelow("7.65.1", 7, 65, 0)); + assertFalse(AgentVersion.isVersionBelow("7.66.0", 7, 65, 0)); + assertFalse(AgentVersion.isVersionBelow("8.0.0", 7, 65, 0)); + assertFalse(AgentVersion.isVersionBelow("7.65.10", 7, 65, 0)); + } + + @Test + void testIsVersionBelow_MajorVersionComparison() { + assertTrue(AgentVersion.isVersionBelow("6.100.100", 7, 0, 0)); + assertFalse(AgentVersion.isVersionBelow("8.0.0", 7, 100, 100)); + } + + @Test + void testIsVersionBelow_MinorVersionComparison() { + assertTrue(AgentVersion.isVersionBelow("7.64.100", 7, 65, 0)); + assertFalse(AgentVersion.isVersionBelow("7.66.0", 7, 65, 100)); + } + + @Test + void testIsVersionBelow_WithSuffix() { + assertTrue(AgentVersion.isVersionBelow("7.64.0-rc.1", 7, 65, 0)); + assertTrue(AgentVersion.isVersionBelow("7.65.0-rc.1", 7, 65, 0)); + assertFalse(AgentVersion.isVersionBelow("7.65.1-snapshot", 7, 65, 0)); + } + + @Test + void testIsVersionBelow_NullOrEmpty() { + assertTrue(AgentVersion.isVersionBelow(null, 7, 65, 0)); + assertTrue(AgentVersion.isVersionBelow("", 7, 65, 0)); + } + + @Test + void testIsVersionBelow_InvalidFormat() { + assertTrue(AgentVersion.isVersionBelow("invalid", 7, 65, 0)); + assertTrue(AgentVersion.isVersionBelow("7", 7, 65, 0)); + assertTrue(AgentVersion.isVersionBelow("7.", 7, 65, 0)); + assertTrue(AgentVersion.isVersionBelow("7.65", 7, 65, 0)); + assertTrue(AgentVersion.isVersionBelow("7.65.", 7, 65, 0)); + assertTrue(AgentVersion.isVersionBelow("a.b.c", 7, 65, 0)); + } +} From 71829fa545192442b975bf3f03d88d6e1f85422f Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 5 Dec 2025 18:25:10 +0100 Subject: [PATCH 4/5] chore: Make spotless happy --- .../main/java/datadog/communication/ddagent/AgentVersion.java | 4 ++-- .../communication/ddagent/DDAgentFeaturesDiscovery.java | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java b/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java index 2e033d1100a..51e4fd136c4 100644 --- a/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java +++ b/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java @@ -15,8 +15,8 @@ public class AgentVersion { * @param maxMajor maximum major version (exclusive) * @param maxMinor maximum minor version (exclusive) * @param maxPatch maximum patch version (exclusive) - * @return true if version is below the specified maximum, false otherwise (including when - * version is null or unparseable) + * @return true if version is below the specified maximum, false otherwise (including when version + * is null or unparseable) */ public static boolean isVersionBelow(String version, int maxMajor, int maxMinor, int maxPatch) { if (version == null || version.isEmpty()) { diff --git a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java index d6498cb521f..1da613eada0 100644 --- a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java +++ b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java @@ -307,8 +307,7 @@ private boolean processInfoResponse(State newState, String response) { Boolean.TRUE.equals(map.getOrDefault("long_running_spans", false)); if (metricsEnabled) { - newState.supportsClientSideStats = - !AgentVersion.isVersionBelow(newState.version, 7, 65, 0); + newState.supportsClientSideStats = !AgentVersion.isVersionBelow(newState.version, 7, 65, 0); Object canDrop = map.get("client_drop_p0s"); newState.supportsDropping = null != canDrop From 6a7a90800f0e9629b08be56f10f6c4b327649e3a Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 9 Dec 2025 16:42:50 +0100 Subject: [PATCH 5/5] fix: Javadoc --- .../main/java/datadog/communication/ddagent/AgentVersion.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java b/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java index 51e4fd136c4..f3b30cb7baf 100644 --- a/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java +++ b/communication/src/main/java/datadog/communication/ddagent/AgentVersion.java @@ -15,8 +15,7 @@ public class AgentVersion { * @param maxMajor maximum major version (exclusive) * @param maxMinor maximum minor version (exclusive) * @param maxPatch maximum patch version (exclusive) - * @return true if version is below the specified maximum, false otherwise (including when version - * is null or unparseable) + * @return true if version is below the specified maximum (or if not parseable), false otherwise */ public static boolean isVersionBelow(String version, int maxMajor, int maxMinor, int maxPatch) { if (version == null || version.isEmpty()) {