diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java index a7929ae56eba..7a38491281f0 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/builder/internal/DefaultHttpClientInstrumenterBuilder.java @@ -20,6 +20,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; +import io.opentelemetry.instrumentation.api.internal.Experimental; import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractor; import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesGetter; @@ -177,6 +178,18 @@ public DefaultHttpClientInstrumenterBuilder setKnownMethods( return this; } + /** + * Configures the instrumentation to redact sensitive URL parameters. + * + * @param redactQueryParameters {@code true} if the sensitive URL parameters have to be redacted. + */ + @CanIgnoreReturnValue + public DefaultHttpClientInstrumenterBuilder setRedactQueryParameters( + boolean redactQueryParameters) { + Experimental.setRedactQueryParameters(httpAttributesExtractorBuilder, redactQueryParameters); + return this; + } + /** Sets custom {@link SpanNameExtractor} via transform function. */ @CanIgnoreReturnValue public DefaultHttpClientInstrumenterBuilder setSpanNameExtractor( @@ -225,6 +238,7 @@ public Instrumenter build() { .addAttributesExtractor(HttpExperimentalAttributesExtractor.create(attributesGetter)) .addOperationMetrics(HttpClientExperimentalMetrics.get()); } + builderCustomizer.accept(builder); if (headerSetter != null) { @@ -248,6 +262,7 @@ public DefaultHttpClientInstrumenterBuilder configure(CommonC set( config::shouldEmitExperimentalHttpClientTelemetry, this::setEmitExperimentalHttpClientMetrics); + set(config::redactQueryParameters, this::setRedactQueryParameters); return this; } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java index 23875d7e8b5f..bb64425c6da5 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java @@ -31,6 +31,7 @@ public final class CommonConfig { private final boolean statementSanitizationEnabled; private final boolean emitExperimentalHttpClientTelemetry; private final boolean emitExperimentalHttpServerTelemetry; + private final boolean redactQueryParameters; private final String loggingTraceIdKey; private final String loggingSpanIdKey; private final String loggingTraceFlagsKey; @@ -57,6 +58,9 @@ public CommonConfig(InstrumentationConfig config) { config.getBoolean("otel.instrumentation.common.db-statement-sanitizer.enabled", true); emitExperimentalHttpClientTelemetry = config.getBoolean("otel.instrumentation.http.client.emit-experimental-telemetry", false); + redactQueryParameters = + config.getBoolean( + "otel.instrumentation.http.client.experimental.redact-query-parameters", true); emitExperimentalHttpServerTelemetry = config.getBoolean("otel.instrumentation.http.server.emit-experimental-telemetry", false); enduserConfig = new EnduserConfig(config); @@ -111,6 +115,10 @@ public boolean shouldEmitExperimentalHttpServerTelemetry() { return emitExperimentalHttpServerTelemetry; } + public boolean redactQueryParameters() { + return redactQueryParameters; + } + public String getTraceIdKey() { return loggingTraceIdKey; } diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/Experimental.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/Experimental.java new file mode 100644 index 000000000000..250cfbd5b2fb --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/Experimental.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.internal; + +import io.opentelemetry.instrumentation.api.semconv.http.HttpClientAttributesExtractorBuilder; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class Experimental { + + @Nullable + private static volatile BiConsumer, Boolean> + redactHttpClientQueryParameters; + + private Experimental() {} + + public static void setRedactQueryParameters( + HttpClientAttributesExtractorBuilder builder, boolean redactQueryParameters) { + if (redactHttpClientQueryParameters != null) { + redactHttpClientQueryParameters.accept(builder, redactQueryParameters); + } + } + + public static void internalSetRedactHttpClientQueryParameters( + BiConsumer, Boolean> + redactHttpClientQueryParameters) { + Experimental.redactHttpClientQueryParameters = redactHttpClientQueryParameters; + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java index 5769d1169c3f..2227eaed7b43 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractor.java @@ -17,6 +17,9 @@ import io.opentelemetry.instrumentation.api.semconv.network.internal.InternalServerAttributesExtractor; import io.opentelemetry.semconv.HttpAttributes; import io.opentelemetry.semconv.UrlAttributes; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import java.util.function.ToIntFunction; import javax.annotation.Nullable; @@ -32,6 +35,9 @@ public final class HttpClientAttributesExtractor REQUEST, RESPONSE, HttpClientAttributesGetter> implements SpanKeyProvider { + private static final Set PARAMS_TO_REDACT = + new HashSet<>(Arrays.asList("AWSAccessKeyId", "Signature", "sig", "X-Goog-Signature")); + /** * Creates the HTTP client attributes extractor with default configuration. * @@ -54,6 +60,7 @@ public static HttpClientAttributesExtractorBuilder internalNetworkExtractor; private final InternalServerAttributesExtractor internalServerExtractor; private final ToIntFunction resendCountIncrementer; + private final boolean redactQueryParameters; HttpClientAttributesExtractor(HttpClientAttributesExtractorBuilder builder) { super( @@ -65,6 +72,7 @@ public static HttpClientAttributesExtractorBuilder { List capturedResponseHeaders = emptyList(); Set knownMethods = HttpConstants.KNOWN_METHODS; ToIntFunction resendCountIncrementer = HttpClientRequestResendCount::getAndIncrement; + boolean redactQueryParameters; + + static { + Experimental.internalSetRedactHttpClientQueryParameters( + (builder, redact) -> builder.redactQueryParameters = redact); + } HttpClientAttributesExtractorBuilder( HttpClientAttributesGetter httpAttributesGetter) { diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java index f4760f0201d3..c04b78346f00 100644 --- a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/semconv/http/HttpClientAttributesExtractorTest.java @@ -23,12 +23,14 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.entry; +import static org.junit.jupiter.params.provider.Arguments.arguments; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.internal.Experimental; import io.opentelemetry.instrumentation.api.internal.HttpConstants; import java.net.ConnectException; import java.util.HashMap; @@ -36,9 +38,13 @@ import java.util.List; import java.util.Map; import java.util.function.ToIntFunction; +import java.util.stream.Stream; import javax.annotation.Nullable; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.ValueSource; @@ -200,6 +206,93 @@ void normal() { entry(NETWORK_PEER_PORT, 456L)); } + @ParameterizedTest + @ArgumentsSource(UrlSourceToRedact.class) + void shouldRedactUserInfoAndQueryParameters(String url, String expectedResult) { + Map request = new HashMap<>(); + request.put("urlFull", url); + + HttpClientAttributesExtractorBuilder, Map> builder = + HttpClientAttributesExtractor.builder(new TestHttpClientAttributesGetter()); + Experimental.setRedactQueryParameters(builder, true); + AttributesExtractor, Map> extractor = builder.build(); + + AttributesBuilder attributes = Attributes.builder(); + extractor.onStart(attributes, Context.root(), request); + + assertThat(attributes.build()).containsOnly(entry(URL_FULL, expectedResult)); + } + + static final class UrlSourceToRedact implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + arguments("https://user1:secret@github.com", "https://REDACTED:REDACTED@github.com"), + arguments( + "https://user1:secret@github.com/path/", + "https://REDACTED:REDACTED@github.com/path/"), + arguments( + "https://user1:secret@github.com#test.html", + "https://REDACTED:REDACTED@github.com#test.html"), + arguments( + "https://user1:secret@github.com?foo=b@r", + "https://REDACTED:REDACTED@github.com?foo=b@r"), + arguments( + "https://user1:secret@github.com/p@th?foo=b@r", + "https://REDACTED:REDACTED@github.com/p@th?foo=b@r"), + arguments("https://github.com/p@th?foo=b@r", "https://github.com/p@th?foo=b@r"), + arguments("https://github.com#t@st.html", "https://github.com#t@st.html"), + arguments("user1:secret@github.com", "user1:secret@github.com"), + arguments("https://github.com@", "https://github.com@"), + arguments( + "https://service.com?paramA=valA¶mB=valB", + "https://service.com?paramA=valA¶mB=valB"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7", + "https://service.com?AWSAccessKeyId=REDACTED"), + arguments( + "https://service.com?Signature=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0%3A377", + "https://service.com?Signature=REDACTED"), + arguments( + "https://service.com?sig=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0", + "https://service.com?sig=REDACTED"), + arguments( + "https://service.com?X-Goog-Signature=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0", + "https://service.com?X-Goog-Signature=REDACTED"), + arguments( + "https://service.com?paramA=valA&AWSAccessKeyId=AKIAIOSFODNN7¶mB=valB", + "https://service.com?paramA=valA&AWSAccessKeyId=REDACTED¶mB=valB"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7¶mA=valA", + "https://service.com?AWSAccessKeyId=REDACTED¶mA=valA"), + arguments( + "https://service.com?paramA=valA&AWSAccessKeyId=AKIAIOSFODNN7", + "https://service.com?paramA=valA&AWSAccessKeyId=REDACTED"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7&AWSAccessKeyId=ZGIAIOSFODNN7", + "https://service.com?AWSAccessKeyId=REDACTED&AWSAccessKeyId=REDACTED"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7#ref", + "https://service.com?AWSAccessKeyId=REDACTED#ref"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7&aa&bb", + "https://service.com?AWSAccessKeyId=REDACTED&aa&bb"), + arguments( + "https://service.com?aa&bb&AWSAccessKeyId=AKIAIOSFODNN7", + "https://service.com?aa&bb&AWSAccessKeyId=REDACTED"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7&&", + "https://service.com?AWSAccessKeyId=REDACTED&&"), + arguments( + "https://service.com?&&AWSAccessKeyId=AKIAIOSFODNN7", + "https://service.com?&&AWSAccessKeyId=REDACTED"), + arguments( + "https://service.com?AWSAccessKeyId=AKIAIOSFODNN7&a&b#fragment", + "https://service.com?AWSAccessKeyId=REDACTED&a&b#fragment")); + } + } + @ParameterizedTest @ArgumentsSource(ValidRequestMethodsProvider.class) void shouldExtractKnownMethods(String requestMethod) { diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index f179b7e7f9bd..e339c3455df8 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -311,6 +311,12 @@ "description": "Enable the capture of experimental HTTP client telemetry. Add the http.request.body.size and http.response.body.size> attributes to spans, and record the http.client.request.size and http.client.response.size metrics.", "defaultValue": false }, + { + "name": "otel.instrumentation.http.client.experimental.redact-query-parameters", + "type": "java.lang.Boolean", + "description": "Redact sensitive URL parameters. See https://opentelemetry.io/docs/specs/semconv/http/http-spans.", + "defaultValue": true + }, { "name": "otel.instrumentation.http.known-methods", "type": "java.util.List", diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java index e4a85eb343f0..62c8239ba3f5 100644 --- a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/AbstractOtelSpringStarterSmokeTest.java @@ -293,4 +293,23 @@ void restTemplate() { span.hasKind(SpanKind.SERVER).hasAttribute(HttpAttributes.HTTP_ROUTE, "/ping"), span -> withSpanAssert(span))); } + + @Test + void shouldRedactSomeUrlParameters() { + testing.clearAllExportedData(); + + RestTemplate restTemplate = restTemplateBuilder.rootUri("http://localhost:" + port).build(); + restTemplate.getForObject( + "/test?X-Goog-Signature=39Up9jzHkxhuIhFE9594DJxe7w6cIRCg0V6ICGS0", String.class); + + testing.waitAndAssertTraces( + traceAssert -> + traceAssert.hasSpansSatisfyingExactly( + span -> + HttpSpanDataAssert.create(span) + .assertClientGetRequest("/test?X-Goog-Signature=REDACTED"), + span -> + span.hasKind(SpanKind.SERVER) + .hasAttribute(HttpAttributes.HTTP_ROUTE, "/test"))); + } } diff --git a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java index c4902b2d65d5..68c6fd0d199f 100644 --- a/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java +++ b/smoke-tests-otel-starter/spring-boot-common/src/main/java/io/opentelemetry/spring/smoketest/OtelSpringStarterSmokeTestController.java @@ -15,6 +15,7 @@ public class OtelSpringStarterSmokeTestController { public static final String PING = "/ping"; + public static final String TEST = "/test"; public static final String TEST_HISTOGRAM = "histogram-test-otel-spring-starter"; public static final String METER_SCOPE_NAME = "scope"; private final LongHistogram histogram; @@ -33,4 +34,9 @@ public String ping() { component.withSpanMethod("from-controller"); return "pong"; } + + @GetMapping(TEST) + public String testUrlToRedact() { + return "ok"; + } }