Skip to content

Commit 316e980

Browse files
Carter Kozakbulldozer-bot[bot]
authored andcommitted
Implement Undertow TracedOperationHandler in new tracing-undertow module (#60)
1 parent ff18c01 commit 316e980

File tree

9 files changed

+516
-3
lines changed

9 files changed

+516
-3
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ allprojects {
4646
}
4747

4848
subprojects {
49-
apply plugin: 'java'
49+
apply plugin: 'java-library'
5050
apply plugin: "org.inferred.processors"
5151

5252
repositories {

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- **com.palantir.tracing:tracing-jaxrs** - utilities to wrap `StreamingOutput` responses with a new trace.
88
- **com.palantir.tracing:tracing-okhttp3** - `OkhttpTraceInterceptor`, which adds the appropriate headers to outgoing requests.
99
- **com.palantir.tracing:tracing-jersey** - `TraceEnrichingFilter`, a jaxrs filter which reads headers from incoming requests and writes headers to outgoing responses. A traceId is stored in the jaxrs request context under the key `com.palantir.tracing.traceId`.
10+
- **com.palantir.tracing:tracing-undertow** - `TracedOperationHandler`, an Undertow handler reads headers from incoming requests and writes headers to outgoing responses.
1011

1112
Clients and servers propagate call trace ids across JVM boundaries according to the
1213
[Zipkin](https://github.com/openzipkin/zipkin) specification. In particular, clients insert `X-B3-TraceId: <Trace ID>`

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ include 'tracing-api'
55
include 'tracing-jaxrs'
66
include 'tracing-okhttp3'
77
include 'tracing-jersey'
8+
include 'tracing-undertow'

tracing-undertow/build.gradle

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* (c) Copyright 2018 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
apply from: "${rootDir}/gradle/publish-jar.gradle"
18+
19+
dependencies {
20+
api project(':tracing')
21+
api 'io.undertow:undertow-core'
22+
23+
implementation 'com.palantir.safe-logging:preconditions'
24+
25+
// Required for tests using the slf4j MDC which is not implemented in slf4j-simple
26+
testImplementation 'ch.qos.logback:logback-classic'
27+
28+
testImplementation 'junit:junit'
29+
testImplementation 'org.assertj:assertj-core'
30+
testImplementation 'org.mockito:mockito-core'
31+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.tracing.undertow;
18+
19+
import com.google.common.base.Strings;
20+
import com.palantir.logsafe.Preconditions;
21+
import com.palantir.tracing.Tracer;
22+
import com.palantir.tracing.Tracers;
23+
import com.palantir.tracing.api.SpanType;
24+
import com.palantir.tracing.api.TraceHttpHeaders;
25+
import io.undertow.server.HttpHandler;
26+
import io.undertow.server.HttpServerExchange;
27+
import io.undertow.util.HeaderMap;
28+
import io.undertow.util.HttpString;
29+
import java.util.Optional;
30+
31+
/**
32+
* Extracts Zipkin-style trace information from the given HTTP request and sets up a corresponding
33+
* {@link com.palantir.tracing.Trace} and {@link com.palantir.tracing.api.Span} for delegating to the configured
34+
* {@link #delegate} handler. See <a href="https://github.com/openzipkin/b3-propagation">b3-propagation</a>.
35+
*
36+
* Note that this handler must be registered after routing, each instance is used for exactly one operation name.
37+
* This {@link HttpHandler handler} traces the execution of the {@link TracedOperationHandler#delegate} handlers
38+
* {@link HttpHandler#handleRequest(HttpServerExchange)}, but does not apply tracing to any asynchronous operations
39+
* that handler may register.
40+
*/
41+
public final class TracedOperationHandler implements HttpHandler {
42+
43+
private static final HttpString TRACE_ID = HttpString.tryFromString(TraceHttpHeaders.TRACE_ID);
44+
private static final HttpString SPAN_ID = HttpString.tryFromString(TraceHttpHeaders.SPAN_ID);
45+
private static final HttpString IS_SAMPLED = HttpString.tryFromString(TraceHttpHeaders.IS_SAMPLED);
46+
47+
// Pre-compute sampled values, there's no need to do this work for each request
48+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
49+
private static final Optional<Boolean> SAMPLED = Optional.of(Boolean.TRUE);
50+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
51+
private static final Optional<Boolean> NOT_SAMPLED = Optional.of(Boolean.FALSE);
52+
53+
private final String operation;
54+
private final HttpHandler delegate;
55+
56+
public TracedOperationHandler(HttpHandler delegate, String operation) {
57+
this.delegate = Preconditions.checkNotNull(delegate, "A delegate HttpHandler is required");
58+
this.operation = "Undertow: " + Preconditions.checkNotNull(operation, "Operation name is required");
59+
}
60+
61+
@Override
62+
public void handleRequest(HttpServerExchange exchange) throws Exception {
63+
String traceId = initializeTrace(exchange);
64+
// Populate response before calling delegate since delegate might commit the response.
65+
exchange.getResponseHeaders().put(TRACE_ID, traceId);
66+
try {
67+
delegate.handleRequest(exchange);
68+
} finally {
69+
Tracer.fastCompleteSpan();
70+
}
71+
}
72+
73+
// Returns true iff the context contains a "1" X-B3-Sampled header, false if the header contains another value,
74+
// or absent if there is no such header.
75+
private static Optional<Boolean> hasSampledHeader(HeaderMap headers) {
76+
String header = headers.getFirst(IS_SAMPLED);
77+
if (header == null) {
78+
return Optional.empty();
79+
} else {
80+
// No need to box the resulting boolean and allocate
81+
// a new Optional wrapper for each invocation.
82+
return header.equals("1") ? SAMPLED : NOT_SAMPLED;
83+
}
84+
}
85+
86+
/** Initializes trace state and a root span for this request, returning the traceId. */
87+
private String initializeTrace(HttpServerExchange exchange) {
88+
HeaderMap headers = exchange.getRequestHeaders();
89+
// TODO(rfink): Log/warn if we find multiple headers?
90+
String traceId = headers.getFirst(TRACE_ID); // nullable
91+
92+
// Set up thread-local span that inherits state from HTTP headers
93+
if (Strings.isNullOrEmpty(traceId)) {
94+
return initializeNewTrace(headers);
95+
} else {
96+
initializeTraceFromExisting(headers, traceId);
97+
}
98+
return traceId;
99+
}
100+
101+
/** Initializes trace state given a trace-id header from the client. */
102+
private void initializeTraceFromExisting(HeaderMap headers, String traceId) {
103+
Tracer.initTrace(hasSampledHeader(headers), traceId);
104+
String spanId = headers.getFirst(SPAN_ID); // nullable
105+
if (spanId == null) {
106+
Tracer.startSpan(operation, SpanType.SERVER_INCOMING);
107+
} else {
108+
// caller's span is this span's parent.
109+
Tracer.startSpan(operation, spanId, SpanType.SERVER_INCOMING);
110+
}
111+
}
112+
113+
/** Initializes trace state for a request without tracing headers. */
114+
private String initializeNewTrace(HeaderMap headers) {
115+
// HTTP request did not indicate a trace; initialize trace state and create a span.
116+
String newTraceId = Tracers.randomId();
117+
Tracer.initTrace(hasSampledHeader(headers), newTraceId);
118+
Tracer.startSpan(operation, SpanType.SERVER_INCOMING);
119+
return newTraceId;
120+
}
121+
122+
@Override
123+
public String toString() {
124+
return "TracedOperationHandler{operation='" + operation + "', delegate=" + delegate + '}';
125+
}
126+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.tracing.undertow;
18+
19+
import static org.mockito.Mockito.lenient;
20+
import static org.mockito.Mockito.mock;
21+
22+
import io.undertow.server.HttpServerExchange;
23+
import io.undertow.server.ServerConnection;
24+
import io.undertow.server.protocol.http.HttpServerConnection;
25+
import io.undertow.util.HeaderMap;
26+
import io.undertow.util.Protocols;
27+
import org.xnio.OptionMap;
28+
import org.xnio.StreamConnection;
29+
import org.xnio.XnioIoThread;
30+
import org.xnio.conduits.ConduitStreamSinkChannel;
31+
import org.xnio.conduits.ConduitStreamSourceChannel;
32+
import org.xnio.conduits.StreamSinkConduit;
33+
import org.xnio.conduits.StreamSourceConduit;
34+
35+
public final class HttpServerExchanges {
36+
37+
private HttpServerExchanges() {}
38+
39+
public static HttpServerExchange createStub() {
40+
return createExchange(new HttpServerConnection(createStreamConnection(), null, null, OptionMap.EMPTY, 0, null));
41+
}
42+
43+
private static StreamConnection createStreamConnection() {
44+
StreamConnection streamConnection = mock(StreamConnection.class);
45+
ConduitStreamSinkChannel sinkChannel = new ConduitStreamSinkChannel(null, mock(StreamSinkConduit.class));
46+
lenient().when(streamConnection.getSinkChannel()).thenReturn(sinkChannel);
47+
ConduitStreamSourceChannel sourceChannel =
48+
new ConduitStreamSourceChannel(null, mock(StreamSourceConduit.class));
49+
lenient().when(streamConnection.getSourceChannel()).thenReturn(sourceChannel);
50+
XnioIoThread ioThread = mock(XnioIoThread.class);
51+
lenient().when(streamConnection.getIoThread()).thenReturn(ioThread);
52+
return streamConnection;
53+
}
54+
55+
private static HttpServerExchange createExchange(ServerConnection connection) {
56+
HttpServerExchange httpServerExchange =
57+
new HttpServerExchange(connection, new HeaderMap(), new HeaderMap(), 200);
58+
httpServerExchange.setProtocol(Protocols.HTTP_1_1);
59+
return httpServerExchange;
60+
}
61+
}

0 commit comments

Comments
 (0)