Skip to content

Commit 82bf06b

Browse files
committed
Merge user-defined attributes with OTEL_RESOURCE_ATTRIBUTES
Introduce OpenTelemetryResourceAttributes to merge user-defined resource attributes with those from OTEL_RESOURCE_ATTRIBUTES. See spring-projectsgh-44400 Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
1 parent de32c6f commit 82bf06b

File tree

3 files changed

+203
-11
lines changed

3 files changed

+203
-11
lines changed

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

+15-11
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,7 +17,6 @@
1717
package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp;
1818

1919
import java.util.Collections;
20-
import java.util.HashMap;
2120
import java.util.Map;
2221
import java.util.concurrent.TimeUnit;
2322

@@ -27,9 +26,9 @@
2726

2827
import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter;
2928
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties;
29+
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryResourceAttributes;
3030
import org.springframework.core.env.Environment;
3131
import org.springframework.util.CollectionUtils;
32-
import org.springframework.util.StringUtils;
3332

3433
/**
3534
* Adapter to convert {@link OtlpMetricsProperties} to an {@link OtlpConfig}.
@@ -77,23 +76,28 @@ public AggregationTemporality aggregationTemporality() {
7776
}
7877

7978
@Override
80-
@SuppressWarnings("removal")
8179
public Map<String, String> resourceAttributes() {
80+
Map<String, String> attributes = new OpenTelemetryResourceAttributes(getResourceAttributes()).asMap();
81+
attributes.computeIfAbsent("service.name", (key) -> getApplicationName());
82+
attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup());
83+
return attributes;
84+
}
85+
86+
@SuppressWarnings("removal")
87+
private Map<String, String> getResourceAttributes() {
8288
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);
89+
if (!CollectionUtils.isEmpty(resourceAttributes)) {
90+
return resourceAttributes;
91+
}
92+
return get(OtlpMetricsProperties::getResourceAttributes, Collections::emptyMap);
8893
}
8994

9095
private String getApplicationName() {
9196
return this.environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME);
9297
}
9398

9499
private String getApplicationGroup() {
95-
String applicationGroup = this.environment.getProperty("spring.application.group");
96-
return (StringUtils.hasLength(applicationGroup)) ? applicationGroup : null;
100+
return this.environment.getProperty("spring.application.group");
97101
}
98102

99103
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 org.springframework.util.StringUtils;
25+
26+
/**
27+
*
28+
* {@link OpenTelemetryResourceAttributes} extracts information from the
29+
* {@code OTEL_RESOURCE_ATTRIBUTES} and {@code OTEL_SERVICE_NAME} environment variables
30+
* and merge them, with resource attributes provided by the user, i.e. the user provided
31+
* resource information has higher priority.
32+
*
33+
* @author Dmytro Nosan
34+
* @since 3.4.4
35+
*/
36+
public final class OpenTelemetryResourceAttributes {
37+
38+
private final Map<String, String> resourceAttributes;
39+
40+
private final Function<String, String> getEnv;
41+
42+
/**
43+
* Creates a new instance of {@link OpenTelemetryResourceAttributes}.
44+
* @param resourceAttributes user provided resource attributes to be used
45+
*/
46+
public OpenTelemetryResourceAttributes(Map<String, String> resourceAttributes) {
47+
this(resourceAttributes, System::getenv);
48+
}
49+
50+
/**
51+
* Creates a new {@link OpenTelemetryResourceAttributes} instance.
52+
* @param resourceAttributes user provided resource attributes to be used
53+
* @param getEnv a function to retrieve environment variables by name
54+
*/
55+
OpenTelemetryResourceAttributes(Map<String, String> resourceAttributes, Function<String, String> getEnv) {
56+
this.resourceAttributes = (resourceAttributes != null) ? resourceAttributes : Collections.emptyMap();
57+
this.getEnv = (getEnv != null) ? getEnv : System::getenv;
58+
}
59+
60+
/**
61+
* Returns OpenTelemetry resource attributes by merging environment-based attributes
62+
* and user-defined resource attributes.
63+
* @return resource attributes
64+
*/
65+
public Map<String, String> asMap() {
66+
Map<String, String> attributes = getResourceAttributesFromEnv();
67+
attributes.putAll(this.resourceAttributes);
68+
return attributes;
69+
}
70+
71+
/**
72+
* Parses OpenTelemetry resource attributes from the {@link System#getenv()}. This
73+
* method fetches attributes defined in the {@code OTEL_RESOURCE_ATTRIBUTES} and
74+
* {@code OTEL_SERVICE_NAME} environment variables and provides them as key-value
75+
* pairs.
76+
* <p>
77+
* If {@code service.name} is also provided in {@code OTEL_RESOURCE_ATTRIBUTES}, then
78+
* {@code OTEL_SERVICE_NAME} takes precedence.
79+
*/
80+
private Map<String, String> getResourceAttributesFromEnv() {
81+
Map<String, String> attributes = new LinkedHashMap<>();
82+
for (String attribute : StringUtils.tokenizeToStringArray(getEnv("OTEL_RESOURCE_ATTRIBUTES"), ",")) {
83+
int index = attribute.indexOf('=');
84+
if (index > 0) {
85+
String key = attribute.substring(0, index);
86+
String value = attribute.substring(index + 1);
87+
attributes.put(key.trim(), value.trim());
88+
}
89+
}
90+
String otelServiceName = getEnv("OTEL_SERVICE_NAME");
91+
if (otelServiceName != null) {
92+
attributes.put("service.name", otelServiceName);
93+
}
94+
return attributes;
95+
}
96+
97+
private String getEnv(String name) {
98+
return this.getEnv.apply(name);
99+
}
100+
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 org.junit.jupiter.api.Test;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
/**
27+
* Tests for {@link OpenTelemetryResourceAttributes}.
28+
*
29+
* @author Dmytro Nosan
30+
*/
31+
class OpenTelemetryResourceAttributesTests {
32+
33+
private final Map<String, String> environmentVariables = new LinkedHashMap<>();
34+
35+
private final Map<String, String> resourceAttributes = new LinkedHashMap<>();
36+
37+
@Test
38+
void otelServiceNameShouldTakePrecedenceOverOtelResourceAttributes() {
39+
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "service.name=ignored");
40+
this.environmentVariables.put("OTEL_SERVICE_NAME", "otel-service");
41+
42+
OpenTelemetryResourceAttributes attributes = getAttributes();
43+
assertThat(attributes.asMap()).hasSize(1).containsEntry("service.name", "otel-service");
44+
}
45+
46+
@Test
47+
void otelResourceAttributesShouldBeUsed() {
48+
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES",
49+
" key1 = value1 ,key2= value2,key3,key4=,,=key5,service.name=otel-service,service.group=otel-group");
50+
OpenTelemetryResourceAttributes attributes = getAttributes();
51+
assertThat(attributes.asMap()).hasSize(5)
52+
.containsEntry("key1", "value1")
53+
.containsEntry("key2", "value2")
54+
.containsEntry("key4", "")
55+
.containsEntry("service.name", "otel-service")
56+
.containsEntry("service.group", "otel-group");
57+
}
58+
59+
@Test
60+
void userResourceAttributesShouldBeMergedWithEnvironmentVariables() {
61+
this.resourceAttributes.put("service.group", "custom-group");
62+
this.environmentVariables.put("OTEL_SERVICE_NAME", "custom-service");
63+
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key1=value1,key2=value2");
64+
65+
OpenTelemetryResourceAttributes attributes = getAttributes();
66+
assertThat(attributes.asMap()).hasSize(4)
67+
.containsEntry("service.name", "custom-service")
68+
.containsEntry("service.group", "custom-group")
69+
.containsEntry("key1", "value1")
70+
.containsEntry("key2", "value2");
71+
}
72+
73+
@Test
74+
void userResourceAttributesShouldBeUsed() {
75+
this.resourceAttributes.put("service.name", "custom-service");
76+
this.resourceAttributes.put("service.group", "custom-group");
77+
OpenTelemetryResourceAttributes attributes = getAttributes();
78+
assertThat(attributes.asMap()).hasSize(2)
79+
.containsEntry("service.name", "custom-service")
80+
.containsEntry("service.group", "custom-group");
81+
}
82+
83+
private OpenTelemetryResourceAttributes getAttributes() {
84+
return new OpenTelemetryResourceAttributes(this.resourceAttributes, this.environmentVariables::get);
85+
}
86+
87+
}

0 commit comments

Comments
 (0)