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 1de7f2fa772..bf963b3ddc9 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 @@ -17,8 +17,6 @@ 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; @@ -67,18 +65,14 @@ public static Resource createEnvironmentResource() { */ 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.displayName())); - } - } 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) { @@ -132,5 +126,35 @@ static Resource filterAttributes(Resource resource, ConfigProperties configPrope return builder.build(); } + private static String decodeResourceAttributes(String value) { + try { + if (value.indexOf('%') < 0) { + return value; + } + + int n = value.length(); + byte[] bytes = new byte[n]; + int pos = 0; + + for (int i = 0; i < n; i++) { + char c = value.charAt(i); + if (c == '%' && i + 2 < n) { + int d1 = Character.digit(value.charAt(i + 1), 16); + int d2 = Character.digit(value.charAt(i + 2), 16); + if (d1 != -1 && d2 != -1) { + bytes[pos++] = (byte) ((d1 << 4) + d2); + i += 2; + continue; + } + } + // Keep '+' as '+' and any other non-encoded chars + bytes[pos++] = (byte) c; + } + return new String(bytes, 0, pos, StandardCharsets.UTF_8); + } catch (RuntimeException e) { + throw new ConfigurationException("Failed to decode resource attributes: " + value, e); + } + } + 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..45f604aa84e 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 @@ -50,6 +50,93 @@ void customConfigResourceWithDisabledKeys() { .build()); } + @Test + void decodePlusSignInCustomConfigResource() { + Map props = new HashMap<>(); + props.put("otel.service.name", "my-app"); + props.put( + "otel.resource.attributes", "food=cheese+cake,drink=juice,animal= ,color=,shape=square"); + + assertThat( + ResourceConfiguration.configureResource( + DefaultConfigProperties.create(props, componentLoader), + SpiHelper.create(ResourceConfigurationTest.class.getClassLoader()), + (r, c) -> r)) + .isEqualTo( + Resource.getDefault().toBuilder() + .put(stringKey("service.name"), "my-app") + .put("food", "cheese+cake") + .put("drink", "juice") + .put("shape", "square") + .build()); + } + + @Test + void decodePercentEncodedSpace() { + Map props = new HashMap<>(); + props.put("otel.resource.attributes", "key=hello%20world"); + + assertThat( + ResourceConfiguration.createEnvironmentResource( + DefaultConfigProperties.createFromMap(props))) + .isEqualTo(Resource.create(Attributes.of(stringKey("key"), "hello world"))); + } + + @Test + void decodeInvalidPercentEncodingPreservesLiteral() { + Map props = new HashMap<>(); + props.put("otel.resource.attributes", "key=abc%2Gdef"); + + assertThat( + ResourceConfiguration.createEnvironmentResource( + DefaultConfigProperties.createFromMap(props))) + .isEqualTo(Resource.create(Attributes.of(stringKey("key"), "abc%2Gdef"))); + } + + @Test + void decodeIncompletePercentEncodingPreservesLiteral() { + Map props = new HashMap<>(); + props.put("otel.resource.attributes", "key=abc%2"); + + assertThat( + ResourceConfiguration.createEnvironmentResource( + DefaultConfigProperties.createFromMap(props))) + .isEqualTo(Resource.create(Attributes.of(stringKey("key"), "abc%2"))); + } + + @Test + void decodePercentAtEndPreservesLiteral() { + Map props = new HashMap<>(); + props.put("otel.resource.attributes", "key=abc%"); + + assertThat( + ResourceConfiguration.createEnvironmentResource( + DefaultConfigProperties.createFromMap(props))) + .isEqualTo(Resource.create(Attributes.of(stringKey("key"), "abc%"))); + } + + @Test + void decodeMultiplePercentEncodings() { + Map props = new HashMap<>(); + props.put("otel.resource.attributes", "key=a%20b%2Bc%3Dd"); + + assertThat( + ResourceConfiguration.createEnvironmentResource( + DefaultConfigProperties.createFromMap(props))) + .isEqualTo(Resource.create(Attributes.of(stringKey("key"), "a b+c=d"))); + } + + @Test + void decodeNoPercentEncoding() { + Map props = new HashMap<>(); + props.put("otel.resource.attributes", "key=plain-value"); + + assertThat( + ResourceConfiguration.createEnvironmentResource( + DefaultConfigProperties.createFromMap(props))) + .isEqualTo(Resource.create(Attributes.of(stringKey("key"), "plain-value"))); + } + @Test void createEnvironmentResource_Empty() { Attributes attributes = ResourceConfiguration.createEnvironmentResource().getAttributes();