Spring Boot Starter service-name is constant (#5359)

* Spring Boot Starter service-name is constant

Pattern-based resource configuration

* Add ResourceProvider beans for spring with ConfigProperties
This commit is contained in:
Andrei Chugunov 2022-03-29 13:34:43 +03:00 committed by GitHub
parent b85696b1a3
commit c5f677e962
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 469 additions and 2 deletions

View File

@ -392,6 +392,26 @@ If an exporter is present in the classpath during runtime and a spring bean of t
<!-- Slf4j Log Correlation otel.springboot.loggers.slf4j.enabled true org.slf4j.MDC -->
##### Resource Properties
| Feature | Property | Default Value |
|----------|--------------------------------------------------|------------------------|
| Resource | otel.springboot.resource.enabled | `true` |
| | otel.springboot.resource.attributes.service.name | `unknown_service:java` |
| | otel.springboot.resource.attributes | `empty map` |
`unknown_service:java` will be used as the service-name if no value has been specified to the
property `spring.application.name` or `otel.springboot.resource.attributes.service.name` (which has
the highest priority)
`otel.springboot.resource.attributes` supports a pattern-based resource configuration in the
application.properties like this:
```
otel.springboot.resource.attributes.environment=dev
otel.springboot.resource.attributes.xyz=foo
```
##### Exporter Properties
| Feature | Property | Default Value |

View File

@ -22,9 +22,11 @@ dependencies {
compileOnly("org.springframework.boot:spring-boot-starter-web:${versions["org.springframework.boot"]}")
compileOnly("org.springframework.boot:spring-boot-starter-webflux:${versions["org.springframework.boot"]}")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
compileOnly("io.opentelemetry:opentelemetry-extension-annotations")
compileOnly("io.opentelemetry:opentelemetry-extension-trace-propagators")
compileOnly("io.opentelemetry:opentelemetry-extension-aws")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-resources")
compileOnly("io.opentelemetry:opentelemetry-exporter-logging")
compileOnly("io.opentelemetry:opentelemetry-exporter-jaeger")
compileOnly("io.opentelemetry:opentelemetry-exporter-otlp")
@ -40,6 +42,8 @@ dependencies {
testImplementation(project(":testing-common"))
testImplementation("io.opentelemetry:opentelemetry-sdk")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-resources")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
testImplementation("io.opentelemetry:opentelemetry-extension-annotations")
testImplementation("io.opentelemetry:opentelemetry-extension-trace-propagators")
testImplementation("io.opentelemetry:opentelemetry-extension-aws")

View File

@ -8,7 +8,11 @@ package io.opentelemetry.instrumentation.spring.autoconfigure;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.TracerProvider;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.instrumentation.spring.autoconfigure.resources.SpringResourceConfigProperties;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
@ -21,6 +25,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
* Create {@link io.opentelemetry.api.trace.Tracer} bean if bean is missing.
@ -41,7 +47,8 @@ public class OpenTelemetryAutoConfiguration {
@ConditionalOnMissingBean
public SdkTracerProvider sdkTracerProvider(
SamplerProperties samplerProperties,
ObjectProvider<List<SpanExporter>> spanExportersProvider) {
ObjectProvider<List<SpanExporter>> spanExportersProvider,
Resource otelResource) {
SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder();
spanExportersProvider.getIfAvailable(Collections::emptyList).stream()
@ -49,10 +56,24 @@ public class OpenTelemetryAutoConfiguration {
.forEach(tracerProviderBuilder::addSpanProcessor);
return tracerProviderBuilder
.setResource(otelResource)
.setSampler(Sampler.traceIdRatioBased(samplerProperties.getProbability()))
.build();
}
@Bean
@ConditionalOnMissingBean
public Resource otelResource(
Environment env, ObjectProvider<List<ResourceProvider>> resourceProviders) {
ConfigProperties config = new SpringResourceConfigProperties(env, new SpelExpressionParser());
Resource resource = Resource.getDefault();
for (ResourceProvider resourceProvider :
resourceProviders.getIfAvailable(Collections::emptyList)) {
resource = resource.merge(resourceProvider.createResource(config));
}
return resource;
}
@Bean
public OpenTelemetry openTelemetry(
ObjectProvider<ContextPropagators> propagatorsProvider, SdkTracerProvider tracerProvider) {

View File

@ -0,0 +1,67 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.autoconfigure.resources;
import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration;
import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
import io.opentelemetry.sdk.extension.resources.ContainerResource;
import io.opentelemetry.sdk.extension.resources.ContainerResourceProvider;
import io.opentelemetry.sdk.extension.resources.HostResource;
import io.opentelemetry.sdk.extension.resources.HostResourceProvider;
import io.opentelemetry.sdk.extension.resources.OsResource;
import io.opentelemetry.sdk.extension.resources.OsResourceProvider;
import io.opentelemetry.sdk.extension.resources.ProcessResource;
import io.opentelemetry.sdk.extension.resources.ProcessResourceProvider;
import io.opentelemetry.sdk.extension.resources.ProcessRuntimeResource;
import io.opentelemetry.sdk.extension.resources.ProcessRuntimeResourceProvider;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(OtelResourceProperties.class)
@AutoConfigureBefore(OpenTelemetryAutoConfiguration.class)
@ConditionalOnProperty(prefix = "otel.springboot.resource", name = "enabled", matchIfMissing = true)
public class OtelResourceAutoConfiguration {
@Bean
public ResourceProvider otelResourceProvider(OtelResourceProperties otelResourceProperties) {
return new SpringResourceProvider(otelResourceProperties);
}
@Bean
@ConditionalOnClass(OsResource.class)
public ResourceProvider otelOsResourceProvider() {
return new OsResourceProvider();
}
@Bean
@ConditionalOnClass(ProcessResource.class)
public ResourceProvider otelProcessResourceProvider() {
return new ProcessResourceProvider();
}
@Bean
@ConditionalOnClass(ProcessRuntimeResource.class)
public ResourceProvider otelProcessRuntimeResourceProvider() {
return new ProcessRuntimeResourceProvider();
}
@Bean
@ConditionalOnClass(HostResource.class)
public ResourceProvider otelHostResourceProvider() {
return new HostResourceProvider();
}
@Bean
@ConditionalOnClass(ContainerResource.class)
public ResourceProvider otelContainerResourceProvider() {
return new ContainerResourceProvider();
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.autoconfigure.resources;
import java.util.Collections;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "otel.springboot.resource")
public class OtelResourceProperties {
private Map<String, String> attributes = Collections.emptyMap();
public Map<String, String> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, String> attributes) {
this.attributes = attributes;
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.autoconfigure.resources;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
import org.springframework.core.env.Environment;
import org.springframework.expression.ExpressionParser;
public class SpringResourceConfigProperties implements ConfigProperties {
private final Environment environment;
private final ExpressionParser parser;
public SpringResourceConfigProperties(Environment environment, ExpressionParser parser) {
this.environment = environment;
this.parser = parser;
}
@Nullable
@Override
public String getString(String name) {
return environment.getProperty(name, String.class);
}
@Nullable
@Override
public Boolean getBoolean(String name) {
return environment.getProperty(name, Boolean.class);
}
@Nullable
@Override
public Integer getInt(String name) {
return environment.getProperty(name, Integer.class);
}
@Nullable
@Override
public Long getLong(String name) {
return environment.getProperty(name, Long.class);
}
@Nullable
@Override
public Double getDouble(String name) {
return environment.getProperty(name, Double.class);
}
@Nullable
@Override
public Duration getDuration(String name) {
return environment.getProperty(name, Duration.class);
}
@SuppressWarnings("unchecked")
@Override
public List<String> getList(String name) {
return (List<String>) environment.getProperty(name, List.class);
}
@SuppressWarnings("unchecked")
@Override
public Map<String, String> getMap(String name) {
String value = environment.getProperty(name);
return (Map<String, String>) parser.parseExpression(Objects.requireNonNull(value)).getValue();
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.autoconfigure.resources;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
import java.util.Map;
public class SpringResourceProvider implements ResourceProvider {
private final OtelResourceProperties otelResourceProperties;
public SpringResourceProvider(OtelResourceProperties otelResourceProperties) {
this.otelResourceProperties = otelResourceProperties;
}
@Override
public Resource createResource(ConfigProperties configProperties) {
String applicationName = configProperties.getString("spring.application.name");
Map<String, String> attributes = otelResourceProperties.getAttributes();
AttributesBuilder attributesBuilder = Attributes.builder();
attributes.forEach(attributesBuilder::put);
return defaultResource(applicationName).merge(Resource.create(attributesBuilder.build()));
}
private static Resource defaultResource(String applicationName) {
if (applicationName == null) {
return Resource.getDefault();
}
return Resource.getDefault()
.merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName)));
}
}

View File

@ -8,4 +8,5 @@ io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfigura
io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.resttemplate.RestTemplateAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.httpclients.webclient.WebClientAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.webmvc.WebMvcFilterAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.aspects.TraceAspectAutoConfiguration
io.opentelemetry.instrumentation.spring.autoconfigure.aspects.TraceAspectAutoConfiguration,\
io.opentelemetry.instrumentation.spring.autoconfigure.resources.OtelResourceAutoConfiguration

View File

@ -5,9 +5,13 @@
package io.opentelemetry.instrumentation.spring.autoconfigure;
import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME;
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.instrumentation.spring.autoconfigure.resources.OtelResourceAutoConfiguration;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@ -69,4 +73,76 @@ class OpenTelemetryAutoConfigurationTest {
.hasBean("customTracerProvider")
.doesNotHaveBean("sdkTracerProvider"));
}
@Test
@DisplayName(
"when spring.application.name is set value should be passed to service name attribute")
void shouldDetermineServiceNameBySpringApplicationName() {
this.contextRunner
.withPropertyValues("spring.application.name=myapp-backend")
.withConfiguration(
AutoConfigurations.of(
OtelResourceAutoConfiguration.class, OpenTelemetryAutoConfiguration.class))
.run(
context -> {
Resource otelResource = context.getBean("otelResource", Resource.class);
assertThat(otelResource.getAttribute(SERVICE_NAME)).isEqualTo("myapp-backend");
});
}
@Test
@DisplayName(
"when spring application name and otel service name are not set service name should be default")
void hasDefaultServiceName() {
this.contextRunner
.withConfiguration(
AutoConfigurations.of(
OtelResourceAutoConfiguration.class, OpenTelemetryAutoConfiguration.class))
.run(
context -> {
Resource otelResource = context.getBean("otelResource", Resource.class);
assertThat(otelResource.getAttribute(SERVICE_NAME)).isEqualTo("unknown_service:java");
});
}
@Test
@DisplayName("when otel service name is set it should be set as service name attribute")
void shouldDetermineServiceNameByOtelServiceName() {
this.contextRunner
.withConfiguration(
AutoConfigurations.of(
OtelResourceAutoConfiguration.class, OpenTelemetryAutoConfiguration.class))
.withPropertyValues("otel.springboot.resource.attributes.service.name=otel-name-backend")
.run(
context -> {
Resource otelResource = context.getBean("otelResource", Resource.class);
assertThat(otelResource.getAttribute(SERVICE_NAME)).isEqualTo("otel-name-backend");
});
}
@Test
@DisplayName("when otel attributes are set in properties they should be put in resource")
void shouldInitializeAttributes() {
this.contextRunner
.withConfiguration(
AutoConfigurations.of(
OtelResourceAutoConfiguration.class, OpenTelemetryAutoConfiguration.class))
.withPropertyValues(
"otel.springboot.resource.attributes.xyz=foo",
"otel.springboot.resource.attributes.environment=dev",
"otel.springboot.resource.attributes.service.instance.id=id-example")
.run(
context -> {
Resource otelResource = context.getBean("otelResource", Resource.class);
assertThat(otelResource.getAttribute(AttributeKey.stringKey("environment")))
.isEqualTo("dev");
assertThat(otelResource.getAttribute(AttributeKey.stringKey("xyz"))).isEqualTo("foo");
assertThat(otelResource.getAttribute(AttributeKey.stringKey("service.instance.id")))
.isEqualTo("id-example");
});
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.autoconfigure.resources;
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
public class OtelResourceAutoConfigurationTest {
private final ApplicationContextRunner contextRunner =
new ApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(
OtelResourceAutoConfiguration.class, OpenTelemetryAutoConfiguration.class));
@Test
@DisplayName(
"when otel.springboot.resource.enabled is set to true configuration should be initialized")
void shouldDetermineServiceNameByOtelServiceName() {
this.contextRunner
.withPropertyValues("otel.springboot.resource.enabled=true")
.run(context -> assertThat(context.containsBean("otelResourceProvider")).isTrue());
}
@Test
@DisplayName(
"when otel.springboot.resource.enabled is not specified configuration should be initialized")
void shouldInitAutoConfigurationByDefault() {
this.contextRunner.run(
context -> assertThat(context.containsBean("otelResourceProvider")).isTrue());
}
@Test
@DisplayName(
"when otel.springboot.resource.enabled is set to false configuration should NOT be initialized")
void shouldNotInitAutoConfiguration() {
this.contextRunner
.withPropertyValues("otel.springboot.resource.enabled=false")
.run(context -> assertThat(context.containsBean("otelResourceProvider")).isFalse());
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.autoconfigure.resources;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
public class OtelResourcePropertiesTest {
private final ApplicationContextRunner contextRunner =
new ApplicationContextRunner()
.withPropertyValues("otel.springboot.resource.enabled=true")
.withConfiguration(AutoConfigurations.of(OtelResourceAutoConfiguration.class));
@Test
@DisplayName("when attributes are SET should set OtelResourceProperties with given attributes")
void hasAttributes() {
this.contextRunner
.withPropertyValues(
"otel.springboot.resource.attributes.environment=dev",
"otel.springboot.resource.attributes.xyz=foo",
"otel.springboot.resource.attributes.service.name=backend-name",
"otel.springboot.resource.attributes.service.instance.id=id-example")
.run(
context -> {
OtelResourceProperties propertiesBean = context.getBean(OtelResourceProperties.class);
assertThat(propertiesBean.getAttributes())
.contains(
entry("environment", "dev"),
entry("xyz", "foo"),
entry("service.name", "backend-name"),
entry("service.instance.id", "id-example"));
});
}
@Test
@DisplayName("when attributes are DEFAULT should set OtelResourceProperties to default values")
void hasDefaultTypes() {
this.contextRunner.run(
context ->
assertThat(context.getBean(OtelResourceProperties.class).getAttributes()).isEmpty());
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.spring.autoconfigure.resources;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.core.env.Environment;
import org.springframework.expression.spel.standard.SpelExpressionParser;
class SpringResourceConfigPropertiesTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
@Test
@DisplayName("when map is set in properties in a row it should be available in config")
void shouldInitializeAttributesByMapInArow() {
this.contextRunner
.withPropertyValues(
"otel.springboot.test.map={'environment':'dev','xyz':'foo','service.instance.id':'id-example'}")
.run(
context -> {
Environment env = context.getBean("environment", Environment.class);
SpringResourceConfigProperties config =
new SpringResourceConfigProperties(env, new SpelExpressionParser());
assertThat(config.getMap("otel.springboot.test.map"))
.contains(
entry("environment", "dev"),
entry("xyz", "foo"),
entry("service.instance.id", "id-example"));
});
}
}