Skip to content

Commit 351254a

Browse files
committed
Refine the handling of OpenTelemetry resource attributes
OpenTelemetryResourceAttributes now has convenient APIs to construct, modify, and merge attributes from various sources, including environment variables and user-defined attribute maps. Signed-off-by: Dmytro Nosan <[email protected]>
1 parent e886785 commit 351254a

File tree

4 files changed

+111
-100
lines changed

4 files changed

+111
-100
lines changed

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,11 @@ public AggregationTemporality aggregationTemporality() {
7777

7878
@Override
7979
public Map<String, String> resourceAttributes() {
80-
Map<String, String> attributes = new OpenTelemetryResourceAttributes(
81-
this.openTelemetryProperties.getResourceAttributes())
82-
.asMap();
83-
attributes.computeIfAbsent("service.name", (key) -> getApplicationName());
84-
attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup());
85-
return Collections.unmodifiableMap(attributes);
80+
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes.fromEnv();
81+
attributes.putAll(this.openTelemetryProperties.getResourceAttributes());
82+
attributes.putIfAbsent("service.name", this::getApplicationName);
83+
attributes.putIfAbsent("service.group", this::getApplicationGroup);
84+
return Collections.unmodifiableMap(attributes.asMap());
8685
}
8786

8887
private String getApplicationName() {

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

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

19-
import java.util.Map;
20-
2119
import io.opentelemetry.api.OpenTelemetry;
2220
import io.opentelemetry.context.propagation.ContextPropagators;
2321
import io.opentelemetry.sdk.OpenTelemetrySdk;
@@ -76,11 +74,11 @@ Resource openTelemetryResource(Environment environment, OpenTelemetryProperties
7674

7775
private Resource toResource(Environment environment, OpenTelemetryProperties properties) {
7876
ResourceBuilder builder = Resource.builder();
79-
Map<String, String> attributes = new OpenTelemetryResourceAttributes(properties.getResourceAttributes())
80-
.asMap();
81-
attributes.computeIfAbsent("service.name", (key) -> getApplicationName(environment));
82-
attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup(environment));
83-
attributes.forEach(builder::put);
77+
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes.fromEnv();
78+
attributes.putAll(properties.getResourceAttributes());
79+
attributes.putIfAbsent("service.name", () -> getApplicationName(environment));
80+
attributes.putIfAbsent("service.group", () -> getApplicationGroup(environment));
81+
attributes.asMap().forEach(builder::put);
8482
return builder.build();
8583
}
8684

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributes.java

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,100 +18,110 @@
1818

1919
import java.io.ByteArrayOutputStream;
2020
import java.nio.charset.StandardCharsets;
21-
import java.util.Collections;
2221
import java.util.LinkedHashMap;
2322
import java.util.Map;
2423
import java.util.function.Function;
24+
import java.util.function.Supplier;
2525

26+
import org.springframework.util.CollectionUtils;
2627
import org.springframework.util.StringUtils;
28+
import org.springframework.util.function.SupplierUtils;
2729

2830
/**
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>
31+
* {@link OpenTelemetryResourceAttributes} for managing OpenTelemetry resource attributes
32+
* and provides convenient API to construct, modify, and merge attributes from different
33+
* sources.
3434
* <p>
3535
* <a href= "https://opentelemetry.io/docs/specs/otel/resource/sdk/">OpenTelemetry
3636
* Resource Specification</a>
3737
*
3838
* @author Dmytro Nosan
3939
* @since 3.5.0
40+
* @see #fromEnv()
4041
*/
4142
public final class OpenTelemetryResourceAttributes {
4243

43-
private final Map<String, String> resourceAttributes;
44-
45-
private final Function<String, String> getEnv;
44+
private final Map<String, String> attributes = new LinkedHashMap<>();
4645

4746
/**
48-
* Creates a new instance of {@link OpenTelemetryResourceAttributes}.
49-
* @param resourceAttributes user provided resource attributes to be used
47+
* Return Resource attributes as a Map.
48+
* @return the resource attributes as key-value pairs
5049
*/
51-
public OpenTelemetryResourceAttributes(Map<String, String> resourceAttributes) {
52-
this(resourceAttributes, null);
50+
public Map<String, String> asMap() {
51+
return new LinkedHashMap<>(this.attributes);
5352
}
5453

5554
/**
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
55+
* Merge attributes with the provided resource attributes.
56+
* <p>
57+
* If a key exists in both, the value from provided resource takes precedence, even if
58+
* it is empty.
59+
* <p>
60+
* <b>Keys that are null or empty will be ignored, and all keys will be trimmed.</b>
61+
* <p>
62+
* <b>Values that are null will be ignored, and all values will be trimmed.</b>
63+
* @param resourceAttributes resource attributes
5964
*/
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;
65+
public void putAll(Map<String, String> resourceAttributes) {
66+
if (!CollectionUtils.isEmpty(resourceAttributes)) {
67+
resourceAttributes.forEach(this::put);
68+
}
6369
}
6470

6571
/**
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
72+
* Adds a name-value pair to the resource attributes. Both the name and supplied value
73+
* will be trimmed.
74+
* @param name the attribute name to add, must not be null or empty
75+
* @param valueSupplier the attribute value supplier
7576
*/
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;
77+
public void putIfAbsent(String name, Supplier<String> valueSupplier) {
78+
if (!contains(name)) {
79+
put(name, SupplierUtils.resolve(valueSupplier));
80+
}
8481
}
8582

8683
/**
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.
84+
* Creates an {@link OpenTelemetryResourceAttributes} instance based on environment
85+
* variables. This method fetches attributes defined in the
86+
* {@code OTEL_RESOURCE_ATTRIBUTES} and {@code OTEL_SERVICE_NAME} environment
87+
* variables.
9188
* <p>
9289
* If {@code service.name} is also provided in {@code OTEL_RESOURCE_ATTRIBUTES}, then
9390
* {@code OTEL_SERVICE_NAME} takes precedence.
94-
* @return resource attributes
91+
* @return an {@link OpenTelemetryResourceAttributes}
9592
*/
96-
private Map<String, String> getResourceAttributesFromEnv() {
97-
Map<String, String> attributes = new LinkedHashMap<>();
98-
for (String attribute : StringUtils.tokenizeToStringArray(getEnv("OTEL_RESOURCE_ATTRIBUTES"), ",")) {
93+
public static OpenTelemetryResourceAttributes fromEnv() {
94+
return fromEnv(System::getenv);
95+
}
96+
97+
static OpenTelemetryResourceAttributes fromEnv(Function<String, String> getEnv) {
98+
OpenTelemetryResourceAttributes attributes = new OpenTelemetryResourceAttributes();
99+
for (String attribute : StringUtils.tokenizeToStringArray(getEnv.apply("OTEL_RESOURCE_ATTRIBUTES"), ",")) {
99100
int index = attribute.indexOf('=');
100101
if (index > 0) {
101102
String key = attribute.substring(0, index);
102103
String value = attribute.substring(index + 1);
103-
attributes.put(key.trim(), decode(value.trim()));
104+
attributes.put(key, decode(value));
104105
}
105106
}
106-
String otelServiceName = getEnv("OTEL_SERVICE_NAME");
107+
String otelServiceName = getEnv.apply("OTEL_SERVICE_NAME");
107108
if (otelServiceName != null) {
108109
attributes.put("service.name", otelServiceName);
109110
}
110111
return attributes;
111112
}
112113

113-
private String getEnv(String name) {
114-
return this.getEnv.apply(name);
114+
private void put(String name, String value) {
115+
if (StringUtils.hasText(name) && value != null) {
116+
this.attributes.put(name.trim(), value.trim());
117+
}
118+
}
119+
120+
private boolean contains(String name) {
121+
if (!StringUtils.hasText(name)) {
122+
return false;
123+
}
124+
return this.attributes.containsKey(name.trim());
115125
}
116126

117127
/**
@@ -122,7 +132,7 @@ private String getEnv(String name) {
122132
* @param value value to decode
123133
* @return the decoded string
124134
*/
125-
public static String decode(String value) {
135+
private static String decode(String value) {
126136
if (value.indexOf('%') < 0) {
127137
return value;
128138
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributesTests.java

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@
1919
import java.util.LinkedHashMap;
2020
import java.util.Map;
2121
import java.util.Random;
22-
import java.util.function.Function;
2322
import java.util.stream.Stream;
2423

2524
import io.opentelemetry.api.internal.PercentEscaper;
26-
import org.assertj.core.api.InstanceOfAssertFactories;
2725
import org.junit.jupiter.api.BeforeAll;
2826
import org.junit.jupiter.api.Test;
2927

@@ -48,47 +46,52 @@ class OpenTelemetryResourceAttributesTests {
4846
@BeforeAll
4947
static void beforeAll() {
5048
long seed = new Random().nextLong();
51-
System.out.println("Seed: " + seed);
5249
random = new Random(seed);
5350
}
5451

5552
@Test
5653
void otelServiceNameShouldTakePrecedenceOverOtelResourceAttributes() {
5754
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "service.name=ignored");
58-
this.environmentVariables.put("OTEL_SERVICE_NAME", "otel-service");
59-
OpenTelemetryResourceAttributes attributes = getAttributes();
55+
this.environmentVariables.put("OTEL_SERVICE_NAME", " otel-service ");
56+
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes
57+
.fromEnv(this.environmentVariables::get);
6058
assertThat(attributes.asMap()).hasSize(1).containsEntry("service.name", "otel-service");
6159
}
6260

6361
@Test
6462
void otelServiceNameWhenEmptyShouldTakePrecedenceOverOtelResourceAttributes() {
6563
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "service.name=ignored");
6664
this.environmentVariables.put("OTEL_SERVICE_NAME", "");
67-
OpenTelemetryResourceAttributes attributes = getAttributes();
65+
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes
66+
.fromEnv(this.environmentVariables::get);
6867
assertThat(attributes.asMap()).hasSize(1).containsEntry("service.name", "");
6968
}
7069

7170
@Test
72-
void otelResourceAttributesShouldBeUsed() {
71+
void resourceAttributesShouldBeCreatedFromEnvironmentVariables() {
7372
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES",
74-
", ,,key1=value1,key2= value2, key3=value3,key4=,=value5,key6,=,key7=spring+boot,key8=ś");
75-
OpenTelemetryResourceAttributes attributes = getAttributes();
76-
assertThat(attributes.asMap()).hasSize(6)
73+
", ,,key1=value1,key2= value2, key3=value3,key4=,=value5,key6,=,key7=spring+boot,key8=ś,key9=%20A%20");
74+
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes
75+
.fromEnv(this.environmentVariables::get);
76+
assertThat(attributes.asMap()).hasSize(7)
7777
.containsEntry("key1", "value1")
7878
.containsEntry("key2", "value2")
7979
.containsEntry("key3", "value3")
8080
.containsEntry("key4", "")
8181
.containsEntry("key7", "spring+boot")
82-
.containsEntry("key8", "ś");
82+
.containsEntry("key8", "ś")
83+
.containsEntry("key9", "A");
8384
}
8485

8586
@Test
8687
void resourceAttributesShouldBeMergedWithEnvironmentVariables() {
8788
this.resourceAttributes.put("service.group", "custom-group");
88-
this.resourceAttributes.put("key2", "");
89+
this.resourceAttributes.put(" key2 ", " ");
8990
this.environmentVariables.put("OTEL_SERVICE_NAME", "custom-service");
9091
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key1=value1,key2=value2");
91-
OpenTelemetryResourceAttributes attributes = getAttributes();
92+
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes
93+
.fromEnv(this.environmentVariables::get);
94+
attributes.putAll(this.resourceAttributes);
9295
assertThat(attributes.asMap()).hasSize(4)
9396
.containsEntry("service.name", "custom-service")
9497
.containsEntry("service.group", "custom-group")
@@ -97,66 +100,67 @@ void resourceAttributesShouldBeMergedWithEnvironmentVariables() {
97100
}
98101

99102
@Test
100-
void resourceAttributesWithNullKeyOrValueShouldBeIgnored() {
103+
void resourceAttributesWithBlankKeyAndNullValueShouldBeIgnoredWhenMergingWithEnvironmentVariables() {
101104
this.resourceAttributes.put("service.group", null);
102105
this.resourceAttributes.put("service.name", null);
103-
this.resourceAttributes.put(null, "value");
106+
this.resourceAttributes.put(null, "null-key");
107+
this.resourceAttributes.put(" ", "blank-key");
104108
this.environmentVariables.put("OTEL_SERVICE_NAME", "custom-service");
105109
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key1=value1,key2=value2");
106-
OpenTelemetryResourceAttributes attributes = getAttributes();
110+
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes
111+
.fromEnv(this.environmentVariables::get);
112+
attributes.putAll(this.resourceAttributes);
107113
assertThat(attributes.asMap()).hasSize(3)
108114
.containsEntry("service.name", "custom-service")
109115
.containsEntry("key1", "value1")
110116
.containsEntry("key2", "value2");
111117
}
112118

113119
@Test
114-
@SuppressWarnings("unchecked")
115-
void systemGetEnvShouldBeUsedAsDefaultEnvFunctionAndResourceAttributesAreEmpty() {
116-
OpenTelemetryResourceAttributes attributes = new OpenTelemetryResourceAttributes(null);
117-
assertThat(attributes).extracting("resourceAttributes")
118-
.asInstanceOf(InstanceOfAssertFactories.MAP)
119-
.isNotNull()
120-
.isEmpty();
121-
Function<String, String> getEnv = assertThat(attributes).extracting("getEnv")
122-
.asInstanceOf(InstanceOfAssertFactories.type(Function.class))
123-
.actual();
124-
System.getenv().forEach((key, value) -> assertThat(getEnv.apply(key)).isEqualTo(value));
125-
}
126-
127-
@Test
128-
void shouldDecodeOtelResourceAttributeValues() {
120+
void otelResourceAttributesShouldBeDecoded() {
129121
Stream.generate(this::generateRandomString).limit(10000).forEach((value) -> {
130122
String key = "key";
131123
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", key + "=" + escaper.escape(value));
132-
OpenTelemetryResourceAttributes attributes = getAttributes();
133-
assertThat(attributes.asMap()).hasSize(1).containsEntry(key, value);
124+
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes
125+
.fromEnv(this.environmentVariables::get);
126+
assertThat(attributes.asMap()).hasSize(1).containsEntry(key, value.trim());
134127
});
135128
}
136129

137130
@Test
138131
void shouldThrowIllegalArgumentExceptionWhenDecodingPercentIllegalHexChar() {
139132
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key=abc%ß");
140-
assertThatIllegalArgumentException().isThrownBy(() -> getAttributes().asMap())
133+
assertThatIllegalArgumentException()
134+
.isThrownBy(() -> OpenTelemetryResourceAttributes.fromEnv(this.environmentVariables::get))
141135
.withMessage("Failed to decode percent-encoded characters at index 3 in the value: 'abc%ß'");
142136
}
143137

144138
@Test
145139
void shouldUseReplacementCharWhenDecodingNonUtf8Character() {
146140
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key=%a3%3e");
147-
OpenTelemetryResourceAttributes attributes = getAttributes();
141+
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes
142+
.fromEnv(this.environmentVariables::get);
148143
assertThat(attributes.asMap()).containsEntry("key", "\ufffd>");
149144
}
150145

151146
@Test
152147
void shouldThrowIllegalArgumentExceptionWhenDecodingPercent() {
153148
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key=%");
154-
assertThatIllegalArgumentException().isThrownBy(() -> getAttributes().asMap())
149+
assertThatIllegalArgumentException()
150+
.isThrownBy(() -> OpenTelemetryResourceAttributes.fromEnv(this.environmentVariables::get))
155151
.withMessage("Failed to decode percent-encoded characters at index 0 in the value: '%'");
156152
}
157153

158-
private OpenTelemetryResourceAttributes getAttributes() {
159-
return new OpenTelemetryResourceAttributes(this.resourceAttributes, this.environmentVariables::get);
154+
@Test
155+
void shouldPutValueIfKeyIsAbsentAndNotBlankAndValueNotNull() {
156+
OpenTelemetryResourceAttributes attributes = new OpenTelemetryResourceAttributes();
157+
attributes.putIfAbsent("key", () -> "value");
158+
attributes.putIfAbsent(" key ", () -> "value1");
159+
attributes.putIfAbsent(" key1 ", () -> "value1");
160+
attributes.putIfAbsent(" key2 ", () -> null);
161+
attributes.putIfAbsent(null, () -> "value3");
162+
attributes.putIfAbsent(" ", () -> "value4");
163+
assertThat(attributes.asMap()).hasSize(2).containsEntry("key", "value").containsEntry("key1", "value1");
160164
}
161165

162166
private String generateRandomString() {

0 commit comments

Comments
 (0)