Skip to content

Commit 560c2d9

Browse files
authored
Migrate away from deprecated OpenTelemetry Tracer API (#87)
* Migrate away from deprecated OpenTelemetry Tracer API * Correct span naming and more fixes * Adapt for tests expectations * Test fix * Polish based on code review comments * Polish * Revert test changes
1 parent 4c57491 commit 560c2d9

File tree

9 files changed

+508
-236
lines changed

9 files changed

+508
-236
lines changed

spring-cloud-sleuth-otel/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
<groupId>io.opentelemetry</groupId>
5555
<artifactId>opentelemetry-api</artifactId>
5656
</dependency>
57+
<dependency>
58+
<groupId>io.opentelemetry</groupId>
59+
<artifactId>opentelemetry-api-metrics</artifactId>
60+
</dependency>
5761
<dependency>
5862
<groupId>io.opentelemetry</groupId>
5963
<artifactId>opentelemetry-extension-aws</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2013-2021 the original author or authors.
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+
* https://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 org.springframework.cloud.sleuth.otel.bridge;
18+
19+
import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor;
20+
21+
import org.springframework.cloud.sleuth.http.HttpRequest;
22+
import org.springframework.cloud.sleuth.http.HttpResponse;
23+
import org.springframework.lang.Nullable;
24+
25+
/**
26+
* Extracts OpenTelemetry network semantic attributes value for client http spans.
27+
*
28+
* @author Nikita Salnikov-Tarnovski
29+
*/
30+
class HttpRequestNetClientAttributesExtractor extends NetClientAttributesExtractor<HttpRequest, HttpResponse> {
31+
32+
@Nullable
33+
@Override
34+
public String transport(HttpRequest httpRequest, @Nullable HttpResponse httpResponse) {
35+
return null;
36+
}
37+
38+
@Nullable
39+
@Override
40+
public String peerName(HttpRequest httpRequest, @Nullable HttpResponse httpResponse) {
41+
return null;
42+
}
43+
44+
@Override
45+
public Integer peerPort(HttpRequest httpRequest, @Nullable HttpResponse httpResponse) {
46+
return httpRequest == null ? null : httpRequest.remotePort();
47+
}
48+
49+
@Nullable
50+
@Override
51+
public String peerIp(HttpRequest httpRequest, @Nullable HttpResponse httpResponse) {
52+
return httpRequest == null ? null : httpRequest.remoteIp();
53+
}
54+
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2013-2021 the original author or authors.
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+
* https://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 org.springframework.cloud.sleuth.otel.bridge;
18+
19+
import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesExtractor;
20+
21+
import org.springframework.cloud.sleuth.http.HttpRequest;
22+
import org.springframework.cloud.sleuth.http.HttpResponse;
23+
import org.springframework.lang.Nullable;
24+
25+
/**
26+
* Extracts OpenTelemetry network semantic attributes value for server http spans.
27+
*
28+
* @author Nikita Salnikov-Tarnovski
29+
*/
30+
class HttpRequestNetServerAttributesExtractor extends NetServerAttributesExtractor<HttpRequest, HttpResponse> {
31+
32+
@Nullable
33+
@Override
34+
public String transport(HttpRequest httpRequest) {
35+
return null;
36+
}
37+
38+
@Nullable
39+
@Override
40+
public String peerName(HttpRequest httpRequest) {
41+
return null;
42+
}
43+
44+
@Override
45+
public Integer peerPort(HttpRequest httpRequest) {
46+
return httpRequest.remotePort();
47+
}
48+
49+
@Nullable
50+
@Override
51+
public String peerIp(HttpRequest httpRequest) {
52+
return httpRequest.remoteIp();
53+
}
54+
55+
}
Lines changed: 50 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2020 the original author or authors.
2+
* Copyright 2013-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,16 +16,12 @@
1616

1717
package org.springframework.cloud.sleuth.otel.bridge;
1818

19-
import java.net.URI;
20-
import java.net.URISyntaxException;
21-
2219
import io.opentelemetry.api.OpenTelemetry;
2320
import io.opentelemetry.context.Context;
24-
import io.opentelemetry.context.Scope;
25-
import io.opentelemetry.context.propagation.TextMapSetter;
26-
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer;
27-
import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes;
28-
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
21+
import io.opentelemetry.context.ContextKey;
22+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
23+
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
24+
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
2925
import org.apache.commons.logging.Log;
3026
import org.apache.commons.logging.LogFactory;
3127

@@ -39,156 +35,102 @@
3935
import org.springframework.cloud.sleuth.http.HttpRequestParser;
4036
import org.springframework.cloud.sleuth.http.HttpResponseParser;
4137
import org.springframework.lang.Nullable;
42-
import org.springframework.util.StringUtils;
4338

4439
/**
4540
* OpenTelemetry implementation of a {@link HttpClientHandler}.
4641
*
4742
* @author Marcin Grzejszczak
43+
* @author Nikita Salnikov-Tarnovski
4844
* @since 1.0.0
4945
*/
50-
public class OtelHttpClientHandler extends HttpClientTracer<HttpClientRequest, HttpClientRequest, HttpClientResponse>
51-
implements HttpClientHandler {
46+
public class OtelHttpClientHandler implements HttpClientHandler {
5247

5348
private static final Log log = LogFactory.getLog(OtelHttpClientHandler.class);
5449

50+
private static final ContextKey<HttpClientRequest> REQUEST_CONTEXT_KEY = ContextKey
51+
.named(OtelHttpClientHandler.class.getName() + ".request");
52+
5553
private final HttpRequestParser httpClientRequestParser;
5654

5755
private final HttpResponseParser httpClientResponseParser;
5856

5957
private final SamplerFunction<HttpRequest> samplerFunction;
6058

59+
private final Instrumenter<HttpClientRequest, HttpClientResponse> instrumenter;
60+
6161
public OtelHttpClientHandler(OpenTelemetry openTelemetry, @Nullable HttpRequestParser httpClientRequestParser,
6262
@Nullable HttpResponseParser httpClientResponseParser, SamplerFunction<HttpRequest> samplerFunction) {
63-
super(openTelemetry, new NetPeerAttributes());
6463
this.httpClientRequestParser = httpClientRequestParser;
6564
this.httpClientResponseParser = httpClientResponseParser;
6665
this.samplerFunction = samplerFunction;
67-
}
6866

69-
@Override
70-
public Context startSpan(Context parentContext, HttpClientRequest request, HttpClientRequest carrier,
71-
long startTimeNanos) {
72-
Context context = super.startSpan(parentContext, request, carrier, startTimeNanos);
73-
io.opentelemetry.api.trace.Span span = io.opentelemetry.api.trace.Span.fromContext(context);
74-
if (this.httpClientRequestParser != null) {
75-
Span fromOtel = OtelSpan.fromOtel(span);
76-
this.httpClientRequestParser.parse(request, fromOtel.context(), fromOtel);
77-
}
78-
String path = request.path();
79-
if (path != null) {
80-
span.setAttribute(SemanticAttributes.HTTP_ROUTE, path);
81-
}
82-
return context;
67+
SpringHttpClientAttributesExtractor httpAttributesExtractor = new SpringHttpClientAttributesExtractor();
68+
this.instrumenter = Instrumenter
69+
.<HttpClientRequest, HttpClientResponse>newBuilder(openTelemetry, "org.springframework.cloud.sleuth",
70+
HttpSpanNameExtractor.create(httpAttributesExtractor))
71+
.setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesExtractor))
72+
.addAttributesExtractor(new HttpRequestNetClientAttributesExtractor())
73+
.addAttributesExtractor(httpAttributesExtractor).addAttributesExtractor(new PathAttributeExtractor())
74+
.newClientInstrumenter(HttpClientRequest::header);
8375
}
8476

8577
@Override
8678
public Span handleSend(HttpClientRequest request) {
87-
if (Boolean.FALSE.equals(this.samplerFunction.trySample(request))) {
88-
if (log.isDebugEnabled()) {
89-
log.debug("The sampler function filtered this request, will return an invalid span");
90-
}
91-
return OtelSpan.fromOtel(io.opentelemetry.api.trace.Span.getInvalid());
92-
}
93-
Context context = startSpan(Context.current(), request, request);
94-
return span(request, context);
79+
Context parentContext = Context.current();
80+
return startSpan(request, parentContext);
9581
}
9682

9783
@Override
9884
public Span handleSend(HttpClientRequest request, TraceContext parent) {
85+
Context parentContext = OtelTraceContext.toOtelContext(parent);
86+
return startSpan(request, parentContext);
87+
}
88+
89+
private Span startSpan(HttpClientRequest request, Context parentContext) {
9990
if (Boolean.FALSE.equals(this.samplerFunction.trySample(request))) {
10091
if (log.isDebugEnabled()) {
10192
log.debug("Returning an invalid span since url [" + request.path() + "] is on a list of urls to skip");
10293
}
10394
return OtelSpan.fromOtel(io.opentelemetry.api.trace.Span.getInvalid());
10495
}
105-
io.opentelemetry.api.trace.Span span = parent != null ? ((OtelTraceContext) parent).span() : null;
106-
if (span == null) {
107-
return span(request, startSpan(Context.current(), request, request));
96+
if (instrumenter.shouldStart(parentContext, request)) {
97+
Context context = instrumenter.start(parentContext, request);
98+
return span(context, request);
10899
}
109-
try (Scope scope = span.makeCurrent()) {
110-
Context withParent = startSpan(Context.current(), request, request);
111-
return span(request, withParent);
112-
}
113-
}
114-
115-
private Span span(HttpClientRequest request, Context context) {
116-
try (Scope scope = context.makeCurrent()) {
117-
io.opentelemetry.api.trace.Span span = io.opentelemetry.api.trace.Span.current();
118-
String remoteIp = request.remoteIp();
119-
if (StringUtils.hasText(remoteIp)) {
120-
span.setAttribute(SemanticAttributes.NET_PEER_IP, remoteIp);
121-
}
122-
span.setAttribute(SemanticAttributes.NET_PEER_PORT, request.remotePort());
123-
return OtelSpan.fromOtel(span);
100+
else {
101+
return OtelSpan.fromOtel(io.opentelemetry.api.trace.Span.getInvalid());
124102
}
125103
}
126104

127-
@Override
128-
protected void onResponse(io.opentelemetry.api.trace.Span span, HttpClientResponse httpClientResponse) {
129-
super.onResponse(span, httpClientResponse);
130-
if (this.httpClientResponseParser != null) {
131-
Span fromOtel = OtelSpan.fromOtel(span);
132-
this.httpClientResponseParser.parse(httpClientResponse, fromOtel.context(), fromOtel);
105+
private Span span(Context context, HttpClientRequest request) {
106+
io.opentelemetry.api.trace.Span span = io.opentelemetry.api.trace.Span.fromContext(context);
107+
Span result = OtelSpan.fromOtel(span, context.with(REQUEST_CONTEXT_KEY, request));
108+
if (this.httpClientRequestParser != null) {
109+
this.httpClientRequestParser.parse(request, result.context(), result);
133110
}
111+
return result;
134112
}
135113

136114
@Override
137115
public void handleReceive(HttpClientResponse response, Span span) {
138-
io.opentelemetry.api.trace.Span otelSpan = OtelSpan.toOtel(span);
139-
if (otelSpan.equals(io.opentelemetry.api.trace.Span.getInvalid())) {
116+
OtelSpan otelSpanWrapper = (OtelSpan) span;
117+
if (!otelSpanWrapper.delegate.getSpanContext().isValid()) {
140118
if (log.isDebugEnabled()) {
141119
log.debug("Not doing anything because the span is invalid");
142120
}
143121
return;
144122
}
145-
if (response.error() != null) {
146-
if (log.isDebugEnabled()) {
147-
log.debug("There was an error, will finish span [" + otelSpan + "] exceptionally");
148-
}
149-
endExceptionally(Context.current().with(otelSpan), response, response.error());
150-
}
151-
else {
152-
if (log.isDebugEnabled()) {
153-
log.debug("There was no error, will finish span [" + otelSpan + "] in a standard way");
154-
}
155-
end(Context.current().with(otelSpan), response);
156-
}
157-
}
158-
159-
@Override
160-
protected String method(HttpClientRequest httpClientRequest) {
161-
return httpClientRequest.method();
162-
}
163123

164-
@Override
165-
protected URI url(HttpClientRequest httpClientRequest) throws URISyntaxException {
166-
return URI.create(httpClientRequest.url());
167-
}
168-
169-
@Override
170-
protected Integer status(HttpClientResponse httpClientResponse) {
171-
return httpClientResponse.statusCode();
172-
}
173-
174-
@Override
175-
protected String requestHeader(HttpClientRequest httpClientRequest, String s) {
176-
return httpClientRequest.header(s);
177-
}
178-
179-
@Override
180-
protected String responseHeader(HttpClientResponse httpClientResponse, String s) {
181-
return httpClientResponse.header(s);
182-
}
183-
184-
@Override
185-
protected TextMapSetter<HttpClientRequest> getSetter() {
186-
return HttpClientRequest::header;
187-
}
188-
189-
@Override
190-
protected String getInstrumentationName() {
191-
return "org.springframework.cloud.sleuth";
124+
if (this.httpClientResponseParser != null) {
125+
this.httpClientResponseParser.parse(response, span.context(), span);
126+
}
127+
OtelTraceContext traceContext = otelSpanWrapper.context();
128+
Context otelContext = traceContext.context();
129+
// TODO this must be otelContext, but OpenTelemetry context handling is not
130+
// entirely correct here atm
131+
Context contextToEnd = Context.current().with(otelSpanWrapper.delegate);
132+
// response.getRequest() too often returns null
133+
instrumenter.end(contextToEnd, otelContext.get(REQUEST_CONTEXT_KEY), response, response.error());
192134
}
193135

194136
}

0 commit comments

Comments
 (0)