diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java index bf663666839..30bd30bdf6c 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceConfiguration.java @@ -11,14 +11,11 @@ import io.opentelemetry.common.ComponentLoader; import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.ResourceBuilder; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashSet; @@ -68,18 +65,13 @@ public static Resource createEnvironmentResource() { @SuppressWarnings("JdkObsolete") // Recommended alternative was introduced in java 10 public static Resource createEnvironmentResource(ConfigProperties config) { AttributesBuilder resourceAttributes = Attributes.builder(); - try { - for (Map.Entry entry : config.getMap(ATTRIBUTE_PROPERTY).entrySet()) { - resourceAttributes.put( - entry.getKey(), - // Attributes specified via otel.resource.attributes follow the W3C Baggage spec and - // characters outside the baggage-octet range are percent encoded - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable - URLDecoder.decode(entry.getValue(), StandardCharsets.UTF_8.name())); - } - } catch (UnsupportedEncodingException e) { - // Should not happen since always using standard charset - throw new ConfigurationException("Unable to decode resource attributes.", e); + for (Map.Entry entry : config.getMap(ATTRIBUTE_PROPERTY).entrySet()) { + resourceAttributes.put( + entry.getKey(), + // Attributes specified via otel.resource.attributes follow the W3C Baggage spec and + // characters outside the baggage-octet range are percent encoded + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable + decodeResourceAttributes(entry.getValue())); } String serviceName = config.getString(SERVICE_NAME_PROPERTY); if (serviceName != null) { @@ -133,5 +125,52 @@ static Resource filterAttributes(Resource resource, ConfigProperties configPrope return builder.build(); } + /** + * Decodes percent-encoded characters in resource attribute values per W3C Baggage spec. + * + *

Unlike {@link java.net.URLDecoder}, this method: + * + *

+ * + * @param value the percent-encoded string + * @return the decoded string + */ + private static String decodeResourceAttributes(String value) { + // no percent signs means nothing to decode + if (value.indexOf('%') < 0) { + return value; + } + + int n = value.length(); + // Use byte array to properly handle multi-byte UTF-8 sequences + byte[] bytes = new byte[n]; + int pos = 0; + + for (int i = 0; i < n; i++) { + char c = value.charAt(i); + // Check for percent-encoded sequence i.e. '%' followed by two hex digits + if (c == '%' && i + 2 < n) { + int d1 = Character.digit(value.charAt(i + 1), 16); + int d2 = Character.digit(value.charAt(i + 2), 16); + // Valid hex digits return 0-15, invalid returns -1 + if (d1 != -1 && d2 != -1) { + // Combine two hex digits into a single byte (e.g., "2F" becomes 0x2F) + bytes[pos++] = (byte) ((d1 << 4) + d2); + // Skip the two hex digits (loop will also do i++) + i += 2; + continue; + } + } + // Keep '+' as '+' (unlike URLDecoder) and preserve invalid percent sequences which will be + // treated as literals + bytes[pos++] = (byte) c; + } + return new String(bytes, 0, pos, StandardCharsets.UTF_8); + } + private ResourceConfiguration() {} } diff --git a/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java b/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java index 1678fb29576..d116bdbf37d 100644 --- a/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java +++ b/sdk-extensions/autoconfigure/src/test/java/io/opentelemetry/sdk/autoconfigure/ResourceConfigurationTest.java @@ -19,8 +19,12 @@ import io.opentelemetry.sdk.resources.Resource; import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -50,6 +54,36 @@ void customConfigResourceWithDisabledKeys() { .build()); } + @ParameterizedTest + @MethodSource("decodeResourceAttributesArgs") + void decodeResourceAttributes(String input, String expectedKey, String expectedValue) { + Map props = new HashMap<>(); + props.put("otel.resource.attributes", input); + + assertThat( + ResourceConfiguration.createEnvironmentResource( + DefaultConfigProperties.createFromMap(props))) + .isEqualTo(Resource.create(Attributes.of(stringKey(expectedKey), expectedValue))); + } + + private static Stream decodeResourceAttributesArgs() { + return Stream.of( + // Plus sign preserved + Arguments.of("food=cheese+cake", "food", "cheese+cake"), + // Percent-encoded space in resource attribute value decoded to space + Arguments.of("key=hello%20world", "key", "hello world"), + // Invalid percent encoding preserved + Arguments.of("key=abc%2Gdef", "key", "abc%2Gdef"), + // Incomplete percent encoding preserved + Arguments.of("key=abc%2", "key", "abc%2"), + // Percent at end preserved + Arguments.of("key=abc%", "key", "abc%"), + // Multiple percent encodings + Arguments.of("key=a%20b%2Bc%3Dd", "key", "a b+c=d"), + // No percent encoding + Arguments.of("key=plain-value", "key", "plain-value")); + } + @Test void createEnvironmentResource_Empty() { Attributes attributes = ResourceConfiguration.createEnvironmentResource().getAttributes();