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

add instrumentation for RestTemplateBuilder #11054

Merged
merged 12 commits into from
Apr 11, 2024
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(
zeitlinger marked this conversation as resolved.
Show resolved Hide resolved
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)
zeitlinger marked this conversation as resolved.
Show resolved Hide resolved
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();
}
}
Loading