Skip to content

Commit 590c024

Browse files
committed
Add Support for OTEL-specific environment variables
This commit introduces the OpenTelementryAttributes class that fetches OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME environment variables and merges it with user-defined resource attributes. Besides that, this commit includes spec-compliant proper handling of OTEL_RESOURCE_ATTRIBUTES in OtlpMetricsPropertiesConfigAdapter and OpenTelemetryAutoConfiguration. See gh-44394 Signed-off-by: Dmytro Nosan <[email protected]>
1 parent 2ec99a7 commit 590c024

File tree

9 files changed

+353
-92
lines changed

9 files changed

+353
-92
lines changed

Diff for: spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsProperties.java

-12
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryProperties;
2626
import org.springframework.boot.context.properties.ConfigurationProperties;
27-
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
2827

2928
/**
3029
* {@link ConfigurationProperties @ConfigurationProperties} for configuring OTLP metrics
@@ -95,17 +94,6 @@ public void setAggregationTemporality(AggregationTemporality aggregationTemporal
9594
this.aggregationTemporality = aggregationTemporality;
9695
}
9796

98-
@Deprecated(since = "3.2.0", forRemoval = true)
99-
@DeprecatedConfigurationProperty(replacement = "management.opentelemetry.resource-attributes", since = "3.2.0")
100-
public Map<String, String> getResourceAttributes() {
101-
return this.resourceAttributes;
102-
}
103-
104-
@Deprecated(since = "3.2.0", forRemoval = true)
105-
public void setResourceAttributes(Map<String, String> resourceAttributes) {
106-
this.resourceAttributes = resourceAttributes;
107-
}
108-
10997
public Map<String, String> getHeaders() {
11098
return this.headers;
11199
}

Diff for: spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsPropertiesConfigAdapter.java

+9-14
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.
@@ -16,8 +16,6 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp;
1818

19-
import java.util.Collections;
20-
import java.util.HashMap;
2119
import java.util.Map;
2220
import java.util.concurrent.TimeUnit;
2321

@@ -27,9 +25,8 @@
2725

2826
import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter;
2927
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties;
28+
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryResourceAttributes;
3029
import org.springframework.core.env.Environment;
31-
import org.springframework.util.CollectionUtils;
32-
import org.springframework.util.StringUtils;
3330

3431
/**
3532
* Adapter to convert {@link OtlpMetricsProperties} to an {@link OtlpConfig}.
@@ -77,23 +74,21 @@ public AggregationTemporality aggregationTemporality() {
7774
}
7875

7976
@Override
80-
@SuppressWarnings("removal")
8177
public Map<String, String> resourceAttributes() {
82-
Map<String, String> resourceAttributes = this.openTelemetryProperties.getResourceAttributes();
83-
Map<String, String> result = new HashMap<>((!CollectionUtils.isEmpty(resourceAttributes)) ? resourceAttributes
84-
: get(OtlpMetricsProperties::getResourceAttributes, OtlpConfig.super::resourceAttributes));
85-
result.computeIfAbsent("service.name", (key) -> getApplicationName());
86-
result.computeIfAbsent("service.group", (key) -> getApplicationGroup());
87-
return Collections.unmodifiableMap(result);
78+
Map<String, String> attributes = new OpenTelemetryResourceAttributes(
79+
this.openTelemetryProperties.getResourceAttributes())
80+
.asMap();
81+
attributes.computeIfAbsent("service.name", (key) -> getApplicationName());
82+
attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup());
83+
return attributes;
8884
}
8985

9086
private String getApplicationName() {
9187
return this.environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME);
9288
}
9389

9490
private String getApplicationGroup() {
95-
String applicationGroup = this.environment.getProperty("spring.application.group");
96-
return (StringUtils.hasLength(applicationGroup)) ? applicationGroup : null;
91+
return this.environment.getProperty("spring.application.group");
9792
}
9893

9994
@Override

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

+19-18
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.
@@ -16,9 +16,9 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
1818

19+
import java.util.Map;
20+
1921
import io.opentelemetry.api.OpenTelemetry;
20-
import io.opentelemetry.api.common.AttributeKey;
21-
import io.opentelemetry.api.common.Attributes;
2222
import io.opentelemetry.context.propagation.ContextPropagators;
2323
import io.opentelemetry.sdk.OpenTelemetrySdk;
2424
import io.opentelemetry.sdk.OpenTelemetrySdkBuilder;
@@ -36,7 +36,6 @@
3636
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3737
import org.springframework.context.annotation.Bean;
3838
import org.springframework.core.env.Environment;
39-
import org.springframework.util.StringUtils;
4039

4140
/**
4241
* {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry.
@@ -54,10 +53,6 @@ public class OpenTelemetryAutoConfiguration {
5453
*/
5554
private static final String DEFAULT_APPLICATION_NAME = "unknown_service";
5655

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-
6156
@Bean
6257
@ConditionalOnMissingBean(OpenTelemetry.class)
6358
OpenTelemetrySdk openTelemetry(ObjectProvider<SdkTracerProvider> tracerProvider,
@@ -74,20 +69,26 @@ OpenTelemetrySdk openTelemetry(ObjectProvider<SdkTracerProvider> tracerProvider,
7469
@Bean
7570
@ConditionalOnMissingBean
7671
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));
72+
Resource resource = Resource.getDefault();
73+
return resource.merge(toResource(environment, properties));
8574
}
8675

