From ec8d43e031cebf25130e32a7c2fd0471545be251 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Mon, 15 Jun 2026 16:28:51 -0400 Subject: [PATCH 1/6] init --- .../datadog/trace/api/ConfigDefaults.java | 2 + .../trace/api/config/GeneralConfig.java | 3 ++ .../datadog/trace/api/config/OtlpConfig.java | 2 + .../main/java/datadog/trace/api/Config.java | 44 +++++++++++++++++++ metadata/supported-configurations.json | 24 ++++++++++ .../provider/OtelEnvironmentConfigSource.java | 4 ++ 6 files changed, 79 insertions(+) diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index cb6f554f0cb..0240a27f61c 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -115,6 +115,8 @@ public final class ConfigDefaults { static final int DEFAULT_METRICS_OTEL_TIMEOUT = 7_500; // ms static final int DEFAULT_METRICS_OTEL_CARDINALITY_LIMIT = 2_000; + static final int DEFAULT_TRACE_STATS_INTERVAL = 10_000; // ms + public static final boolean DEFAULT_METRICS_OTEL_EXPERIMENTAL_ENABLED = true; public static final int DEFAULT_OTLP_TRACES_TIMEOUT = 10_000; // ms diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java index 60af53815fc..32ec8a0f324 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java @@ -69,6 +69,9 @@ public final class GeneralConfig { public static final String TRACE_STATS_COMPUTATION_ENABLED = "trace.stats.computation.enabled"; public static final String TRACE_STATS_COMPUTATION_IGNORE_AGENT_VERSION = "trace.stats.computation.ignore.agent.version"; + + public static final String TRACE_OTEL_SEMANTICS_ENABLED = "trace.otel.semantics.enabled"; + public static final String TRACER_METRICS_ENABLED = "trace.tracer.metrics.enabled"; public static final String TRACER_METRICS_BUFFERING_ENABLED = "trace.tracer.metrics.buffering.enabled"; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/OtlpConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/OtlpConfig.java index e3925dbeb40..7b9aae5d76a 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/OtlpConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/OtlpConfig.java @@ -31,6 +31,8 @@ public final class OtlpConfig { public static final String OTLP_METRICS_TEMPORALITY_PREFERENCE = "otlp.metrics.temporality.preference"; + public static final String TRACES_SPAN_METRICS_ENABLED = "traces.span.metrics.enabled"; + public static final String TRACE_OTEL_ENABLED = "trace.otel.enabled"; public static final String TRACE_OTEL_EXPORTER = "trace.otel.exporter"; diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 6418bab301e..b96afc3c76e 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -187,6 +187,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_RATE_LIMIT; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_REPORT_HOSTNAME; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_RESOLVER_ENABLED; +import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_STATS_INTERVAL; import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_X_DATADOG_TAGS_MAX_LENGTH; import static datadog.trace.api.ConfigDefaults.DEFAULT_WEBSOCKET_MESSAGES_INHERIT_SAMPLING; import static datadog.trace.api.ConfigDefaults.DEFAULT_WEBSOCKET_MESSAGES_SEPARATE_TRACES; @@ -421,6 +422,7 @@ import static datadog.trace.api.config.GeneralConfig.TRACER_METRICS_MAX_PENDING; import static datadog.trace.api.config.GeneralConfig.TRACE_DEBUG; import static datadog.trace.api.config.GeneralConfig.TRACE_LOG_LEVEL; +import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED; import static datadog.trace.api.config.GeneralConfig.TRACE_STATS_COMPUTATION_ENABLED; import static datadog.trace.api.config.GeneralConfig.TRACE_STATS_COMPUTATION_IGNORE_AGENT_VERSION; import static datadog.trace.api.config.GeneralConfig.TRACE_TAGS; @@ -490,6 +492,7 @@ import static datadog.trace.api.config.OtlpConfig.OTLP_TRACES_HEADERS; import static datadog.trace.api.config.OtlpConfig.OTLP_TRACES_PROTOCOL; import static datadog.trace.api.config.OtlpConfig.OTLP_TRACES_TIMEOUT; +import static datadog.trace.api.config.OtlpConfig.TRACES_SPAN_METRICS_ENABLED; import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_EXPORTER; import static datadog.trace.api.config.ProfilingConfig.PROFILING_AGENTLESS; import static datadog.trace.api.config.ProfilingConfig.PROFILING_AGENTLESS_DEFAULT; @@ -1000,6 +1003,9 @@ public static String getHostName() { private final int otlpMetricsTimeout; private final OtlpConfig.Temporality otlpMetricsTemporalityPreference; + private final boolean tracesSpanMetricsEnabled; + private final boolean traceOtelSemanticsEnabled; + private final String traceOtelExporter; private final String otlpTracesEndpoint; private final Map otlpTracesHeaders; @@ -1018,6 +1024,7 @@ public static String getHostName() { private final boolean tracerMetricsBufferingEnabled; private final int tracerMetricsMaxAggregates; private final int tracerMetricsMaxPending; + private final int traceStatsInterval; private final boolean reportHostName; @@ -2128,6 +2135,13 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins OtlpConfig.Temporality.class, OtlpConfig.Temporality.DELTA); + traceOtelSemanticsEnabled = configProvider.getBoolean(TRACE_OTEL_SEMANTICS_ENABLED, false); + // Tri-state default: when unset, SDK-computed OTLP span metrics are emitted iff OTLP trace + // export and OTel metrics export are both enabled. + tracesSpanMetricsEnabled = + configProvider.getBoolean( + TRACES_SPAN_METRICS_ENABLED, isTraceOtlpExporterEnabled() && isMetricsOtelEnabled()); + otlpTimeout = configProvider.getInteger(OTLP_TRACES_TIMEOUT, DEFAULT_OTLP_TRACES_TIMEOUT); if (otlpTimeout < 0) { log.warn("Invalid OTLP traces timeout: {}. The value must be positive", otlpTimeout); @@ -2207,6 +2221,18 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins configProvider.getBoolean(TRACE_STATS_COMPUTATION_IGNORE_AGENT_VERSION, false); tracerMetricsBufferingEnabled = configProvider.getBoolean(TRACER_METRICS_BUFFERING_ENABLED, false); + // Internal, test-only override of the stats flush interval. Read directly from the + // underscore-prefixed env var so it bypasses config-inversion validation and telemetry. + int statsInterval = DEFAULT_TRACE_STATS_INTERVAL; + String statsIntervalOverride = ConfigHelper.env("_DD_TRACE_STATS_INTERVAL"); + if (statsIntervalOverride != null) { + try { + statsInterval = Integer.parseInt(statsIntervalOverride); + } catch (NumberFormatException ignored) { + // fall back to the default + } + } + traceStatsInterval = statsInterval; // The metrics inbox is an MpscArrayQueue; each saturated slot holds one // ~120 B SpanSnapshot. The historical default TRACER_METRICS_MAX_PENDING=2048 (logical) * // LEGACY_BATCH_SIZE=64 = 131072 slots was sized for the prior conflating-Batch model where @@ -3845,6 +3871,10 @@ public int getTracerMetricsMaxPending() { return tracerMetricsMaxPending; } + public int getTraceStatsInterval() { + return traceStatsInterval; + } + public boolean isLogsInjectionEnabled() { return logsInjectionEnabled; } @@ -5605,6 +5635,14 @@ public OtlpConfig.Temporality getOtlpMetricsTemporalityPreference() { return otlpMetricsTemporalityPreference; } + public boolean isTracesSpanMetricsEnabled() { + return tracesSpanMetricsEnabled; + } + + public boolean isTraceOtelSemanticsEnabled() { + return traceOtelSemanticsEnabled; + } + public boolean isTraceOtelEnabled() { return instrumenterConfig.isTraceOtelEnabled(); } @@ -6713,6 +6751,12 @@ public String toString() { + otlpMetricsTimeout + ", otlpMetricsTemporalityPreference=" + otlpMetricsTemporalityPreference + + ", tracesSpanMetricsEnabled=" + + tracesSpanMetricsEnabled + + ", traceOtelSemanticsEnabled=" + + traceOtelSemanticsEnabled + + ", traceStatsInterval=" + + traceStatsInterval + ", traceOtelExporter=" + traceOtelExporter + ", otlpTracesEndpoint=" diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index e2d171c3c3f..8a02346c011 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -8633,6 +8633,22 @@ "aliases": [] } ], + "DD_TRACE_OTEL_SEMANTICS_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], + "DD_TRACES_SPAN_METRICS_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": null, + "aliases": [] + } + ], "DD_TRACE_ORG_GUARD_ENABLED": [ { "version": "A", @@ -11625,6 +11641,14 @@ "aliases": [] } ], + "OTEL_TRACES_SPAN_METRICS_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": null, + "aliases": [] + } + ], "OTEL_TRACES_SAMPLER_ARG": [ { "version": "C", diff --git a/utils/config-utils/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java b/utils/config-utils/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java index 4d923dead01..1aea89c9a2d 100644 --- a/utils/config-utils/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java +++ b/utils/config-utils/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java @@ -37,6 +37,7 @@ import static datadog.trace.api.config.OtlpConfig.OTLP_TRACES_HEADERS; import static datadog.trace.api.config.OtlpConfig.OTLP_TRACES_PROTOCOL; import static datadog.trace.api.config.OtlpConfig.OTLP_TRACES_TIMEOUT; +import static datadog.trace.api.config.OtlpConfig.TRACES_SPAN_METRICS_ENABLED; import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_ENABLED; import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_EXPORTER; import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ENABLED; @@ -161,6 +162,9 @@ private void setupTraceOtelEnvironment() { capture(REQUEST_HEADER_TAGS, mapHeaderTags("http.request.header.", requestHeaders)); capture(RESPONSE_HEADER_TAGS, mapHeaderTags("http.response.header.", responseHeaders)); capture(TRACE_EXTENSIONS_PATH, extensions); + capture( + TRACES_SPAN_METRICS_ENABLED, + getOtelProperty("otel.traces.span.metrics.enabled", "dd." + TRACES_SPAN_METRICS_ENABLED)); String exporter = getOtelProperty("otel.traces.exporter"); if ("otlp".equalsIgnoreCase(exporter)) { // traces defaults to non-OTLP (i.e. datadog) From 5353b055d7d92e438302e5e979c11ce13de012d0 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 17 Jun 2026 15:05:50 -0400 Subject: [PATCH 2/6] adding tests --- .../datadog/trace/api/ConfigTest.groovy | 36 ++++++++++++++++ .../OtelEnvironmentConfigSourceTest.groovy | 43 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy index faa4d04311e..00d89eff44a 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy @@ -53,6 +53,7 @@ import static datadog.trace.api.config.GeneralConfig.SSI_INJECTION_ENABLED import static datadog.trace.api.config.GeneralConfig.SSI_INJECTION_FORCE import static datadog.trace.api.config.GeneralConfig.TAGS import static datadog.trace.api.config.GeneralConfig.TRACER_METRICS_IGNORED_RESOURCES +import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED import static datadog.trace.api.config.GeneralConfig.VERSION import static datadog.trace.api.config.JmxFetchConfig.JMX_FETCH_CHECK_PERIOD import static datadog.trace.api.config.JmxFetchConfig.JMX_FETCH_ENABLED @@ -148,7 +149,9 @@ import static datadog.trace.api.config.OtlpConfig.OTLP_TRACES_ENDPOINT import static datadog.trace.api.config.OtlpConfig.OTLP_TRACES_HEADERS import static datadog.trace.api.config.OtlpConfig.OTLP_TRACES_PROTOCOL import static datadog.trace.api.config.OtlpConfig.OTLP_TRACES_TIMEOUT +import static datadog.trace.api.config.OtlpConfig.TRACES_SPAN_METRICS_ENABLED import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_ENABLED +import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_EXPORTER import datadog.trace.config.inversion.ConfigHelper import datadog.trace.api.env.FixedCapturedEnvironment @@ -555,6 +558,10 @@ class ConfigTest extends DDSpecification { config.otlpTracesHeaders == [:] config.otlpTracesProtocol == HTTP_PROTOBUF config.otlpTracesTimeout == 10000 + + !config.tracesSpanMetricsEnabled + !config.traceOtelSemanticsEnabled + config.traceStatsInterval == 10000 } def "otel: check syntax for OTLP headers"() { @@ -715,6 +722,31 @@ class ConfigTest extends DDSpecification { config.otlpTracesEndpoint == "http://localhost:4318/v1/traces" } + def "traces span metrics tri-state default: exporter=#exporter metricsOtel=#metricsOtel override=#override"() { + setup: + System.setProperty(PREFIX + TRACE_OTEL_EXPORTER, exporter) + System.setProperty(PREFIX + METRICS_OTEL_ENABLED, metricsOtel) + if (override != null) { + System.setProperty(PREFIX + TRACES_SPAN_METRICS_ENABLED, override) + } + + when: + Config config = new Config() + + then: + // Unset: emit iff OTLP trace export and OTel metrics export are both on. An explicit + // dd.traces.span.metrics.enabled always wins. + config.tracesSpanMetricsEnabled == expected + + where: + exporter | metricsOtel | override | expected + "otlp" | "true" | null | true + "otlp" | "false" | null | false + "none" | "true" | null | false + "none" | "false" | null | false + "none" | "false" | "true" | true + "otlp" | "true" | "false" | false + } def "specify overrides via system properties"() { setup: @@ -831,6 +863,7 @@ class ConfigTest extends DDSpecification { System.setProperty(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL_PROP, "http/protobuf") System.setProperty(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT_PROP, "5002") System.setProperty(OTEL_RESOURCE_ATTRIBUTES_PROP, "service.name=my=app,service.version=1.0.0,deployment.environment=production") + System.setProperty(PREFIX + TRACE_OTEL_SEMANTICS_ENABLED, "true") when: Config config = new Config() @@ -953,6 +986,9 @@ class ConfigTest extends DDSpecification { config.otlpTracesHeaders["traces-config-value"] == "T" config.otlpTracesProtocol == HTTP_PROTOBUF config.otlpTracesTimeout == 5002 + + config.traceOtelSemanticsEnabled + config.tracesSpanMetricsEnabled // tri-state default: OTLP trace export + OTel metrics both on } def "specify overrides via env vars"() { diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy index 537345a2b23..783df9e76f4 100644 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy @@ -9,6 +9,7 @@ import static datadog.trace.api.config.GeneralConfig.RUNTIME_METRICS_ENABLED import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME import static datadog.trace.api.config.GeneralConfig.TAGS import static datadog.trace.api.config.GeneralConfig.VERSION +import static datadog.trace.api.config.OtlpConfig.TRACES_SPAN_METRICS_ENABLED import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_ENABLED import static datadog.trace.api.config.OtlpConfig.TRACE_OTEL_EXPORTER import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ENABLED @@ -258,6 +259,48 @@ class OtelEnvironmentConfigSourceTest extends DDSpecification { source.get(TRACE_OTEL_EXPORTER) == 'otlp' } + def "otel traces span metrics enabled system property is mapped when otel is enabled"() { + setup: + injectSysConfig('dd.trace.otel.enabled', 'true', false) + injectSysConfig('otel.traces.span.metrics.enabled', value, false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(TRACES_SPAN_METRICS_ENABLED) == value + + where: + value << ['true', 'false'] + } + + def "otel traces span metrics enabled environment variable is mapped when otel is enabled"() { + setup: + injectEnvConfig('DD_TRACE_OTEL_ENABLED', 'true', false) + injectEnvConfig('OTEL_TRACES_SPAN_METRICS_ENABLED', value, false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(TRACES_SPAN_METRICS_ENABLED) == value + + where: + value << ['true', 'false'] + } + + def "otel traces span metrics enabled is not mapped when otel is disabled"() { + setup: + // Without dd.trace.otel.enabled, setupTraceOtelEnvironment() does not run. + injectSysConfig('otel.traces.span.metrics.enabled', 'true', false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(TRACES_SPAN_METRICS_ENABLED) == null + } + def "otel traces exporter none still disables tracing"() { setup: injectEnvConfig('DD_TRACE_OTEL_ENABLED', 'true', false) From 23105e10a10cb1ec62b5608fe6a57599723a4d2e Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Tue, 23 Jun 2026 16:28:32 -0400 Subject: [PATCH 3/6] init --- .../trace/common/metrics/AggregateEntry.java | 52 +++++++++++++++---- .../metrics/ConflatingMetricsAggregator.java | 8 +-- .../trace/common/metrics/SpanSnapshot.java | 15 ++++-- .../common/metrics/AggregateEntryTest.java | 4 +- .../metrics/AggregateEntryTestUtils.java | 38 +++++++++++++- .../common/metrics/AggregateTableTest.java | 6 +-- 6 files changed, 96 insertions(+), 27 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateEntry.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateEntry.java index 5bc985491de..912fcf84a1f 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateEntry.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateEntry.java @@ -80,6 +80,9 @@ final class AggregateEntry extends Hashtable.Entry { DDCaches.newFixedSizeCache(32); private static final DDCache GRPC_STATUS_CODE_CACHE = DDCaches.newFixedSizeCache(32); + // Origin is a small fixed vocabulary (synthetics, synthetics-browser, rum, ciapp-test, lambda). + private static final DDCache ORIGIN_CACHE = + DDCaches.newFixedSizeCache(8); /** * Outer cache keyed by peer-tag name, with an inner per-name cache keyed by value. The inner @@ -108,8 +111,13 @@ final class AggregateEntry extends Hashtable.Entry { @Nullable private final UTF8BytesString grpcStatusCode; private final short httpStatusCode; - /** Whether the root span carried the {@code synthetics} origin tag (synthetic-monitoring run). */ - private final boolean synthetic; + /** + * Trace origin (e.g. {@code synthetics}, {@code rum}, {@code ciapp-test}, {@code lambda}), or + * {@code null} when the root span carried no origin. Part of the bucket key, so spans with + * distinct origins aggregate separately. The OTLP export emits this as {@code datadog.origin}; + * the native msgpack path reads {@link #isSynthetics()}, derived from it. + */ + @Nullable private final UTF8BytesString origin; /** Whether this span is the trace root ({@code parentId == 0}). */ private final boolean traceRoot; @@ -139,7 +147,8 @@ final class AggregateEntry extends Hashtable.Entry { private int errorCount; private int hitCount; private int topLevelCount; - private long duration; + private long okDuration; + private long errorDuration; /** Hot-path constructor for the producer/consumer flow. Builds UTF8 fields via the caches. */ AggregateEntry(SpanSnapshot s, long keyHash) { @@ -154,7 +163,7 @@ final class AggregateEntry extends Hashtable.Entry { this.httpEndpoint = canonicalizeOptional(HTTP_ENDPOINT_CACHE, s.httpEndpoint); this.grpcStatusCode = canonicalizeOptional(GRPC_STATUS_CODE_CACHE, s.grpcStatusCode); this.httpStatusCode = s.httpStatusCode; - this.synthetic = s.synthetic; + this.origin = canonicalizeOptional(ORIGIN_CACHE, s.origin); this.traceRoot = s.traceRoot; this.peerTagNames = s.peerTagSchema == null ? null : s.peerTagSchema.names; this.peerTagValues = s.peerTagValues; @@ -174,11 +183,12 @@ void recordOneDuration(long tagAndDuration) { if ((tagAndDuration & ERROR_TAG) == ERROR_TAG) { tagAndDuration ^= ERROR_TAG; errorLatenciesForWrite().accept(tagAndDuration); + errorDuration += tagAndDuration; ++errorCount; } else { okLatencies.accept(tagAndDuration); + okDuration += tagAndDuration; } - duration += tagAndDuration; } int getErrorCount() { @@ -194,7 +204,15 @@ int getTopLevelCount() { } long getDuration() { - return duration; + return okDuration + errorDuration; + } + + long getOkDuration() { + return okDuration; + } + + long getErrorDuration() { + return errorDuration; } Histogram getOkLatencies() { @@ -232,7 +250,8 @@ void clear() { this.errorCount = 0; this.hitCount = 0; this.topLevelCount = 0; - this.duration = 0; + this.okDuration = 0; + this.errorDuration = 0; this.okLatencies.clear(); // errorLatencies stays null on entries that never errored. Only clear if it was allocated. if (this.errorLatencies != null) { @@ -243,7 +262,7 @@ void clear() { boolean matches(SpanSnapshot s) { String[] snapshotNames = s.peerTagSchema == null ? null : s.peerTagSchema.names; return httpStatusCode == s.httpStatusCode - && synthetic == s.synthetic + && contentEquals(origin, s.origin) && traceRoot == s.traceRoot && contentEquals(resource, s.resourceName) && contentEquals(service, s.serviceName) @@ -284,7 +303,7 @@ static long hashOf(SpanSnapshot s) { h = LongHashingUtils.addToHash(h, s.serviceNameSource); h = LongHashingUtils.addToHash(h, s.spanType); h = LongHashingUtils.addToHash(h, s.httpStatusCode); - h = LongHashingUtils.addToHash(h, s.synthetic); + h = LongHashingUtils.addToHash(h, s.origin); h = LongHashingUtils.addToHash(h, s.traceRoot); h = LongHashingUtils.addToHash(h, s.spanKind); // Always mix in both the schema's content hash and the values' content hash, unconditionally @@ -352,8 +371,21 @@ int getHttpStatusCode() { return httpStatusCode; } + /** + * The full trace origin, or {@code null} when unset. Used by {@link OtlpStatsMetricWriter} to + * emit {@code datadog.origin}. + */ + @Nullable + UTF8BytesString getOrigin() { + return origin; + } + + /** + * Whether the origin is {@code synthetics}. Derived from {@link #origin} for the native msgpack + * writer, which emits a synthetics boolean rather than the full origin. + */ boolean isSynthetics() { - return synthetic; + return origin != null && "synthetics".contentEquals(origin); } boolean isTraceRoot() { diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java index 895ee434854..594d3e6c4c0 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/ConflatingMetricsAggregator.java @@ -47,8 +47,6 @@ public final class ConflatingMetricsAggregator implements MetricsAggregator, Eve private static final Map DEFAULT_HEADERS = Collections.singletonMap(DDAgentApi.DATADOG_META_TRACER_VERSION, DDTraceCoreInfo.VERSION); - private static final CharSequence SYNTHETICS_ORIGIN = "synthetics"; - private static final SpanKindFilter METRICS_ELIGIBLE_KINDS = SpanKindFilter.builder() .includeServer() @@ -346,7 +344,7 @@ private boolean publish(CoreSpan span, boolean isTopLevel) { span.getServiceNameSource(), spanType, span.getHttpStatusCode(), - isSynthetic(span), + span.getOrigin(), span.getParentId() == 0, spanKind, peerTagSchema, @@ -466,10 +464,6 @@ private static String[] capturePeerTagValues(CoreSpan span, PeerTagSchema sch return values; } - private static boolean isSynthetic(CoreSpan span) { - return span.getOrigin() != null && SYNTHETICS_ORIGIN.equals(span.getOrigin().toString()); - } - public void stop() { if (null != cancellation) { cancellation.cancel(); diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/SpanSnapshot.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/SpanSnapshot.java index 152ac42bb55..a462e5968e9 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/SpanSnapshot.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/SpanSnapshot.java @@ -16,7 +16,16 @@ final class SpanSnapshot implements InboxItem { final CharSequence serviceNameSource; final CharSequence spanType; final short httpStatusCode; - final boolean synthetic; + + /** + * Trace origin (e.g. {@code synthetics}, {@code synthetics-browser}, {@code rum}, {@code + * ciapp-test}, {@code lambda}), or {@code null} when the root span carried no origin. Captured in + * full -- rather than collapsed to a synthetics flag -- so the OTLP export can emit {@code + * datadog.origin} with the recognized value; the native msgpack path derives its synthetics + * boolean from it via {@link AggregateEntry#isSynthetics()}. + */ + final CharSequence origin; + final boolean traceRoot; final String spanKind; @@ -48,7 +57,7 @@ final class SpanSnapshot implements InboxItem { CharSequence serviceNameSource, CharSequence spanType, short httpStatusCode, - boolean synthetic, + CharSequence origin, boolean traceRoot, String spanKind, PeerTagSchema peerTagSchema, @@ -63,7 +72,7 @@ final class SpanSnapshot implements InboxItem { this.serviceNameSource = serviceNameSource; this.spanType = spanType; this.httpStatusCode = httpStatusCode; - this.synthetic = synthetic; + this.origin = origin; this.traceRoot = traceRoot; this.spanKind = spanKind; this.peerTagSchema = peerTagSchema; diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTest.java index 7fd767533c7..02693ee0edd 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTest.java @@ -131,7 +131,7 @@ private static SpanSnapshot snapshotWithPeerTags(String[] names, String[] values null, "type", (short) 200, - false, + null, true, "client", PeerTagSchema.testSchema(names), @@ -151,7 +151,7 @@ private static AggregateEntry newEntry() { null, "type", (short) 200, - false, + null, true, "client", null, diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTestUtils.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTestUtils.java index ed6fd5a3a7e..a36ca8b907f 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTestUtils.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTestUtils.java @@ -76,7 +76,9 @@ public static AggregateEntry of( serviceSource, type, (short) httpStatusCode, - synthetic, + // The legacy boolean maps onto the full origin field: true => "synthetics", false => + // no origin. Tests needing a non-synthetics origin use ofOrigin(...). + synthetic ? "synthetics" : null, traceRoot, spanKind == null ? null : spanKind.toString(), schema, @@ -88,6 +90,38 @@ public static AggregateEntry of( return forSnapshot(syntheticSnapshot); } + /** + * Builds a minimal {@link AggregateEntry} carrying an explicit trace {@code origin} (e.g. {@code + * rum}, {@code ciapp-test}, {@code lambda}). A trace-root server entry with no HTTP/RPC/peer-tag + * fields; durations are recorded by the caller. + */ + public static AggregateEntry ofOrigin( + CharSequence resource, + CharSequence service, + CharSequence operationName, + CharSequence type, + CharSequence spanKind, + @Nullable CharSequence origin) { + SpanSnapshot snapshot = + new SpanSnapshot( + resource, + service == null ? null : service.toString(), + operationName, + null, + type, + (short) 0, + origin, + true, + spanKind == null ? null : spanKind.toString(), + null, + null, + null, + null, + null, + 0L); + return forSnapshot(snapshot); + } + /** * Builds an {@link AggregateEntry} from {@code s} by computing its lookup hash via {@link * AggregateEntry#hashOf(SpanSnapshot)} and calling the package-private constructor directly. @@ -106,7 +140,7 @@ public static boolean equals(AggregateEntry a, AggregateEntry b) { if (a == b) return true; if (a == null || b == null) return false; return a.getHttpStatusCode() == b.getHttpStatusCode() - && a.isSynthetics() == b.isSynthetics() + && Objects.equals(a.getOrigin(), b.getOrigin()) && a.isTraceRoot() == b.isTraceRoot() && Objects.equals(a.getResource(), b.getResource()) && Objects.equals(a.getService(), b.getService()) diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java index 618ead2ab43..c3463337079 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java @@ -274,7 +274,7 @@ private static SpanSnapshot nullServiceKindSnapshot(String service, String spanK null, "web", (short) 200, - false, + null, true, spanKind, null, @@ -294,7 +294,7 @@ private static SpanSnapshot nullableSnapshot( serviceNameSource, type, (short) 200, - false, + null, true, "client", null, @@ -350,7 +350,7 @@ SpanSnapshot build() { null, "web", (short) 200, - false, + null, true, spanKind, peerTagSchema, From 7224bc2e0fd6c2b38c9742a3f2e12f851ab7d68d Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Tue, 23 Jun 2026 17:11:41 -0400 Subject: [PATCH 4/6] bugfix --- .../main/java/datadog/trace/api/Config.java | 5 ++-- .../datadog/trace/api/ConfigTest.groovy | 25 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index b29d6ad15ad..66790764938 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -2131,10 +2131,11 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins traceOtelSemanticsEnabled = configProvider.getBoolean(TRACE_OTEL_SEMANTICS_ENABLED, false); // Tri-state default: when unset, SDK-computed OTLP span metrics are emitted iff OTLP trace - // export and OTel metrics export are both enabled. + // export and OTLP metrics export are both enabled. tracesSpanMetricsEnabled = configProvider.getBoolean( - TRACES_SPAN_METRICS_ENABLED, isTraceOtlpExporterEnabled() && isMetricsOtelEnabled()); + TRACES_SPAN_METRICS_ENABLED, + isTraceOtlpExporterEnabled() && isMetricsOtlpExporterEnabled()); otlpTimeout = configProvider.getInteger(OTLP_TRACES_TIMEOUT, DEFAULT_OTLP_TRACES_TIMEOUT); if (otlpTimeout < 0) { diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy index 00d89eff44a..09bb71ab863 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy @@ -138,6 +138,7 @@ import static datadog.trace.api.config.OtlpConfig.Protocol.HTTP_JSON import static datadog.trace.api.config.OtlpConfig.Temporality.CUMULATIVE import static datadog.trace.api.config.OtlpConfig.Temporality.DELTA import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_ENABLED +import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_EXPORTER import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_INTERVAL import static datadog.trace.api.config.OtlpConfig.METRICS_OTEL_TIMEOUT import static datadog.trace.api.config.OtlpConfig.OTLP_METRICS_ENDPOINT @@ -722,10 +723,13 @@ class ConfigTest extends DDSpecification { config.otlpTracesEndpoint == "http://localhost:4318/v1/traces" } - def "traces span metrics tri-state default: exporter=#exporter metricsOtel=#metricsOtel override=#override"() { + def "traces span metrics tri-state default: exporter=#exporter metricsExporter=#metricsExporter override=#override"() { setup: System.setProperty(PREFIX + TRACE_OTEL_EXPORTER, exporter) - System.setProperty(PREFIX + METRICS_OTEL_ENABLED, metricsOtel) + System.setProperty(PREFIX + METRICS_OTEL_ENABLED, "true") + if (metricsExporter != null) { + System.setProperty(PREFIX + METRICS_OTEL_EXPORTER, metricsExporter) + } if (override != null) { System.setProperty(PREFIX + TRACES_SPAN_METRICS_ENABLED, override) } @@ -734,18 +738,19 @@ class ConfigTest extends DDSpecification { Config config = new Config() then: - // Unset: emit iff OTLP trace export and OTel metrics export are both on. An explicit + // Unset: emit iff OTLP trace export and OTLP metrics export are both on. An explicit // dd.traces.span.metrics.enabled always wins. config.tracesSpanMetricsEnabled == expected where: - exporter | metricsOtel | override | expected - "otlp" | "true" | null | true - "otlp" | "false" | null | false - "none" | "true" | null | false - "none" | "false" | null | false - "none" | "false" | "true" | true - "otlp" | "true" | "false" | false + exporter | metricsExporter | override | expected + "otlp" | null | null | true // metrics defaults to otlp + "otlp" | "otlp" | null | true + "otlp" | "none" | null | false // metrics exporter explicitly disabled + "none" | null | null | false + "none" | "otlp" | null | false + "none" | "none" | "true" | true // explicit override wins + "otlp" | "otlp" | "false" | false // explicit override wins } def "specify overrides via system properties"() { From 5953baa1c390b8e9c341c41493935a1c58a6327e Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 24 Jun 2026 14:03:17 -0400 Subject: [PATCH 5/6] refactor origin usage in test helpers and add origin specific testS --- .../trace/common/metrics/AggregateEntry.java | 7 -- .../trace/common/metrics/SpanSnapshot.java | 8 --- .../ConflatingMetricAggregatorTest.groovy | 72 +++++++++---------- .../SerializingMetricWriterTest.groovy | 32 ++++----- .../common/metrics/AggregateEntryTest.java | 71 +++++++++++++----- .../metrics/AggregateEntryTestUtils.java | 40 +---------- .../common/metrics/AggregateTableTest.java | 42 ++++++++++- 7 files changed, 149 insertions(+), 123 deletions(-) diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateEntry.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateEntry.java index 912fcf84a1f..4d35de40e43 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateEntry.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateEntry.java @@ -110,13 +110,6 @@ final class AggregateEntry extends Hashtable.Entry { @Nullable private final UTF8BytesString httpEndpoint; @Nullable private final UTF8BytesString grpcStatusCode; private final short httpStatusCode; - - /** - * Trace origin (e.g. {@code synthetics}, {@code rum}, {@code ciapp-test}, {@code lambda}), or - * {@code null} when the root span carried no origin. Part of the bucket key, so spans with - * distinct origins aggregate separately. The OTLP export emits this as {@code datadog.origin}; - * the native msgpack path reads {@link #isSynthetics()}, derived from it. - */ @Nullable private final UTF8BytesString origin; /** Whether this span is the trace root ({@code parentId == 0}). */ diff --git a/dd-trace-core/src/main/java/datadog/trace/common/metrics/SpanSnapshot.java b/dd-trace-core/src/main/java/datadog/trace/common/metrics/SpanSnapshot.java index a462e5968e9..aebb037f977 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/metrics/SpanSnapshot.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/metrics/SpanSnapshot.java @@ -16,14 +16,6 @@ final class SpanSnapshot implements InboxItem { final CharSequence serviceNameSource; final CharSequence spanType; final short httpStatusCode; - - /** - * Trace origin (e.g. {@code synthetics}, {@code synthetics-browser}, {@code rum}, {@code - * ciapp-test}, {@code lambda}), or {@code null} when the root span carried no origin. Captured in - * full -- rather than collapsed to a synthetics flag -- so the OTLP export can emit {@code - * datadog.origin} with the recognized value; the native msgpack path derives its synthetics - * boolean from it via {@link AggregateEntry#isSynthetics()}. - */ final CharSequence origin; final boolean traceRoot; diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy index 00bd706b8fb..38d44863196 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/ConflatingMetricAggregatorTest.groovy @@ -127,7 +127,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "baz", [], @@ -175,7 +175,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "baz", [], @@ -229,7 +229,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, kind, [], @@ -308,7 +308,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "client", [UTF8BytesString.create("country:france")], @@ -328,7 +328,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "client", [UTF8BytesString.create("country:france"), UTF8BytesString.create("georegion:europe")], @@ -377,7 +377,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, kind, expectedPeerTags, @@ -431,7 +431,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "baz", [], @@ -492,7 +492,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "baz", [], @@ -511,7 +511,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "baz", [], @@ -567,7 +567,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "server", [], @@ -610,7 +610,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "server", [], @@ -629,7 +629,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "server", [], @@ -648,7 +648,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "server", [], @@ -714,7 +714,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", 200, - false, + null, false, "server", [], @@ -733,7 +733,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", 200, - false, + null, false, "server", [], @@ -752,7 +752,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", 404, - false, + null, false, "server", [], @@ -771,7 +771,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", 200, - false, + null, false, "server", [], @@ -826,7 +826,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", 200, - false, + null, false, "server", [], @@ -845,7 +845,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", 200, - false, + null, false, "server", [], @@ -898,7 +898,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { "source", "type", 200, - false, + null, false, "server", [], @@ -917,7 +917,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", 200, - false, + null, false, "server", [], @@ -972,7 +972,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "baz", [], @@ -991,7 +991,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "baz", [], @@ -1121,7 +1121,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "baz", [], @@ -1156,7 +1156,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "baz", [], @@ -1175,7 +1175,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "baz", [], @@ -1225,7 +1225,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "quux", [], @@ -1284,7 +1284,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, true, "garply", [], @@ -1452,7 +1452,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, true, "", [], @@ -1509,7 +1509,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "server", [], @@ -1566,7 +1566,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "server", [], @@ -1586,7 +1586,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "server", [], @@ -1606,7 +1606,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "type", HTTP_OK, - false, + null, false, "server", [], @@ -1660,7 +1660,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "rpc", 0, - false, + null, false, "server", [], @@ -1677,7 +1677,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "rpc", 0, - false, + null, false, "server", [], @@ -1694,7 +1694,7 @@ class ConflatingMetricAggregatorTest extends DDSpecification { null, "web", 200, - false, + null, false, "server", [], diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SerializingMetricWriterTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SerializingMetricWriterTest.groovy index cc0880bc30a..a5f0d28b6d1 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SerializingMetricWriterTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/metrics/SerializingMetricWriterTest.groovy @@ -32,7 +32,7 @@ class SerializingMetricWriterTest extends DDSpecification { CharSequence serviceSource, CharSequence type, int httpStatusCode, - boolean synthetic, + CharSequence origin, boolean traceRoot, CharSequence spanKind, List peerTags, @@ -42,7 +42,7 @@ class SerializingMetricWriterTest extends DDSpecification { int hitCount) { AggregateEntry e = AggregateEntryTestUtils.of( resource, service, operationName, serviceSource, type, - httpStatusCode, synthetic, traceRoot, spanKind, peerTags, + httpStatusCode, origin, traceRoot, spanKind, peerTags, httpMethod, httpEndpoint, grpcStatusCode) hitCount.times { e.recordOneDuration(1L) } return e @@ -79,7 +79,7 @@ class SerializingMetricWriterTest extends DDSpecification { [ entry( "resource1", "service1", "operation1", null, "type", 0, - false, false, "client", + null, false, "client", [ UTF8BytesString.create("country:canada"), UTF8BytesString.create("georegion:amer"), @@ -89,7 +89,7 @@ class SerializingMetricWriterTest extends DDSpecification { 10), entry( "resource2", "service2", "operation2", null, "type2", 200, - true, false, "producer", + "synthetics", false, "producer", [ UTF8BytesString.create("country:canada"), UTF8BytesString.create("georegion:amer"), @@ -99,7 +99,7 @@ class SerializingMetricWriterTest extends DDSpecification { 9), entry( "GET /api/users/:id", "web-service", "http.request", null, "web", 200, - false, true, "server", + null, true, "server", [], null, null, null, 5) @@ -107,7 +107,7 @@ class SerializingMetricWriterTest extends DDSpecification { (0..10000).collect({ i -> entry( "resource" + i, "service" + i, "operation" + i, null, "type", 0, - false, false, "producer", + null, false, "producer", [UTF8BytesString.create("messaging.destination:dest" + i)], null, null, null, 10) @@ -122,8 +122,8 @@ class SerializingMetricWriterTest extends DDSpecification { long duration = SECONDS.toNanos(10) WellKnownTags wellKnownTags = new WellKnownTags("runtimeid", "hostname", "env", "service", "version", "language") - def entryNoSource = entry("resource", "service", "operation", null, "type", 200, false, false, "server", [], "GET", "/api/users", null, 1) - def entryWithSource = entry("resource", "service", "operation", "source", "type", 200, false, false, "server", [], "POST", null, null, 1) + def entryNoSource = entry("resource", "service", "operation", null, "type", 200, null, false, "server", [], "GET", "/api/users", null, 1) + def entryWithSource = entry("resource", "service", "operation", "source", "type", 200, null, false, "server", [], "POST", null, null, 1) def content = [entryNoSource, entryWithSource] @@ -147,10 +147,10 @@ class SerializingMetricWriterTest extends DDSpecification { long duration = SECONDS.toNanos(10) WellKnownTags wellKnownTags = new WellKnownTags("runtimeid", "hostname", "env", "service", "version", "language") - def entryWithBoth = entry("resource", "service", "operation", null, "type", 200, false, false, "server", [], "GET", "/api/users", null, 1) - def entryWithMethodOnly = entry("resource", "service", "operation", null, "type", 200, false, false, "server", [], "POST", null, null, 1) - def entryWithEndpointOnly = entry("resource", "service", "operation", null, "type", 200, false, false, "server", [], null, "/api/orders", null, 1) - def entryWithNeither = entry("resource", "service", "operation", null, "type", 200, false, false, "client", [], null, null, null, 1) + def entryWithBoth = entry("resource", "service", "operation", null, "type", 200, null, false, "server", [], "GET", "/api/users", null, 1) + def entryWithMethodOnly = entry("resource", "service", "operation", null, "type", 200, null, false, "server", [], "POST", null, null, 1) + def entryWithEndpointOnly = entry("resource", "service", "operation", null, "type", 200, null, false, "server", [], null, "/api/orders", null, 1) + def entryWithNeither = entry("resource", "service", "operation", null, "type", 200, null, false, "client", [], null, null, null, 1) def content = [entryWithBoth, entryWithMethodOnly, entryWithEndpointOnly, entryWithNeither] @@ -177,7 +177,7 @@ class SerializingMetricWriterTest extends DDSpecification { long duration = SECONDS.toNanos(10) WellKnownTags wellKnownTags = new WellKnownTags("runtimeid", "hostname", "env", "service", "version", "language") - def e = entry("resource", "service", "operation", null, "type", 200, false, false, "server", [], "GET", "/api/users", null, 1) + def e = entry("resource", "service", "operation", null, "type", 200, null, false, "server", [], "GET", "/api/users", null, 1) def content = [e] @@ -204,9 +204,9 @@ class SerializingMetricWriterTest extends DDSpecification { long duration = SECONDS.toNanos(10) WellKnownTags wellKnownTags = new WellKnownTags("runtimeid", "hostname", "env", "service", "version", "language") - def entryWithGrpc = entry("grpc.service/Method", "grpc-service", "grpc.server", null, "rpc", 0, false, false, "server", [], null, null, "OK", 1) - def entryWithGrpcError = entry("grpc.service/Method", "grpc-service", "grpc.server", null, "rpc", 0, false, false, "client", [], null, null, "NOT_FOUND", 1) - def entryWithoutGrpc = entry("resource", "service", "operation", null, "web", 200, false, false, "server", [], null, null, null, 1) + def entryWithGrpc = entry("grpc.service/Method", "grpc-service", "grpc.server", null, "rpc", 0, null, false, "server", [], null, null, "OK", 1) + def entryWithGrpcError = entry("grpc.service/Method", "grpc-service", "grpc.server", null, "rpc", 0, null, false, "client", [], null, null, "NOT_FOUND", 1) + def entryWithoutGrpc = entry("resource", "service", "operation", null, "web", 200, null, false, "server", [], null, null, null, 1) def content = [entryWithGrpc, entryWithGrpcError, entryWithoutGrpc] diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTest.java index 02693ee0edd..3d109215063 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import datadog.metrics.agent.AgentMeter; @@ -123,6 +124,57 @@ void testUtilsEqualEntriesHaveEqualHashCodes() { assertEquals(AggregateEntryTestUtils.hashCode(a), AggregateEntryTestUtils.hashCode(b)); } + @Test + void getOriginCarriesFullOriginAndIsSyntheticsIsDerived() { + // The full origin is preserved on the entry; the native msgpack writer reads the derived + // isSynthetics() boolean, which must be true only for the exact "synthetics" origin. + AggregateEntry synthetics = entryWithOrigin("synthetics"); + assertEquals("synthetics", synthetics.getOrigin().toString()); + assertTrue(synthetics.isSynthetics()); + + // A different origin is carried verbatim but is not synthetics. + AggregateEntry rum = entryWithOrigin("rum"); + assertEquals("rum", rum.getOrigin().toString()); + assertFalse(rum.isSynthetics()); + + AggregateEntry ciapp = entryWithOrigin("ciapp-test"); + assertEquals("ciapp-test", ciapp.getOrigin().toString()); + assertFalse(ciapp.isSynthetics()); + + // synthetics-browser is a distinct origin -- a prefix match must not count as synthetics. + AggregateEntry browser = entryWithOrigin("synthetics-browser"); + assertEquals("synthetics-browser", browser.getOrigin().toString()); + assertFalse(browser.isSynthetics()); + } + + @Test + void noOriginIsNullAndNotSynthetics() { + AggregateEntry entry = entryWithOrigin(null); + assertNull(entry.getOrigin()); + assertFalse(entry.isSynthetics()); + } + + private static AggregateEntry entryWithOrigin(String origin) { + SpanSnapshot snapshot = + new SpanSnapshot( + "resource", + "svc", + "op", + null, + "type", + (short) 200, + origin, + true, + "client", + null, + null, + null, + null, + null, + 0L); + return AggregateEntryTestUtils.forSnapshot(snapshot); + } + private static SpanSnapshot snapshotWithPeerTags(String[] names, String[] values) { return new SpanSnapshot( "resource", @@ -143,23 +195,6 @@ private static SpanSnapshot snapshotWithPeerTags(String[] names, String[] values } private static AggregateEntry newEntry() { - SpanSnapshot snapshot = - new SpanSnapshot( - "resource", - "svc", - "op", - null, - "type", - (short) 200, - null, - true, - "client", - null, - null, - null, - null, - null, - 0L); - return AggregateEntryTestUtils.forSnapshot(snapshot); + return entryWithOrigin(null); } } diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTestUtils.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTestUtils.java index a36ca8b907f..bc189f7b60d 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTestUtils.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTestUtils.java @@ -46,7 +46,7 @@ public static AggregateEntry of( @Nullable CharSequence serviceSource, CharSequence type, int httpStatusCode, - boolean synthetic, + @Nullable CharSequence origin, boolean traceRoot, CharSequence spanKind, @Nullable List peerTags, @@ -68,7 +68,7 @@ public static AggregateEntry of( } schema = PeerTagSchema.testSchema(names); } - SpanSnapshot syntheticSnapshot = + SpanSnapshot snapshot = new SpanSnapshot( resource, service == null ? null : service.toString(), @@ -76,9 +76,7 @@ public static AggregateEntry of( serviceSource, type, (short) httpStatusCode, - // The legacy boolean maps onto the full origin field: true => "synthetics", false => - // no origin. Tests needing a non-synthetics origin use ofOrigin(...). - synthetic ? "synthetics" : null, + origin, traceRoot, spanKind == null ? null : spanKind.toString(), schema, @@ -87,38 +85,6 @@ public static AggregateEntry of( httpEndpoint == null ? null : httpEndpoint.toString(), grpcStatusCode == null ? null : grpcStatusCode.toString(), 0L); - return forSnapshot(syntheticSnapshot); - } - - /** - * Builds a minimal {@link AggregateEntry} carrying an explicit trace {@code origin} (e.g. {@code - * rum}, {@code ciapp-test}, {@code lambda}). A trace-root server entry with no HTTP/RPC/peer-tag - * fields; durations are recorded by the caller. - */ - public static AggregateEntry ofOrigin( - CharSequence resource, - CharSequence service, - CharSequence operationName, - CharSequence type, - CharSequence spanKind, - @Nullable CharSequence origin) { - SpanSnapshot snapshot = - new SpanSnapshot( - resource, - service == null ? null : service.toString(), - operationName, - null, - type, - (short) 0, - origin, - true, - spanKind == null ? null : spanKind.toString(), - null, - null, - null, - null, - null, - 0L); return forSnapshot(snapshot); } diff --git a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java index c3463337079..2118d3f7dfb 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateTableTest.java @@ -86,6 +86,40 @@ void peerTagPairsParticipateInIdentity() { assertEquals(3, table.size()); } + @Test + void distinctOriginsAggregateSeparately() { + // Trace origin is part of the bucket key (AggregateEntry.matches/hashOf), so spans that + // differ only by origin must land on separate entries + AggregateTable table = new AggregateTable(8); + + AggregateEntry none = table.findOrInsert(builder("svc", "op", "server").build()); + AggregateEntry synthetics = + table.findOrInsert(builder("svc", "op", "server").origin("synthetics").build()); + AggregateEntry rum = table.findOrInsert(builder("svc", "op", "server").origin("rum").build()); + AggregateEntry ciapp = + table.findOrInsert(builder("svc", "op", "server").origin("ciapp-test").build()); + + assertNotSame(none, synthetics); + assertNotSame(none, rum); + assertNotSame(synthetics, rum); + assertNotSame(rum, ciapp); + assertEquals(4, table.size()); + } + + @Test + void sameOriginHitsSameEntry() { + // The inverse of distinctOriginsAggregateSeparately: two snapshots with an identical + // non-null origin must conflate onto one entry. + AggregateTable table = new AggregateTable(8); + + AggregateEntry first = table.findOrInsert(builder("svc", "op", "server").origin("rum").build()); + AggregateEntry second = + table.findOrInsert(builder("svc", "op", "server").origin("rum").build()); + + assertSame(first, second); + assertEquals(1, table.size()); + } + @Test void capOverrunEvictsStaleEntry() { AggregateTable table = new AggregateTable(2); @@ -321,6 +355,7 @@ private static final class SnapshotBuilder { private final String spanKind; private PeerTagSchema peerTagSchema; private String[] peerTagValues; + private String origin; private long tagAndDuration = 0L; SnapshotBuilder(String service, String operation, String spanKind) { @@ -329,6 +364,11 @@ private static final class SnapshotBuilder { this.spanKind = spanKind; } + SnapshotBuilder origin(String origin) { + this.origin = origin; + return this; + } + SnapshotBuilder peerTags(String... namesAndValues) { int pairCount = namesAndValues.length / 2; String[] names = new String[pairCount]; @@ -350,7 +390,7 @@ SpanSnapshot build() { null, "web", (short) 200, - null, + origin, true, spanKind, peerTagSchema, From fb6b228e99fb5ae8bee05654ded554b549565f35 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Tue, 30 Jun 2026 15:14:29 -0400 Subject: [PATCH 6/6] update metricintegrationtest --- .../trace/common/metrics/MetricsIntegrationTest.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dd-trace-core/src/traceAgentTest/groovy/datadog/trace/common/metrics/MetricsIntegrationTest.groovy b/dd-trace-core/src/traceAgentTest/groovy/datadog/trace/common/metrics/MetricsIntegrationTest.groovy index 401aaef4b7e..de436ca97e3 100644 --- a/dd-trace-core/src/traceAgentTest/groovy/datadog/trace/common/metrics/MetricsIntegrationTest.groovy +++ b/dd-trace-core/src/traceAgentTest/groovy/datadog/trace/common/metrics/MetricsIntegrationTest.groovy @@ -40,13 +40,13 @@ class MetricsIntegrationTest extends AbstractTraceAgentTest { PeerTagSchema schema = PeerTagSchema.testSchema(["grault"] as String[]) SpanSnapshot snap1 = new SpanSnapshot( "resource1", "service1", "operation1", null, "sql", (short) 0, - false, true, "xyzzy", schema, ["quux"] as String[], null, null, null, 0L) + (CharSequence) null, true, "xyzzy", schema, ["quux"] as String[], null, null, null, 0L) def entry1 = new AggregateEntry(snap1, AggregateEntry.hashOf(snap1)) [2, 1, 2, 250, 4].each { entry1.recordOneDuration(it as long) } writer.add(entry1) SpanSnapshot snap2 = new SpanSnapshot( "resource2", "service2", "operation2", null, "web", (short) 200, - false, true, "xyzzy", schema, ["quux"] as String[], null, null, null, 0L) + (CharSequence) null, true, "xyzzy", schema, ["quux"] as String[], null, null, null, 0L) def entry2 = new AggregateEntry(snap2, AggregateEntry.hashOf(snap2)) [1, 1, 200, 2, 3, 4, 5, 6, 7, 8].each { entry2.recordOneDuration(it as long) } writer.add(entry2)