Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

10702 - rest client instrumentation #10731

Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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;

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 restClient)) {
return bean;
}
ClientHttpRequestInterceptor interceptor = SpringWebTelemetry.create(openTelemetryProvider.getObject()).newInterceptor();
return addRestClientInterceptorIfNotPresent(restClient, interceptor);
}

private static RestClient addRestClientInterceptorIfNotPresent(RestClient restClient, ClientHttpRequestInterceptor instrumentationInterceptor) {
return restClient.mutate().requestInterceptors(interceptors -> {
if (interceptors.stream().noneMatch(interceptor -> interceptor.getClass() == instrumentationInterceptor.getClass())) {
interceptors.addFirst(instrumentationInterceptor);
}
}).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,33 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;

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

public SpringWebInstrumentationAutoConfiguration() {}

@ConditionalOnClass(RestTemplate.class)
@Bean
static RestTemplateBeanPostProcessor otelRestTemplateBeanPostProcessor(
ObjectProvider<OpenTelemetry> openTelemetryProvider) {
return new RestTemplateBeanPostProcessor(openTelemetryProvider);
}

@ConditionalOnClass(RestClient.class)
@Bean
static RestClientBeanPostProcessor otelRestClientBeanPostProcessor(
ObjectProvider<OpenTelemetry> openTelemetryProvider) {
return new RestClientBeanPostProcessor(openTelemetryProvider);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestClient;

class RestClienteBeanPostProcessorTest {
private static final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

static {
beanFactory.registerSingleton("openTelemetry", OpenTelemetry.noop());
}

@Test
@DisplayName("when processed bean is not of type RestClient should return object")
void returnsObject() {
BeanPostProcessor underTest =
new RestClientBeanPostProcessor(beanFactory.getBeanProvider(OpenTelemetry.class));

assertThat(underTest.postProcessAfterInitialization(new Object(), "testObject"))
.isExactlyInstanceOf(Object.class);
}

@Test
@DisplayName("when processed bean is of type RestClient should return RestClient")
void returnsRestClient() {
BeanPostProcessor underTest =
new RestClientBeanPostProcessor(beanFactory.getBeanProvider(OpenTelemetry.class));

assertThat(underTest.postProcessAfterInitialization(new RestClient(), "testRestClient"))
.isInstanceOf(RestClient.class);
}

@Test
@DisplayName("when processed bean is of type RestClient should add ONE RestClientInterceptor")
void addsRestClientInterceptor() {
BeanPostProcessor underTest =
new RestClientBeanPostProcessor(beanFactory.getBeanProvider(OpenTelemetry.class));

RestClient restClient = new RestClient();

underTest.postProcessAfterInitialization(restClient, "testRestClient");
underTest.postProcessAfterInitialization(restClient, "testRestClient");
underTest.postProcessAfterInitialization(restClient, "testRestClient");

assertThat(
restClient.getInterceptors().stream()
.filter(RestClientBeanPostProcessorTest::isOtelInstrumentationInterceptor)
.count())
.isEqualTo(1);
}

private static boolean isOtelInstrumentationInterceptor(ClientHttpRequestInterceptor rti) {
return rti.getClass().getName().startsWith("io.opentelemetry.instrumentation");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,43 @@ void instrumentationEnabled() {
contextRunner
.withPropertyValues("otel.instrumentation.spring-web.enabled=true")
.run(
context ->
context -> {
assertThat(
context.getBean(
"otelRestTemplateBeanPostProcessor",
RestTemplateBeanPostProcessor.class))
.isNotNull());
.isNotNull();
assertThat(
context.getBean(
"otelRestClientBeanPostProcessor",
RestClientBeanPostProcessor.class))
.isNotNull();
});
}

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

@Test
void defaultConfiguration() {
contextRunner.run(
context ->
context -> {
assertThat(
context.getBean(
"otelRestTemplateBeanPostProcessor", RestTemplateBeanPostProcessor.class))
.isNotNull());
.isNotNull();
assertThat(
context.getBean(
"otelRestClientBeanPostProcessor", RestClientBeanPostProcessor.class))
.isNotNull()
});
}
}
Loading