diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java index caa590742b..9bf6b6624c 100644 --- a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java @@ -17,6 +17,7 @@ import com.newrelic.api.agent.DatastoreParameters; import com.newrelic.api.agent.HttpParameters; import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Segment; import com.newrelic.api.agent.Token; import com.nr.agent.instrumentation.utils.AttributesHelper; import com.nr.agent.instrumentation.utils.span.AttributeMapper; @@ -83,6 +84,7 @@ public class ExitTracerSpan implements ReadWriteSpan { .collect(Collectors.toSet())); final ExitTracer tracer; + private final Segment segment; private final SpanKind spanKind; private final InstrumentationLibraryInfo instrumentationLibraryInfo; private final Map attributes; @@ -110,7 +112,15 @@ public class ExitTracerSpan implements ReadWriteSpan { ExitTracerSpan(ExitTracer tracer, InstrumentationLibraryInfo instrumentationLibraryInfo, SpanKind spanKind, String spanName, SpanContext parentSpanContext, Resource resource, Clock tracerClock, Map attributes, Consumer onEnd, List links, int totalNumberOfLinksAdded, long userStartEpochNanos) { + this(tracer, instrumentationLibraryInfo, spanKind, spanName, parentSpanContext, + resource, tracerClock, attributes, onEnd, links, totalNumberOfLinksAdded, userStartEpochNanos, null); + } + + ExitTracerSpan(ExitTracer tracer, InstrumentationLibraryInfo instrumentationLibraryInfo, SpanKind spanKind, String spanName, SpanContext parentSpanContext, + Resource resource, Clock tracerClock, Map attributes, Consumer onEnd, List links, + int totalNumberOfLinksAdded, long userStartEpochNanos, Segment segment) { this.tracer = tracer; + this.segment = segment; this.spanKind = spanKind; this.spanName = spanName; this.parentSpanContext = parentSpanContext; @@ -285,7 +295,11 @@ public void end() { copySpanLinksToTracer(links); List immutableEvents = this.events == null ? Collections.emptyList() : Collections.unmodifiableList(this.events); copySpanEventsToTracer(immutableEvents); - tracer.finish(); + if (segment != null) { + segment.end(); + } else { + tracer.finish(); + } endEpochNanos = System.nanoTime(); ended = true; onEnd.accept(this); diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java index 54f51cb61d..fc258c8216 100644 --- a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java @@ -1,6 +1,6 @@ /* * - * * Copyright 2024 New Relic Corporation. All rights reserved. + * * Copyright 2026 New Relic Corporation. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 * */ @@ -10,13 +10,18 @@ import com.newrelic.agent.bridge.AgentBridge; import com.newrelic.agent.bridge.ExitTracer; import com.newrelic.agent.bridge.Instrumentation; +import com.newrelic.agent.bridge.NoOpSegment; +import com.newrelic.agent.bridge.NoOpTransaction; import com.newrelic.agent.bridge.Transaction; import com.newrelic.agent.tracers.TracerFlags; import com.newrelic.api.agent.ExtendedRequest; import com.newrelic.api.agent.ExtendedResponse; import com.newrelic.api.agent.HeaderType; +import com.newrelic.api.agent.Headers; import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Segment; import com.newrelic.api.agent.TracedMethod; +import com.newrelic.api.agent.TransportType; import com.nr.agent.instrumentation.utils.header.W3CTraceParentHeader; import com.nr.agent.instrumentation.utils.span.AttributeMapper; import com.nr.agent.instrumentation.utils.span.AttributeType; @@ -33,6 +38,7 @@ import io.opentelemetry.sdk.trace.data.LinkData; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -72,6 +78,7 @@ class NRSpanBuilder implements SpanBuilder { private static final int MAX_LINKS_PER_SPAN = 100; private static final int MAX_LINK_ATTRIBUTES = 64; private static final int MAX_LINK_ATTRIBUTE_LENGTH = 255; + private static final Boolean sendMessageQueueNotSampledHeader = NewRelic.getAgent().getConfig().getValue("distributed_tracing.send_message_queue_not_sampled_header", false); public NRSpanBuilder(Instrumentation instrumentation, String instrumentationScopeName, String instrumentationScopeVersion, TracerSharedState sharedState, String spanName) { @@ -206,26 +213,57 @@ public Span startSpan() { if (SpanKind.SERVER == spanKind) { return startServerSpan(parentSpanContext); } + final boolean dispatcher = SpanKind.CONSUMER.equals(spanKind); if (dispatcher) { - AgentBridge.getAgent().getTransaction(true); + Transaction tx = AgentBridge.getAgent().getTransaction(true); + acceptMessageQueueNotSampledHeaderIfNecessary(tx); } - final ExitTracer tracer = instrumentation.createTracer(spanName, getTracerFlags(dispatcher)); - if (tracer == null) { - return NO_OP_SPAN; + + Segment segment = null; + ExitTracer tracer; + if (SpanKind.PRODUCER == spanKind) { + insertMessageQueueNotSampledHeaderIfNecessary(); + segment = NewRelic.getAgent().getTransaction().startSegment(spanName); + if (segment == null || segment == NoOpSegment.INSTANCE) { + return NO_OP_SPAN; + } + tracer = ((com.newrelic.agent.Segment)segment).getTracer(); + if (tracer == null) { + // no active transaction + segment.end(); + return NO_OP_SPAN; + } + } else { + tracer = instrumentation.createTracer(spanName, getTracerFlags(dispatcher)); + if (tracer == null) { + return NO_OP_SPAN; + } } + if (SpanKind.INTERNAL != spanKind) { tracer.addCustomAttribute("span.kind", spanKind.name()); } List immutableLinks = this.links == null ? Collections.emptyList() : Collections.unmodifiableList(this.links); // TODO REVIEW - we're not picking up the global resources return onStart(new ExitTracerSpan(tracer, instrumentationLibraryInfo, spanKind, spanName, parentSpanContext, sharedState.getResource(), sharedState.getClock(), attributes, - endHandler, immutableLinks, totalNumberOfLinksAdded, startEpochNanos)); + endHandler, immutableLinks, totalNumberOfLinksAdded, startEpochNanos, segment)); } private Span startServerSpan(SpanContext parentSpanContext) { Transaction transaction = AgentBridge.getAgent().getTransaction(true); - final ExtendedRequest request = new ExtendedRequest() { + final ExtendedRequest request = generateExtendedRequestForServerSpan(); + final ExtendedResponse response = generateExtendedResponseForServerSpan(); + + transaction.requestInitialized(request, response); + TracedMethod tracedMethod = transaction.getTracedMethod(); + List immutableLinks = this.links == null ? Collections.emptyList() : Collections.unmodifiableList(this.links); + return onStart(new ExitTracerSpan((ExitTracer) tracedMethod, instrumentationLibraryInfo, spanKind, spanName, + parentSpanContext, sharedState.getResource(), sharedState.getClock(), attributes, endHandler, immutableLinks, totalNumberOfLinksAdded, startEpochNanos)); + } + + private ExtendedRequest generateExtendedRequestForServerSpan() { + return new ExtendedRequest() { @Override public String getRequestURI() { @@ -265,45 +303,12 @@ public HeaderType getHeaderType() { @Override public String getHeader(String name) { - if ("User-Agent".equalsIgnoreCase(name)) { - return (String) attributes.get(attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Host, attributes.keySet())); - } - // TODO is it possible to get the newrelic DT header from OTel??? It doesn't seem so. - if (NEWRELIC.equalsIgnoreCase(name)) { - return null; - } - return null; + return getHeaderFromAttributes(name); } @Override public List getHeaders(String name) { - if (name.isEmpty()) { - return Collections.emptyList(); - } - List headers = new ArrayList<>(); - - if (W3C_TRACESTATE.equalsIgnoreCase(name)) { - Map traceState = parentSpanContext.getTraceState().asMap(); - StringBuilder tracestateStringBuilder = new StringBuilder(); - // Build full tracestate header incase there are multiple vendors - for (Map.Entry entry : traceState.entrySet()) { - if (tracestateStringBuilder.length() == 0) { - tracestateStringBuilder.append(entry.toString()); - } else { - tracestateStringBuilder.append(",").append(entry.toString()); - } - } - headers.add(tracestateStringBuilder.toString()); - return headers; - } - if (W3C_TRACEPARENT.equalsIgnoreCase(name)) { - String traceParent = W3CTraceParentHeader.create(parentSpanContext); - if (!traceParent.isEmpty()) { - headers.add(traceParent); - return headers; - } - } - return headers; + return getHeadersFromAttributes(name); } @Override @@ -311,8 +316,10 @@ public String getMethod() { return (String) attributes.get(attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Method, attributes.keySet())); } }; + } - final ExtendedResponse response = new ExtendedResponse() { + private ExtendedResponse generateExtendedResponseForServerSpan() { + return new ExtendedResponse() { @Override public int getStatus() throws Exception { @@ -345,15 +352,122 @@ public long getContentLength() { return 0; } }; - transaction.requestInitialized(request, response); - TracedMethod tracedMethod = transaction.getTracedMethod(); - List immutableLinks = this.links == null ? Collections.emptyList() : Collections.unmodifiableList(this.links); - return onStart(new ExitTracerSpan((ExitTracer) tracedMethod, instrumentationLibraryInfo, spanKind, spanName, - parentSpanContext, sharedState.getResource(), sharedState.getClock(), attributes, endHandler, immutableLinks, totalNumberOfLinksAdded, startEpochNanos)); + } + + private void acceptMessageQueueNotSampledHeaderIfNecessary(Transaction tx) { + if (sendMessageQueueNotSampledHeader) tx.acceptDistributedTraceHeaders(TransportType.Queue, generateDtHeaders()); + } + + private void insertMessageQueueNotSampledHeaderIfNecessary() { + Transaction tx = AgentBridge.getAgent().getTransaction(false); + if (tx == null) return; + + // ask the agent to insert the header, if necessary + tx.insertDistributedTraceHeaders(generateDtHeaders()); + // if it did, then set the corresponding attribute on the span + if (attributes.containsKey("nrns")) { + attributes.put("new_relic_not_sampled", ""); + } + } + + private Headers generateDtHeaders() { + return new Headers() { + @Override + public HeaderType getHeaderType() { + return HeaderType.MESSAGE; + } + + @Override + public String getHeader(String name) { + String result = getHeaderFromAttributes(name); + return result; + } + + @Override + public Collection getHeaders(String name) { + Collection result = getHeadersFromAttributes(name); + return result; + } + + @Override + public void setHeader(String name, String value) { + attributes.put(name, value); + } + + @Override + public void addHeader(String name, String value) { + attributes.put(name, value); + } + + @Override + public Collection getHeaderNames() { + return null; + } + + @Override + public boolean containsHeader(String name) { + return attributes.containsKey(name); + } + }; + } + + private String getHeaderFromAttributes(String name) { + if ("User-Agent".equalsIgnoreCase(name)) { + return (String) attributes.get(attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Host, attributes.keySet())); + } + // yes, the 2 if statements are purposefully cross-matched + // if we are looking for nrns, then we'll want to know if it already has new_relic_not_sampled + // and vice versa + if ("nrns".equalsIgnoreCase(name)) { + return attributes.containsKey("new_relic_not_sampled") ? "" : null; + } + if ("new_relic_not_sampled".equalsIgnoreCase(name)) { + return attributes.containsKey("nrns") ? "" : null; + } + // it is not currently possible to get the newrelic DT header from OTel + if (NEWRELIC.equalsIgnoreCase(name)) { + return null; + } + return null; + } + + private List getHeadersFromAttributes(String name) { + if (name.isEmpty()) { + return Collections.emptyList(); + } + List headers = new ArrayList<>(); + + if ("nrns".equalsIgnoreCase(name) && + attributes.containsKey("new_relic_not_sampled") && + sendMessageQueueNotSampledHeader) { + headers.add(""); // intentionally blank + return headers; + } + if (W3C_TRACESTATE.equalsIgnoreCase(name)) { + Map traceState = parentSpanContext.getTraceState().asMap(); + StringBuilder tracestateStringBuilder = new StringBuilder(); + // Build full tracestate header incase there are multiple vendors + for (Map.Entry entry : traceState.entrySet()) { + if (tracestateStringBuilder.length() == 0) { + tracestateStringBuilder.append(entry.toString()); + } else { + tracestateStringBuilder.append(",").append(entry.toString()); + } + } + headers.add(tracestateStringBuilder.toString()); + return headers; + } + if (W3C_TRACEPARENT.equalsIgnoreCase(name)) { + String traceParent = W3CTraceParentHeader.create(parentSpanContext); + if (!traceParent.isEmpty()) { + headers.add(traceParent); + return headers; + } + } + return headers; } Span onStart(ReadWriteSpan span) { - // FIXME Context parent = Context.current(); if (sharedState.getActiveSpanProcessor().isStartRequired()) { sharedState.getActiveSpanProcessor().onStart(parent, span); diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.59.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.59.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java index 0b6c744020..b837d3418f 100644 --- a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.59.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.59.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java @@ -17,6 +17,7 @@ import com.newrelic.api.agent.DatastoreParameters; import com.newrelic.api.agent.HttpParameters; import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Segment; import com.newrelic.api.agent.Token; import com.nr.agent.instrumentation.utils.AttributesHelper; import com.nr.agent.instrumentation.utils.span.AttributeMapper; @@ -85,6 +86,7 @@ public class ExitTracerSpan implements ReadWriteSpan { .collect(Collectors.toSet())); final ExitTracer tracer; + private final Segment segment; private final SpanKind spanKind; private final InstrumentationScopeInfo instrumentationScopeInfo; private final Map attributes; @@ -112,7 +114,15 @@ public class ExitTracerSpan implements ReadWriteSpan { ExitTracerSpan(ExitTracer tracer, InstrumentationScopeInfo instrumentationScopeInfo, SpanKind spanKind, String spanName, SpanContext parentSpanContext, Resource resource, Clock tracerClock, Map attributes, Consumer onEnd, List links, int totalNumberOfLinksAdded, long userStartEpochNanos) { + this(tracer, instrumentationScopeInfo, spanKind, spanName, parentSpanContext, + resource, tracerClock, attributes, onEnd, links, totalNumberOfLinksAdded, userStartEpochNanos, null); + } + + ExitTracerSpan(ExitTracer tracer, InstrumentationScopeInfo instrumentationScopeInfo, SpanKind spanKind, String spanName, SpanContext parentSpanContext, + Resource resource, Clock tracerClock, Map attributes, Consumer onEnd, List links, + int totalNumberOfLinksAdded, long userStartEpochNanos, Segment segment) { this.tracer = tracer; + this.segment = segment; this.spanKind = spanKind; this.spanName = spanName; this.parentSpanContext = parentSpanContext; @@ -287,7 +297,11 @@ public void end() { copySpanLinksToTracer(links); List immutableEvents = this.events == null ? Collections.emptyList() : Collections.unmodifiableList(this.events); copySpanEventsToTracer(immutableEvents); - tracer.finish(); + if (segment != null) { + segment.end(); + } else { + tracer.finish(); + } endEpochNanos = System.nanoTime(); ended = true; onEnd.accept(this); diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.59.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.59.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java index 0a20eec276..40e68f03a6 100644 --- a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.59.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.59.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java @@ -10,13 +10,18 @@ import com.newrelic.agent.bridge.AgentBridge; import com.newrelic.agent.bridge.ExitTracer; import com.newrelic.agent.bridge.Instrumentation; +import com.newrelic.agent.bridge.NoOpSegment; +import com.newrelic.agent.bridge.NoOpTransaction; import com.newrelic.agent.bridge.Transaction; import com.newrelic.agent.tracers.TracerFlags; import com.newrelic.api.agent.ExtendedRequest; import com.newrelic.api.agent.ExtendedResponse; import com.newrelic.api.agent.HeaderType; +import com.newrelic.api.agent.Headers; import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Segment; import com.newrelic.api.agent.TracedMethod; +import com.newrelic.api.agent.TransportType; import com.nr.agent.instrumentation.utils.header.W3CTraceParentHeader; import com.nr.agent.instrumentation.utils.span.AttributeMapper; import com.nr.agent.instrumentation.utils.span.AttributeType; @@ -33,6 +38,7 @@ import io.opentelemetry.sdk.trace.data.LinkData; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -72,6 +78,7 @@ class NRSpanBuilder implements SpanBuilder { private static final int MAX_LINKS_PER_SPAN = 100; private static final int MAX_LINK_ATTRIBUTES = 64; private static final int MAX_LINK_ATTRIBUTE_LENGTH = 255; + private static final Boolean sendMessageQueueNotSampledHeader = NewRelic.getAgent().getConfig().getValue("distributed_tracing.send_message_queue_not_sampled_header", false); public NRSpanBuilder(Instrumentation instrumentation, String instrumentationScopeName, String instrumentationScopeVersion, TracerSharedState sharedState, String spanName) { @@ -211,14 +218,34 @@ public Span startSpan() { if (SpanKind.SERVER == spanKind) { return startServerSpan(parentSpanContext); } + final boolean dispatcher = SpanKind.CONSUMER.equals(spanKind); if (dispatcher) { - AgentBridge.getAgent().getTransaction(true); + Transaction tx = AgentBridge.getAgent().getTransaction(true); + acceptMessageQueueNotSampledHeaderIfNecessary(tx); } - final ExitTracer tracer = instrumentation.createTracer(spanName, getTracerFlags(dispatcher)); - if (tracer == null) { - return NO_OP_SPAN; + + Segment segment = null; + ExitTracer tracer; + if (SpanKind.PRODUCER == spanKind) { + insertMessageQueueNotSampledHeaderIfNecessary(); + segment = NewRelic.getAgent().getTransaction().startSegment(spanName); + if (segment == null || segment == NoOpSegment.INSTANCE) { + return NO_OP_SPAN; + } + tracer = ((com.newrelic.agent.Segment)segment).getTracer(); + if (tracer == null) { + // no active transaction + segment.end(); + return NO_OP_SPAN; + } + } else { + tracer = instrumentation.createTracer(spanName, getTracerFlags(dispatcher)); + if (tracer == null) { + return NO_OP_SPAN; + } } + if (SpanKind.INTERNAL != spanKind) { tracer.addCustomAttribute("span.kind", spanKind.name()); } @@ -227,12 +254,24 @@ public Span startSpan() { return onStart( new ExitTracerSpan(tracer, instrumentationScopeInfo, spanKind, spanName, parentSpanContext, sharedState.getResource(), sharedState.getClock(), attributes, - endHandler, immutableLinks, totalNumberOfLinksAdded, startEpochNanos)); + endHandler, immutableLinks, totalNumberOfLinksAdded, startEpochNanos, segment)); } private Span startServerSpan(SpanContext parentSpanContext) { Transaction transaction = AgentBridge.getAgent().getTransaction(true); - final ExtendedRequest request = new ExtendedRequest() { + final ExtendedRequest request = generateExtendedRequestForServerSpan(); + final ExtendedResponse response = generateExtendedResponseForServerSpan(); + + transaction.requestInitialized(request, response); + TracedMethod tracedMethod = transaction.getTracedMethod(); + List immutableLinks = this.links == null ? Collections.emptyList() : Collections.unmodifiableList(this.links); + return onStart(new ExitTracerSpan((ExitTracer) tracedMethod, instrumentationScopeInfo, spanKind, spanName, + parentSpanContext, sharedState.getResource(), sharedState.getClock(), attributes, endHandler, immutableLinks, totalNumberOfLinksAdded, + startEpochNanos)); + } + + private ExtendedRequest generateExtendedRequestForServerSpan() { + return new ExtendedRequest() { @Override public String getRequestURI() { @@ -272,45 +311,12 @@ public HeaderType getHeaderType() { @Override public String getHeader(String name) { - if ("User-Agent".equalsIgnoreCase(name)) { - return (String) attributes.get(attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Host, attributes.keySet())); - } - // TODO is it possible to get the newrelic DT header from OTel??? It doesn't seem so. - if (NEWRELIC.equalsIgnoreCase(name)) { - return null; - } - return null; + return getHeaderFromAttributes(name); } @Override public List getHeaders(String name) { - if (name.isEmpty()) { - return Collections.emptyList(); - } - List headers = new ArrayList<>(); - - if (W3C_TRACESTATE.equalsIgnoreCase(name)) { - Map traceState = parentSpanContext.getTraceState().asMap(); - StringBuilder tracestateStringBuilder = new StringBuilder(); - // Build full tracestate header incase there are multiple vendors - for (Map.Entry entry : traceState.entrySet()) { - if (tracestateStringBuilder.length() == 0) { - tracestateStringBuilder.append(entry.toString()); - } else { - tracestateStringBuilder.append(",").append(entry.toString()); - } - } - headers.add(tracestateStringBuilder.toString()); - return headers; - } - if (W3C_TRACEPARENT.equalsIgnoreCase(name)) { - String traceParent = W3CTraceParentHeader.create(parentSpanContext); - if (!traceParent.isEmpty()) { - headers.add(traceParent); - return headers; - } - } - return headers; + return getHeadersFromAttributes(name); } @Override @@ -318,8 +324,10 @@ public String getMethod() { return (String) attributes.get(attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Method, attributes.keySet())); } }; + } - final ExtendedResponse response = new ExtendedResponse() { + private ExtendedResponse generateExtendedResponseForServerSpan() { + return new ExtendedResponse() { @Override public int getStatus() throws Exception { @@ -352,12 +360,119 @@ public long getContentLength() { return 0; } }; - transaction.requestInitialized(request, response); - TracedMethod tracedMethod = transaction.getTracedMethod(); - List immutableLinks = this.links == null ? Collections.emptyList() : Collections.unmodifiableList(this.links); - return onStart(new ExitTracerSpan((ExitTracer) tracedMethod, instrumentationScopeInfo, spanKind, spanName, - parentSpanContext, sharedState.getResource(), sharedState.getClock(), attributes, endHandler, immutableLinks, totalNumberOfLinksAdded, - startEpochNanos)); + } + + private void acceptMessageQueueNotSampledHeaderIfNecessary(Transaction tx) { + if (sendMessageQueueNotSampledHeader) tx.acceptDistributedTraceHeaders(TransportType.Queue, generateDtHeaders()); + } + + private void insertMessageQueueNotSampledHeaderIfNecessary() { + Transaction tx = AgentBridge.getAgent().getTransaction(false); + if (tx == null) return; + + // ask the agent to insert the header, if necessary + tx.insertDistributedTraceHeaders(generateDtHeaders()); + // if it did, then set the corresponding attribute on the span + if (attributes.containsKey("nrns")) { + attributes.put("new_relic_not_sampled", ""); + } + } + + private Headers generateDtHeaders() { + return new Headers() { + @Override + public HeaderType getHeaderType() { + return HeaderType.MESSAGE; + } + + @Override + public String getHeader(String name) { + String result = getHeaderFromAttributes(name); + return result; + } + + @Override + public Collection getHeaders(String name) { + Collection result = getHeadersFromAttributes(name); + return result; + } + + @Override + public void setHeader(String name, String value) { + attributes.put(name, value); + } + + @Override + public void addHeader(String name, String value) { + attributes.put(name, value); + } + + @Override + public Collection getHeaderNames() { + return null; + } + + @Override + public boolean containsHeader(String name) { + return attributes.containsKey(name); + } + }; + } + + private String getHeaderFromAttributes(String name) { + if ("User-Agent".equalsIgnoreCase(name)) { + return (String) attributes.get(attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Host, attributes.keySet())); + } + // yes, the 2 if statements are purposefully cross-matched + // if we are looking for nrns, then we'll want to know if it already has new_relic_not_sampled + // and vice versa + if ("nrns".equalsIgnoreCase(name)) { + return attributes.containsKey("new_relic_not_sampled") ? "" : null; + } + if ("new_relic_not_sampled".equalsIgnoreCase(name)) { + return attributes.containsKey("nrns") ? "" : null; + } + // it is not currently possible to get the newrelic DT header from OTel + if (NEWRELIC.equalsIgnoreCase(name)) { + return null; + } + return null; + } + + private List getHeadersFromAttributes(String name) { + if (name.isEmpty()) { + return Collections.emptyList(); + } + List headers = new ArrayList<>(); + + if ("nrns".equalsIgnoreCase(name) && + attributes.containsKey("new_relic_not_sampled") && + sendMessageQueueNotSampledHeader) { + headers.add(""); // intentionally blank + return headers; + } + if (W3C_TRACESTATE.equalsIgnoreCase(name)) { + Map traceState = parentSpanContext.getTraceState().asMap(); + StringBuilder tracestateStringBuilder = new StringBuilder(); + // Build full tracestate header incase there are multiple vendors + for (Map.Entry entry : traceState.entrySet()) { + if (tracestateStringBuilder.length() == 0) { + tracestateStringBuilder.append(entry.toString()); + } else { + tracestateStringBuilder.append(",").append(entry.toString()); + } + } + headers.add(tracestateStringBuilder.toString()); + return headers; + } + if (W3C_TRACEPARENT.equalsIgnoreCase(name)) { + String traceParent = W3CTraceParentHeader.create(parentSpanContext); + if (!traceParent.isEmpty()) { + headers.add(traceParent); + return headers; + } + } + return headers; } Span onStart(ReadWriteSpan span) { diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/HeadersUtil.java b/newrelic-agent/src/main/java/com/newrelic/agent/HeadersUtil.java index 2d3aa5c959..6b7e9542ae 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/HeadersUtil.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/HeadersUtil.java @@ -92,6 +92,7 @@ public class HeadersUtil { static final String W3C_TRACEPARENT_HEADER = "traceparent"; private static final String W3C_TRACEPARENT_HEADER_CAMEL = "TraceParent"; private static final String W3C_TRACEPARENT_HEADER_CAPS = "TRACEPARENT"; + private static final String MESSAGE_QUEUE_NOT_SAMPLED_HEADER = "nrns"; // New Relic Not Sampled abreviated header for message queues /** * Minimum supported version of New Relic Synthetics protocol. @@ -248,6 +249,10 @@ public static void setTraceParentHeader(OutboundHeaders headers, String value) { headers.setHeader(W3C_TRACEPARENT_HEADER, value); } + public static void setMessageQueueNotSampledHeader(OutboundHeaders headers) { + headers.setHeader(MESSAGE_QUEUE_NOT_SAMPLED_HEADER, ""); + } + /** * parse headers from the inbound payload. It prioritizes trace context headers over newrelic headers. It then accepts the distributed * trace payload if it was able to parse it from the headers. @@ -256,6 +261,22 @@ public static void setTraceParentHeader(OutboundHeaders headers, String value) { * @param inboundHeaders the request headers containing the distributed trace payload */ public static void parseAndAcceptDistributedTraceHeaders(Transaction tx, InboundHeaders inboundHeaders) { + + boolean containsMessageQueueNotSampledHeader = null != inboundHeaders.getHeader(MESSAGE_QUEUE_NOT_SAMPLED_HEADER); + boolean sendMessageQueueNotSampledHeader = NewRelic.getAgent().getConfig().getValue("distributed_tracing.send_message_queue_not_sampled_header", false); + if (sendMessageQueueNotSampledHeader && containsMessageQueueNotSampledHeader) { + // only if we haven't already accepted inbound headers + if (tx.getSpanProxy().getInboundDistributedTracePayload() == null) { + Agent.LOG.log(Level.FINEST, "Accepting Message Queue Not Sampled (nrns) header for transaction {0}", tx); + // we have a nrns header, short circuit and treat this as a remote_parent_not_sampled case + // where we have no other trace information + // note: this priority could get overridden later, if a subsequent call to acceptDistributedTracePayload is made + tx.assignPriorityFromRemoteParent(false); + tx.getMetricAggregator().incrementCounter(MetricNames.SUPPORTABILITY_TRACE_CONTEXT_ACCEPT_NRNS); + } + return; + } + List traceParent = HeadersUtil.getTraceParentHeader(inboundHeaders); if (traceParent != null && !traceParent.isEmpty()) { List traceState = HeadersUtil.getTraceStateHeader(inboundHeaders); @@ -287,6 +308,8 @@ public static void parseAndAcceptDistributedTraceHeaders(Transaction tx, Inbound /** * creates new trace context distributed trace headers (and maybe new relic headers) and adds them to the headers object passed in + * if this is a Message Queue header type, and the decision is not to sample, then we may send the abbreviated "nrns" or + * New Relic Not Sampled header to save space, but only if the config has been set to do so * * @param tx current transaction * @param tracedMethod the current traced method, used to grab the span id @@ -303,6 +326,15 @@ public static boolean createAndSetDistributedTraceHeaders(Transaction tx, com.ne Agent.LOG.log(Level.FINER, "Sending distributed trace header in transaction {0}", tx); DistributedTracingConfig distributedTracingConfig = tx.getAgentConfig().getDistributedTracingConfig(); + + if (HeaderType.MESSAGE.equals(headers.getHeaderType()) && !payload.sampled.booleanValue() && + distributedTracingConfig.useMessageQueueNotSampleHeader()) { + Agent.LOG.log(Level.FINEST, "Using Message Queue Not Sampled (nrns) header for transaction {0}", tx); + tx.getMetricAggregator().incrementCounter(MetricNames.SUPPORTABILITY_TRACE_CONTEXT_CREATE_NRNS); + HeadersUtil.setMessageQueueNotSampledHeader(headers); + return true; + } + boolean includeNewRelicHeader = distributedTracingConfig.isIncludeNewRelicHeader(); if (includeNewRelicHeader) { HeadersUtil.setNewRelicTraceHeader(headers, payload.httpSafe()); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java b/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java index 1b6028bb46..a2d047c11c 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java @@ -477,6 +477,11 @@ public class MetricNames { public static final String SUPPORTABILITY_TRACE_CONTEXT_UNTRUSTED_ACCOUNT = "Supportability/TraceContext/TraceState/Ignored/UntrustedAccount"; public static final String SUPPORTABILITY_TRACE_CONTEXT_STATE_PARSE_EXCEPTION = "Supportability/TraceContext/TraceState/Parse/Exception"; + // New Relic Not Sampled Message Queue header + public static final String SUPPORTABILITY_TRACE_CONTEXT_ACCEPT_NRNS = "Supportability/TraceContext/Accept/NRNS"; + public static final String SUPPORTABILITY_TRACE_CONTEXT_CREATE_NRNS = "Supportability/TraceContext/Create/NRNS"; + + // Span events public static final String SUPPORTABILITY_SPAN_EVENTS = "Supportability/SpanEvents"; // feature is enabled public static final String SUPPORTABILITY_SPAN_EVENT_TOTAL_EVENTS_SENT = "Supportability/SpanEvent/TotalEventsSent"; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/DistributedTracingConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/DistributedTracingConfig.java index e7a1503912..55b02ae1be 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/DistributedTracingConfig.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/DistributedTracingConfig.java @@ -17,6 +17,7 @@ public class DistributedTracingConfig extends BaseConfig { private static final boolean DEFAULT_DISTRIBUTED_TRACING = true; + private static final boolean DEFAULT_MESSAGE_QUEUE_NOT_SAMPLED_HEADER = false; private static final String SYSTEM_PROPERTY_ROOT = "newrelic.config.distributed_tracing."; //public setting names @@ -29,6 +30,7 @@ public class DistributedTracingConfig extends BaseConfig { public static final String EXCLUDE_NEWRELIC_HEADER = "exclude_newrelic_header"; public static final String FULL_GRANULARITY = "full_granularity"; public static final String PARTIAL_GRANULARITY = "partial_granularity"; + public static final String USE_MESSAGE_QUEUE_NOT_SAMPLED_HEADER = "send_message_queue_not_sampled_header"; private final boolean enabled; private final String trustedAccountKey; @@ -42,6 +44,7 @@ public class DistributedTracingConfig extends BaseConfig { // which will get assigned to transaction_events.target_samples_stored // so, even though it's not directly used in code here, it is necessary private final Integer adaptiveSamplingTarget; + private final boolean useMessageQueueNotSampleHeader; DistributedTracingConfig(Map props) { super(props, SYSTEM_PROPERTY_ROOT); @@ -52,6 +55,7 @@ public class DistributedTracingConfig extends BaseConfig { this.includeNewRelicHeader = !getProperty(EXCLUDE_NEWRELIC_HEADER, false); this.baseSamplerConfig = new BaseSamplerCoreTracingConfig(nestedProps("sampler"), SYSTEM_PROPERTY_ROOT); this.adaptiveSamplingTarget = this.baseSamplerConfig.getSharedAdaptiveSamplingTarget(); + this.useMessageQueueNotSampleHeader = getProperty(USE_MESSAGE_QUEUE_NOT_SAMPLED_HEADER, DEFAULT_MESSAGE_QUEUE_NOT_SAMPLED_HEADER); } public String getTrustedAccountKey() { @@ -74,6 +78,8 @@ public boolean isIncludeNewRelicHeader() { return includeNewRelicHeader; } + public boolean useMessageQueueNotSampleHeader() { return useMessageQueueNotSampleHeader; } + public int getAdaptiveSamplingTarget() { return adaptiveSamplingTarget; } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/tracing/W3CTraceStateHeader.java b/newrelic-agent/src/main/java/com/newrelic/agent/tracing/W3CTraceStateHeader.java index e6d01ab336..dbcc691e38 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/tracing/W3CTraceStateHeader.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/tracing/W3CTraceStateHeader.java @@ -50,7 +50,7 @@ String createTraceStateHeader(DistributedTracePayloadImpl payload) { } String createTraceStateHeader(DistributedTracePayloadImpl payload, String version) { - String spanId = getSpanId(payload); + String spanId = ""; // deprecated because it's duplicated in the traceparent header, see spec String transactionId = getTransactionId(payload); String priority = BigDecimal.valueOf(payload.priority) .setScale(6, RoundingMode.HALF_UP) @@ -74,11 +74,4 @@ private String getTransactionId(DistributedTracePayloadImpl payload) { } return ""; } - - private String getSpanId(DistributedTracePayloadImpl payload) { - if (spanEventsEnabled) { - return payload.guid == null ? TransactionGuidFactory.generate16CharGuid() : payload.guid; - } - return ""; - } } diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/HeadersUtilTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/HeadersUtilTest.java index 5b92e7487e..1778984f5b 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/HeadersUtilTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/HeadersUtilTest.java @@ -79,7 +79,7 @@ public void createDTHeadersSetsSpanIdEvenIfTxNotSampled() { assertEquals("traceparent parentId field should match span id.", mockTracer.getGuid(), traceParent.split("-")[2]); String traceState = map.get("tracestate"); - assertEquals("tracestate spanId field should match span id.", mockTracer.getGuid(), traceState.split("-")[4]); + assertEquals("tracestate spanId field should be empty.", "", traceState.split("-")[4]); assertEquals("tracestate txId field should match tx id.", tx.getGuid(), traceState.split("-")[5]); } diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/tracing/DistributedTracingApiTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/tracing/DistributedTracingApiTest.java index 563c7f4289..23e1710f05 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/tracing/DistributedTracingApiTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/tracing/DistributedTracingApiTest.java @@ -169,7 +169,7 @@ public void testAcceptThenInsert() { // assertions String traceparentPattern = "00-da8bc8cc6d062849b0efcf3c169afb5a-.{16}-01"; - String tracestatePattern = "33@nr=0-0-33-2827902-.{16}-.{16}-1-1.23456-.*"; + String tracestatePattern = "33@nr=0-0-33-2827902--.{16}-1-1.23456-.*"; assertTrue(responseHeaders.getHeader("traceparent").matches(traceparentPattern)); assertTrue(responseHeaders.getHeader("tracestate").matches(tracestatePattern)); assertNotNull(responseHeaders.getHeader("newrelic")); diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/tracing/W3CTraceStateHeaderTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/tracing/W3CTraceStateHeaderTest.java index 845749c054..ba6e31729e 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/tracing/W3CTraceStateHeaderTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/tracing/W3CTraceStateHeaderTest.java @@ -27,7 +27,7 @@ public void testCreateTraceStateHeader() { String traceStateHeader = new W3CTraceStateHeader(true, true).createTraceStateHeader( new DistributedTracePayloadImpl(1234L, "parentType", "accountId", "trustKey", "appId", "guid", "traceId", "txnId", 0.789f, Sampled.SAMPLED_NO), "0"); - assertEquals("trustKey@nr=0-0-accountId-appId-guid-txnId-0-0.789-1234", traceStateHeader); + assertEquals("trustKey@nr=0-0-accountId-appId--txnId-0-0.789-1234", traceStateHeader); } @Test @@ -47,7 +47,7 @@ public void testTransactionIdOmittedWhenSpansAreDisabled() { String traceStateHeader = testClass.createTraceStateHeader( new DistributedTracePayloadImpl(1234L, "parentType", "accountId", "trustKey", "appId", "667", "traceId", "NO_SOUP", 0.789f, Sampled.SAMPLED_NO ), "0"); - assertEquals("trustKey@nr=0-0-accountId-appId-667--0-0.789-1234", traceStateHeader); + assertEquals("trustKey@nr=0-0-accountId-appId---0-0.789-1234", traceStateHeader); } @Test @@ -66,7 +66,7 @@ public void testSmallPrecisionPriority() throws Exception { String traceStateHeader = testClass.createTraceStateHeader( new DistributedTracePayloadImpl(1234L, "parentType", "accountId", "trustKey", "appId", "broop", "traceId", "txnid", 0.000001f, Sampled.SAMPLED_NO), "0"); - assertEquals("trustKey@nr=0-0-accountId-appId-broop-txnid-0-0.000001-1234", traceStateHeader); + assertEquals("trustKey@nr=0-0-accountId-appId--txnid-0-0.000001-1234", traceStateHeader); } @Test @@ -75,6 +75,6 @@ public void testVerySmallPrecisionPriority() throws Exception { String traceStateHeader = testClass.createTraceStateHeader( new DistributedTracePayloadImpl(1234L, "parentType", "accountId", "trustKey", "appId", "broop", "traceId", "txnid", 0.0000000000000000001f, Sampled.SAMPLED_NO), "0"); - assertEquals("trustKey@nr=0-0-accountId-appId-broop-txnid-0-0.000000-1234", traceStateHeader); + assertEquals("trustKey@nr=0-0-accountId-appId--txnid-0-0.000000-1234", traceStateHeader); } } \ No newline at end of file diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/tracing/W3CTraceStateSupportTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/tracing/W3CTraceStateSupportTest.java index 52a33aff68..969d998d47 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/tracing/W3CTraceStateSupportTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/tracing/W3CTraceStateSupportTest.java @@ -165,7 +165,7 @@ public void testParseHeadersDup() { transaction.createDistributedTracePayload(newSpanId); String outboundPayload = new W3CTraceStateHeader(true, true).create(transaction.getSpanProxy()); //timestamp is generated - assertTrue(outboundPayload.startsWith("190@nr=0-0-accountId-appID-" + newSpanId + "-" + transaction.getGuid() + "-0-0.789-")); + assertTrue(outboundPayload.startsWith("190@nr=0-0-accountId-appID--" + transaction.getGuid() + "-0-0.789-")); } @Test