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 8, 2024
1 parent 5122888 commit 1282773
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 16 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"))
api("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,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,6 +13,7 @@
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
Expand All @@ -22,24 +23,30 @@ public class OtelSpringStarterSmokeTestController {
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(URL)
public String ping() {
histogram.record(10);
return restClient
.map(c -> c.get().uri("/rest-template").retrieve().body(String.class))
.orElseThrow(() -> new IllegalStateException("RestClient not available"));
}

@GetMapping("/rest-template")
public String restTemplate() {
return restTemplate
.map(t -> t.getForObject("/pong", String.class))
.orElseThrow(() -> new IllegalStateException("RestTemplate not available"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,11 @@ void propertyConversion() {

@Test
void shouldSendTelemetry() {

testRestTemplate.getForObject(OtelSpringStarterSmokeTestController.URL, String.class);

await()
.atMost(Duration.ofSeconds(1))
.until(() -> SPAN_EXPORTER.getFinishedSpanItems().size() == 2);
.until(() -> SPAN_EXPORTER.getFinishedSpanItems().size() == 7);

List<SpanData> exportedSpans = SPAN_EXPORTER.getFinishedSpanItems();

Expand All @@ -194,15 +193,15 @@ void shouldSendTelemetry() {
"create table test_table (id bigint not null, primary key (id))")),
traceAssert ->
traceAssert.hasSpansSatisfyingExactly(
clientSpan ->
clientSpan
pingClient ->
pingClient
.hasKind(SpanKind.CLIENT)
.hasAttributesSatisfying(
a ->
assertThat(a.get(SemanticAttributes.URL_FULL))
.endsWith("/ping")),
serverSpan ->
serverSpan
pingServer ->
pingServer
.hasKind(SpanKind.SERVER)
.hasResourceSatisfying(
r ->
Expand All @@ -214,15 +213,26 @@ void shouldSendTelemetry() {
.hasAttribute(SemanticAttributes.HTTP_REQUEST_METHOD, "GET")
.hasAttribute(SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, 200L)
.hasAttribute(SemanticAttributes.HTTP_ROUTE, "/ping"),
nestedClientSpan ->
nestedClientSpan
restTemplateClient ->
restTemplateClient
.hasKind(SpanKind.CLIENT)
.hasAttributesSatisfying(
a ->
assertThat(a.get(SemanticAttributes.URL_FULL))
.endsWith("/rest-template")),
restTemplateServer ->
restTemplateServer
.hasKind(SpanKind.SERVER)
.hasAttribute(SemanticAttributes.HTTP_ROUTE, "/rest-template"),
pongClient ->
pongClient
.hasKind(SpanKind.CLIENT)
.hasAttributesSatisfying(
a ->
assertThat(a.get(SemanticAttributes.URL_FULL))
.endsWith("/pong")),
nestedServerSpan ->
nestedServerSpan
pongServer ->
pongServer
.hasKind(SpanKind.SERVER)
.hasAttribute(SemanticAttributes.HTTP_ROUTE, "/pong")));

Expand Down

0 comments on commit 1282773

Please sign in to comment.