Spring web instrumenter 2 (#3731)

* Refactor spring-web library instrumentation to Instrumenter API

* errorprone

* fix typo
This commit is contained in:
Mateusz Rzeszutek 2021-07-30 18:28:27 +02:00 committed by GitHub
parent 78a41261d9
commit cbd8bb29fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 334 additions and 125 deletions

View File

@ -6,7 +6,7 @@
package io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.spring.httpclients.RestTemplateInterceptor;
import io.opentelemetry.instrumentation.spring.web.SpringWebTracing;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
@ -29,17 +29,20 @@ final class RestTemplateBeanPostProcessor implements BeanPostProcessor {
RestTemplate restTemplate = (RestTemplate) bean;
OpenTelemetry openTelemetry = openTelemetryProvider.getIfUnique();
if (openTelemetry != null) {
addRestTemplateInterceptorIfNotPresent(restTemplate, openTelemetry);
ClientHttpRequestInterceptor interceptor =
SpringWebTracing.create(openTelemetry).newInterceptor();
addRestTemplateInterceptorIfNotPresent(restTemplate, interceptor);
}
return restTemplate;
}
private static void addRestTemplateInterceptorIfNotPresent(
RestTemplate restTemplate, OpenTelemetry openTelemetry) {
RestTemplate restTemplate, ClientHttpRequestInterceptor instrumentationInterceptor) {
List<ClientHttpRequestInterceptor> restTemplateInterceptors = restTemplate.getInterceptors();
if (restTemplateInterceptors.stream()
.noneMatch(interceptor -> interceptor instanceof RestTemplateInterceptor)) {
restTemplateInterceptors.add(0, new RestTemplateInterceptor(openTelemetry));
.noneMatch(
interceptor -> interceptor.getClass() == instrumentationInterceptor.getClass())) {
restTemplateInterceptors.add(0, instrumentationInterceptor);
}
}
}

View File

@ -12,7 +12,6 @@ import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.spring.httpclients.RestTemplateInterceptor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@ -20,6 +19,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
@ExtendWith(MockitoExtension.class)
@ -71,7 +71,7 @@ class RestTemplateBeanPostProcessorTest {
assertThat(
restTemplate.getInterceptors().stream()
.filter(rti -> rti instanceof RestTemplateInterceptor)
.filter(RestTemplateBeanPostProcessorTest::isOtelInstrumentationInterceptor)
.count())
.isEqualTo(1);
@ -90,10 +90,14 @@ class RestTemplateBeanPostProcessorTest {
assertThat(
restTemplate.getInterceptors().stream()
.filter(rti -> rti instanceof RestTemplateInterceptor)
.filter(RestTemplateBeanPostProcessorTest::isOtelInstrumentationInterceptor)
.count())
.isEqualTo(0);
verify(openTelemetryProvider, times(3)).getIfUnique();
}
private static boolean isOtelInstrumentationInterceptor(ClientHttpRequestInterceptor rti) {
return rti.getClass().getName().startsWith("io.opentelemetry.instrumentation");
}
}

View File

