Skip to content

Commit

Permalink
add instrumentation for RestClient
Browse files Browse the repository at this point in the history
  • Loading branch information
zeitlinger committed Apr 12, 2024
1 parent 0f1090d commit d355f78
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
id("otel.library-instrumentation")
}

// Name the Spring Boot modules in accordance with https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.custom-starter
base.archivesName.set("opentelemetry-spring-boot-3")
group = "io.opentelemetry.instrumentation"

dependencies {
val springBootVersion = "3.2.4"
// implementation("org.springframework.boot:spring-boot-autoconfigure:$springBootVersion")
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
implementation(project(":instrumentation:spring:spring-boot-autoconfigure"))
implementation(project(":instrumentation:spring:spring-web:spring-web-3.1:library"))

testLibrary("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
exclude("org.junit.vintage", "junit-vintage-engine")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.web;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.spring.web.v3_1.SpringWebTelemetry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestClient;

public final class RestClientBeanPostProcessor implements BeanPostProcessor {

private final ObjectProvider<OpenTelemetry> openTelemetryProvider;

public RestClientBeanPostProcessor(ObjectProvider<OpenTelemetry> openTelemetryProvider) {
this.openTelemetryProvider = openTelemetryProvider;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (!(bean instanceof RestClient)) {
return bean;
}
return addRestClientInterceptorIfNotPresent(
((RestClient) bean), openTelemetryProvider.getObject());
}

private static RestClient addRestClientInterceptorIfNotPresent(
RestClient restClient, OpenTelemetry openTelemetry) {
ClientHttpRequestInterceptor instrumentationInterceptor =
SpringWebTelemetry.create(openTelemetry).newInterceptor();

return restClient
.mutate()
.requestInterceptors(
interceptors -> {
if (interceptors.stream()
.noneMatch(
interceptor ->
interceptor.getClass() == instrumentationInterceptor.getClass())) {
interceptors.add(0, instrumentationInterceptor);
}
})
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.web;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.SdkEnabled;
import io.opentelemetry.instrumentation.spring.web.v3_1.SpringWebTelemetry;
import io.opentelemetry.testing.internal.armeria.client.RestClient;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

/**
* Configures {@link RestClient} for tracing.
*
* <p>Adds Open Telemetry instrumentation to {@link RestClient} beans after initialization
*/
@ConditionalOnBean(OpenTelemetry.class)
@ConditionalOnProperty(name = "otel.instrumentation.spring-web.enabled", matchIfMissing = true)
@ConditionalOnClass(RestClient.class)
@Conditional(SdkEnabled.class)
@Configuration
public class SpringWeb6InstrumentationAutoConfiguration {

public SpringWeb6InstrumentationAutoConfiguration() {}

@Bean
static RestClientBeanPostProcessor otelRestClientBeanPostProcessor(
ObjectProvider<OpenTelemetry> openTelemetryProvider) {
return new RestClientBeanPostProcessor(openTelemetryProvider);
}

@Bean
static RestClientCustomizer otelRestClientCustomizer(
ObjectProvider<OpenTelemetry> openTelemetryProvider) {
return builder ->
builder.requestInterceptor(
SpringWebTelemetry.create(openTelemetryProvider.getObject()).newInterceptor());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.web.SpringWeb6InstrumentationAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.web.SpringWeb6InstrumentationAutoConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.web;

import static org.assertj.core.api.Assertions.assertThat;

import io.opentelemetry.api.OpenTelemetry;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.web.client.RestClient;

class SpringWeb6InstrumentationAutoConfigurationTest {

private final ApplicationContextRunner contextRunner =
new ApplicationContextRunner()
.withBean(OpenTelemetry.class, OpenTelemetry::noop)
.withBean(RestClient.class, RestClient::create)
.withConfiguration(
AutoConfigurations.of(SpringWeb6InstrumentationAutoConfiguration.class));

/**
* Tests the case that users create a {@link RestClient} bean themselves.
*
* <pre>{@code
* @Bean public RestClient restClient() {
* return new RestClient();
* }
* }</pre>
*/
@Test
void instrumentationEnabled() {
contextRunner
.withPropertyValues("otel.instrumentation.spring-web.enabled=true")
.run(
context -> {
assertThat(
context.getBean(
"otelRestClientBeanPostProcessor", RestClientBeanPostProcessor.class))
.isNotNull();

context
.getBean(RestClient.class)
.mutate()
.requestInterceptors(
interceptors -> {
long count =
interceptors.stream()
.filter(
rti ->
rti.getClass()
.getName()
.startsWith("io.opentelemetry.instrumentation"))
.count();
assertThat(count).isEqualTo(1);
});
});
}

@Test
void instrumentationDisabled() {
contextRunner
.withPropertyValues("otel.instrumentation.spring-web.enabled=false")
.run(
context ->
assertThat(context.containsBean("otelRestClientBeanPostProcessor")).isFalse());
}

@Test
void defaultConfiguration() {
contextRunner.run(
context ->
assertThat(
context.getBean(
"otelRestClientBeanPostProcessor", RestClientBeanPostProcessor.class))
.isNotNull());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
api("org.springframework.boot:spring-boot-starter:$springBootVersion")
api("org.springframework.boot:spring-boot-starter-aop:$springBootVersion")
api(project(":instrumentation:spring:spring-boot-autoconfigure"))
api(project(":instrumentation:spring:spring-boot-autoconfigure-3"))
api(project(":instrumentation-annotations"))
implementation(project(":instrumentation:resources:library"))
implementation("io.opentelemetry:opentelemetry-sdk-extension-incubator")
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ include(":instrumentation:spark-2.3:javaagent")
include(":instrumentation:spring:spring-batch-3.0:javaagent")
include(":instrumentation:spring:spring-boot-actuator-autoconfigure-2.0:javaagent")
include(":instrumentation:spring:spring-boot-autoconfigure")
include(":instrumentation:spring:spring-boot-autoconfigure-3")
include(":instrumentation:spring:spring-boot-resources:javaagent")
include(":instrumentation:spring:spring-boot-resources:javaagent-unit-tests")
include(":instrumentation:spring:spring-cloud-gateway:spring-cloud-gateway-2.0:javaagent")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,30 @@
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;

@RestController
public class OtelSpringStarterSmokeTestController {

public static final String PING = "/ping";
public static final String REST_CLIENT = "/rest-client";
public static final String REST_TEMPLATE = "/rest-template";
public static final String TEST_HISTOGRAM = "histogram-test-otel-spring-starter";
private final LongHistogram histogram;
private final Optional<RestTemplate> restTemplate;
private final Optional<RestClient> restClient;

public OtelSpringStarterSmokeTestController(
OpenTelemetry openTelemetry,
RestClient.Builder restClientBuilder,
RestTemplateBuilder restTemplateBuilder,
Optional<ServletWebServerApplicationContext> server) {
Meter meter = openTelemetry.getMeter(OtelSpringStarterSmokeTestApplication.class.getName());
histogram = meter.histogramBuilder(TEST_HISTOGRAM).ofLongs().build();
restTemplate =
server.map(
s ->
restTemplateBuilder
.rootUri("http://localhost:" + s.getWebServer().getPort())
.build());
Optional<String> rootUri = server.map(s -> "http://localhost:" + s.getWebServer().getPort());
restClient = rootUri.map(uri -> restClientBuilder.baseUrl(uri).build());
restTemplate = rootUri.map(uri -> restTemplateBuilder.rootUri(uri).build());
}

@GetMapping(PING)
Expand All @@ -44,10 +45,17 @@ public String ping() {
return "pong";
}

@GetMapping(REST_CLIENT)
public String restClient() {
return restClient
.map(c -> c.get().uri(PING).retrieve().body(String.class))
.orElseThrow(() -> new IllegalStateException("RestClient not available"));
}

@GetMapping(REST_TEMPLATE)
public String restTemplate() {
return restTemplate
.map(t -> t.getForObject("/ping", String.class))
.map(t -> t.getForObject(PING, String.class))
.orElseThrow(() -> new IllegalStateException("RestTemplate not available"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,20 @@ void shouldSendTelemetry() {

@Test
@org.junit.jupiter.api.Order(2)
void restTemplateClient() {
void restTemplate() {
assertClient(OtelSpringStarterSmokeTestController.REST_TEMPLATE);
}

@Test
@org.junit.jupiter.api.Order(3)
void restClient() {
assertClient(OtelSpringStarterSmokeTestController.REST_CLIENT);
}

private void assertClient(String url) {
resetExporters(); // ignore the telemetry from application startup

testRestTemplate.getForObject(OtelSpringStarterSmokeTestController.REST_TEMPLATE, String.class);
testRestTemplate.getForObject(url, String.class);

TracesAssert.assertThat(expectSpans(4))
.hasTracesSatisfyingExactly(
Expand All @@ -272,13 +282,11 @@ void restTemplateClient() {
clientSpan
.hasKind(SpanKind.CLIENT)
.hasAttributesSatisfying(
a ->
assertThat(a.get(UrlAttributes.URL_FULL))
.endsWith("/rest-template")),
a -> assertThat(a.get(UrlAttributes.URL_FULL)).endsWith(url)),
serverSpan ->
serverSpan
.hasKind(SpanKind.SERVER)
.hasAttribute(HttpAttributes.HTTP_ROUTE, "/rest-template"),
.hasAttribute(HttpAttributes.HTTP_ROUTE, url),
nestedClientSpan ->
nestedClientSpan
.hasKind(SpanKind.CLIENT)
Expand Down

0 comments on commit d355f78

Please sign in to comment.