Skip to content

Commit 37b6382

Browse files
authored
Introduce ForUserAgent (#790)
User agent specified inside `ForUserAgent` http header will be propagated to requests made in the same trace.
1 parent 172eca0 commit 37b6382

File tree

20 files changed

+416
-45
lines changed

20 files changed

+416
-45
lines changed

changelog/6.5.0-rc1/pr-790.v2.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: feature
2+
feature:
3+
description: User agent specified inside `ForUserAgent` http header will be propagated
4+
to requests made in the same trace.
5+
links:
6+
- https://github.com/palantir/tracing-java/pull/790

tracing-api/src/main/java/com/palantir/tracing/api/TraceHttpHeaders.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public interface TraceHttpHeaders {
3030
String SPAN_ID = "X-B3-SpanId";
3131
String IS_SAMPLED = "X-B3-Sampled"; // Boolean (either “1” or “0”, can be absent)
3232

33+
String FOR_USER_AGENT = "For-User-Agent";
34+
3335
/**
3436
* Field is no longer used by the tracing library.
3537
*

tracing-api/src/main/java/com/palantir/tracing/api/TraceTags.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public final class TraceTags {
6262
public static final String HTTP_REQUEST_ID = "http.request_id";
6363
/** The User-Agent as it is sent (raw format). */
6464
public static final String HTTP_USER_AGENT = "http.useragent";
65+
/** The User-Agent propagated across service boundaries as it is sent (raw format). */
66+
public static final String HTTP_FOR_USER_AGENT = "http.for_useragent";
6567
/** The version of HTTP used for the request. */
6668
public static final String HTTP_VERSION = "http.version";
6769

tracing-jersey/src/main/java/com/palantir/tracing/jersey/TraceEnrichingFilter.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.palantir.tracing.jersey;
1818

19+
import com.google.common.annotations.VisibleForTesting;
1920
import com.google.common.base.Strings;
2021
import com.palantir.tracing.Observability;
2122
import com.palantir.tracing.TagTranslator;
@@ -35,6 +36,7 @@
3536
import javax.ws.rs.container.ContainerResponseContext;
3637
import javax.ws.rs.container.ContainerResponseFilter;
3738
import javax.ws.rs.core.Context;
39+
import javax.ws.rs.core.HttpHeaders;
3840
import javax.ws.rs.core.MultivaluedMap;
3941
import javax.ws.rs.ext.Provider;
4042
import org.glassfish.jersey.server.ExtendedUriInfo;
@@ -55,6 +57,9 @@ public final class TraceEnrichingFilter implements ContainerRequestFilter, Conta
5557

5658
public static final String SAMPLED_PROPERTY_NAME = "com.palantir.tracing.sampled";
5759

60+
@VisibleForTesting
61+
static final String FETCH_USER_AGENT_HEADER = "Fetch-User-Agent";
62+
5863
@Context
5964
@SuppressWarnings("NullAway") // instantiated using by Jersey using reflection
6065
private ExtendedUriInfo uriInfo;
@@ -68,22 +73,33 @@ public void filter(ContainerRequestContext requestContext) throws IOException {
6873
// The following strings are all nullable
6974
String traceId = requestContext.getHeaderString(TraceHttpHeaders.TRACE_ID);
7075
String spanId = requestContext.getHeaderString(TraceHttpHeaders.SPAN_ID);
76+
Optional<String> forUserAgent = getForUserAgent(requestContext);
7177

7278
// Set up thread-local span that inherits state from HTTP headers
7379
if (Strings.isNullOrEmpty(traceId)) {
7480
// HTTP request did not indicate a trace; initialize trace state and create a span.
7581
Tracer.initTraceWithSpan(
7682
getObservabilityFromHeader(requestContext),
7783
Tracers.randomId(),
84+
forUserAgent,
7885
operation,
7986
SpanType.SERVER_INCOMING);
8087
} else if (spanId == null) {
8188
Tracer.initTraceWithSpan(
82-
getObservabilityFromHeader(requestContext), traceId, operation, SpanType.SERVER_INCOMING);
89+
getObservabilityFromHeader(requestContext),
90+
traceId,
91+
forUserAgent,
92+
operation,
93+
SpanType.SERVER_INCOMING);
8394
} else {
8495
// caller's span is this span's parent.
8596
Tracer.initTraceWithSpan(
86-
getObservabilityFromHeader(requestContext), traceId, operation, spanId, SpanType.SERVER_INCOMING);
97+
getObservabilityFromHeader(requestContext),
98+
traceId,
99+
forUserAgent,
100+
operation,
101+
spanId,
102+
SpanType.SERVER_INCOMING);
87103
}
88104

89105
// Give asynchronous downstream handlers access to the trace id
@@ -131,6 +147,18 @@ private static Observability getObservabilityFromHeader(ContainerRequestContext
131147
}
132148
}
133149