87-
private static Resource toResource(OpenTelemetryProperties properties) {
76+
private Resource toResource(Environment environment, OpenTelemetryProperties properties) {
8877
ResourceBuilder builder = Resource.builder();
89-
properties.getResourceAttributes().forEach(builder::put);
78+
Map<String, String> attributes = new OpenTelemetryResourceAttributes(properties.getResourceAttributes())
79+
.asMap();
80+
attributes.computeIfAbsent("service.name", (key) -> getApplicationName(environment));
81+
attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup(environment));
82+
attributes.forEach(builder::put);
9083
return builder.build();
9184
}
9285

86+
private String getApplicationName(Environment environment) {
87+
return environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME);
88+
}
89+
90+
private String getApplicationGroup(Environment environment) {
91+
return environment.getProperty("spring.application.group");
92+
}
93+
9394
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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.io.ByteArrayOutputStream;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.Collections;
22+
import java.util.LinkedHashMap;
23+
import java.util.Map;
24+
import java.util.function.Function;
25+
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* OpenTelemetryResourceAttributes retrieves information from the
30+
* {@code OTEL_RESOURCE_ATTRIBUTES} and {@code OTEL_SERVICE_NAME} environment variables
31+
* and merges it with the resource attributes provided by the user.
32+
* <p>
33+
* <b>User-provided resource attributes take precedence.</b>
34+
* <p>
35+
* <a href= "https://opentelemetry.io/docs/specs/otel/resource/sdk/">OpenTelemetry
36+
* Resource Specification</a>
37+
*
38+
* @author Dmytro Nosan
39+
* @since 3.5.0
40+
*/
41+
public final class OpenTelemetryResourceAttributes {
42+
43+
private final Map<String, String> resourceAttributes;
44+
45+
private final Function<String, String> getEnv;
46+
47+
/**
48+
* Creates a new instance of {@link OpenTelemetryResourceAttributes}.
49+
* @param resourceAttributes user provided resource attributes to be used
50+
*/
51+
public OpenTelemetryResourceAttributes(Map<String, String> resourceAttributes) {
52+
this(resourceAttributes, null);
53+
}
54+
55+
/**
56+
* Creates a new {@link OpenTelemetryResourceAttributes} instance.
57+
* @param resourceAttributes user provided resource attributes to be used
58+
* @param getEnv a function to retrieve environment variables by name
59+
*/
60+
OpenTelemetryResourceAttributes(Map<String, String> resourceAttributes, Function<String, String> getEnv) {
61+
this.resourceAttributes = (resourceAttributes != null) ? resourceAttributes : Collections.emptyMap();
62+
this.getEnv = (getEnv != null) ? getEnv : System::getenv;
63+
}
64+
65+
/**
66+
* Returns resource attributes by combining attributes from environment variables and
67+
* user-defined resource attributes. The final resource contains all attributes from
68+
* both sources.
69+
* <p>
70+
* If a key exists in both environment variables and user-defined resources, the value
71+
* from the user-defined resource takes precedence, even if it is empty.
72+
* <p>
73+
* <b>Null keys and values are ignored.</b>
74+
* @return the resource attributes
75+
*/
76+
public Map<String, String> asMap() {
77+
Map<String, String> attributes = getResourceAttributesFromEnv();
78+
this.resourceAttributes.forEach((name, value) -> {
79+
if (name != null && value != null) {
80+
attributes.put(name, value);
81+
}
82+
});
83+
return attributes;
84+
}
85+
86+
/**
87+
* Parses resource attributes from the {@link System#getenv()}. This method fetches
88+
* attributes defined in the {@code OTEL_RESOURCE_ATTRIBUTES} and
89+
* {@code OTEL_SERVICE_NAME} environment variables and provides them as key-value
90+
* pairs.
91+
* <p>
92+
* If {@code service.name} is also provided in {@code OTEL_RESOURCE_ATTRIBUTES}, then
93+
* {@code OTEL_SERVICE_NAME} takes precedence.
94+
* @return resource attributes
95+
*/
96+
private Map<String, String> getResourceAttributesFromEnv() {
97+
Map<String, String> attributes = new LinkedHashMap<>();
98+
for (String attribute : StringUtils.tokenizeToStringArray(getEnv("OTEL_RESOURCE_ATTRIBUTES"), ",")) {
99+
int index = attribute.indexOf('=');
100+
if (index > 0) {
101+
String key = attribute.substring(0, index);
102+
String value = attribute.substring(index + 1);
103+
attributes.put(key.trim(), decode(value.trim()));
104+
}
105+
}
106+
String otelServiceName = getEnv("OTEL_SERVICE_NAME");
107+
if (otelServiceName != null) {
108+
attributes.put("service.name", otelServiceName);
109+
}
110+
return attributes;
111+
}
112+
113+
private String getEnv(String name) {
114+
return this.getEnv.apply(name);
115+
}
116+
117+
/**
118+
* Decodes a percent-encoded string. Converts sequences like '%HH' (where HH
119+
* represents hexadecimal digits) back into their literal representations.
120+
* @param value value to decode
121+
* @return the decoded string
122+
*/
123+
public static String decode(String value) {
124+
if (value.indexOf('%') < 0) {
125+
return value;
126+
}
127+
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
128+
ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length);
129+
for (int i = 0; i < bytes.length; i++) {
130+
byte b = bytes[i];
131+
if (b == '%') {
132+
int u = digit(bytes, ++i);
133+
int l = digit(bytes, ++i);
134+
if (u >= 0 && l >= 0) {
135+
bos.write((u << 4) + l);
136+
}
137+
else {
138+
bos.write(0xFF);
139+
}
140+
}
141+
else {
142+
bos.write(b);
143+
}
144+
}
145+
return bos.toString(StandardCharsets.UTF_8);
146+
}
147+
148+
private static int digit(byte[] bytes, int index) {
149+
return (index < bytes.length) ? Character.digit(bytes[index], 16) : -1;
150+
}
151+
152+
}

