Skip to content

Commit 5fdf53a

Browse files
committed
Add Support for OTEL-specific environment variables
This update introduces support for OTEL-specific environment variables for OpenTelemetryAutoConfiguration. The behavior now aligns with OtlpMetricsPropertiesConfigAdapter, which automatically handles this through OtlpConfig.resourceAttributes. The attribute order for OpenTelemetryResourceAttributes is as follows: - management.opentelemetry.resource-attributes - OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME environment variables - Spring Environment 'spring.application.name' and 'spring.application.group' always apply if 'service.name' and 'service.group' are missing See gh-43712 Signed-off-by: Dmytro Nosan <[email protected]>
1 parent ea0eea3 commit 5fdf53a

File tree

3 files changed

+254
-23
lines changed

3 files changed

+254
-23
lines changed

Diff for: spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java

+7-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,8 +17,6 @@
1717
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
1818

1919
import io.opentelemetry.api.OpenTelemetry;
20-
import io.opentelemetry.api.common.AttributeKey;
21-
import io.opentelemetry.api.common.Attributes;
2220
import io.opentelemetry.context.propagation.ContextPropagators;
2321
import io.opentelemetry.sdk.OpenTelemetrySdk;
2422
import io.opentelemetry.sdk.OpenTelemetrySdkBuilder;
@@ -36,7 +34,6 @@
3634
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3735
import org.springframework.context.annotation.Bean;
3836
import org.springframework.core.env.Environment;
39-
import org.springframework.util.StringUtils;
4037

4138
/**
4239
* {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry.
@@ -49,15 +46,6 @@
4946
@EnableConfigurationProperties(OpenTelemetryProperties.class)
5047
public class OpenTelemetryAutoConfiguration {
5148

52-
/**
53-
* Default value for application name if {@code spring.application.name} is not set.
54-
*/
55-
private static final String DEFAULT_APPLICATION_NAME = "unknown_service";
56-
57-
private static final AttributeKey<String> ATTRIBUTE_KEY_SERVICE_NAME = AttributeKey.stringKey("service.name");
58-
59-
private static final AttributeKey<String> ATTRIBUTE_KEY_SERVICE_GROUP = AttributeKey.stringKey("service.group");
60-
6149
@Bean
6250
@ConditionalOnMissingBean(OpenTelemetry.class)
6351
OpenTelemetrySdk openTelemetry(ObjectProvider<SdkTracerProvider> tracerProvider,
@@ -74,19 +62,15 @@ OpenTelemetrySdk openTelemetry(ObjectProvider<SdkTracerProvider> tracerProvider,
7462
@Bean
7563
@ConditionalOnMissingBean
7664
Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) {
77-
String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME);
78-
String applicationGroup = environment.getProperty("spring.application.group");
79-
Resource resource = Resource.getDefault()
80-
.merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_NAME, applicationName)));
81-
if (StringUtils.hasLength(applicationGroup)) {
82-
resource = resource.merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_GROUP, applicationGroup)));
83-
}
84-
return resource.merge(toResource(properties));
65+
Resource resource = Resource.getDefault();
66+
return resource.merge(toResource(environment, properties));
8567
}
8668

