Skip to content

Commit

Permalink
add instrumentation for RestTemplateBuilder (#11054)
Browse files Browse the repository at this point in the history
Co-authored-by: Jean Bisutti <jean.bisutti@gmail.com>
  • Loading branch information
zeitlinger and jeanbisutti committed Apr 11, 2024
1 parent 4c77b48 commit 7de246b
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@
package io.opentelemetry.instrumentation.spring.autoconfigure.instrumentation.web;

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

final class RestTemplateBeanPostProcessor implements BeanPostProcessor {
Expand All @@ -27,20 +24,7 @@ public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}

RestTemplate restTemplate = (RestTemplate) bean;
ClientHttpRequestInterceptor interceptor =
SpringWebTelemetry.create(openTelemetryProvider.getObject()).newInterceptor();
addRestTemplateInterceptorIfNotPresent(restTemplate, interceptor);
return restTemplate;
}

private static void addRestTemplateInterceptorIfNotPresent(
RestTemplate restTemplate, ClientHttpRequestInterceptor instrumentationInterceptor) {
List<ClientHttpRequestInterceptor> restTemplateInterceptors = restTemplate.getInterceptors();
if (restTemplateInterceptors.stream()
.noneMatch(
interceptor -> interceptor.getClass() == instrumentationInterceptor.getClass())) {
restTemplateInterceptors.add(0, instrumentationInterceptor);
}
return RestTemplateInstrumentation.addIfNotPresent(
(RestTemplate) bean, openTelemetryProvider.getObject());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

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

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.spring.web.v3_1.SpringWebTelemetry;
import java.util.List;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;

class RestTemplateInstrumentation {

private RestTemplateInstrumentation() {}

@CanIgnoreReturnValue
static RestTemplate addIfNotPresent(RestTemplate restTemplate, OpenTelemetry openTelemetry) {
ClientHttpRequestInterceptor instrumentationInterceptor =
SpringWebTelemetry.create(openTelemetry).newInterceptor();

List<ClientHttpRequestInterceptor> restTemplateInterceptors = restTemplate.getInterceptors();
if (restTemplateInterceptors.stream()
.noneMatch(
interceptor -> interceptor.getClass() == instrumentationInterceptor.getClass())) {
restTemplateInterceptors.add(0, instrumentationInterceptor);
}
return restTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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.RestTemplateCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
Expand All @@ -35,4 +36,12 @@ static RestTemplateBeanPostProcessor otelRestTemplateBeanPostProcessor(
ObjectProvider<OpenTelemetry> openTelemetryProvider) {
return new RestTemplateBeanPostProcessor(openTelemetryProvider);
}

@Bean
static RestTemplateCustomizer otelRestTemplateCustomizer(
ObjectProvider<OpenTelemetry> openTelemetryProvider) {
return restTemplate ->
RestTemplateInstrumentation.addIfNotPresent(
restTemplate, openTelemetryProvider.getObject());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,46 @@
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.metrics.LongHistogram;
import io.opentelemetry.api.metrics.Meter;
import java.util.Optional;
import org.springframework.boot.web.client.RestTemplateBuilder;
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.RestTemplate;

@RestController
public class OtelSpringStarterSmokeTestController {

public static final String URL = "/ping";
public static final String PING = "/ping";
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;

public OtelSpringStarterSmokeTestController(OpenTelemetry openTelemetry) {
public OtelSpringStarterSmokeTestController(
OpenTelemetry openTelemetry,
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());
}

@GetMapping(URL)
@GetMapping(PING)
public String ping() {
histogram.record(10);
return "pong";
}

@GetMapping(REST_TEMPLATE)
public String restTemplate() {
return restTemplate
.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 @@ -6,7 +6,7 @@
package io.opentelemetry.smoketest;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.awaitility.Awaitility.with;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
Expand Down Expand Up @@ -35,6 +35,7 @@
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.semconv.HttpAttributes;
import io.opentelemetry.semconv.UrlAttributes;
import io.opentelemetry.semconv.incubating.CodeIncubatingAttributes;
import io.opentelemetry.semconv.incubating.DbIncubatingAttributes;
import io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes;
Expand All @@ -44,7 +45,14 @@
import java.util.Collections;
import java.util.List;
import org.assertj.core.api.AbstractCharSequenceAssert;
import org.awaitility.core.ConditionEvaluationLogger;
import org.awaitility.core.EvaluatedCondition;
import org.awaitility.core.TimeoutEvent;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
Expand Down Expand Up @@ -72,13 +80,15 @@
"otel.metrics.exporter=memory",
"otel.logs.exporter=memory"
})
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OtelSpringStarterSmokeTest {

public static final InMemoryMetricExporter METRIC_EXPORTER =
private static final InMemoryMetricExporter METRIC_EXPORTER =
InMemoryMetricExporter.create(AggregationTemporality.DELTA);
private static final InMemoryLogRecordExporter LOG_RECORD_EXPORTER =
InMemoryLogRecordExporter.create();
public static final InMemorySpanExporter SPAN_EXPORTER = InMemorySpanExporter.create();
private static final InMemorySpanExporter SPAN_EXPORTER = InMemorySpanExporter.create();
private static final Logger logger = LoggerFactory.getLogger(OtelSpringStarterSmokeTest.class);

@Autowired private TestRestTemplate testRestTemplate;

Expand Down Expand Up @@ -160,7 +170,14 @@ AutoConfigurationCustomizerProvider propagatorCustomizer() {
}
}

private static void resetExporters() {
SPAN_EXPORTER.reset();
METRIC_EXPORTER.reset();
LOG_RECORD_EXPORTER.reset();
}

@Test
@org.junit.jupiter.api.Order(10)
void propertyConversion() {
ConfigProperties configProperties =
SpringConfigProperties.create(
Expand All @@ -178,18 +195,12 @@ void propertyConversion() {
}

@Test
@org.junit.jupiter.api.Order(1)
void shouldSendTelemetry() {

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

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

List<SpanData> exportedSpans = SPAN_EXPORTER.getFinishedSpanItems();
testRestTemplate.getForObject(OtelSpringStarterSmokeTestController.PING, String.class);

// Span
TracesAssert.assertThat(exportedSpans)
TracesAssert.assertThat(expectSpans(3))
.hasTracesSatisfyingExactly(
traceAssert ->
traceAssert.hasSpansSatisfyingExactly(
Expand All @@ -201,8 +212,13 @@ void shouldSendTelemetry() {
"create table test_table (id bigint not null, primary key (id))")),
traceAssert ->
traceAssert.hasSpansSatisfyingExactly(
spanDataAssert ->
spanDataAssert
clientSpan ->
clientSpan
.hasKind(SpanKind.CLIENT)
.hasAttributesSatisfying(
a -> assertThat(a.get(UrlAttributes.URL_FULL)).endsWith("/ping")),
serverSpan ->
serverSpan
.hasKind(SpanKind.SERVER)
.hasResourceSatisfying(
r ->
Expand Down Expand Up @@ -230,8 +246,7 @@ void shouldSendTelemetry() {
});

// Log
List<LogRecordData> logs = LOG_RECORD_EXPORTER.getFinishedLogRecordItems();
LogRecordData firstLog = logs.get(0);
LogRecordData firstLog = LOG_RECORD_EXPORTER.getFinishedLogRecordItems().get(0);
assertThat(firstLog.getBody().asString())
.as("Should instrument logs")
.startsWith("Starting ")
Expand All @@ -241,4 +256,56 @@ void shouldSendTelemetry() {
.containsEntry(
CodeIncubatingAttributes.CODE_NAMESPACE, "org.springframework.boot.StartupInfoLogger");
}

@Test
@org.junit.jupiter.api.Order(2)
void restTemplateClient() {
resetExporters(); // ignore the telemetry from application startup

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

TracesAssert.assertThat(expectSpans(4))
.hasTracesSatisfyingExactly(
traceAssert ->
traceAssert.hasSpansSatisfyingExactly(
clientSpan ->
clientSpan
.hasKind(SpanKind.CLIENT)
.hasAttributesSatisfying(
a ->
assertThat(a.get(UrlAttributes.URL_FULL))
.endsWith("/rest-template")),
serverSpan ->
serverSpan
.hasKind(SpanKind.SERVER)
.hasAttribute(HttpAttributes.HTTP_ROUTE, "/rest-template"),
nestedClientSpan ->
nestedClientSpan
.hasKind(SpanKind.CLIENT)
.hasAttributesSatisfying(
a -> assertThat(a.get(UrlAttributes.URL_FULL)).endsWith("/ping")),
nestedServerSpan ->
nestedServerSpan
.hasKind(SpanKind.SERVER)
.hasAttribute(HttpAttributes.HTTP_ROUTE, "/ping")));
}

private static List<SpanData> expectSpans(int spans) {
with()
.conditionEvaluationListener(
new ConditionEvaluationLogger() {
@Override
public void conditionEvaluated(EvaluatedCondition<Object> condition) {}

@Override
public void onTimeout(TimeoutEvent timeoutEvent) {
logger.info("Spans: {}", SPAN_EXPORTER.getFinishedSpanItems());
}
})
.await()
.atMost(Duration.ofSeconds(30))
.until(() -> SPAN_EXPORTER.getFinishedSpanItems().size() == spans);

return SPAN_EXPORTER.getFinishedSpanItems();
}
}

0 comments on commit 7de246b

Please sign in to comment.