150+
private static Optional<String> getForUserAgent(ContainerRequestContext context) {
151+
String forUserAgent = context.getHeaderString(TraceHttpHeaders.FOR_USER_AGENT);
152+
if (forUserAgent != null) {
153+
return Optional.of(forUserAgent);
154+
}
155+
String fetchUserAgent = context.getHeaderString(FETCH_USER_AGENT_HEADER);
156+
if (fetchUserAgent != null) {
157+
return Optional.of(fetchUserAgent);
158+
}
159+
return Optional.ofNullable(context.getHeaderString(HttpHeaders.USER_AGENT));
160+
}
161+
134162
private String getPathTemplate() {
135163
return Optional.ofNullable(uriInfo)
136164
.map(ExtendedUriInfo::getMatchedModelResource)

tracing-jersey/src/test/java/com/palantir/tracing/jersey/TraceEnrichingFilterTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static org.mockito.Mockito.verify;
2626
import static org.mockito.Mockito.when;
2727

28+
import com.palantir.tracing.InternalTracers;
2829
import com.palantir.tracing.TraceSampler;
2930
import com.palantir.tracing.Tracer;
3031
import com.palantir.tracing.Tracers;
@@ -45,6 +46,7 @@
4546
import javax.ws.rs.client.Entity;
4647
import javax.ws.rs.client.WebTarget;
4748
import javax.ws.rs.container.ContainerRequestContext;
49+
import javax.ws.rs.core.HttpHeaders;
4850
import javax.ws.rs.core.MediaType;
4951
import javax.ws.rs.core.Response;
5052
import javax.ws.rs.core.StreamingOutput;
@@ -238,6 +240,34 @@ public void testFilter_setsMdcIfTraceIdHeaderIsPresent() throws Exception {
238240
verify(request).setProperty(eq(TraceEnrichingFilter.REQUEST_ID_PROPERTY_NAME), anyString());
239241
}
240242

243+
@Test
244+
public void testFilter_setsUserAgentAsForUserAgent() throws Exception {
245+
when(request.getHeaderString(TraceHttpHeaders.TRACE_ID)).thenReturn("traceId");
246+
when(request.getHeaderString(HttpHeaders.USER_AGENT)).thenReturn("userAgent");
247+
TraceEnrichingFilter.INSTANCE.filter(request);
248+
249+
assertThat(InternalTracers.getForUserAgent()).contains("userAgent");
250+
}
251+
252+
@Test
253+
public void testFilter_setsFetchUserAgentAsForUserAgent() throws Exception {
254+
when(request.getHeaderString(TraceHttpHeaders.TRACE_ID)).thenReturn("traceId");
255+
when(request.getHeaderString(TraceEnrichingFilter.FETCH_USER_AGENT_HEADER))
256+
.thenReturn("fetchUserAgent");
257+
TraceEnrichingFilter.INSTANCE.filter(request);
258+
259+
assertThat(InternalTracers.getForUserAgent()).contains("fetchUserAgent");
260+
}
261+
262+
@Test
263+
public void testFilter_propagatesProvidedForUserAgent() throws Exception {
264+
when(request.getHeaderString(TraceHttpHeaders.TRACE_ID)).thenReturn("traceId");
265+
when(request.getHeaderString(TraceHttpHeaders.FOR_USER_AGENT)).thenReturn("forUserAgent");
266+
TraceEnrichingFilter.INSTANCE.filter(request);
267+
268+
assertThat(InternalTracers.getForUserAgent()).contains("forUserAgent");
269+
}
270+
241271
@Test
242272
public void testFilter_createsReceiveAndSendEvents() throws Exception {
243273
target.path("/trace").request().header(TraceHttpHeaders.TRACE_ID, "").get();

tracing-okhttp3/src/main/java/com/palantir/tracing/OkhttpTraceInterceptor2.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package com.palantir.tracing;
1818

1919
import com.palantir.logsafe.exceptions.SafeRuntimeException;
20-
import com.palantir.tracing.api.TraceHttpHeaders;
2120
import java.io.Closeable;
2221
import java.io.IOException;
2322
import java.util.function.Function;
@@ -44,14 +43,24 @@ public Response intercept(Chain chain) throws IOException {
4443
Request request = chain.request();
4544

4645
try (Closeable span = createNetworkCallSpan.apply(request)) {
47-
TraceMetadata metadata = Tracer.maybeGetTraceMetadata()
48-
.orElseThrow(() -> new SafeRuntimeException("Trace with no spans in progress"));
49-
50-
return chain.proceed(request.newBuilder()
51-
.header(TraceHttpHeaders.TRACE_ID, Tracer.getTraceId())
52-
.header(TraceHttpHeaders.SPAN_ID, metadata.getSpanId())
53-
.header(TraceHttpHeaders.IS_SAMPLED, Tracer.isTraceObservable() ? "1" : "0")
54-
.build());
46+
if (!Tracer.hasTraceId()) {
47+
throw new SafeRuntimeException("Trace with no spans in progress");
48+
}
49+
50+
Request.Builder requestBuilder = request.newBuilder();
51+
52+
Tracers.addTracingHeaders(requestBuilder, EnrichingFunction.INSTANCE);
53+
54+
return chain.proceed(requestBuilder.build());
55+
}
56+
}
57+
58+
private enum EnrichingFunction implements TracingHeadersEnrichingFunction<Request.Builder> {
59+
INSTANCE;
60+
61+
@Override
62+
public void addHeader(String headerName, String headerValue, Request.Builder state) {
63+
state.header(headerName, headerValue);
5564
}
5665
}
5766
}

tracing-okhttp3/src/main/java/com/palantir/tracing/okhttp3/OkhttpTraceInterceptor.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
package com.palantir.tracing.okhttp3;
1818

1919
import com.palantir.tracing.Tracer;
20-
import com.palantir.tracing.api.OpenSpan;
20+
import com.palantir.tracing.Tracers;
21+
import com.palantir.tracing.TracingHeadersEnrichingFunction;
2122
import com.palantir.tracing.api.SpanType;
22-
import com.palantir.tracing.api.TraceHttpHeaders;
2323
import java.io.IOException;
2424
import okhttp3.Interceptor;
2525
import okhttp3.Request;
@@ -48,11 +48,9 @@ public Response intercept(Chain chain) throws IOException {
4848
request = request.newBuilder().removeHeader(PATH_TEMPLATE_HEADER).build();
4949
}
5050

51-
OpenSpan span = Tracer.startSpan(spanName, SpanType.CLIENT_OUTGOING);
52-
Request.Builder tracedRequest = request.newBuilder()
53-
.header(TraceHttpHeaders.TRACE_ID, Tracer.getTraceId())
54-
.header(TraceHttpHeaders.SPAN_ID, span.getSpanId())
55-
.header(TraceHttpHeaders.IS_SAMPLED, Tracer.isTraceObservable() ? "1" : "0");
51+
Tracer.fastStartSpan(spanName, SpanType.CLIENT_OUTGOING);
52+
Request.Builder tracedRequest = request.newBuilder();
53+
Tracers.addTracingHeaders(tracedRequest, EnrichingFunction.INSTANCE);
5654

5755
Response response;
5856
try {
@@ -63,4 +61,13 @@ public Response intercept(Chain chain) throws IOException {
6361

6462
return response;
6563
}
64+
65+
private enum EnrichingFunction implements TracingHeadersEnrichingFunction<Request.Builder> {
66+
INSTANCE;
67+
68+
@Override
69+
public void addHeader(String headerName, String headerValue, Request.Builder state) {
70+
state.header(headerName, headerValue);
71+
}
72+
}
6673
}

tracing-okhttp3/src/test/java/com/palantir/tracing/okhttp3/OkhttpTraceInterceptorTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.palantir.tracing.api.SpanType;
3232
import com.palantir.tracing.api.TraceHttpHeaders;
3333
import java.io.IOException;
34+
import java.util.Optional;
3435
import okhttp3.Interceptor;
3536
import okhttp3.Request;
3637
import org.junit.After;
@@ -104,6 +105,28 @@ public void testPopulatesNewTrace_whenParentTraceIsPresent() throws IOException
104105
assertThat(intercepted.headers(TraceHttpHeaders.TRACE_ID)).containsOnly(traceId);
105106
}
106107