Diff for: spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java

+1-43
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.
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp;
1818

19-
import java.util.Collections;
2019
import java.util.Map;
2120
import java.util.concurrent.TimeUnit;
2221

@@ -30,7 +29,6 @@
3029
import org.springframework.mock.env.MockEnvironment;
3130

3231
import static org.assertj.core.api.Assertions.assertThat;
33-
import static org.assertj.core.api.Assertions.entry;
3432

3533
/**
3634
* Tests for {@link OtlpMetricsPropertiesConfigAdapter}.
@@ -73,13 +71,6 @@ void whenPropertiesAggregationTemporalityIsSetAdapterAggregationTemporalityRetur
7371
assertThat(createAdapter().aggregationTemporality()).isSameAs(AggregationTemporality.DELTA);
7472
}
7573

76-
@Test
77-
@SuppressWarnings("removal")
78-
void whenPropertiesResourceAttributesIsSetAdapterResourceAttributesReturnsIt() {
79-
this.properties.setResourceAttributes(Map.of("service.name", "boot-service"));
80-
assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "boot-service");
81-
}
82-
8374
@Test
8475
void whenPropertiesHeadersIsSetAdapterHeadersReturnsIt() {
8576
this.properties.setHeaders(Map.of("header", "value"));
@@ -130,31 +121,6 @@ void whenPropertiesBaseTimeUnitIsSetAdapterBaseTimeUnitReturnsIt() {
130121
assertThat(createAdapter().baseTimeUnit()).isSameAs(TimeUnit.SECONDS);
131122
}
132123

133-
@Test
134-
@SuppressWarnings("removal")
135-
void openTelemetryPropertiesShouldOverrideOtlpPropertiesIfNotEmpty() {
136-
this.properties.setResourceAttributes(Map.of("a", "alpha"));
137-
this.openTelemetryProperties.setResourceAttributes(Map.of("b", "beta"));
138-
assertThat(createAdapter().resourceAttributes()).contains(entry("b", "beta"));
139-
assertThat(createAdapter().resourceAttributes()).doesNotContain(entry("a", "alpha"));
140-
}
141-
142-
@Test
143-
@SuppressWarnings("removal")
144-
void openTelemetryPropertiesShouldNotOverrideOtlpPropertiesIfEmpty() {
145-
this.properties.setResourceAttributes(Map.of("a", "alpha"));
146-
this.openTelemetryProperties.setResourceAttributes(Collections.emptyMap());
147-
assertThat(createAdapter().resourceAttributes()).contains(entry("a", "alpha"));
148-
}
149-
150-
@Test
151-
@SuppressWarnings("removal")
152-
void serviceNameOverridesApplicationName() {
153-
this.environment.setProperty("spring.application.name", "alpha");
154-
this.properties.setResourceAttributes(Map.of("service.name", "beta"));
155-
assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "beta");
156-
}
157-
158124
@Test
159125
void serviceNameOverridesApplicationNameWhenUsingOtelProperties() {
160126
this.environment.setProperty("spring.application.name", "alpha");
@@ -173,14 +139,6 @@ void shouldUseDefaultApplicationNameIfApplicationNameIsNotSet() {
173139
assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "unknown_service");
174140
}
175141

176-
@Test
177-
@SuppressWarnings("removal")
178-
void serviceGroupOverridesApplicationGroup() {
179-
this.environment.setProperty("spring.application.group", "alpha");
180-
this.properties.setResourceAttributes(Map.of("service.group", "beta"));
181-
assertThat(createAdapter().resourceAttributes()).containsEntry("service.group", "beta");
182-
}
183-
184142
@Test
185143
void serviceGroupOverridesApplicationGroupWhenUsingOtelProperties() {
186144
this.environment.setProperty("spring.application.group", "alpha");

0 commit comments

Comments
 (0)