87-
private static Resource toResource(OpenTelemetryProperties properties) {
69+
private Resource toResource(Environment environment, OpenTelemetryProperties properties) {
8870
ResourceBuilder builder = Resource.builder();
89-
properties.getResourceAttributes().forEach(builder::put);
71+
OpenTelemetryResourceAttributes attributes = new OpenTelemetryResourceAttributes(environment,
72+
properties.getResourceAttributes());
73+
attributes.applyTo(builder);
9074
return builder.build();
9175
}
9276

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
18+
19+
import java.util.Collections;
20+
import java.util.LinkedHashMap;
21+
import java.util.Map;
22+
import java.util.function.Function;
23+
24+
import io.opentelemetry.sdk.resources.ResourceBuilder;
25+
26+
import org.springframework.core.env.Environment;
27+
import org.springframework.util.CollectionUtils;
28+
import org.springframework.util.StringUtils;
29+
30+
/**
31+
* A class responsible for managing and applying OpenTelemetry resource attributes. The
32+
* class supports resource definition from multiple sources:
33+
* <li>Resource attributes explicitly provided by
34+
* {@code management.opentelemetry.resource-attributes}</li>
35+
* <li>Environmental variables, such as {@code OTEL_RESOURCE_ATTRIBUTES} or
36+
* {@code OTEL_SERVICE_NAME}.</li>
37+
* <li>Spring application properties, such as {@code spring.application.name} or
38+
* {@code spring.application.group}.</li>
39+
* <p>
40+
* If {@code service.name} and {@code spring.application.name} are not explicitly set, the
41+
* service name defaults to "unknown_service".
42+
*
43+
* @author Dmytro Nosan
44+
*/
45+
final class OpenTelemetryResourceAttributes {
46+
47+
/**
48+
* Default value for application name if {@code spring.application.name} and
49+
* {@code service.name} are not set.
50+
*/
51+
private static final String DEFAULT_APPLICATION_NAME = "unknown_service";
52+
53+
private final Environment environment;
54+
55+
private final Map<String, String> resourceAttributes;
56+
57+
private final Function<String, String> getEnv;
58+
59+
OpenTelemetryResourceAttributes(Environment environment, Map<String, String> resourceAttributes) {
60+
this(environment, resourceAttributes, System::getenv);
61+
}
62+
63+
OpenTelemetryResourceAttributes(Environment environment, Map<String, String> resourceAttributes,
64+
Function<String, String> getEnv) {
65+
this.environment = environment;
66+
this.resourceAttributes = (resourceAttributes != null) ? resourceAttributes : Collections.emptyMap();
67+
this.getEnv = getEnv;
68+
}
69+
70+
/**
71+
* Applies resource attributes to the provided {@code ResourceBuilder}.
72+
* @param builder the {@code ResourceBuilder} to which the resource attributes will be
73+
* applied
74+
*/
75+
void applyTo(ResourceBuilder builder) {
76+
getResourceAttributes().forEach(builder::put);
77+
}
78+
79+
private Map<String, String> getResourceAttributes() {
80+
Map<String, String> attributes = new LinkedHashMap<>(this.resourceAttributes);
81+
if (CollectionUtils.isEmpty(attributes)) {
82+
attributes = getResourceAttributesFromEnv();
83+
}
84+
attributes.computeIfAbsent("service.name", (key) -> getApplicationName());
85+
attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup());
86+
return attributes;
87+
}
88+
89+
private String getApplicationGroup() {
90+
return this.environment.getProperty("spring.application.group");
91+
}
92+
93+
private String getApplicationName() {
94+
return this.environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME);
95+
}
96+
97+
/**
98+
* Parses OpenTelemetry resource attributes from the {@link System#getenv()}. This
99+
* method fetches attributes defined in the {@code OTEL_RESOURCE_ATTRIBUTES}
100+
* environment variable and provides them as key-value pairs.
101+
* <p>
102+
* Additionally, if the {@code OTEL_SERVICE_NAME} environment variable is present and
103+
* the {@code service.name} attribute is not explicitly set, it automatically includes
104+
* {@code service.name} with the value from {@code OTEL_SERVICE_NAME}.
105+
* @return the resource attributes
106+
*/
107+
private Map<String, String> getResourceAttributesFromEnv() {
108+
Map<String, String> attributes = new LinkedHashMap<>();
109+
for (String attribute : StringUtils.tokenizeToStringArray(getEnv("OTEL_RESOURCE_ATTRIBUTES"), ",")) {
110+
attribute = attribute.trim();
111+
if (!StringUtils.hasText(attribute)) {
112+
continue;
113+
}
114+
int index = attribute.indexOf('=');
115+
String key = (index > 0) ? attribute.substring(0, index) : attribute;
116+
String value = (index > 0) ? attribute.substring(index + 1) : "";
117+
attributes.put(key.trim(), value.trim());
118+
}
119+
String otelServiceName = getEnv("OTEL_SERVICE_NAME");
120+
if (otelServiceName != null && !attributes.containsKey("service.name")) {
121+
attributes.put("service.name", otelServiceName);
122+
}
123+
return attributes;
124+
}
125+
126+
private String getEnv(String name) {
127+
return this.getEnv.apply(name);
128+
}
129+
130+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
18+
19+
import java.util.LinkedHashMap;
20+
import java.util.Map;
21+
22+
import io.opentelemetry.api.common.AttributeKey;
23+
import io.opentelemetry.sdk.resources.Resource;
24+
import io.opentelemetry.sdk.resources.ResourceBuilder;
25+
import org.junit.jupiter.api.Test;
26+
27+
import org.springframework.mock.env.MockEnvironment;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
/**
32+
* Tests for {@link OpenTelemetryResourceAttributes}.
33+
*
34+
* @author Dmytro Nosan
35+
*/
36+
class OpenTelemetryResourceAttributesTests {
37+
38+
private final MockEnvironment environment = new MockEnvironment();
39+
40+
private final Map<String, String> environmentVariables = new LinkedHashMap<>();
41+
42+
private final Map<String, String> resourceAttributes = new LinkedHashMap<>();
43+
44+
@Test
45+
void shouldUseUnknownServiceName() {
46+
Map<AttributeKey<?>, Object> attributes = apply();
47+
assertThat(attributes).hasSize(1);
48+
assertThat(attributes).containsEntry(AttributeKey.stringKey("service.name"), "unknown_service");
49+
}
50+
51+
@Test
52+
void shouldUseServiceNameAngGroupFromSpringEnvironment() {
53+
this.environment.setProperty("spring.application.name", "test-service");
54+
this.environment.setProperty("spring.application.group", "test-group");
55+
56+
Map<AttributeKey<?>, Object> attributes = apply();
57+
assertThat(attributes).hasSize(2);
58+
assertThat(attributes).containsEntry(AttributeKey.stringKey("service.name"), "test-service")
59+
.containsEntry(AttributeKey.stringKey("service.group"), "test-group");
60+
}
61+
62+
@Test
63+
void shouldUseAttributesFromOtelEnvVariablesSpringApplicationNameAndGroupAndOtelServiceNameEnvAreIgnored() {
64+
this.environment.setProperty("spring.application.name", "ignored");
65+
this.environment.setProperty("spring.application.group", "ignored");
66+
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES",
67+
"key1=value1,key2=value2,key3,key4=,,service.name=otel-service,service.group=otel-group");
68+
this.environmentVariables.put("OTEL_SERVICE_NAME", "ignored");
69+
70+
Map<AttributeKey<?>, Object> attributes = apply();
71+
assertThat(attributes).hasSize(6);
72+
assertThat(attributes).containsEntry(AttributeKey.stringKey("key1"), "value1")
73+
.containsEntry(AttributeKey.stringKey("key2"), "value2")
74+
.containsEntry(AttributeKey.stringKey("key3"), "")
75+
.containsEntry(AttributeKey.stringKey("key4"), "")
76+
.containsEntry(AttributeKey.stringKey("service.name"), "otel-service")
77+
.containsEntry(AttributeKey.stringKey("service.group"), "otel-group");
78+
}
79+
80+
@Test
81+
void shouldUseServiceNameFromOtelEnvVariableSpringApplicationNameIsIgnored() {
82+
this.environment.setProperty("spring.application.name", "ignored");
83+
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key1=value1,key2=value2");
84+
this.environmentVariables.put("OTEL_SERVICE_NAME", "otel-service");
85+
86+
Map<AttributeKey<?>, Object> attributes = apply();
87+
assertThat(attributes).hasSize(3);
88+
assertThat(attributes).containsEntry(AttributeKey.stringKey("key1"), "value1")
89+
.containsEntry(AttributeKey.stringKey("key2"), "value2")
90+
.containsEntry(AttributeKey.stringKey("service.name"), "otel-service");
91+
}
92+
93+
@Test
94+
void shouldResourceAttributesIgnoreEverythingElse() {
95+
this.resourceAttributes.put("service.name", "custom-service");
96+
this.resourceAttributes.put("service.group", "custom-group");
97+
this.environment.setProperty("spring.application.name", "ignored");
98+
this.environment.setProperty("spring.application.group", "ignored");
99+
this.environmentVariables.put("OTEL_SERVICE_NAME", "ignored");
100+
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES",
101+
"key1=value1,key2=value2,service.name=ignored,service.group=ignored");
102+
103+
Map<AttributeKey<?>, Object> attributes = apply();
104+
assertThat(attributes).hasSize(2);
105+
assertThat(attributes).containsEntry(AttributeKey.stringKey("service.name"), "custom-service")
106+
.containsEntry(AttributeKey.stringKey("service.group"), "custom-group");
107+
}
108+
109+
private Map<AttributeKey<?>, Object> apply() {
110+
ResourceBuilder builder = Resource.builder();
111+
new OpenTelemetryResourceAttributes(this.environment, this.resourceAttributes, this.environmentVariables::get)
112+
.applyTo(builder);
113+
return builder.build().getAttributes().asMap();
114+
115+
}
116+
117+
}

0 commit comments

Comments
 (0)