108+
@Test
109+
public void testPopulatesNewTrace_whenForUserAgentIsPresent() throws IOException {
110+
String forUserAgent = "forUserAgent";
111+
Tracer.initTraceWithSpan(
112+
Observability.SAMPLE, "id", Optional.of(forUserAgent), "operation", "parent", SpanType.SERVER_INCOMING);
113+
String traceId = Tracer.getTraceId();
114+
try {
115+
OkhttpTraceInterceptor.INSTANCE.intercept(chain);
116+
} finally {
117+
Tracer.fastCompleteSpan();
118+
}
119+
120+
verify(chain).request();
121+
verify(chain).proceed(requestCaptor.capture());
122+
verifyNoMoreInteractions(chain);
123+
124+
Request intercepted = requestCaptor.getValue();
125+
assertThat(intercepted.headers(TraceHttpHeaders.SPAN_ID)).isNotNull();
126+
assertThat(intercepted.headers(TraceHttpHeaders.TRACE_ID)).containsOnly(traceId);
127+
assertThat(intercepted.headers(TraceHttpHeaders.FOR_USER_AGENT)).containsOnly(forUserAgent);
128+
}
129+
107130
@Test
108131
public void testAddsIsSampledHeader_whenTraceIsObservable() throws IOException {
109132
Tracer.initTraceWithSpan(Observability.SAMPLE, Tracers.randomId(), "op", SpanType.LOCAL);

tracing-undertow/src/main/java/com/palantir/tracing/undertow/UndertowTracing.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.palantir.tracing.undertow;
1818

19+
import com.google.common.annotations.VisibleForTesting;
1920
import com.google.common.base.Strings;
2021
import com.palantir.logsafe.SafeArg;
2122
import com.palantir.logsafe.exceptions.SafeIllegalStateException;
@@ -32,6 +33,7 @@
3233
import io.undertow.server.HttpServerExchange;
3334
import io.undertow.util.AttachmentKey;
3435
import io.undertow.util.HeaderMap;
36+
import io.undertow.util.Headers;
3537
import io.undertow.util.HttpString;
3638
import java.util.Optional;
3739

@@ -48,6 +50,11 @@ final class UndertowTracing {
4850
private static final HttpString TRACE_ID = HttpString.tryFromString(TraceHttpHeaders.TRACE_ID);
4951
private static final HttpString SPAN_ID = HttpString.tryFromString(TraceHttpHeaders.SPAN_ID);
5052
private static final HttpString IS_SAMPLED = HttpString.tryFromString(TraceHttpHeaders.IS_SAMPLED);
53+
// Tracing headers for obtaining for constructing forUserAgent.
54+
private static final HttpString FOR_USER_AGENT = HttpString.tryFromString(TraceHttpHeaders.FOR_USER_AGENT);
55+
56+
@VisibleForTesting
57+
static final HttpString FETCH_USER_AGENT = HttpString.tryFromString("Fetch-User-Agent");
5158

5259
// Consider moving this to TracingAttachments and making it public. For now it's well encapsulated
5360
// here because we expect the two handler implementations to be sufficient.
@@ -77,7 +84,8 @@ private static DetachedSpan initializeRequestTrace(
7784
String maybeTraceId = requestHeaders.getFirst(TRACE_ID);
7885
boolean newTraceId = maybeTraceId == null;
7986
String traceId = newTraceId ? Tracers.randomId() : maybeTraceId;
80-
DetachedSpan detachedSpan = detachedSpan(operationName, newTraceId, traceId, requestHeaders);
87+
Optional<String> forUserAgent = getForUserAgent(requestHeaders);
88+
DetachedSpan detachedSpan = detachedSpan(operationName, newTraceId, traceId, forUserAgent, requestHeaders);
8189
setExchangeState(exchange, detachedSpan, traceId, translator);
8290
return detachedSpan;
8391
}
@@ -92,7 +100,7 @@ private static void setExchangeState(
92100
boolean isSampled = InternalTracers.isSampled(detachedSpan);
93101
exchange.putAttachment(TracingAttachments.IS_SAMPLED, isSampled);
94102
Optional<String> requestId = InternalTracers.getRequestId(detachedSpan);
95-
if (!requestId.isPresent()) {
103+
if (requestId.isEmpty()) {
96104
throw new SafeIllegalStateException("No requestId is set", SafeArg.of("span", detachedSpan));
97105
}
98106
exchange.putAttachment(TracingAttachments.REQUEST_ID, requestId.get());
@@ -102,10 +110,15 @@ private static void setExchangeState(
102110
}
103111

104112
private static DetachedSpan detachedSpan(
105-
String operationName, boolean newTrace, String traceId, HeaderMap requestHeaders) {
113+
String operationName,
114+
boolean newTrace,
115+
String traceId,
116+
Optional<String> forUserAgent,
117+
HeaderMap requestHeaders) {
106118
return DetachedSpan.start(
107119
getObservabilityFromHeader(requestHeaders),
108120
traceId,
121+
forUserAgent,
109122
newTrace ? Optional.empty() : Optional.ofNullable(requestHeaders.getFirst(SPAN_ID)),
110123
operationName,
111124
SpanType.SERVER_INCOMING);
@@ -148,5 +161,17 @@ private static Observability getObservabilityFromHeader(HeaderMap headers) {
148161
}
149162
}
150163

164+
private static Optional<String> getForUserAgent(HeaderMap requestHeaders) {
165+
String forUserAgent = requestHeaders.getFirst(FOR_USER_AGENT);
166+
if (forUserAgent != null) {
167+
return Optional.of(forUserAgent);
168+
}
169+
String fetchUserAgent = requestHeaders.getFirst(FETCH_USER_AGENT);
170+
if (fetchUserAgent != null) {
171+
return Optional.of(fetchUserAgent);
172+
}
173+
return Optional.ofNullable(requestHeaders.getFirst(Headers.USER_AGENT));
174+
}
175+
151176
private UndertowTracing() {}
152177
}

0 commit comments

Comments
 (0)