|
1 | 1 | /* |
2 | | - * Copyright 2013-2020 the original author or authors. |
| 2 | + * Copyright 2013-2021 the original author or authors. |
3 | 3 | * |
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | 5 | * you may not use this file except in compliance with the License. |
|
16 | 16 |
|
17 | 17 | package org.springframework.cloud.sleuth.otel.bridge; |
18 | 18 |
|
19 | | -import java.net.URI; |
20 | | -import java.net.URISyntaxException; |
21 | | - |
22 | 19 | import io.opentelemetry.api.OpenTelemetry; |
23 | 20 | 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; |
29 | 25 | import org.apache.commons.logging.Log; |
30 | 26 | import org.apache.commons.logging.LogFactory; |
31 | 27 |
|
|
39 | 35 | import org.springframework.cloud.sleuth.http.HttpRequestParser; |
40 | 36 | import org.springframework.cloud.sleuth.http.HttpResponseParser; |
41 | 37 | import org.springframework.lang.Nullable; |
42 | | -import org.springframework.util.StringUtils; |
43 | 38 |
|
44 | 39 | /** |
45 | 40 | * OpenTelemetry implementation of a {@link HttpClientHandler}. |
46 | 41 | * |
47 | 42 | * @author Marcin Grzejszczak |
| 43 | + * @author Nikita Salnikov-Tarnovski |
48 | 44 | * @since 1.0.0 |
49 | 45 | */ |
50 | | -public class OtelHttpClientHandler extends HttpClientTracer<HttpClientRequest, HttpClientRequest, HttpClientResponse> |
51 | | - implements HttpClientHandler { |
| 46 | +public class OtelHttpClientHandler implements HttpClientHandler { |
52 | 47 |
|
53 | 48 | private static final Log log = LogFactory.getLog(OtelHttpClientHandler.class); |
54 | 49 |
|
| 50 | + private static final ContextKey<HttpClientRequest> REQUEST_CONTEXT_KEY = ContextKey |
| 51 | + .named(OtelHttpClientHandler.class.getName() + ".request"); |
| 52 | + |
55 | 53 | private final HttpRequestParser httpClientRequestParser; |
56 | 54 |
|
57 | 55 | private final HttpResponseParser httpClientResponseParser; |
58 | 56 |
|
59 | 57 | private final SamplerFunction<HttpRequest> samplerFunction; |
60 | 58 |
|
| 59 | + private final Instrumenter<HttpClientRequest, HttpClientResponse> instrumenter; |
| 60 | + |
61 | 61 | public OtelHttpClientHandler(OpenTelemetry openTelemetry, @Nullable HttpRequestParser httpClientRequestParser, |
62 | 62 | @Nullable HttpResponseParser httpClientResponseParser, SamplerFunction<HttpRequest> samplerFunction) { |
63 | | - super(openTelemetry, new NetPeerAttributes()); |
64 | 63 | this.httpClientRequestParser = httpClientRequestParser; |
65 | 64 | this.httpClientResponseParser = httpClientResponseParser; |
66 | 65 | this.samplerFunction = samplerFunction; |
67 | | - } |
68 | 66 |
|
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); |
83 | 75 | } |
84 | 76 |
|
85 | 77 | @Override |
86 | 78 | 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); |
95 | 81 | } |
96 | 82 |
|
97 | 83 | @Override |
98 | 84 | 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) { |
99 | 90 | if (Boolean.FALSE.equals(this.samplerFunction.trySample(request))) { |
100 | 91 | if (log.isDebugEnabled()) { |
101 | 92 | log.debug("Returning an invalid span since url [" + request.path() + "] is on a list of urls to skip"); |
102 | 93 | } |
103 | 94 | return OtelSpan.fromOtel(io.opentelemetry.api.trace.Span.getInvalid()); |
104 | 95 | } |
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); |
108 | 99 | } |
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()); |
124 | 102 | } |
125 | 103 | } |
126 | 104 |
|
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); |
133 | 110 | } |
| 111 | + return result; |
134 | 112 | } |
135 | 113 |
|
136 | 114 | @Override |
137 | 115 | 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()) { |
140 | 118 | if (log.isDebugEnabled()) { |
141 | 119 | log.debug("Not doing anything because the span is invalid"); |
142 | 120 | } |
143 | 121 | return; |
144 | 122 | } |
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 | | - } |
163 | 123 |
|
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()); |
192 | 134 | } |
193 | 135 |
|
194 | 136 | } |
0 commit comments