From bf015bfeb9f8ebb62e310d3a7120a62975815d03 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Fri, 21 Feb 2025 14:06:52 +0200 Subject: [PATCH 1/2] 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 --- .../OtlpMetricsPropertiesConfigAdapter.java | 22 +-- .../OpenTelemetryAutoConfiguration.java | 37 ++-- .../OpenTelemetryResourceAttributes.java | 156 ++++++++++++++++ .../OpenTelemetryResourceAttributesTests.java | 166 ++++++++++++++++++ .../pages/actuator/observability.adoc | 7 +- 5 files changed, 356 insertions(+), 32 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributes.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributesTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsPropertiesConfigAdapter.java index cd520db9e8c7..1f5a0d07c9a7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -27,9 +25,8 @@ import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter; import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryResourceAttributes; import org.springframework.core.env.Environment; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; /** * Adapter to convert {@link OtlpMetricsProperties} to an {@link OtlpConfig}. @@ -78,12 +75,12 @@ public AggregationTemporality aggregationTemporality() { @Override public Map resourceAttributes() { - Map resourceAttributes = this.openTelemetryProperties.getResourceAttributes(); - Map result = new HashMap<>((!CollectionUtils.isEmpty(resourceAttributes)) ? resourceAttributes - : OtlpConfig.super.resourceAttributes()); - result.computeIfAbsent("service.name", (key) -> getApplicationName()); - result.computeIfAbsent("service.group", (key) -> getApplicationGroup()); - return Collections.unmodifiableMap(result); + Map attributes = new OpenTelemetryResourceAttributes( + this.openTelemetryProperties.getResourceAttributes()) + .asMap(); + attributes.computeIfAbsent("service.name", (key) -> getApplicationName()); + attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup()); + return attributes; } private String getApplicationName() { @@ -91,8 +88,7 @@ private String getApplicationName() { } private String getApplicationGroup() { - String applicationGroup = this.environment.getProperty("spring.application.group"); - return (StringUtils.hasLength(applicationGroup)) ? applicationGroup : null; + return this.environment.getProperty("spring.application.group"); } @Override diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java index 720e79142f61..92e66ba61a8c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,9 @@ package org.springframework.boot.actuate.autoconfigure.opentelemetry; +import java.util.Map; + import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.OpenTelemetrySdkBuilder; @@ -36,7 +36,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; -import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry. @@ -54,10 +53,6 @@ public class OpenTelemetryAutoConfiguration { */ private static final String DEFAULT_APPLICATION_NAME = "unknown_service"; - private static final AttributeKey ATTRIBUTE_KEY_SERVICE_NAME = AttributeKey.stringKey("service.name"); - - private static final AttributeKey ATTRIBUTE_KEY_SERVICE_GROUP = AttributeKey.stringKey("service.group"); - @Bean @ConditionalOnMissingBean(OpenTelemetry.class) OpenTelemetrySdk openTelemetry(ObjectProvider tracerProvider, @@ -74,20 +69,26 @@ OpenTelemetrySdk openTelemetry(ObjectProvider tracerProvider, @Bean @ConditionalOnMissingBean Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) { - String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); - String applicationGroup = environment.getProperty("spring.application.group"); - Resource resource = Resource.getDefault() - .merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_NAME, applicationName))); - if (StringUtils.hasLength(applicationGroup)) { - resource = resource.merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_GROUP, applicationGroup))); - } - return resource.merge(toResource(properties)); + Resource resource = Resource.getDefault(); + return resource.merge(toResource(environment, properties)); } - private static Resource toResource(OpenTelemetryProperties properties) { + private Resource toResource(Environment environment, OpenTelemetryProperties properties) { ResourceBuilder builder = Resource.builder(); - properties.getResourceAttributes().forEach(builder::put); + Map attributes = new OpenTelemetryResourceAttributes(properties.getResourceAttributes()) + .asMap(); + attributes.computeIfAbsent("service.name", (key) -> getApplicationName(environment)); + attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup(environment)); + attributes.forEach(builder::put); return builder.build(); } + private String getApplicationName(Environment environment) { + return environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); + } + + private String getApplicationGroup(Environment environment) { + return environment.getProperty("spring.application.group"); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributes.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributes.java new file mode 100644 index 000000000000..fa72bc6be1f9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributes.java @@ -0,0 +1,156 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.opentelemetry; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.util.StringUtils; + +/** + * OpenTelemetryResourceAttributes retrieves information from the + * {@code OTEL_RESOURCE_ATTRIBUTES} and {@code OTEL_SERVICE_NAME} environment variables + * and merges it with the resource attributes provided by the user. + *

+ * User-provided resource attributes take precedence. + *

+ * OpenTelemetry + * Resource Specification + * + * @author Dmytro Nosan + * @since 3.5.0 + */ +public final class OpenTelemetryResourceAttributes { + + private final Map resourceAttributes; + + private final Function getEnv; + + /** + * Creates a new instance of {@link OpenTelemetryResourceAttributes}. + * @param resourceAttributes user provided resource attributes to be used + */ + public OpenTelemetryResourceAttributes(Map resourceAttributes) { + this(resourceAttributes, null); + } + + /** + * Creates a new {@link OpenTelemetryResourceAttributes} instance. + * @param resourceAttributes user provided resource attributes to be used + * @param getEnv a function to retrieve environment variables by name + */ + OpenTelemetryResourceAttributes(Map resourceAttributes, Function getEnv) { + this.resourceAttributes = (resourceAttributes != null) ? resourceAttributes : Collections.emptyMap(); + this.getEnv = (getEnv != null) ? getEnv : System::getenv; + } + + /** + * Returns resource attributes by combining attributes from environment variables and + * user-defined resource attributes. The final resource contains all attributes from + * both sources. + *

+ * If a key exists in both environment variables and user-defined resources, the value + * from the user-defined resource takes precedence, even if it is empty. + *

+ * Null keys and values are ignored. + * @return the resource attributes + */ + public Map asMap() { + Map attributes = getResourceAttributesFromEnv(); + this.resourceAttributes.forEach((name, value) -> { + if (name != null && value != null) { + attributes.put(name, value); + } + }); + return attributes; + } + + /** + * Parses resource attributes from the {@link System#getenv()}. This method fetches + * attributes defined in the {@code OTEL_RESOURCE_ATTRIBUTES} and + * {@code OTEL_SERVICE_NAME} environment variables and provides them as key-value + * pairs. + *

+ * If {@code service.name} is also provided in {@code OTEL_RESOURCE_ATTRIBUTES}, then + * {@code OTEL_SERVICE_NAME} takes precedence. + * @return resource attributes + */ + private Map getResourceAttributesFromEnv() { + Map attributes = new LinkedHashMap<>(); + for (String attribute : StringUtils.tokenizeToStringArray(getEnv("OTEL_RESOURCE_ATTRIBUTES"), ",")) { + int index = attribute.indexOf('='); + if (index > 0) { + String key = attribute.substring(0, index); + String value = attribute.substring(index + 1); + attributes.put(key.trim(), decode(value.trim())); + } + } + String otelServiceName = getEnv("OTEL_SERVICE_NAME"); + if (otelServiceName != null) { + attributes.put("service.name", otelServiceName); + } + return attributes; + } + + private String getEnv(String name) { + return this.getEnv.apply(name); + } + + /** + * Decodes a percent-encoded string. Converts sequences like '%HH' (where HH + * represents hexadecimal digits) back into their literal representations. + *

+ * Inspired by {@code org.apache.commons.codec.net.PercentCodec}. + * @param value value to decode + * @return the decoded string + */ + public static String decode(String value) { + if (value.indexOf('%') < 0) { + return value; + } + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length); + for (int i = 0; i < bytes.length; i++) { + byte b = bytes[i]; + if (b == '%') { + int u = decodeHex(bytes, ++i); + int l = decodeHex(bytes, ++i); + if (u >= 0 && l >= 0) { + bos.write((u << 4) + l); + } + else { + throw new IllegalArgumentException( + "Failed to decode percent-encoded characters at index %d in the value: '%s'" + .formatted(i - 2, value)); + } + } + else { + bos.write(b); + } + } + return bos.toString(StandardCharsets.UTF_8); + } + + private static int decodeHex(byte[] bytes, int index) { + return (index < bytes.length) ? Character.digit(bytes[index], 16) : -1; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributesTests.java new file mode 100644 index 000000000000..455ba39c603c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributesTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.opentelemetry; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; +import java.util.function.Function; +import java.util.stream.Stream; + +import io.opentelemetry.api.internal.PercentEscaper; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link OpenTelemetryResourceAttributes}. + * + * @author Dmytro Nosan + */ +class OpenTelemetryResourceAttributesTests { + + private static final Random random = new Random(); + + private static final PercentEscaper escaper = PercentEscaper.create(); + + private final Map environmentVariables = new LinkedHashMap<>(); + + private final Map resourceAttributes = new LinkedHashMap<>(); + + @Test + void otelServiceNameShouldTakePrecedenceOverOtelResourceAttributes() { + this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "service.name=ignored"); + this.environmentVariables.put("OTEL_SERVICE_NAME", "otel-service"); + + OpenTelemetryResourceAttributes attributes = getAttributes(); + assertThat(attributes.asMap()).hasSize(1).containsEntry("service.name", "otel-service"); + } + + @Test + void otelServiceNameWhenEmptyShouldTakePrecedenceOverOtelResourceAttributes() { + this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "service.name=ignored"); + this.environmentVariables.put("OTEL_SERVICE_NAME", ""); + + OpenTelemetryResourceAttributes attributes = getAttributes(); + assertThat(attributes.asMap()).hasSize(1).containsEntry("service.name", ""); + } + + @Test + void otelResourceAttributesShouldBeUsed() { + this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", + ", ,,key1=value1,key2= value2, key3=value3,key4=,=value5,key6,=,key7=spring+boot,key8=ś"); + + OpenTelemetryResourceAttributes attributes = getAttributes(); + assertThat(attributes.asMap()).hasSize(6) + .containsEntry("key1", "value1") + .containsEntry("key2", "value2") + .containsEntry("key3", "value3") + .containsEntry("key4", "") + .containsEntry("key7", "spring+boot") + .containsEntry("key8", "ś"); + } + + @Test + void resourceAttributesShouldBeMergedWithEnvironmentVariables() { + this.resourceAttributes.put("service.group", "custom-group"); + this.resourceAttributes.put("key2", ""); + this.environmentVariables.put("OTEL_SERVICE_NAME", "custom-service"); + this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key1=value1,key2=value2"); + + OpenTelemetryResourceAttributes attributes = getAttributes(); + assertThat(attributes.asMap()).hasSize(4) + .containsEntry("service.name", "custom-service") + .containsEntry("service.group", "custom-group") + .containsEntry("key1", "value1") + .containsEntry("key2", ""); + } + + @Test + void resourceAttributesWithNullKeyOrValueShouldBeIgnored() { + this.resourceAttributes.put("service.group", null); + this.resourceAttributes.put("service.name", null); + this.resourceAttributes.put(null, "value"); + this.environmentVariables.put("OTEL_SERVICE_NAME", "custom-service"); + this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key1=value1,key2=value2"); + + OpenTelemetryResourceAttributes attributes = getAttributes(); + assertThat(attributes.asMap()).hasSize(3) + .containsEntry("service.name", "custom-service") + .containsEntry("key1", "value1") + .containsEntry("key2", "value2"); + } + + @Test + @SuppressWarnings("unchecked") + void systemGetEnvShouldBeUsedAsDefaultEnvFunctionAndResourceAttributesAreEmpty() { + OpenTelemetryResourceAttributes attributes = new OpenTelemetryResourceAttributes(null); + assertThat(attributes).extracting("resourceAttributes") + .asInstanceOf(InstanceOfAssertFactories.MAP) + .isNotNull() + .isEmpty(); + Function getEnv = assertThat(attributes).extracting("getEnv") + .asInstanceOf(InstanceOfAssertFactories.type(Function.class)) + .actual(); + System.getenv().forEach((key, value) -> assertThat(getEnv.apply(key)).isEqualTo(value)); + } + + @Test + void shouldDecodeOtelResourceAttributeValues() { + Stream.generate(this::generateRandomString).limit(10000).forEach((value) -> { + String key = "key"; + this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", key + "=" + escaper.escape(value)); + OpenTelemetryResourceAttributes attributes = getAttributes(); + assertThat(attributes.asMap()).hasSize(1).containsEntry(key, value); + }); + } + + @Test + void shouldThrowIllegalArgumentExceptionWhenDecodingPercentIllegalHexChar() { + this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key=abc%ß"); + assertThatIllegalArgumentException().isThrownBy(() -> getAttributes().asMap()) + .withMessage("Failed to decode percent-encoded characters at index 3 in the value: 'abc%ß'"); + } + + @Test + void shouldUseReplacementCharWhenDecodingNonUtf8Character() { + this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key=%a3%3e"); + OpenTelemetryResourceAttributes attributes = getAttributes(); + assertThat(attributes.asMap()).containsEntry("key", "\ufffd>"); + } + + @Test + void shouldThrowIllegalArgumentExceptionWhenDecodingPercent() { + this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key=%"); + assertThatIllegalArgumentException().isThrownBy(() -> getAttributes().asMap()) + .withMessage("Failed to decode percent-encoded characters at index 0 in the value: '%'"); + } + + private OpenTelemetryResourceAttributes getAttributes() { + return new OpenTelemetryResourceAttributes(this.resourceAttributes, this.environmentVariables::get); + } + + private String generateRandomString() { + return random.ints(32, 127) + .limit(64) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc index 349ae05782ec..1896ce200f14 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc @@ -95,12 +95,17 @@ Spring Boot's actuator module includes basic support for OpenTelemetry. It provides a bean of type javadoc:io.opentelemetry.api.OpenTelemetry[], and if there are beans of type javadoc:io.opentelemetry.sdk.trace.SdkTracerProvider[], javadoc:io.opentelemetry.context.propagation.ContextPropagators[], javadoc:io.opentelemetry.sdk.logs.SdkLoggerProvider[] or javadoc:io.opentelemetry.sdk.metrics.SdkMeterProvider[] in the application context, they automatically get registered. Additionally, it provides a javadoc:io.opentelemetry.sdk.resources.Resource[] bean. -The attributes of the auto-configured javadoc:io.opentelemetry.sdk.resources.Resource[] can be configured via the configprop:management.opentelemetry.resource-attributes[] configuration property. +The attributes of the auto-configured javadoc:io.opentelemetry.sdk.resources.Resource[] can be configured via the configprop:management.opentelemetry.resource-attributes[] configuration property. Auto-configured attributes will be merged with attributes from the `OTEL_RESOURCE_ATTRIBUTES` and `OTEL_SERVICE_NAME` environment variables, with attributes configured through the configuration property taking precedence over those from the environment variables. + + If you have defined your own javadoc:io.opentelemetry.sdk.resources.Resource[] bean, this will no longer be the case. NOTE: Spring Boot does not provide auto-configuration for OpenTelemetry metrics or logging. OpenTelemetry tracing is only auto-configured when used together with xref:actuator/tracing.adoc[Micrometer Tracing]. +NOTE: The `OTEL_RESOURCE_ATTRIBUTES` environment variable consist of a list of key-value pairs. For example: `key1=value1,key2=value2,key3=spring%20boot`. All attribute values are treated as strings, and any characters outside the baggage-octet range must be **percent-encoded**. + + The next sections will provide more details about logging, metrics and traces. From 8f4e0518f320b9dc02a68bdde357380d453c5ee4 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 28 Feb 2025 14:35:12 +0100 Subject: [PATCH 2/2] Polish "Add support for OTel-specific environment variables" See gh-44394 --- .../OtlpMetricsPropertiesConfigAdapter.java | 7 ++++-- .../OpenTelemetryAutoConfiguration.java | 4 +++- .../OpenTelemetryResourceAttributes.java | 24 +++++++++---------- .../OpenTelemetryResourceAttributesTests.java | 15 +++++++----- .../pages/actuator/observability.adoc | 7 ++++-- 5 files changed, 34 insertions(+), 23 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsPropertiesConfigAdapter.java index 1f5a0d07c9a7..dbbcd6efe244 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsPropertiesConfigAdapter.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp; +import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -27,6 +28,7 @@ import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryResourceAttributes; import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; /** * Adapter to convert {@link OtlpMetricsProperties} to an {@link OtlpConfig}. @@ -80,7 +82,7 @@ public Map resourceAttributes() { .asMap(); attributes.computeIfAbsent("service.name", (key) -> getApplicationName()); attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup()); - return attributes; + return Collections.unmodifiableMap(attributes); } private String getApplicationName() { @@ -88,7 +90,8 @@ private String getApplicationName() { } private String getApplicationGroup() { - return this.environment.getProperty("spring.application.group"); + String applicationGroup = this.environment.getProperty("spring.application.group"); + return (StringUtils.hasLength(applicationGroup)) ? applicationGroup : null; } @Override diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java index 92e66ba61a8c..173b76ca83bb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -36,6 +36,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry. @@ -88,7 +89,8 @@ private String getApplicationName(Environment environment) { } private String getApplicationGroup(Environment environment) { - return environment.getProperty("spring.application.group"); + String applicationGroup = environment.getProperty("spring.application.group"); + return (StringUtils.hasLength(applicationGroup)) ? applicationGroup : null; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributes.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributes.java index fa72bc6be1f9..d6ba172e0dcc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributes.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributes.java @@ -130,21 +130,21 @@ public static String decode(String value) { ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length); for (int i = 0; i < bytes.length; i++) { byte b = bytes[i]; - if (b == '%') { - int u = decodeHex(bytes, ++i); - int l = decodeHex(bytes, ++i); - if (u >= 0 && l >= 0) { - bos.write((u << 4) + l); - } - else { - throw new IllegalArgumentException( - "Failed to decode percent-encoded characters at index %d in the value: '%s'" - .formatted(i - 2, value)); - } + if (b != '%') { + bos.write(b); + continue; + } + int u = decodeHex(bytes, i + 1); + int l = decodeHex(bytes, i + 2); + if (u >= 0 && l >= 0) { + bos.write((u << 4) + l); } else { - bos.write(b); + throw new IllegalArgumentException( + "Failed to decode percent-encoded characters at index %d in the value: '%s'".formatted(i, + value)); } + i += 2; } return bos.toString(StandardCharsets.UTF_8); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributesTests.java index 455ba39c603c..799ea7bc2605 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryResourceAttributesTests.java @@ -24,6 +24,7 @@ import io.opentelemetry.api.internal.PercentEscaper; import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -36,7 +37,7 @@ */ class OpenTelemetryResourceAttributesTests { - private static final Random random = new Random(); + private static Random random; private static final PercentEscaper escaper = PercentEscaper.create(); @@ -44,11 +45,17 @@ class OpenTelemetryResourceAttributesTests { private final Map resourceAttributes = new LinkedHashMap<>(); + @BeforeAll + static void beforeAll() { + long seed = new Random().nextLong(); + System.out.println("Seed: " + seed); + random = new Random(seed); + } + @Test void otelServiceNameShouldTakePrecedenceOverOtelResourceAttributes() { this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "service.name=ignored"); this.environmentVariables.put("OTEL_SERVICE_NAME", "otel-service"); - OpenTelemetryResourceAttributes attributes = getAttributes(); assertThat(attributes.asMap()).hasSize(1).containsEntry("service.name", "otel-service"); } @@ -57,7 +64,6 @@ void otelServiceNameShouldTakePrecedenceOverOtelResourceAttributes() { void otelServiceNameWhenEmptyShouldTakePrecedenceOverOtelResourceAttributes() { this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "service.name=ignored"); this.environmentVariables.put("OTEL_SERVICE_NAME", ""); - OpenTelemetryResourceAttributes attributes = getAttributes(); assertThat(attributes.asMap()).hasSize(1).containsEntry("service.name", ""); } @@ -66,7 +72,6 @@ void otelServiceNameWhenEmptyShouldTakePrecedenceOverOtelResourceAttributes() { void otelResourceAttributesShouldBeUsed() { this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", ", ,,key1=value1,key2= value2, key3=value3,key4=,=value5,key6,=,key7=spring+boot,key8=ś"); - OpenTelemetryResourceAttributes attributes = getAttributes(); assertThat(attributes.asMap()).hasSize(6) .containsEntry("key1", "value1") @@ -83,7 +88,6 @@ void resourceAttributesShouldBeMergedWithEnvironmentVariables() { this.resourceAttributes.put("key2", ""); this.environmentVariables.put("OTEL_SERVICE_NAME", "custom-service"); this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key1=value1,key2=value2"); - OpenTelemetryResourceAttributes attributes = getAttributes(); assertThat(attributes.asMap()).hasSize(4) .containsEntry("service.name", "custom-service") @@ -99,7 +103,6 @@ void resourceAttributesWithNullKeyOrValueShouldBeIgnored() { this.resourceAttributes.put(null, "value"); this.environmentVariables.put("OTEL_SERVICE_NAME", "custom-service"); this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key1=value1,key2=value2"); - OpenTelemetryResourceAttributes attributes = getAttributes(); assertThat(attributes.asMap()).hasSize(3) .containsEntry("service.name", "custom-service") diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc index 1896ce200f14..2a37108a344a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/observability.adoc @@ -95,7 +95,8 @@ Spring Boot's actuator module includes basic support for OpenTelemetry. It provides a bean of type javadoc:io.opentelemetry.api.OpenTelemetry[], and if there are beans of type javadoc:io.opentelemetry.sdk.trace.SdkTracerProvider[], javadoc:io.opentelemetry.context.propagation.ContextPropagators[], javadoc:io.opentelemetry.sdk.logs.SdkLoggerProvider[] or javadoc:io.opentelemetry.sdk.metrics.SdkMeterProvider[] in the application context, they automatically get registered. Additionally, it provides a javadoc:io.opentelemetry.sdk.resources.Resource[] bean. -The attributes of the auto-configured javadoc:io.opentelemetry.sdk.resources.Resource[] can be configured via the configprop:management.opentelemetry.resource-attributes[] configuration property. Auto-configured attributes will be merged with attributes from the `OTEL_RESOURCE_ATTRIBUTES` and `OTEL_SERVICE_NAME` environment variables, with attributes configured through the configuration property taking precedence over those from the environment variables. +The attributes of the auto-configured javadoc:io.opentelemetry.sdk.resources.Resource[] can be configured via the configprop:management.opentelemetry.resource-attributes[] configuration property. +Auto-configured attributes will be merged with attributes from the `OTEL_RESOURCE_ATTRIBUTES` and `OTEL_SERVICE_NAME` environment variables, with attributes configured through the configuration property taking precedence over those from the environment variables. If you have defined your own javadoc:io.opentelemetry.sdk.resources.Resource[] bean, this will no longer be the case. @@ -103,7 +104,9 @@ If you have defined your own javadoc:io.opentelemetry.sdk.resources.Resource[] b NOTE: Spring Boot does not provide auto-configuration for OpenTelemetry metrics or logging. OpenTelemetry tracing is only auto-configured when used together with xref:actuator/tracing.adoc[Micrometer Tracing]. -NOTE: The `OTEL_RESOURCE_ATTRIBUTES` environment variable consist of a list of key-value pairs. For example: `key1=value1,key2=value2,key3=spring%20boot`. All attribute values are treated as strings, and any characters outside the baggage-octet range must be **percent-encoded**. +NOTE: The `OTEL_RESOURCE_ATTRIBUTES` environment variable consist of a list of key-value pairs. +For example: `key1=value1,key2=value2,key3=spring%20boot`. +All attribute values are treated as strings, and any characters outside the baggage-octet range must be **percent-encoded**. The next sections will provide more details about logging, metrics and traces.