diff --git a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java index 10c1e57efd7..55929c73f51 100644 --- a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java +++ b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java @@ -101,6 +101,7 @@ private static class State { String version; String telemetryProxyEndpoint; Set peerTags = emptySet(); + String orgPropagationMarker; long lastTimeDiscovered; } @@ -316,6 +317,8 @@ private boolean processInfoResponse(State newState, String response) { ? unmodifiableSet(new HashSet<>((List) peer_tags)) : emptySet(); } + Object opm = map.get("org_prop_marker"); + newState.orgPropagationMarker = (opm instanceof String) ? (String) opm : null; try { newState.state = Strings.sha256(response); } catch (Throwable ex) { @@ -403,6 +406,10 @@ public Set peerTags() { return discoveryState.peerTags; } + public String getOrgPropagationMarker() { + return discoveryState.orgPropagationMarker; + } + public String getMetricsEndpoint() { return discoveryState.metricsEndpoint; } diff --git a/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy b/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy index 8bcda6eb811..ff97ef6d3a2 100644 --- a/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy +++ b/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy @@ -50,6 +50,7 @@ class DDAgentFeaturesDiscoveryTest extends DDSpecification { static final String INFO_WITH_LONG_RUNNING_SPANS = loadJsonFile("agent-info-with-long-running-spans.json") static final String INFO_WITH_TELEMETRY_PROXY_RESPONSE = loadJsonFile("agent-info-with-telemetry-proxy.json") static final String INFO_WITH_OLD_EVP_PROXY = loadJsonFile("agent-info-with-old-evp-proxy.json") + static final String INFO_WITH_OPM = loadJsonFile("agent-info-with-opm.json") static final String PROBE_STATE = "probestate" def "test parse /info response"() { @@ -209,6 +210,34 @@ class DDAgentFeaturesDiscoveryTest extends DDSpecification { 0 * _ } + def "test parse /info response with org propagation marker"() { + setup: + OkHttpClient client = Mock(OkHttpClient) + DDAgentFeaturesDiscovery features = new DDAgentFeaturesDiscovery(client, monitoring, agentUrl, V0_5, true, false) + + when: "/info available" + features.discover() + + then: + 1 * client.newCall(_) >> { Request request -> infoResponse(request, INFO_WITH_OPM) } + features.getOrgPropagationMarker() == "abc123def0" + 0 * _ + } + + def "test parse /info response without org propagation marker"() { + setup: + OkHttpClient client = Mock(OkHttpClient) + DDAgentFeaturesDiscovery features = new DDAgentFeaturesDiscovery(client, monitoring, agentUrl, V0_5, true, false) + + when: "/info available" + features.discover() + + then: + 1 * client.newCall(_) >> { Request request -> infoResponse(request, INFO_RESPONSE) } + features.getOrgPropagationMarker() == null + 0 * _ + } + def "test fallback when /info empty"() { setup: OkHttpClient client = Mock(OkHttpClient) diff --git a/communication/src/test/resources/agent-features/agent-info-with-opm.json b/communication/src/test/resources/agent-features/agent-info-with-opm.json new file mode 100644 index 00000000000..1496f8b6fbc --- /dev/null +++ b/communication/src/test/resources/agent-features/agent-info-with-opm.json @@ -0,0 +1,20 @@ +{ + "version": "7.67.0", + "git_commit": "bdf863ccc9", + "endpoints": [ + "/v0.3/traces", + "/v0.4/traces", + "/v0.5/traces", + "/v0.6/stats", + "/v0.1/pipeline_stats", + "/telemetry/proxy/", + "/evp_proxy/v4/", + "/debugger/v1/input" + ], + "client_drop_p0s": true, + "long_running_spans": true, + "config": { + "statsd_port": 8125 + }, + "org_prop_marker": "abc123def0" +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java index 78c0f4b5908..9faf4f4ea8e 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java @@ -169,5 +169,9 @@ public final class TracerConfig { "trace.cloud.payload.tagging.max-tags"; public static final String TRACE_SERVICE_DISCOVERY_ENABLED = "trace.service.discovery.enabled"; + public static final String TRACE_ORG_GUARD_ENABLED = "trace.org.guard.enabled"; + public static final String TRACE_ORG_GUARD_STRICT = "trace.org.guard.strict"; + public static final String TRACE_ORG_GUARD_TRUSTED_OPMS = "trace.org.guard.trusted.opms"; + private TracerConfig() {} } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 6c5efb4a0d1..32b19c07722 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -102,6 +102,7 @@ import datadog.trace.core.propagation.PropagationTags; import datadog.trace.core.propagation.TracingPropagator; import datadog.trace.core.propagation.XRayPropagator; +import datadog.trace.core.propagation.opg.OrgGuard; import datadog.trace.core.scopemanager.ContinuableScopeManager; import datadog.trace.core.servicediscovery.ServiceDiscovery; import datadog.trace.core.servicediscovery.ServiceDiscoveryFactory; @@ -827,11 +828,21 @@ private CoreTracer( sharedCommunicationObjects.whenReady(this.dataStreamsMonitoring::start); + propagationTagsFactory = PropagationTags.factory(config); + // Register context propagators - HttpCodec.Extractor tracingExtractor = + HttpCodec.Extractor baseExtractor = extractor == null ? HttpCodec.createExtractor(config, this::captureTraceConfig) : extractor; + OrgGuard orgGuard = + OrgGuard.create( + config, + featuresDiscovery::getOrgPropagationMarker, + propagationTagsFactory, + this.healthMetrics); + HttpCodec.Extractor tracingExtractor = orgGuard.decorateExtractor(baseExtractor); + HttpCodec.Injector tracingInjector = orgGuard.decorateInjector(injector); TracingPropagator tracingPropagator = - new TracingPropagator(config.isApmTracingEnabled(), injector, tracingExtractor); + new TracingPropagator(config.isApmTracingEnabled(), tracingInjector, tracingExtractor); Propagators.register(TRACING_CONCERN, tracingPropagator); Propagators.register(XRAY_TRACING_CONCERN, new XRayPropagator(config), false); if (config.isDataStreamsEnabled()) { @@ -889,7 +900,6 @@ private CoreTracer( StatusLogger.logStatus(config); - propagationTagsFactory = PropagationTags.factory(config); this.profilingContextIntegration = profilingContextIntegration; this.injectBaggageAsTags = injectBaggageAsTags; this.injectLinksAsTags = injectLinksAsTags; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/monitor/HealthMetrics.java b/dd-trace-core/src/main/java/datadog/trace/core/monitor/HealthMetrics.java index 257d887029b..35fdb58313d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/monitor/HealthMetrics.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/monitor/HealthMetrics.java @@ -2,6 +2,7 @@ import datadog.trace.common.writer.RemoteApi; import datadog.trace.core.DDSpan; +import datadog.trace.core.propagation.opg.OrgGuard; import java.util.List; /** @@ -65,6 +66,12 @@ public void onCloseScope() {} public void onScopeStackOverflow() {} + /** + * Reports that the Org Propagation Guard dropped the inbound Datadog context for an extracted + * trace. + */ + public void onOrgGuardEnforce(OrgGuard.Reason reason) {} + public void onSend( final int traceCount, final int sizeInBytes, final RemoteApi.Response response) {} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/monitor/TracerHealthMetrics.java b/dd-trace-core/src/main/java/datadog/trace/core/monitor/TracerHealthMetrics.java index 2df54241e56..d439b5f5184 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/monitor/TracerHealthMetrics.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/monitor/TracerHealthMetrics.java @@ -13,6 +13,7 @@ import datadog.trace.api.cache.RadixTreeCache; import datadog.trace.common.writer.RemoteApi; import datadog.trace.core.DDSpan; +import datadog.trace.core.propagation.opg.OrgGuard; import datadog.trace.util.AgentTaskScheduler; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.List; @@ -89,6 +90,9 @@ public class TracerHealthMetrics extends HealthMetrics implements AutoCloseable private final LongAdder longRunningTracesDropped = new LongAdder(); private final LongAdder longRunningTracesExpired = new LongAdder(); + private final LongAdder orgGuardEnforceMismatch = new LongAdder(); + private final LongAdder orgGuardEnforceStrictMissing = new LongAdder(); + private final LongAdder clientStatsProcessedSpans = new LongAdder(); private final LongAdder clientStatsProcessedTraces = new LongAdder(); private final LongAdder clientStatsP0DroppedSpans = new LongAdder(); @@ -285,6 +289,18 @@ public void onScopeStackOverflow() { scopeStackOverflow.increment(); } + @Override + public void onOrgGuardEnforce(OrgGuard.Reason reason) { + switch (reason) { + case MISMATCH: + orgGuardEnforceMismatch.increment(); + break; + case STRICT_MISSING: + orgGuardEnforceStrictMissing.increment(); + break; + } + } + @Override public void onSend( final int traceCount, final int sizeInBytes, final RemoteApi.Response response) { @@ -374,8 +390,11 @@ private static class Flush implements AgentTaskScheduler.Task createTagMap() { HashMap result = new HashMap<>(); fillTagMap(result); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OpmStampingInjector.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OpmStampingInjector.java new file mode 100644 index 00000000000..6ee131b8df5 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OpmStampingInjector.java @@ -0,0 +1,35 @@ +package datadog.trace.core.propagation.opg; + +import datadog.context.propagation.CarrierSetter; +import datadog.trace.core.DDSpanContext; +import datadog.trace.core.propagation.HttpCodec; +import java.util.function.Supplier; + +/** + * Decorates an {@link HttpCodec.Injector} so that, just before delegating to the underlying codecs, + * it stamps the local Org Propagation Marker (OPM) onto the span's propagation tags. The codecs + * (Datadog, W3C tracecontext) then serialize whatever is in the propagation tags, which causes the + * local OPM to overwrite any inbound OPM in {@code _dd.p.opm} / {@code t.opm}. + * + *

If the supplier returns {@code null} (the agent hasn't reported an OPM yet), this is a no-op + * and any inbound OPM is forwarded as-is, per the RFC. + */ +final class OpmStampingInjector implements HttpCodec.Injector { + + private final HttpCodec.Injector delegate; + private final Supplier localOpmSupplier; + + OpmStampingInjector(HttpCodec.Injector delegate, Supplier localOpmSupplier) { + this.delegate = delegate; + this.localOpmSupplier = localOpmSupplier; + } + + @Override + public void inject(DDSpanContext context, C carrier, CarrierSetter setter) { + String localOpm = localOpmSupplier.get(); + if (localOpm != null) { + context.getPropagationTags().updateOrgPropagationMarker(localOpm); + } + delegate.inject(context, carrier, setter); + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OrgGuard.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OrgGuard.java new file mode 100644 index 00000000000..870bb80fd61 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OrgGuard.java @@ -0,0 +1,63 @@ +package datadog.trace.core.propagation.opg; + +import datadog.trace.api.Config; +import datadog.trace.core.monitor.HealthMetrics; +import datadog.trace.core.propagation.HttpCodec; +import datadog.trace.core.propagation.PropagationTags; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * Single entry point for the Org Propagation Guard (OPG). Owns the gating decision and constructs + * the extract-side and inject-side decorators when OPG is enabled. When disabled, {@link + * #decorateExtractor} and {@link #decorateInjector} return their argument unchanged so the + * propagation chain pays zero overhead and any inbound OPM passes through the codec layer + * untouched. + */ +public final class OrgGuard { + + @Nullable private final OrgGuardEnforcer enforcer; + + public static OrgGuard create( + Config config, + Supplier localOpmSupplier, + PropagationTags.Factory propagationTagsFactory, + HealthMetrics healthMetrics) { + if (!config.isTraceOrgGuardEnabled()) { + return new OrgGuard(null); + } + return new OrgGuard( + new OrgGuardEnforcer(config, localOpmSupplier, propagationTagsFactory, healthMetrics)); + } + + private OrgGuard(@Nullable OrgGuardEnforcer enforcer) { + this.enforcer = enforcer; + } + + public HttpCodec.Extractor decorateExtractor(HttpCodec.Extractor delegate) { + return enforcer == null ? delegate : new OrgGuardEnforcingExtractor(delegate, enforcer); + } + + public HttpCodec.Injector decorateInjector(HttpCodec.Injector delegate) { + return enforcer == null + ? delegate + : new OpmStampingInjector(delegate, enforcer.localOpmSupplier()); + } + + /** Reason an extracted Datadog context was dropped by the OPG enforcer. */ + public enum Reason { + MISMATCH("mismatch"), + STRICT_MISSING("strict_missing"); + + private final String tag; + + Reason(String tag) { + this.tag = tag; + } + + /** Statsd tag value for the {@code reason} dimension on {@code org_guard.enforce} metrics. */ + public String tag() { + return tag; + } + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OrgGuardEnforcer.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OrgGuardEnforcer.java new file mode 100644 index 00000000000..d98412cade4 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OrgGuardEnforcer.java @@ -0,0 +1,126 @@ +package datadog.trace.core.propagation.opg; + +import datadog.trace.api.Config; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.bootstrap.instrumentation.api.TagContext; +import datadog.trace.core.monitor.HealthMetrics; +import datadog.trace.core.propagation.ExtractedContext; +import datadog.trace.core.propagation.PropagationTags; +import java.util.Set; +import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Enforces the Org Propagation Guard (OPG) on extracted contexts. When an inbound trace carries an + * Org Propagation Marker (OPM) that does not match the local one, the Datadog-side context + * (sampling priority, origin, {@code _dd.p.*} propagated tags) is dropped while parent identifiers, + * baggage, and non-{@code dd} tracestate vendor sections are preserved. + * + *

The master {@code DD_TRACE_ORG_GUARD_ENABLED} switch is handled at the {@link OrgGuard} + * factory; when disabled this class is never instantiated. Two configuration knobs shape behavior + * once enabled: + * + *

    + *
  • {@code DD_TRACE_ORG_GUARD_STRICT} — when {@code true}, also enforces when the inbound OPM + * is absent. + *
  • {@code DD_TRACE_ORG_GUARD_TRUSTED_OPMS} — comma-separated allow-list of inbound OPMs that + * should be treated as trusted. + *
+ * + *

Enforcement never runs when the local OPM is unknown (the agent has not yet reported one). + */ +final class OrgGuardEnforcer { + + private static final Logger log = LoggerFactory.getLogger(OrgGuardEnforcer.class); + + private final boolean strict; + private final Set trustedOpms; + private final Supplier localOpmSupplier; + private final PropagationTags.Factory factory; + private final HealthMetrics healthMetrics; + + OrgGuardEnforcer( + Config config, + Supplier localOpmSupplier, + PropagationTags.Factory factory, + HealthMetrics healthMetrics) { + this( + config.isTraceOrgGuardStrict(), + config.getTraceOrgGuardTrustedOpms(), + localOpmSupplier, + factory, + healthMetrics); + } + + // Visible for testing. + OrgGuardEnforcer( + boolean strict, + Set trustedOpms, + Supplier localOpmSupplier, + PropagationTags.Factory factory, + HealthMetrics healthMetrics) { + this.strict = strict; + this.trustedOpms = trustedOpms; + this.localOpmSupplier = localOpmSupplier; + this.factory = factory; + this.healthMetrics = healthMetrics; + } + + Supplier localOpmSupplier() { + return localOpmSupplier; + } + + /** + * Returns {@code extracted} unchanged unless OPG enforcement applies, in which case it returns a + * fresh {@link ExtractedContext} with the Datadog-side context dropped. + */ + public TagContext enforce(TagContext extracted) { + if (!(extracted instanceof ExtractedContext)) { + return extracted; + } + ExtractedContext ctx = (ExtractedContext) extracted; + String localOpm = localOpmSupplier.get(); + if (localOpm == null) { + // We don't know our own OPM yet — never enforce. + return extracted; + } + CharSequence inboundCs = ctx.getPropagationTags().getOrgPropagationMarker(); + String inbound = inboundCs == null ? null : inboundCs.toString(); + + if (inbound == null) { + if (!strict) { + return extracted; + } + return strip(ctx, OrgGuard.Reason.STRICT_MISSING, localOpm, null); + } + if (localOpm.equals(inbound) || trustedOpms.contains(inbound)) { + return extracted; + } + return strip(ctx, OrgGuard.Reason.MISMATCH, localOpm, inbound); + } + + private ExtractedContext strip( + ExtractedContext ctx, OrgGuard.Reason reason, String localOpm, String inbound) { + log.debug( + "OPG enforcement: dropping dd context (reason={}, inbound={}, local={})", + reason.tag(), + inbound, + localOpm); + healthMetrics.onOrgGuardEnforce(reason); + + PropagationTags stripped = factory.emptyW3C(ctx.getPropagationTags().getW3CTracestate()); + return new ExtractedContext( + ctx.getTraceId(), + ctx.getSpanId(), + PrioritySampling.UNSET, + /* origin */ null, + ctx.getEndToEndStartTime(), + ctx.getBaggage(), + ctx.getTags(), + /* httpHeaders */ null, + stripped, + ctx.getTraceConfig(), + ctx.getPropagationStyle()); + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OrgGuardEnforcingExtractor.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OrgGuardEnforcingExtractor.java new file mode 100644 index 00000000000..c0526bf6dd8 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/opg/OrgGuardEnforcingExtractor.java @@ -0,0 +1,31 @@ +package datadog.trace.core.propagation.opg; + +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; +import datadog.trace.bootstrap.instrumentation.api.TagContext; +import datadog.trace.core.propagation.HttpCodec; + +/** + * Decorates an {@link HttpCodec.Extractor} so that, after the underlying codec extracts a context, + * it is run through the {@link OrgGuardEnforcer} which may strip the Datadog-side context when the + * inbound Org Propagation Marker (OPM) does not match the local one. + */ +final class OrgGuardEnforcingExtractor implements HttpCodec.Extractor { + + private final HttpCodec.Extractor delegate; + private final OrgGuardEnforcer enforcer; + + OrgGuardEnforcingExtractor(HttpCodec.Extractor delegate, OrgGuardEnforcer enforcer) { + this.delegate = delegate; + this.enforcer = enforcer; + } + + @Override + public TagContext extract(C carrier, AgentPropagation.ContextVisitor getter) { + return enforcer.enforce(delegate.extract(carrier, getter)); + } + + @Override + public void cleanup() { + delegate.cleanup(); + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java index c1bcd4535a7..ec7a9a05feb 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/DatadogPTagsCodec.java @@ -63,6 +63,7 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { TagValue decisionMakerTagValue = null; TagValue traceIdTagValue = null; int traceSource = 0; + TagValue orgPropagationMarkerTagValue = null; while (tagPos < len) { int tagKeyEndsAt = validateCharsUntilSeparatorOrEnd( @@ -99,6 +100,8 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { traceIdTagValue = tagValue; } else if (tagKey.equals(TRACE_SOURCE_TAG)) { traceSource = ProductTraceSource.parseBitfieldHex(tagValue.toString()); + } else if (tagKey.equals(ORG_PROPAGATION_MARKER_TAG)) { + orgPropagationMarkerTagValue = tagValue; } else { if (tagPairs == null) { // This is roughly the size of a two element linked list but can hold six @@ -111,7 +114,12 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { } tagPos = tagValueEndsAt + 1; } - return tagsFactory.createValid(tagPairs, decisionMakerTagValue, traceIdTagValue, traceSource); + return tagsFactory.createValid( + tagPairs, + decisionMakerTagValue, + traceIdTagValue, + traceSource, + orgPropagationMarkerTagValue); } @Override diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java index 9447cb5d021..5dc2eaafc7d 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java @@ -19,6 +19,7 @@ abstract class PTagsCodec { protected static final TagKey TRACE_SOURCE_TAG = TagKey.from("ts"); protected static final TagKey DEBUG_TAG = TagKey.from("debug"); protected static final TagKey KNUTH_SAMPLING_RATE_TAG = TagKey.from("ksr"); + protected static final TagKey ORG_PROPAGATION_MARKER_TAG = TagKey.from("opm"); protected static final String PROPAGATION_ERROR_MALFORMED_TID = "malformed_tid "; protected static final String PROPAGATION_ERROR_INCONSISTENT_TID = "inconsistent_tid "; protected static final TagKey UPSTREAM_SERVICES_DEPRECATED_TAG = TagKey.from("upstream_services"); @@ -55,6 +56,11 @@ static String headerValue(PTagsCodec codec, PTags ptags) { codec.appendTag( sb, KNUTH_SAMPLING_RATE_TAG, ptags.getKnuthSamplingRateTagValue(), size); } + if (ptags.getOrgPropagationMarkerTagValue() != null) { + size = + codec.appendTag( + sb, ORG_PROPAGATION_MARKER_TAG, ptags.getOrgPropagationMarkerTagValue(), size); + } Iterator it = ptags.getTagPairs().iterator(); while (it.hasNext() && !codec.isTooLarge(sb, size)) { TagElement tagKey = it.next(); @@ -114,6 +120,11 @@ static void fillTagMap(PTags propagationTags, Map tagMap) { KNUTH_SAMPLING_RATE_TAG.forType(Encoding.DATADOG).toString(), propagationTags.getKnuthSamplingRateTagValue().forType(Encoding.DATADOG).toString()); } + if (propagationTags.getOrgPropagationMarkerTagValue() != null) { + tagMap.put( + ORG_PROPAGATION_MARKER_TAG.forType(Encoding.DATADOG).toString(), + propagationTags.getOrgPropagationMarkerTagValue().forType(Encoding.DATADOG).toString()); + } if (propagationTags.getTraceIdHighOrderBitsHexTagValue() != null) { tagMap.put( TRACE_ID_TAG.forType(Encoding.DATADOG).toString(), diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index bb544f926ac..e11171e2165 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java @@ -4,6 +4,7 @@ import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; import static datadog.trace.core.propagation.ptags.PTagsCodec.DECISION_MAKER_TAG; import static datadog.trace.core.propagation.ptags.PTagsCodec.KNUTH_SAMPLING_RATE_TAG; +import static datadog.trace.core.propagation.ptags.PTagsCodec.ORG_PROPAGATION_MARKER_TAG; import static datadog.trace.core.propagation.ptags.PTagsCodec.TRACE_ID_TAG; import static datadog.trace.core.propagation.ptags.PTagsCodec.TRACE_SOURCE_TAG; @@ -49,7 +50,7 @@ PTagsCodec getDecoderEncoder(@Nonnull HeaderType headerType) { @Override public final PropagationTags empty() { - return createValid(null, null, null, ProductTraceSource.UNSET); + return createValid(null, null, null, ProductTraceSource.UNSET, null); } @Override @@ -57,12 +58,27 @@ public final PropagationTags fromHeaderValue(@Nonnull HeaderType headerType, Str return DEC_ENC_MAP.get(headerType).fromHeaderValue(this, value); } + @Override + public final PropagationTags emptyW3C(String originalTracestate) { + if (originalTracestate == null || originalTracestate.isEmpty()) { + return empty(); + } + return W3CPTagsCodec.empty(this, originalTracestate); + } + PropagationTags createValid( List tagPairs, TagValue decisionMakerTagValue, TagValue traceIdTagValue, - int productTraceSource) { - return new PTags(this, tagPairs, decisionMakerTagValue, traceIdTagValue, productTraceSource); + int productTraceSource, + TagValue orgPropagationMarkerTagValue) { + return new PTags( + this, + tagPairs, + decisionMakerTagValue, + traceIdTagValue, + productTraceSource, + orgPropagationMarkerTagValue); } PropagationTags createInvalid(String error) { @@ -94,6 +110,8 @@ static class PTags extends PropagationTags { private volatile double knuthSamplingRate = Double.NaN; private volatile TagValue knuthSamplingRateTagValue; + private volatile TagValue orgPropagationMarkerTagValue; + // Static cache for the most-recently-seen rate → TagValue. In steady state a service uses one // rate, so this eliminates the char[] + String allocation on every new PTags instance. // Writes are benign-racy: two threads computing the same rate produce equal TagValues. @@ -134,12 +152,13 @@ static class PTags extends PropagationTags { */ private volatile CharSequence lastParentId; - public PTags( + PTags( PTagsFactory factory, List tagPairs, TagValue decisionMakerTagValue, TagValue traceIdTagValue, - int traceSource) { + int traceSource, + TagValue orgPropagationMarkerTagValue) { this( factory, tagPairs, @@ -148,7 +167,8 @@ public PTags( traceSource, PrioritySampling.UNSET, null, - null); + null, + orgPropagationMarkerTagValue); } PTags( @@ -159,7 +179,8 @@ public PTags( int traceSource, int samplingPriority, CharSequence origin, - CharSequence lastParentId) { + CharSequence lastParentId, + TagValue orgPropagationMarkerTagValue) { assert tagPairs == null || tagPairs.size() % 2 == 0; this.factory = factory; this.tagPairs = tagPairs; @@ -169,6 +190,7 @@ public PTags( this.samplingPriority = samplingPriority; this.origin = origin; this.lastParentId = lastParentId; + this.orgPropagationMarkerTagValue = orgPropagationMarkerTagValue; if (traceIdTagValue != null) { CharSequence traceIdHighOrderBitsHex = traceIdTagValue.forType(TagElement.Encoding.DATADOG); this.traceIdHighOrderBits = @@ -189,6 +211,7 @@ static PTags withError(PTagsFactory factory, String error) { ProductTraceSource.UNSET, PrioritySampling.UNSET, null, + null, null); pTags.error = error; return pTags; @@ -335,6 +358,26 @@ TagValue getKnuthSamplingRateTagValue() { return knuthSamplingRateTagValue; } + @Override + public CharSequence getOrgPropagationMarker() { + return orgPropagationMarkerTagValue; + } + + @Override + public void updateOrgPropagationMarker(CharSequence opm) { + TagValue newValue = opm == null ? null : TagValue.from(opm); + if (!Objects.equals(this.orgPropagationMarkerTagValue, newValue)) { + clearCachedHeader(DATADOG); + clearCachedHeader(W3C); + invalidateXDatadogTagsSize(); + this.orgPropagationMarkerTagValue = newValue; + } + } + + TagValue getOrgPropagationMarkerTagValue() { + return orgPropagationMarkerTagValue; + } + @Override public int getSamplingPriority() { return samplingPriority; @@ -463,6 +506,9 @@ int getXDatadogTagsSize() { size = PTagsCodec.calcXDatadogTagsSize( size, KNUTH_SAMPLING_RATE_TAG, getKnuthSamplingRateTagValue()); + size = + PTagsCodec.calcXDatadogTagsSize( + size, ORG_PROPAGATION_MARKER_TAG, getOrgPropagationMarkerTagValue()); int currentProductTraceSource = traceSource; if (currentProductTraceSource != ProductTraceSource.UNSET) { size = diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java index 8fe534ccfd4..d7f0d0e02a3 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java @@ -97,6 +97,7 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { int traceSource = ProductTraceSource.UNSET; int maxUnknownSize = 0; CharSequence lastParentId = null; + TagValue orgPropagationMarkerTagValue = null; while (tagPos < ddMemberValueEnd) { int tagKeyEndsAt = validateCharsUntilSeparatorOrEnd( @@ -159,6 +160,8 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { traceIdTagValue = tagValue; } else if (tagKey.equals(TRACE_SOURCE_TAG)) { traceSource = ProductTraceSource.parseBitfieldHex(tagValue.toString()); + } else if (tagKey.equals(ORG_PROPAGATION_MARKER_TAG)) { + orgPropagationMarkerTagValue = tagValue; } else { if (tagPairs == null) { // This is roughly the size of a two element linked list but can hold six @@ -191,7 +194,8 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { ddMemberStart, ddMemberValueEnd, maxUnknownSize, - lastParentId); + lastParentId, + orgPropagationMarkerTagValue); } @Override @@ -693,7 +697,7 @@ private static int cleanUpAndAppendSuffix(StringBuilder sb, PTags ptags, int siz return size; } - private static W3CPTags empty(PTagsFactory factory, String original) { + static W3CPTags empty(PTagsFactory factory, String original) { return empty(factory, original, 0, -1, -1); } @@ -716,6 +720,7 @@ private static W3CPTags empty( ddMemberStart, ddMemberValueEnd, 0, + null, null); } @@ -750,7 +755,8 @@ public W3CPTags( int ddMemberStart, int ddMemberValueEnd, int maxUnknownSize, - CharSequence lastParentId) { + CharSequence lastParentId, + TagValue orgPropagationMarkerTagValue) { super( factory, tagPairs, @@ -759,7 +765,8 @@ public W3CPTags( traceSource, samplingPriority, origin, - lastParentId); + lastParentId, + orgPropagationMarkerTagValue); this.tracestate = original; this.firstMemberStart = firstMemberStart; this.ddMemberStart = ddMemberStart; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/OrgGuardEndToEndTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OrgGuardEndToEndTest.java new file mode 100644 index 00000000000..8d8a110250e --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/OrgGuardEndToEndTest.java @@ -0,0 +1,216 @@ +package datadog.trace.core.propagation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.RETURNS_DEFAULTS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.context.Context; +import datadog.context.propagation.CarrierSetter; +import datadog.context.propagation.CarrierVisitor; +import datadog.trace.api.Config; +import datadog.trace.api.DDTraceId; +import datadog.trace.api.TraceConfig; +import datadog.trace.api.TracePropagationStyle; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.core.DDSpanContext; +import datadog.trace.core.TraceCollector; +import datadog.trace.core.monitor.HealthMetrics; +import datadog.trace.core.propagation.opg.OrgGuard; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Org Propagation Guard end-to-end propagator wiring") +class OrgGuardEndToEndTest { + + private static final MapSetter SETTER = new MapSetter(); + private static final MapVisitor VISITOR = new MapVisitor(); + + private PropagationTags.Factory factory; + private HealthMetrics healthMetrics; + private Supplier traceConfigSupplier; + + @BeforeEach + void setUp() { + factory = PropagationTags.factory(); + healthMetrics = mock(HealthMetrics.class); + TraceConfig tc = mock(TraceConfig.class); + traceConfigSupplier = () -> tc; + } + + @Test + @DisplayName("inject stamps the local OPM into x-datadog-tags and tracestate") + void injectStampsLocalOpm() { + TracingPropagator propagator = buildPropagator(true, false, Collections.emptySet(), () -> "L1"); + + Map carrier = new HashMap<>(); + Context ctx = Context.root().with(buildSpanForInjection(/*opmInTags*/ null)); + propagator.inject(ctx, carrier, SETTER); + + String datadogTags = carrier.get(DatadogHttpCodec.DATADOG_TAGS_KEY); + assertNotNull(datadogTags, "x-datadog-tags missing: " + carrier); + assertTrue(datadogTags.contains("_dd.p.opm=L1"), "datadog-tags = " + datadogTags); + + String tracestate = carrier.get("tracestate"); + assertNotNull(tracestate, "tracestate missing: " + carrier); + assertTrue(tracestate.contains("t.opm:L1"), "tracestate = " + tracestate); + } + + @Test + @DisplayName("inject overrides any inherited OPM with the local one") + void injectOverridesInheritedOpm() { + TracingPropagator propagator = buildPropagator(true, false, Collections.emptySet(), () -> "L1"); + + Map carrier = new HashMap<>(); + Context ctx = Context.root().with(buildSpanForInjection("upstream-X")); + propagator.inject(ctx, carrier, SETTER); + + String datadogTags = carrier.get(DatadogHttpCodec.DATADOG_TAGS_KEY); + assertNotNull(datadogTags); + assertTrue(datadogTags.contains("_dd.p.opm=L1"), "datadog-tags = " + datadogTags); + assertFalse(datadogTags.contains("_dd.p.opm=upstream-X"), "datadog-tags = " + datadogTags); + } + + @Test + @DisplayName("extract drops dd context when OPM mismatches and enforcement is on") + void extractStripsOnMismatch() { + TracingPropagator propagator = buildPropagator(true, false, Collections.emptySet(), () -> "L1"); + + Map headers = new HashMap<>(); + headers.put(DatadogHttpCodec.TRACE_ID_KEY, "123"); + headers.put(DatadogHttpCodec.SPAN_ID_KEY, "456"); + headers.put(DatadogHttpCodec.SAMPLING_PRIORITY_KEY, "2"); + headers.put(DatadogHttpCodec.DATADOG_TAGS_KEY, "_dd.p.opm=X1,_dd.p.dm=-4"); + + Context extracted = propagator.extract(Context.root(), headers, VISITOR); + AgentSpan span = AgentSpan.fromContext(extracted); + assertNotNull(span, "extracted span missing"); + ExtractedContext ec = (ExtractedContext) span.context(); + assertEquals(DDTraceId.from(123L), ec.getTraceId()); + assertEquals(456L, ec.getSpanId()); + assertEquals(PrioritySampling.UNSET, ec.getSamplingPriority()); + assertNull(ec.getOrigin()); + assertNull(ec.getPropagationTags().getOrgPropagationMarker()); + } + + @Test + @DisplayName("extract honors trusted OPMs even when they differ from the local one") + void extractTrustedOpm() { + Set trusted = new HashSet<>(); + trusted.add("TRUSTED1"); + TracingPropagator propagator = buildPropagator(true, false, trusted, () -> "L1"); + + Map headers = new HashMap<>(); + headers.put(DatadogHttpCodec.TRACE_ID_KEY, "123"); + headers.put(DatadogHttpCodec.SPAN_ID_KEY, "456"); + headers.put(DatadogHttpCodec.SAMPLING_PRIORITY_KEY, "2"); + headers.put(DatadogHttpCodec.DATADOG_TAGS_KEY, "_dd.p.opm=TRUSTED1,_dd.p.dm=-4"); + + Context extracted = propagator.extract(Context.root(), headers, VISITOR); + ExtractedContext ec = (ExtractedContext) AgentSpan.fromContext(extracted).context(); + assertEquals(2, ec.getSamplingPriority()); + assertEquals("TRUSTED1", ec.getPropagationTags().getOrgPropagationMarker().toString()); + } + + @Test + @DisplayName("extract+inject preserves non-dd tracestate vendors after stripping") + void roundTripPreservesForeignVendors() { + TracingPropagator propagator = buildPropagator(true, false, Collections.emptySet(), () -> "L1"); + + Map headers = new HashMap<>(); + headers.put( + "traceparent", + "00-0000000000000000000000000000007b-00000000000001c8-01"); // 0x7b=123, 0x1c8=456 + headers.put("tracestate", "dd=s:2;o:foo;t.opm:upstream-X;t.dm:-4,vendor1=abc,vendor2=def"); + + Context extracted = propagator.extract(Context.root(), headers, VISITOR); + ExtractedContext ec = (ExtractedContext) AgentSpan.fromContext(extracted).context(); + assertEquals(PrioritySampling.UNSET, ec.getSamplingPriority(), "should be stripped"); + + String reEncoded = ec.getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); + assertNotNull(reEncoded, "re-encoded tracestate is null"); + assertTrue(reEncoded.contains("vendor1=abc"), "vendor1 missing: " + reEncoded); + assertTrue(reEncoded.contains("vendor2=def"), "vendor2 missing: " + reEncoded); + } + + // ---- helpers ---- + + private TracingPropagator buildPropagator( + boolean enabled, boolean strict, Set trusted, Supplier localOpmSupplier) { + Config config = mock(Config.class, RETURNS_DEFAULTS); + when(config.getxDatadogTagsMaxLength()).thenReturn(512); + when(config.getTracePropagationStylesToExtract()) + .thenReturn(EnumSet.of(TracePropagationStyle.DATADOG, TracePropagationStyle.TRACECONTEXT)); + when(config.getTracePropagationStylesToInject()) + .thenReturn(EnumSet.of(TracePropagationStyle.DATADOG, TracePropagationStyle.TRACECONTEXT)); + when(config.isTracePropagationExtractFirst()).thenReturn(false); + when(config.isAwsPropagationEnabled()).thenReturn(false); + when(config.getBaggageMapping()).thenReturn(Collections.emptyMap()); + when(config.isTracePropagationStyleB3PaddingEnabled()).thenReturn(false); + when(config.isApmTracingEnabled()).thenReturn(true); + when(config.isTraceOrgGuardEnabled()).thenReturn(enabled); + when(config.isTraceOrgGuardStrict()).thenReturn(strict); + when(config.getTraceOrgGuardTrustedOpms()).thenReturn(trusted); + + HttpCodec.Extractor extractor = HttpCodec.createExtractor(config, traceConfigSupplier); + HttpCodec.Injector injector = + HttpCodec.createInjector( + config, config.getTracePropagationStylesToInject(), Collections.emptyMap()); + OrgGuard orgGuard = OrgGuard.create(config, localOpmSupplier, factory, healthMetrics); + return new TracingPropagator( + true, orgGuard.decorateInjector(injector), orgGuard.decorateExtractor(extractor)); + } + + /** + * Build a minimal AgentSpan wrapping a (mocked) DDSpanContext whose propagation tags can be + * controlled by the test. + */ + private AgentSpan buildSpanForInjection(String preExistingOpm) { + PropagationTags tags = factory.empty(); + if (preExistingOpm != null) { + tags.updateOrgPropagationMarker(preExistingOpm); + } + DDSpanContext ddCtx = mock(DDSpanContext.class); + when(ddCtx.getTraceId()).thenReturn(DDTraceId.from(123L)); + when(ddCtx.getSpanId()).thenReturn(456L); + when(ddCtx.getSamplingPriority()).thenReturn(2); + when(ddCtx.lockSamplingPriority()).thenReturn(true); + when(ddCtx.getOrigin()).thenReturn(null); + when(ddCtx.getEndToEndStartTime()).thenReturn(0L); + when(ddCtx.baggageItems()).thenReturn(Collections.emptySet()); + when(ddCtx.getPropagationTags()).thenReturn(tags); + TraceCollector collector = mock(TraceCollector.class); + when(ddCtx.getTraceCollector()).thenReturn(collector); + return AgentSpan.fromSpanContext(ddCtx); + } + + private static final class MapSetter implements CarrierSetter> { + @Override + public void set(Map carrier, String key, String value) { + carrier.put(key, value); + } + } + + private static final class MapVisitor implements CarrierVisitor> { + @Override + public void forEachKeyValue( + Map carrier, java.util.function.BiConsumer classifier) { + for (Map.Entry e : carrier.entrySet()) { + classifier.accept(e.getKey(), e.getValue()); + } + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/PropagationTagsOpmTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/PropagationTagsOpmTest.java new file mode 100644 index 00000000000..ed7c1c98dde --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/PropagationTagsOpmTest.java @@ -0,0 +1,123 @@ +package datadog.trace.core.propagation; + +import static datadog.trace.core.propagation.PropagationTags.HeaderType.DATADOG; +import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("PropagationTags OPM round-trip") +class PropagationTagsOpmTest { + + private PropagationTags.Factory factory; + + @BeforeEach + void setUp() { + factory = PropagationTags.factory(); + } + + @Test + @DisplayName("Datadog: extract _dd.p.opm from x-datadog-tags then re-serialize it") + void datadogRoundTrip() { + PropagationTags tags = factory.fromHeaderValue(DATADOG, "_dd.p.opm=abc123def0"); + assertNotNull(tags.getOrgPropagationMarker()); + assertEquals("abc123def0", tags.getOrgPropagationMarker().toString()); + String header = tags.headerValue(DATADOG); + assertNotNull(header); + assertTrue(header.contains("_dd.p.opm=abc123def0"), "header was: " + header); + } + + @Test + @DisplayName("W3C: extract t.opm from tracestate dd= section then re-serialize it") + void w3cRoundTrip() { + PropagationTags tags = factory.fromHeaderValue(W3C, "dd=t.opm:abc123def0"); + assertNotNull(tags.getOrgPropagationMarker()); + assertEquals("abc123def0", tags.getOrgPropagationMarker().toString()); + String header = tags.headerValue(W3C); + assertNotNull(header); + assertTrue(header.contains("t.opm:abc123def0"), "header was: " + header); + } + + @Test + @DisplayName("update overrides any previously extracted OPM (W3C)") + void updateOverridesExtractedW3C() { + PropagationTags tags = factory.fromHeaderValue(W3C, "dd=t.opm:upstream-abc"); + tags.updateOrgPropagationMarker("local-xyz"); + assertEquals("local-xyz", tags.getOrgPropagationMarker().toString()); + String header = tags.headerValue(W3C); + assertNotNull(header); + assertTrue(header.contains("t.opm:local-xyz"), "header was: " + header); + } + + @Test + @DisplayName("update overrides any previously extracted OPM (Datadog)") + void updateOverridesExtractedDatadog() { + PropagationTags tags = factory.fromHeaderValue(DATADOG, "_dd.p.opm=upstream-abc"); + tags.updateOrgPropagationMarker("local-xyz"); + assertEquals("local-xyz", tags.getOrgPropagationMarker().toString()); + String header = tags.headerValue(DATADOG); + assertNotNull(header); + assertTrue(header.contains("_dd.p.opm=local-xyz"), "header was: " + header); + } + + @Test + @DisplayName("update with null clears the OPM") + void updateWithNullClears() { + PropagationTags tags = factory.fromHeaderValue(W3C, "dd=t.opm:abc"); + tags.updateOrgPropagationMarker(null); + assertNull(tags.getOrgPropagationMarker()); + String header = tags.headerValue(W3C); + if (header != null) { + assertTrue(!header.contains("t.opm"), "header still had t.opm: " + header); + } + } + + @Test + @DisplayName("emptyW3C preserves non-dd vendor tracestate sections and drops dd content") + void emptyW3CPreservesNonDdVendors() { + String original = "dd=s:1;o:foo;t.dm:-4;t.opm:upstream-abc,vendor1=abc,vendor2=def"; + PropagationTags stripped = factory.emptyW3C(original); + assertNull(stripped.getOrgPropagationMarker()); + String reEncoded = stripped.headerValue(W3C); + assertNotNull(reEncoded); + assertTrue(!reEncoded.contains("dd="), "should drop dd member but was: " + reEncoded); + assertTrue(reEncoded.contains("vendor1=abc"), "vendor1 missing: " + reEncoded); + assertTrue(reEncoded.contains("vendor2=def"), "vendor2 missing: " + reEncoded); + } + + @Test + @DisplayName("emptyW3C with null tracestate behaves like empty()") + void emptyW3CNullTracestate() { + PropagationTags stripped = factory.emptyW3C(null); + assertNull(stripped.getOrgPropagationMarker()); + assertNull(stripped.headerValue(W3C)); + } + + @Test + @DisplayName("emptyW3C with empty string tracestate behaves like empty()") + void emptyW3CEmptyTracestate() { + PropagationTags stripped = factory.emptyW3C(""); + assertNull(stripped.getOrgPropagationMarker()); + assertNull(stripped.headerValue(W3C)); + } + + @Test + @DisplayName("empty tags can have an OPM stamped on them and serialize it") + void emptyTagsCanReceiveOpm() { + PropagationTags tags = factory.empty(); + assertNull(tags.getOrgPropagationMarker()); + tags.updateOrgPropagationMarker("local-xyz"); + assertEquals("local-xyz", tags.getOrgPropagationMarker().toString()); + String datadog = tags.headerValue(DATADOG); + assertNotNull(datadog); + assertTrue(datadog.contains("_dd.p.opm=local-xyz"), "datadog header: " + datadog); + String w3c = tags.headerValue(W3C); + assertNotNull(w3c); + assertTrue(w3c.contains("t.opm:local-xyz"), "w3c header: " + w3c); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OpmStampingInjectorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OpmStampingInjectorTest.java new file mode 100644 index 00000000000..95241e36e7f --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OpmStampingInjectorTest.java @@ -0,0 +1,122 @@ +package datadog.trace.core.propagation.opg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.context.propagation.CarrierSetter; +import datadog.trace.core.DDSpanContext; +import datadog.trace.core.propagation.HttpCodec; +import datadog.trace.core.propagation.PropagationTags; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; +import javax.annotation.ParametersAreNonnullByDefault; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("OpmStampingInjector") +class OpmStampingInjectorTest { + + private static final MapSetter MAP_SETTER = new MapSetter(); + + @Test + @DisplayName("stamps the local OPM into PropagationTags before delegating") + void stampsLocalOpm() { + PropagationTags.Factory factory = PropagationTags.factory(); + PropagationTags tags = factory.fromHeaderValue(PropagationTags.HeaderType.W3C, ""); + + DDSpanContext ctx = mock(DDSpanContext.class); + when(ctx.getPropagationTags()).thenReturn(tags); + + HttpCodec.Injector delegate = new NoopInjector(); + Supplier supplier = () -> "local-opm-1"; + OpmStampingInjector wrapped = new OpmStampingInjector(delegate, supplier); + + Map carrier = new HashMap<>(); + wrapped.inject(ctx, carrier, MAP_SETTER); + + assertNotNull(tags.getOrgPropagationMarker()); + assertEquals("local-opm-1", tags.getOrgPropagationMarker().toString()); + } + + @Test + @DisplayName("with null supplier value, leaves PropagationTags untouched") + void supplierNullLeavesTagsUntouched() { + PropagationTags.Factory factory = PropagationTags.factory(); + PropagationTags tags = + factory.fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.opm=upstream-abc"); + + DDSpanContext ctx = mock(DDSpanContext.class); + when(ctx.getPropagationTags()).thenReturn(tags); + + HttpCodec.Injector delegate = new NoopInjector(); + OpmStampingInjector wrapped = new OpmStampingInjector(delegate, () -> null); + + Map carrier = new HashMap<>(); + wrapped.inject(ctx, carrier, MAP_SETTER); + + assertNotNull(tags.getOrgPropagationMarker()); + assertEquals("upstream-abc", tags.getOrgPropagationMarker().toString()); + } + + @Test + @DisplayName("local supplier value overrides any inherited OPM") + void localOpmOverridesInherited() { + PropagationTags.Factory factory = PropagationTags.factory(); + PropagationTags tags = + factory.fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.opm=upstream-abc"); + + DDSpanContext ctx = mock(DDSpanContext.class); + when(ctx.getPropagationTags()).thenReturn(tags); + + HttpCodec.Injector delegate = new NoopInjector(); + OpmStampingInjector wrapped = new OpmStampingInjector(delegate, () -> "local-xyz"); + + Map carrier = new HashMap<>(); + wrapped.inject(ctx, carrier, MAP_SETTER); + + assertEquals("local-xyz", tags.getOrgPropagationMarker().toString()); + } + + @Test + @DisplayName("delegate is invoked exactly once per inject call") + void delegateInvoked() { + PropagationTags.Factory factory = PropagationTags.factory(); + PropagationTags tags = factory.empty(); + + DDSpanContext ctx = mock(DDSpanContext.class); + when(ctx.getPropagationTags()).thenReturn(tags); + + CountingInjector delegate = new CountingInjector(); + OpmStampingInjector wrapped = new OpmStampingInjector(delegate, () -> null); + + wrapped.inject(ctx, new HashMap<>(), MAP_SETTER); + assertEquals(1, delegate.calls); + assertNull(tags.getOrgPropagationMarker()); + } + + private static final class NoopInjector implements HttpCodec.Injector { + @Override + public void inject(DDSpanContext context, C carrier, CarrierSetter setter) {} + } + + private static final class CountingInjector implements HttpCodec.Injector { + int calls = 0; + + @Override + public void inject(DDSpanContext context, C carrier, CarrierSetter setter) { + calls++; + } + } + + @ParametersAreNonnullByDefault + private static final class MapSetter implements CarrierSetter> { + @Override + public void set(Map carrier, String key, String value) { + carrier.put(key, value); + } + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java new file mode 100644 index 00000000000..f0abed549b3 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardEnforcerTest.java @@ -0,0 +1,184 @@ +package datadog.trace.core.propagation.opg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import datadog.trace.api.DDTraceId; +import datadog.trace.api.TracePropagationStyle; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.bootstrap.instrumentation.api.TagContext; +import datadog.trace.core.monitor.HealthMetrics; +import datadog.trace.core.propagation.ExtractedContext; +import datadog.trace.core.propagation.PropagationTags; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("OrgGuardEnforcer truth table") +class OrgGuardEnforcerTest { + + private PropagationTags.Factory factory; + private HealthMetrics healthMetrics; + + @BeforeEach + void setUp() { + factory = PropagationTags.factory(); + healthMetrics = mock(HealthMetrics.class); + } + + @Test + @DisplayName("local OPM unknown -> no enforcement") + void localUnknown() { + OrgGuardEnforcer enforcer = enforcer(false, Collections.emptySet(), () -> null); + ExtractedContext ctx = ctxWithOpm("X"); + assertSame(ctx, enforcer.enforce(ctx)); + verify(healthMetrics, never()).onOrgGuardEnforce(any(OrgGuard.Reason.class)); + } + + @Test + @DisplayName("lax: inbound OPM missing -> no enforcement") + void laxInboundMissing() { + OrgGuardEnforcer enforcer = enforcer(false, Collections.emptySet(), () -> "L"); + ExtractedContext ctx = ctxWithOpm(null); + assertSame(ctx, enforcer.enforce(ctx)); + verify(healthMetrics, never()).onOrgGuardEnforce(any(OrgGuard.Reason.class)); + } + + @Test + @DisplayName("strict: inbound OPM missing -> strip with strict_missing") + void strictInboundMissing() { + OrgGuardEnforcer enforcer = enforcer(true, Collections.emptySet(), () -> "L"); + ExtractedContext ctx = ctxWithOpm(null, /*samplingPriority*/ 2, "synthetics"); + TagContext result = enforcer.enforce(ctx); + assertNotSame(ctx, result); + assertStripped((ExtractedContext) result, ctx); + verify(healthMetrics, times(1)).onOrgGuardEnforce(OrgGuard.Reason.STRICT_MISSING); + } + + @Test + @DisplayName("inbound OPM matches local -> no enforcement") + void match() { + OrgGuardEnforcer enforcer = enforcer(false, Collections.emptySet(), () -> "L"); + ExtractedContext ctx = ctxWithOpm("L"); + assertSame(ctx, enforcer.enforce(ctx)); + verify(healthMetrics, never()).onOrgGuardEnforce(any(OrgGuard.Reason.class)); + } + + @Test + @DisplayName("inbound OPM is in trusted list -> no enforcement") + void trusted() { + Set trusted = new HashSet<>(); + trusted.add("X"); + trusted.add("Y"); + OrgGuardEnforcer enforcer = enforcer(false, trusted, () -> "L"); + ExtractedContext ctx = ctxWithOpm("X"); + assertSame(ctx, enforcer.enforce(ctx)); + verify(healthMetrics, never()).onOrgGuardEnforce(any(OrgGuard.Reason.class)); + } + + @Test + @DisplayName("lax: inbound != local -> strip with mismatch") + void mismatchLax() { + OrgGuardEnforcer enforcer = enforcer(false, Collections.emptySet(), () -> "L"); + ExtractedContext ctx = ctxWithOpm("X", /*samplingPriority*/ 2, "synthetics"); + TagContext result = enforcer.enforce(ctx); + assertNotSame(ctx, result); + assertStripped((ExtractedContext) result, ctx); + verify(healthMetrics, times(1)).onOrgGuardEnforce(OrgGuard.Reason.MISMATCH); + } + + @Test + @DisplayName("strict: inbound != local -> strip with mismatch") + void mismatchStrict() { + OrgGuardEnforcer enforcer = enforcer(true, Collections.emptySet(), () -> "L"); + ExtractedContext ctx = ctxWithOpm("X", /*samplingPriority*/ 2, "synthetics"); + TagContext result = enforcer.enforce(ctx); + assertNotSame(ctx, result); + assertStripped((ExtractedContext) result, ctx); + verify(healthMetrics, times(1)).onOrgGuardEnforce(OrgGuard.Reason.MISMATCH); + } + + @Test + @DisplayName("partial TagContext (not ExtractedContext) -> always pass through") + void partialContext() { + OrgGuardEnforcer enforcer = enforcer(true, Collections.emptySet(), () -> "L"); + TagContext partial = new TagContext("upstream", null); + assertSame(partial, enforcer.enforce(partial)); + verify(healthMetrics, never()).onOrgGuardEnforce(any(OrgGuard.Reason.class)); + } + + @Test + @DisplayName("null input is passed through") + void nullInput() { + OrgGuardEnforcer enforcer = enforcer(true, Collections.emptySet(), () -> "L"); + assertNull(enforcer.enforce(null)); + verify(healthMetrics, never()).onOrgGuardEnforce(any(OrgGuard.Reason.class)); + } + + @Test + @DisplayName("strip preserves W3C non-dd vendor tracestate sections") + void stripPreservesNonDdVendors() { + OrgGuardEnforcer enforcer = enforcer(false, Collections.emptySet(), () -> "L"); + PropagationTags tags = + factory.fromHeaderValue( + PropagationTags.HeaderType.W3C, + "dd=s:1;o:foo;t.opm:upstream-X,vendor1=abc,vendor2=def"); + ExtractedContext ctx = + new ExtractedContext( + DDTraceId.from(123L), 456L, 2, "origin", tags, TracePropagationStyle.TRACECONTEXT); + TagContext result = enforcer.enforce(ctx); + assertNotSame(ctx, result); + ExtractedContext stripped = (ExtractedContext) result; + String reEncoded = stripped.getPropagationTags().headerValue(PropagationTags.HeaderType.W3C); + assertNotNull(reEncoded); + assertFalse(reEncoded.contains("dd="), "dd= should be dropped: " + reEncoded); + assertTrue(reEncoded.contains("vendor1=abc"), "vendor1 missing: " + reEncoded); + assertTrue(reEncoded.contains("vendor2=def"), "vendor2 missing: " + reEncoded); + } + + // ---- helpers ---- + + private OrgGuardEnforcer enforcer( + boolean strict, Set trusted, Supplier localOpmSupplier) { + return new OrgGuardEnforcer(strict, trusted, localOpmSupplier, factory, healthMetrics); + } + + private ExtractedContext ctxWithOpm(String opm) { + return ctxWithOpm(opm, PrioritySampling.SAMPLER_KEEP, "origin"); + } + + private ExtractedContext ctxWithOpm(String opm, int samplingPriority, String origin) { + PropagationTags tags = factory.empty(); + if (opm != null) { + tags.updateOrgPropagationMarker(opm); + } + tags.updateTraceSamplingPriority(samplingPriority, /* mechanism = MANUAL */ 4); + tags.updateTraceOrigin(origin); + return new ExtractedContext( + DDTraceId.from(123L), 456L, samplingPriority, origin, tags, TracePropagationStyle.DATADOG); + } + + private static void assertStripped(ExtractedContext stripped, ExtractedContext original) { + assertEquals(original.getTraceId(), stripped.getTraceId()); + assertEquals(original.getSpanId(), stripped.getSpanId()); + assertEquals(PrioritySampling.UNSET, stripped.getSamplingPriority()); + assertNull(stripped.getOrigin()); + assertNull(stripped.getPropagationTags().getOrgPropagationMarker()); + assertNull(stripped.getPropagationTags().getOrigin()); + assertEquals(PrioritySampling.UNSET, stripped.getPropagationTags().getSamplingPriority()); + } +} diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardTest.java new file mode 100644 index 00000000000..f8521e34fdf --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/opg/OrgGuardTest.java @@ -0,0 +1,56 @@ +package datadog.trace.core.propagation.opg; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.RETURNS_DEFAULTS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import datadog.trace.api.Config; +import datadog.trace.core.monitor.HealthMetrics; +import datadog.trace.core.propagation.HttpCodec; +import datadog.trace.core.propagation.PropagationTags; +import java.util.Collections; +import java.util.function.Supplier; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("OrgGuard factory gating") +class OrgGuardTest { + + private static final Supplier LOCAL_OPM = () -> "L"; + + @Test + @DisplayName("disabled: decorate methods return the input unchanged") + void disabledIsZeroCost() { + Config config = mock(Config.class, RETURNS_DEFAULTS); + when(config.isTraceOrgGuardEnabled()).thenReturn(false); + + OrgGuard orgGuard = + OrgGuard.create(config, LOCAL_OPM, PropagationTags.factory(), mock(HealthMetrics.class)); + + HttpCodec.Extractor extractor = mock(HttpCodec.Extractor.class); + HttpCodec.Injector injector = mock(HttpCodec.Injector.class); + + assertSame(extractor, orgGuard.decorateExtractor(extractor)); + assertSame(injector, orgGuard.decorateInjector(injector)); + } + + @Test + @DisplayName("enabled: decorate methods wrap with the OPG decorators") + void enabledWrapsBothSides() { + Config config = mock(Config.class, RETURNS_DEFAULTS); + when(config.isTraceOrgGuardEnabled()).thenReturn(true); + when(config.isTraceOrgGuardStrict()).thenReturn(false); + when(config.getTraceOrgGuardTrustedOpms()).thenReturn(Collections.emptySet()); + + OrgGuard orgGuard = + OrgGuard.create(config, LOCAL_OPM, PropagationTags.factory(), mock(HealthMetrics.class)); + + HttpCodec.Extractor extractor = mock(HttpCodec.Extractor.class); + HttpCodec.Injector injector = mock(HttpCodec.Injector.class); + + assertInstanceOf(OrgGuardEnforcingExtractor.class, orgGuard.decorateExtractor(extractor)); + assertInstanceOf(OpmStampingInjector.class, orgGuard.decorateInjector(injector)); + } +} 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 a463887f61a..c7aaca56418 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -683,6 +683,9 @@ import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_FLUSH_INTERVAL; import static datadog.trace.api.config.TracerConfig.TRACE_LONG_RUNNING_INITIAL_FLUSH_INTERVAL; +import static datadog.trace.api.config.TracerConfig.TRACE_ORG_GUARD_ENABLED; +import static datadog.trace.api.config.TracerConfig.TRACE_ORG_GUARD_STRICT; +import static datadog.trace.api.config.TracerConfig.TRACE_ORG_GUARD_TRUSTED_OPMS; import static datadog.trace.api.config.TracerConfig.TRACE_PEER_HOSTNAME_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_PEER_SERVICE_COMPONENT_OVERRIDES; import static datadog.trace.api.config.TracerConfig.TRACE_PEER_SERVICE_DEFAULTS_ENABLED; @@ -927,6 +930,9 @@ public static String getHostName() { private final Set tracePropagationStylesToInject; private final TracePropagationBehaviorExtract tracePropagationBehaviorExtract; private final boolean tracePropagationExtractFirst; + private final boolean traceOrgGuardEnabled; + private final boolean traceOrgGuardStrict; + private final Set traceOrgGuardTrustedOpms; private final int traceBaggageMaxItems; private final int traceBaggageMaxBytes; private final List traceBaggageTagKeys; @@ -1933,6 +1939,10 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins tracePropagationExtractFirst = configProvider.getBoolean( TRACE_PROPAGATION_EXTRACT_FIRST, DEFAULT_TRACE_PROPAGATION_EXTRACT_FIRST); + traceOrgGuardEnabled = configProvider.getBoolean(TRACE_ORG_GUARD_ENABLED, false); + traceOrgGuardStrict = configProvider.getBoolean(TRACE_ORG_GUARD_STRICT, false); + traceOrgGuardTrustedOpms = + configProvider.getSet(TRACE_ORG_GUARD_TRUSTED_OPMS, Collections.emptySet()); traceInferredProxyEnabled = configProvider.getBoolean(TRACE_INFERRED_PROXY_SERVICES_ENABLED, false); @@ -3626,6 +3636,18 @@ public boolean isTracePropagationExtractFirst() { return tracePropagationExtractFirst; } + public boolean isTraceOrgGuardEnabled() { + return traceOrgGuardEnabled; + } + + public boolean isTraceOrgGuardStrict() { + return traceOrgGuardStrict; + } + + public Set getTraceOrgGuardTrustedOpms() { + return traceOrgGuardTrustedOpms; + } + public boolean isInferredProxyPropagationEnabled() { return traceInferredProxyEnabled; } diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 8db93e05399..d07ebf36f22 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -8601,6 +8601,30 @@ "aliases": [] } ], + "DD_TRACE_ORG_GUARD_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], + "DD_TRACE_ORG_GUARD_STRICT": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], + "DD_TRACE_ORG_GUARD_TRUSTED_OPMS": [ + { + "version": "A", + "type": "list", + "default": "", + "aliases": [] + } + ], "DD_TRACE_PARTIAL_FLUSH_ENABLED": [ { "version": "B",