@ -9,11 +9,14 @@ Provides OpenTelemetry instrumentation for Spring's RestTemplate.
Replace `SPRING_VERSION` with the version of spring you're using.
`Minimum version: 3.1`
Replace `OPENTELEMETRY_VERSION` with the latest stable [release](https://mvnrepository.com/artifact/io.opentelemetry).
`Minimum version: 0.17.0`
Replace `OPENTELEMETRY_VERSION` with the latest
stable [release](https://mvnrepository.com/artifact/io.opentelemetry).
`Minimum version: 1.4.0`
For Maven add to your `pom.xml`:
```xml
<dependencies>
<!-- opentelemetry -->
<dependency>
@ -22,8 +25,8 @@ For Maven add to your `pom.xml`:
<version>OPENTELEMETRY_VERSION</version>
</dependency>
<!-- provides opentelemetry-sdk -->
<dependency>
<!-- provides opentelemetry-sdk -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporters-logging</artifactId>
<version>OPENTELEMETRY_VERSION</version>
@ -41,6 +44,7 @@ For Maven add to your `pom.xml`:
```
For Gradle add to your dependencies:
```groovy
implementation("io.opentelemetry.instrumentation:opentelemetry-spring-web-3.1:OPENTELEMETRY_VERSION")
implementation("io.opentelemetry:opentelemetry-exporters-logging:OPENTELEMETRY_VERSION")
@ -51,16 +55,17 @@ implementation("org.springframework:spring-web:SPRING_VERSION")
### Features
#### RestTemplateInterceptor
#### Telemetry-producing `ClientHttpRequestInterceptor` implementation
RestTemplateInterceptor adds OpenTelemetry client spans to requests sent using RestTemplate by implementing the [ClientHttpRequestInterceptor](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/http/client/ClientHttpRequestInterceptor.html)
interface. An example is shown below:
`SpringWebTracing` allows creating a
custom [ClientHttpRequestInterceptor](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/http/client/ClientHttpRequestInterceptor.html)
that produces telemetry for HTTP requests sent using a `RestTemplate`. Example:
##### Usage
```java
import io.opentelemetry.instrumentation.spring.httpclients.RestTemplateInterceptor;
import io.opentelemetry.instrumentation.spring.web.SpringWebTracing;
import io.opentelemetry.api.OpenTelemetry;
import org.springframework.beans.factory.annotation.Autowired;
@ -72,18 +77,20 @@ import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(OpenTelemetry openTelemetry) {
@Bean
public RestTemplate restTemplate(OpenTelemetry openTelemetry) {
RestTemplate restTemplate = new RestTemplate();
RestTemplateInterceptor restTemplateInterceptor = new RestTemplateInterceptor(openTelemetry);
restTemplate.getInterceptors().add(restTemplateInterceptor);
RestTemplate restTemplate = new RestTemplate();
SpringWebTracing springWebTracing = SpringWebTracing.create(openTelemetry);
restTemplate.getInterceptors().add(springWebTracing.newInterceptor());
return restTemplate;
}
return restTemplate;
}
}
```
### Starter Guide
Check out the opentelemetry [quick start](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md) to learn more about OpenTelemetry instrumentation.
Check out the
OpenTelemetry [quick start](https://github.com/open-telemetry/opentelemetry-java/blob/master/QUICKSTART.md)
to learn more about OpenTelemetry instrumentation.

View File

@ -1,19 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.httpclients;
import io.opentelemetry.context.propagation.TextMapSetter;
import org.springframework.http.HttpHeaders;
class HttpHeadersInjectAdapter implements TextMapSetter<HttpHeaders> {
public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter();
@Override
public void set(HttpHeaders carrier, String key, String value) {
carrier.set(key, value);
}
}

View File

@ -1,64 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.httpclients;
import static io.opentelemetry.instrumentation.spring.httpclients.HttpHeadersInjectAdapter.SETTER;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.propagation.TextMapSetter;
import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer;
import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes;
import java.io.IOException;
import java.net.URI;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
class RestTemplateTracer extends HttpClientTracer<HttpRequest, HttpHeaders, ClientHttpResponse> {
RestTemplateTracer(OpenTelemetry openTelemetry) {
super(openTelemetry, new NetPeerAttributes());
}
@Override
protected String method(HttpRequest httpRequest) {
return httpRequest.getMethod().name();
}
@Override
protected URI url(HttpRequest request) {
return request.getURI();
}
@Override
protected Integer status(ClientHttpResponse response) {
try {
return response.getStatusCode().value();
} catch (IOException e) {
return HttpStatus.INTERNAL_SERVER_ERROR.value();
}
}
@Override
protected String requestHeader(HttpRequest request, String name) {
return request.getHeaders().getFirst(name);
}
@Override
protected String responseHeader(ClientHttpResponse response, String name) {
return response.getHeaders().getFirst(name);
}
@Override
protected TextMapSetter<HttpHeaders> getSetter() {
return SETTER;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.spring-web-3.1";
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.web;
import io.opentelemetry.context.propagation.TextMapSetter;
import org.springframework.http.HttpRequest;
final class HttpRequestSetter implements TextMapSetter<HttpRequest> {
@Override
public void set(HttpRequest httpRequest, String key, String value) {
httpRequest.getHeaders().set(key, value);
}
}

View File

@ -3,42 +3,40 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.httpclients;
package io.opentelemetry.instrumentation.spring.web;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import java.io.IOException;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
/** Wraps RestTemplate requests in a span. Adds the current span context to request headers. */
public final class RestTemplateInterceptor implements ClientHttpRequestInterceptor {
final class RestTemplateInterceptor implements ClientHttpRequestInterceptor {
private final RestTemplateTracer tracer;
private final Instrumenter<HttpRequest, ClientHttpResponse> instrumenter;
// TODO: create a SpringWebTracing class that follows the new library instrumentation pattern
public RestTemplateInterceptor(OpenTelemetry openTelemetry) {
this.tracer = new RestTemplateTracer(openTelemetry);
RestTemplateInterceptor(Instrumenter<HttpRequest, ClientHttpResponse> instrumenter) {
this.instrumenter = instrumenter;
}
@Override
public ClientHttpResponse intercept(
HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
Context parentContext = Context.current();
if (!tracer.shouldStartSpan(parentContext)) {
if (!instrumenter.shouldStart(parentContext, request)) {
return execution.execute(request, body);
}
Context context = tracer.startSpan(parentContext, request, request.getHeaders());
Context context = instrumenter.start(parentContext, request);
try (Scope ignored = context.makeCurrent()) {
ClientHttpResponse response = execution.execute(request, body);
tracer.end(context, response);
instrumenter.end(context, request, response, null);
return response;
} catch (Throwable t) {
tracer.endExceptionally(context, t);
instrumenter.end(context, request, null, t);
throw t;
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.web;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
final class SpringWebHttpAttributesExtractor
extends HttpAttributesExtractor<HttpRequest, ClientHttpResponse> {
@Override
protected String method(HttpRequest httpRequest) {
return httpRequest.getMethod().name();
}
@Override
protected @Nullable String url(HttpRequest httpRequest) {
return httpRequest.getURI().toString();
}
@Override
protected @Nullable String target(HttpRequest httpRequest) {
return null;
}
@Override
protected @Nullable String host(HttpRequest httpRequest) {
return httpRequest.getHeaders().getFirst("host");
}
@Override
protected @Nullable String route(HttpRequest httpRequest) {
return null;
}
@Override
protected @Nullable String scheme(HttpRequest httpRequest) {
return httpRequest.getURI().getScheme();
}
@Override
protected @Nullable String userAgent(HttpRequest httpRequest) {
// using lowercase header name intentionally to ensure extraction is not case-sensitive
return httpRequest.getHeaders().getFirst("user-agent");
}
@Override
protected @Nullable Long requestContentLength(
HttpRequest httpRequest, @Nullable ClientHttpResponse clientHttpResponse) {
return null;
}
@Override
protected @Nullable Long requestContentLengthUncompressed(
HttpRequest httpRequest, @Nullable ClientHttpResponse clientHttpResponse) {
return null;
}
@Override
protected @Nullable String flavor(
HttpRequest httpRequest, @Nullable ClientHttpResponse clientHttpResponse) {
return null;
}
@Override
protected @Nullable String serverName(
HttpRequest httpRequest, @Nullable ClientHttpResponse clientHttpResponse) {
return null;
}
@Override
protected @Nullable String clientIp(
HttpRequest httpRequest, @Nullable ClientHttpResponse clientHttpResponse) {
return null;
}
@Override
protected Integer statusCode(HttpRequest httpRequest, ClientHttpResponse clientHttpResponse) {
try {
return clientHttpResponse.getStatusCode().value();
} catch (IOException e) {
return HttpStatus.INTERNAL_SERVER_ERROR.value();
}
}
@Override
protected @Nullable Long responseContentLength(
HttpRequest httpRequest, ClientHttpResponse clientHttpResponse) {
return null;
}
@Override
protected @Nullable Long responseContentLengthUncompressed(
HttpRequest httpRequest, ClientHttpResponse clientHttpResponse) {
return null;
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.web;
import io.opentelemetry.instrumentation.api.instrumenter.net.NetAttributesExtractor;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpResponse;
final class SpringWebNetAttributesExtractor
extends NetAttributesExtractor<HttpRequest, ClientHttpResponse> {
@Override
public String transport(HttpRequest httpRequest) {
return SemanticAttributes.NetTransportValues.IP_TCP;
}
@Override
public @Nullable String peerName(
HttpRequest httpRequest, @Nullable ClientHttpResponse clientHttpResponse) {
return httpRequest.getURI().getHost();
}
@Override
public Integer peerPort(
HttpRequest httpRequest, @Nullable ClientHttpResponse clientHttpResponse) {
return httpRequest.getURI().getPort();
}
@Override
public @Nullable String peerIp(
HttpRequest httpRequest, @Nullable ClientHttpResponse clientHttpResponse) {
return null;
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.web;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.RestTemplate;
/** Entrypoint for tracing Spring {@link org.springframework.web.client.RestTemplate}. */
public final class SpringWebTracing {
/** Returns a new {@link SpringWebTracing} configured with the given {@link OpenTelemetry}. */
public static SpringWebTracing create(OpenTelemetry openTelemetry) {
return newBuilder(openTelemetry).build();
}
/**
* Returns a new {@link SpringWebTracingBuilder} configured with the given {@link OpenTelemetry}.
*/
public static SpringWebTracingBuilder newBuilder(OpenTelemetry openTelemetry) {
return new SpringWebTracingBuilder(openTelemetry);
}
private final Instrumenter<HttpRequest, ClientHttpResponse> instrumenter;
SpringWebTracing(Instrumenter<HttpRequest, ClientHttpResponse> instrumenter) {
this.instrumenter = instrumenter;
}
/**
* Returns a new {@link ClientHttpRequestInterceptor} that can be used with {@link
* RestTemplate#getInterceptors()}. For example:
*
* <pre>{@code
* restTemplate.getInterceptors().add(SpringWebTracing.create(openTelemetry).newInterceptor());
* }</pre>
*/
public ClientHttpRequestInterceptor newInterceptor() {
return new RestTemplateInterceptor(instrumenter);
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.web;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpResponse;
/** A builder of {@link SpringWebTracing}. */
public final class SpringWebTracingBuilder {
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.spring-web-3.1";
private final OpenTelemetry openTelemetry;
private final List<AttributesExtractor<HttpRequest, ClientHttpResponse>> additionalExtractors =
new ArrayList<>();
SpringWebTracingBuilder(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
/**
* Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented
* items.
*/
public SpringWebTracingBuilder addAttributesExtractor(
AttributesExtractor<HttpRequest, ClientHttpResponse> attributesExtractor) {
additionalExtractors.add(attributesExtractor);
return this;
}
/**
* Returns a new {@link SpringWebTracing} with the settings of this {@link
* SpringWebTracingBuilder}.
*/
public SpringWebTracing build() {
SpringWebHttpAttributesExtractor httpAttributesExtractor =
new SpringWebHttpAttributesExtractor();
SpringWebNetAttributesExtractor netAttributesExtractor = new SpringWebNetAttributesExtractor();
Instrumenter<HttpRequest, ClientHttpResponse> instrumenter =
Instrumenter.<HttpRequest, ClientHttpResponse>newBuilder(
openTelemetry,
INSTRUMENTATION_NAME,
HttpSpanNameExtractor.create(httpAttributesExtractor))
.setSpanStatusExtractor(HttpSpanStatusExtractor.create(httpAttributesExtractor))
.addAttributesExtractor(httpAttributesExtractor)
.addAttributesExtractor(netAttributesExtractor)
.addAttributesExtractors(additionalExtractors)
.addRequestMetrics(HttpClientMetrics.get())
.newClientInstrumenter(new HttpRequestSetter());
return new SpringWebTracing(instrumenter);
}
}

View File

@ -3,10 +3,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.instrumentation.spring.httpclients.RestTemplateInterceptor
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.instrumentation.spring.web.SpringWebTracing
import io.opentelemetry.instrumentation.test.LibraryTestTrait
import io.opentelemetry.instrumentation.test.base.HttpClientTest
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
@ -14,14 +16,14 @@ import org.springframework.web.client.ResourceAccessException
import org.springframework.web.client.RestTemplate
import spock.lang.Shared
class RestTemplateInstrumentationTest extends HttpClientTest<HttpEntity<String>> implements LibraryTestTrait {
class SpringWebInstrumentationTest extends HttpClientTest<HttpEntity<String>> implements LibraryTestTrait {
@Shared
RestTemplate restTemplate
def setupSpec() {
if (restTemplate == null) {
restTemplate = new RestTemplate()
restTemplate.getInterceptors().add(new RestTemplateInterceptor(getOpenTelemetry()))
restTemplate.getInterceptors().add(SpringWebTracing.create(getOpenTelemetry()).newInterceptor())
}
}
@ -66,4 +68,12 @@ class RestTemplateInstrumentationTest extends HttpClientTest<HttpEntity<String>>
boolean testWithClientParent() {
false
}
@Override
Set<AttributeKey<?>> httpAttributes(URI uri) {
def attributes = super.httpAttributes(uri)
attributes.remove(SemanticAttributes.HTTP_FLAVOR)
attributes.add(SemanticAttributes.HTTP_SCHEME)
attributes
}
}

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.httpclients;
package io.opentelemetry.instrumentation.spring.web;
import static io.opentelemetry.sdk.testing.assertj.TracesAssert.assertThat;
import static org.mockito.BDDMockito.then;
@ -17,9 +17,10 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
@ExtendWith(MockitoExtension.class)
class RestTemplateInterceptorTest {
class SpringWebTracingTest {
@RegisterExtension
static final LibraryInstrumentationExtension testing = LibraryInstrumentationExtension.create();
@ -30,7 +31,8 @@ class RestTemplateInterceptorTest {
@Test
void shouldSkipWhenContextHasClientSpan() throws Exception {
// given
RestTemplateInterceptor interceptor = new RestTemplateInterceptor(testing.getOpenTelemetry());
ClientHttpRequestInterceptor interceptor =
SpringWebTracing.create(testing.getOpenTelemetry()).newInterceptor();
// when
testing.runWithClientSpan(