Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> 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<String, String> 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) {
Expand Down Expand Up @@ -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.
*
* <p>Unlike {@link java.net.URLDecoder}, this method:
*
* <ul>
* <li>Preserves '+' as a literal plus sign (URLDecoder decodes '+' as space)
* <li>Preserves invalid percent sequences as literals (e.g., "%2G", "%", "%2")
* <li>Supports multi-byte UTF-8 sequences (e.g., "%C3%A9" decodes to "é")
* </ul>
*
* @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() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -50,6 +54,36 @@ void customConfigResourceWithDisabledKeys() {
.build());
}

@ParameterizedTest
@MethodSource("decodeResourceAttributesArgs")
void decodeResourceAttributes(String input, String expectedKey, String expectedValue) {
Map<String, String> 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<Arguments> 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();
Expand Down
Loading