Skip to content

Commit 9a1c6a4

Browse files
committed
Fix broken trace propagation with w3c headers
1 parent df75b02 commit 9a1c6a4

File tree

4 files changed

+222
-1
lines changed

4 files changed

+222
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
*
3+
* * Copyright 2025 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
8+
package com.nr.agent.instrumentation.header.utils;
9+
10+
public class HeaderType {
11+
public static final String NEWRELIC = "newrelic";
12+
public static final String W3C_TRACEPARENT = "traceparent";
13+
public static final String W3C_TRACESTATE = "tracestate";
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
*
3+
* * Copyright 2025 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
8+
package com.nr.agent.instrumentation.header.utils;
9+
10+
import io.opentelemetry.api.trace.SpanContext;
11+
12+
public class W3CTraceParentHeader {
13+
14+
static final String W3C_VERSION = "00";
15+
static final String W3C_TRACE_PARENT_DELIMITER = "-";
16+
17+
public static String create(SpanContext parentSpanContext) {
18+
final String traceId = parentSpanContext.getTraceId();
19+
final String spanId = parentSpanContext.getSpanId();
20+
final boolean sampled = parentSpanContext.isSampled();
21+
22+
String traceParentHeader =
23+
W3C_VERSION + W3C_TRACE_PARENT_DELIMITER + traceId + W3C_TRACE_PARENT_DELIMITER + spanId + W3C_TRACE_PARENT_DELIMITER + sampledToFlags(sampled);
24+
25+
boolean valid = W3CTraceParentValidator.forHeader(traceParentHeader)
26+
.version(W3C_VERSION)
27+
.traceId(traceId)
28+
.parentId(spanId)
29+
.flags(parentSpanContext.getTraceFlags().asHex())
30+
.isValid();
31+
32+
return valid ? traceParentHeader : "";
33+
}
34+
35+
private static String sampledToFlags(boolean sampled) {
36+
return sampled ? "01" : "00";
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
*
3+
* * Copyright 2025 New Relic Corporation. All rights reserved.
4+
* * SPDX-License-Identifier: Apache-2.0
5+
*
6+
*/
7+
8+
package com.nr.agent.instrumentation.header.utils;
9+
10+
import java.util.regex.Matcher;
11+
import java.util.regex.Pattern;
12+
13+
import static com.nr.agent.instrumentation.header.utils.W3CTraceParentHeader.W3C_VERSION;
14+
import static java.util.regex.Pattern.compile;
15+
16+
public class W3CTraceParentValidator {
17+
18+
private static final String INVALID_VERSION = "ff";
19+
private static final String INVALID_TRACE_ID = "00000000000000000000000000000000"; // 32 characters
20+
private static final String INVALID_PARENT_ID = "0000000000000000"; // 16 characters
21+
private static final Pattern HEXADECIMAL_PATTERN = compile("\\p{XDigit}+");
22+
23+
private final String traceParentHeader;
24+
private final String version;
25+
private final String traceId;
26+
private final String parentId;
27+
private final String flags;
28+
29+
private W3CTraceParentValidator(Builder builder) {
30+
this.traceParentHeader = builder.traceParentHeader;
31+
this.version = builder.version;
32+
this.traceId = builder.traceId;
33+
this.parentId = builder.parentId;
34+
this.flags = builder.flags;
35+
}
36+
37+
private boolean isValid() {
38+
return isValidVersion() && isValidTraceId() && isValidParentId() && isValidFlags();
39+
}
40+
41+
/**
42+
* Version can only be 2 hexadecimal characters, `ff` is not allowed and if it matches our expected version the length must be 55 characters
43+
*/
44+
boolean isValidVersion() {
45+
return version.length() == 2 && isHexadecimal(version.charAt(0)) && isHexadecimal(version.charAt(1)) && !version.equals(INVALID_VERSION) &&
46+
!(version.equals(W3C_VERSION) && traceParentHeaderLengthIsInvalid());
47+
}
48+
49+
private boolean traceParentHeaderLengthIsInvalid() {
50+
return traceParentHeader.length() != 55;
51+
}
52+
53+
boolean isHexadecimal(char character) {
54+
return Character.digit(character, 16) != -1;
55+
}
56+
57+
boolean isHexadecimal(String input) {
58+
final Matcher matcher = HEXADECIMAL_PATTERN.matcher(input);
59+
return matcher.matches();
60+
}
61+
62+
/**
63+
* TraceId must be 32 characters, not all zeros and must be hexadecimal
64+
*/
65+
boolean isValidTraceId() {
66+
return traceId.length() == 32 && !traceId.equals(INVALID_TRACE_ID) && isHexadecimal(traceId);
67+
}
68+
69+
/**
70+
* ParentId must be 16 characters, not all zeros and must be hexadecimal
71+
*/
72+
boolean isValidParentId() {
73+
return parentId.length() == 16 && !parentId.equals(INVALID_PARENT_ID) && isHexadecimal(parentId);
74+
}
75+
76+
/**
77+
* Flags must be 2 characters and must be hexadecimal
78+
*/
79+
boolean isValidFlags() {
80+
return flags.length() == 2 && isHexadecimal(flags);
81+
}
82+
83+
static Builder forHeader(String traceParentHeader) {
84+
return new Builder(traceParentHeader);
85+
}
86+
87+
static class Builder {
88+
private final String traceParentHeader;
89+
private String version;
90+
private String traceId;
91+
private String parentId;
92+
private String flags;
93+
94+
Builder(String traceParentHeader) {
95+
this.traceParentHeader = traceParentHeader;
96+
}
97+
98+
public Builder version(String version) {
99+
this.version = version;
100+
return this;
101+
}
102+
103+
public Builder traceId(String traceId) {
104+
this.traceId = traceId;
105+
return this;
106+
}
107+
108+
public Builder parentId(String parentId) {
109+
this.parentId = parentId;
110+
return this;
111+
}
112+
113+
public Builder flags(String flags) {
114+
this.flags = flags;
115+
return this;
116+
}
117+
118+
W3CTraceParentValidator build() {
119+
return new W3CTraceParentValidator(this);
120+
}
121+
122+
public boolean isValid() {
123+
return build().isValid();
124+
}
125+
}
126+
}

instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java

+44-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.newrelic.api.agent.ExtendedResponse;
1818
import com.newrelic.api.agent.HeaderType;
1919
import com.newrelic.api.agent.TracedMethod;
20+
import com.nr.agent.instrumentation.header.utils.W3CTraceParentHeader;
2021
import io.opentelemetry.api.OpenTelemetry;
2122
import io.opentelemetry.api.common.AttributeKey;
2223
import io.opentelemetry.api.common.Attributes;
@@ -27,13 +28,19 @@
2728
import io.opentelemetry.context.Context;
2829
import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
2930

31+
import java.util.ArrayList;
3032
import java.util.Collections;
3133
import java.util.Enumeration;
3234
import java.util.HashMap;
35+
import java.util.List;
3336
import java.util.Map;
3437
import java.util.concurrent.TimeUnit;
3538
import java.util.function.Consumer;
3639

40+
import static com.nr.agent.instrumentation.header.utils.HeaderType.NEWRELIC;
41+
import static com.nr.agent.instrumentation.header.utils.HeaderType.W3C_TRACEPARENT;
42+
import static com.nr.agent.instrumentation.header.utils.HeaderType.W3C_TRACESTATE;
43+
3744
/**
3845
* New Relic Java agent implementation of an OpenTelemetry SpanBuilder,
3946
* which is used to construct Span instances. Instead of starting an OpenTelemetry
@@ -216,12 +223,48 @@ public HeaderType getHeaderType() {
216223

217224
@Override
218225
public String getHeader(String name) {
219-
if ("User-Agent".equals(name)) {
226+
if ("User-Agent".equalsIgnoreCase(name)) {
220227
return (String) attributes.get("user_agent.original");
221228
}
229+
// TODO is it possible to get the newrelic DT header from OTel???
230+
if (NEWRELIC.equalsIgnoreCase(name)) {
231+
return null;
232+
}
222233
return null;
223234
}
224235

236+
@Override
237+
public List<String> getHeaders(String name) {
238+
if (name.isEmpty()) {
239+
return Collections.emptyList();
240+
}
241+
String nameLowerCase = name.toLowerCase();
242+
List<String> headers = new ArrayList<>();
243+
244+
if (W3C_TRACESTATE.equals(nameLowerCase)) {
245+
Map<String, String> traceState = parentSpanContext.getTraceState().asMap();
246+
StringBuilder tracestateStringBuilder = new StringBuilder();
247+
// Build full tracestate header incase there are multiple vendors
248+
for (Map.Entry<String, String> entry : traceState.entrySet()) {
249+
if (tracestateStringBuilder.length() == 0) {
250+
tracestateStringBuilder.append(entry.toString());
251+
} else {
252+
tracestateStringBuilder.append(",").append(entry.toString());
253+
}
254+
}
255+
headers.add(tracestateStringBuilder.toString());
256+
return headers;
257+
}
258+
if (W3C_TRACEPARENT.equals(nameLowerCase)) {
259+
String traceParent = W3CTraceParentHeader.create(parentSpanContext);
260+
if (!traceParent.isEmpty()) {
261+
headers.add(traceParent);
262+
return headers;
263+
}
264+
}
265+
return headers;
266+
}
267+
225268
@Override
226269
public String getMethod() {
227270
return (String) attributes.get("http.request.method");

0 commit comments

Comments
 (0)