From 58cfdbfa2da46d222165741146149e922e4e2e4b Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Fri, 30 Jun 2023 16:24:34 -0700 Subject: [PATCH] Add missing OTel Span attributes OTel semantic conventions mandate certain resource attributes to present on exported spans. This commits make sure that the attribute we add are merged with the defaults, rather than replacing them. See gh-36155 --- .../OpenTelemetryAutoConfiguration.java | 3 +- .../OpenTelemetryAutoConfigurationTests.java | 80 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index 952b6ed7c1..e2b895d3e3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -97,9 +97,10 @@ public class OpenTelemetryAutoConfiguration { SdkTracerProvider otelSdkTracerProvider(Environment environment, ObjectProvider spanProcessors, Sampler sampler) { String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); + Resource springResource = Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName)); SdkTracerProviderBuilder builder = SdkTracerProvider.builder() .setSampler(sampler) - .setResource(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName))); + .setResource(Resource.getDefault().merge(springResource)); spanProcessors.orderedStream().forEach(builder::addSpanProcessor); return builder.build(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java index 128dad34e0..8f611a2c9c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java @@ -16,8 +16,14 @@ package org.springframework.boot.actuate.autoconfigure.tracing; +import java.time.Duration; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Stream; import io.micrometer.tracing.SpanCustomizer; @@ -30,18 +36,26 @@ import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener; import io.micrometer.tracing.otel.bridge.Slf4JEventListener; import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -143,6 +157,26 @@ class OpenTelemetryAutoConfigurationTests { }); } + @Test + void shouldSetupDefaultResourceAttributes() { + this.contextRunner + .withConfiguration( + AutoConfigurations.of(ObservationAutoConfiguration.class, MicrometerTracingAutoConfiguration.class)) + .withUserConfiguration(InMemoryRecordingSpanExporterConfiguration.class) + .withPropertyValues("management.tracing.sampling.probability=1.0") + .run((context) -> { + context.getBean(io.micrometer.tracing.Tracer.class).nextSpan().name("test").end(); + InMemoryRecordingSpanExporter exporter = context.getBean(InMemoryRecordingSpanExporter.class); + exporter.await(Duration.ofSeconds(10)); + SpanData spanData = exporter.getExportedSpans().get(0); + Map, Object> expectedAttributes = Resource.getDefault() + .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "application"))) + .getAttributes() + .asMap(); + assertThat(spanData.getResource().getAttributes().asMap()).isEqualTo(expectedAttributes); + }); + } + @Test void shouldAllowMultipleSpanProcessors() { this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { @@ -297,4 +331,50 @@ class OpenTelemetryAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + private static class InMemoryRecordingSpanExporterConfiguration { + + @Bean + InMemoryRecordingSpanExporter spanExporter() { + return new InMemoryRecordingSpanExporter(); + } + + } + + private static class InMemoryRecordingSpanExporter implements SpanExporter { + + private final List exportedSpans = new ArrayList<>(); + + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + public CompletableResultCode export(Collection spans) { + this.exportedSpans.addAll(spans); + this.latch.countDown(); + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + this.exportedSpans.clear(); + return CompletableResultCode.ofSuccess(); + } + + List getExportedSpans() { + return this.exportedSpans; + } + + void await(Duration timeout) throws InterruptedException, TimeoutException { + if (!this.latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Waiting for exporting spans timed out (%s)".formatted(timeout)); + } + } + + } + }