diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLogDetailLevel.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLogDetailLevel.java index 7bbfbc3d2b807..82ced4912e91a 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLogDetailLevel.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLogDetailLevel.java @@ -23,6 +23,7 @@ public enum HttpLogDetailLevel { /** * Logs everything in BASIC, plus all the request and response headers. + * Values of the headers will be logged only for allowed headers. See {@link HttpLogOptions#getAllowedHeaderNames()}. */ HEADERS, @@ -35,6 +36,7 @@ public enum HttpLogDetailLevel { /** * Logs everything in HEADERS and BODY. + * Values of the headers will be logged only for allowed headers. See {@link HttpLogOptions#getAllowedHeaderNames()}. */ BODY_AND_HEADERS; diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLogOptions.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLogOptions.java index c24e90f91ebe5..0ffb806e4113c 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLogOptions.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLogOptions.java @@ -23,6 +23,7 @@ public class HttpLogOptions { private Set allowedHeaderNames; private Set allowedQueryParamNames; private boolean prettyPrintBody; + private boolean disableRedactedHeaderLogging; private HttpRequestLogger requestLogger; private HttpResponseLogger responseLogger; @@ -279,4 +280,28 @@ public HttpLogOptions setResponseLogger(HttpResponseLogger responseLogger) { this.responseLogger = responseLogger; return this; } + + /** + * Sets the flag that controls if header names which value is redacted should be logged. + *

+ * Applies only if logging request and response headers is enabled. See {@link HttpLogOptions#setLogLevel(HttpLogDetailLevel)} for details. + * Defaults to `false` - redacted header names are logged. + * + * @param disableRedactedHeaderLogging If true, redacted header names are not logged. + * Otherwise, they are logged as a comma separated list under `redactedHeaders` property. + * @return The updated HttpLogOptions object. + */ + public HttpLogOptions disableRedactedHeaderLogging(boolean disableRedactedHeaderLogging) { + this.disableRedactedHeaderLogging = disableRedactedHeaderLogging; + return this; + } + + /** + * Gets the flag that controls if header names with redacted values should be logged. + * + * @return true if header names with redacted values should be logged. + */ + public boolean isRedactedHeaderLoggingDisabled() { + return disableRedactedHeaderLogging; + } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLoggingPolicy.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLoggingPolicy.java index 63624ffbc2615..2135a091d8c04 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLoggingPolicy.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLoggingPolicy.java @@ -73,7 +73,7 @@ public class HttpLoggingPolicy implements HttpPipelinePolicy { private final Set allowedHeaderNames; private final Set allowedQueryParameterNames; private final boolean prettyPrintBody; - + private final boolean disableRedactedHeaderLogging; private final HttpRequestLogger requestLogger; private final HttpResponseLogger responseLogger; @@ -102,6 +102,7 @@ public HttpLoggingPolicy(HttpLogOptions httpLogOptions) { .map(queryParamName -> queryParamName.toLowerCase(Locale.ROOT)) .collect(Collectors.toSet()); this.prettyPrintBody = false; + this.disableRedactedHeaderLogging = false; this.requestLogger = new DefaultHttpRequestLogger(); this.responseLogger = new DefaultHttpResponseLogger(); @@ -116,6 +117,7 @@ public HttpLoggingPolicy(HttpLogOptions httpLogOptions) { .map(queryParamName -> queryParamName.toLowerCase(Locale.ROOT)) .collect(Collectors.toSet()); this.prettyPrintBody = httpLogOptions.isPrettyPrintBody(); + this.disableRedactedHeaderLogging = httpLogOptions.isRedactedHeaderLoggingDisabled(); this.requestLogger = (httpLogOptions.getRequestLogger() == null) ? new DefaultHttpRequestLogger() @@ -237,7 +239,7 @@ private void log(LogLevel logLevel, ClientLogger logger, HttpRequestLoggingConte } if (httpLogDetailLevel.shouldLogHeaders() && logger.canLogAtLevel(LogLevel.INFORMATIONAL)) { - addHeadersToLogMessage(allowedHeaderNames, request.getHeaders(), logBuilder); + addHeadersToLogMessage(allowedHeaderNames, request.getHeaders(), logBuilder, disableRedactedHeaderLogging); } if (request.getBody() == null) { @@ -326,7 +328,7 @@ public Mono logResponse(ClientLogger logger, HttpResponseLoggingCo private void logHeaders(ClientLogger logger, HttpResponse response, LoggingEventBuilder logBuilder) { if (httpLogDetailLevel.shouldLogHeaders() && logger.canLogAtLevel(LogLevel.INFORMATIONAL)) { - addHeadersToLogMessage(allowedHeaderNames, response.getHeaders(), logBuilder); + addHeadersToLogMessage(allowedHeaderNames, response.getHeaders(), logBuilder, disableRedactedHeaderLogging); } } @@ -413,10 +415,25 @@ private static String getRedactedUrl(URL url, Set allowedQueryParameterN * @param logLevel Log level the environment is configured to use. */ private static void addHeadersToLogMessage(Set allowedHeaderNames, HttpHeaders headers, - LoggingEventBuilder logBuilder) { + LoggingEventBuilder logBuilder, boolean disableRedactedHeaderLogging) { + + final StringBuilder redactedHeaders = new StringBuilder(); + // The raw header map uses keys that are already lower-cased. - HttpHeadersAccessHelper.getRawHeaderMap(headers).forEach((key, value) -> logBuilder.addKeyValue(value.getName(), - allowedHeaderNames.contains(key) ? value.getValue() : REDACTED_PLACEHOLDER)); + HttpHeadersAccessHelper.getRawHeaderMap(headers).forEach((key, value) -> { + if (allowedHeaderNames.contains(key)) { + logBuilder.addKeyValue(value.getName(), value.getValue()); + } else if (!disableRedactedHeaderLogging) { + if (redactedHeaders.length() > 0) { + redactedHeaders.append(','); + } + redactedHeaders.append(value.getName()); + } + }); + + if (redactedHeaders.length() > 0) { + logBuilder.addKeyValue("redactedHeaders", redactedHeaders.toString()); + } } /* diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/HttpLoggingPolicyTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/HttpLoggingPolicyTests.java index 69d0fecdccb93..da4f2323ef4c6 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/HttpLoggingPolicyTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/http/policy/HttpLoggingPolicyTests.java @@ -36,7 +36,6 @@ import org.junit.jupiter.api.parallel.Resources; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -195,8 +194,9 @@ public void validateLoggingDoesNotConsumeRequest(Flux stream, byte[] .set(HttpHeaderName.CONTENT_TYPE, ContentType.APPLICATION_JSON) .set(HttpHeaderName.CONTENT_LENGTH, Integer.toString(contentLength)); + HttpLogOptions logOptions = new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY); HttpPipeline pipeline = new HttpPipelineBuilder() - .policies(new HttpLoggingPolicy(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY))) + .policies(new HttpLoggingPolicy(logOptions)) .httpClient(request -> FluxUtil.collectBytesInByteBufferStream(request.getBody()) .doOnSuccess(bytes -> assertArraysEqual(data, bytes)) .then(Mono.empty())) @@ -211,7 +211,7 @@ public void validateLoggingDoesNotConsumeRequest(Flux stream, byte[] assertEquals(1, messages.size()); HttpLogMessage expectedRequest = HttpLogMessage.request(HttpMethod.POST, url, data); - expectedRequest.assertEqual(messages.get(0), HttpLogDetailLevel.BODY, LogLevel.INFORMATIONAL); + expectedRequest.assertEqual(messages.get(0), logOptions, LogLevel.INFORMATIONAL); } /** @@ -227,8 +227,9 @@ public void validateLoggingDoesNotConsumeRequestSync(BinaryData requestBody, byt .set(HttpHeaderName.CONTENT_TYPE, ContentType.APPLICATION_JSON) .set(HttpHeaderName.CONTENT_LENGTH, Integer.toString(contentLength)); + HttpLogOptions logOptions = new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY); HttpPipeline pipeline = new HttpPipelineBuilder() - .policies(new HttpLoggingPolicy(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY))) + .policies(new HttpLoggingPolicy(logOptions)) .httpClient(request -> FluxUtil.collectBytesInByteBufferStream(request.getBody()) .doOnSuccess(bytes -> assertArraysEqual(data, bytes)) .then(Mono.empty())) @@ -242,7 +243,7 @@ public void validateLoggingDoesNotConsumeRequestSync(BinaryData requestBody, byt assertEquals(1, messages.size()); HttpLogMessage expectedRequest = HttpLogMessage.request(HttpMethod.POST, url, data); - expectedRequest.assertEqual(messages.get(0), HttpLogDetailLevel.BODY, LogLevel.INFORMATIONAL); + expectedRequest.assertEqual(messages.get(0), logOptions, LogLevel.INFORMATIONAL); } /** @@ -412,11 +413,10 @@ public Mono getBodyAsString(Charset charset) { } @ParameterizedTest(name = "[{index}] {displayName}") - @EnumSource(value = HttpLogDetailLevel.class, mode = EnumSource.Mode.INCLUDE, - names = {"BASIC", "HEADERS", "BODY", "BODY_AND_HEADERS"}) - public void loggingIncludesRetryCount(HttpLogDetailLevel logLevel) { + @MethodSource("logOptionsSupplier") + public void loggingIncludesRetryCount(HttpLogOptions logOptions) { AtomicInteger requestCount = new AtomicInteger(); - String url = "https://test.com/loggingIncludesRetryCount/" + logLevel; + String url = "https://test.com/loggingIncludesRetryCount/" + logOptions.getLogLevel(); HttpRequest request = new HttpRequest(HttpMethod.GET, url) .setHeader(HttpHeaderName.X_MS_CLIENT_REQUEST_ID, "client-request-id"); @@ -426,7 +426,7 @@ public void loggingIncludesRetryCount(HttpLogDetailLevel logLevel) { .set(X_MS_REQUEST_ID, "server-request-id"); HttpPipeline pipeline = new HttpPipelineBuilder() - .policies(new RetryPolicy(), new HttpLoggingPolicy(new HttpLogOptions().setLogLevel(logLevel))) + .policies(new RetryPolicy(), new HttpLoggingPolicy(logOptions)) .httpClient(ignored -> (requestCount.getAndIncrement() == 0) ? Mono.error(new RuntimeException("Try again!")) : Mono.just(new com.azure.core.http.MockHttpResponse(ignored, 200, responseHeaders, responseBody))) @@ -454,32 +454,33 @@ public void loggingIncludesRetryCount(HttpLogDetailLevel logLevel) { List messages = HttpLogMessage.fromString(logString).stream() .filter(m -> !m.getMessage().equals("Error resume.")).collect(Collectors.toList()); - expectedRetry1.assertEqual(messages.get(0), logLevel, LogLevel.INFORMATIONAL); + expectedRetry1.assertEqual(messages.get(0), logOptions, LogLevel.INFORMATIONAL); assertEquals("HTTP FAILED", messages.get(1).getMessage()); - expectedRetry2.assertEqual(messages.get(2), logLevel, LogLevel.INFORMATIONAL); - expectedResponse.assertEqual(messages.get(3), logLevel, LogLevel.INFORMATIONAL); + expectedRetry2.assertEqual(messages.get(2), logOptions, LogLevel.INFORMATIONAL); + expectedResponse.assertEqual(messages.get(3), logOptions, LogLevel.INFORMATIONAL); assertEquals(4, messages.size()); } @ParameterizedTest(name = "[{index}] {displayName}") - @EnumSource(value = HttpLogDetailLevel.class, mode = EnumSource.Mode.INCLUDE, - names = {"BASIC", "HEADERS", "BODY", "BODY_AND_HEADERS"}) - public void loggingHeadersAndBodyVerbose(HttpLogDetailLevel logLevel) { + @MethodSource("logOptionsSupplier") + public void loggingHeadersAndBodyVerbose(HttpLogOptions logOptions) { setupLogLevel(LogLevel.VERBOSE.getLogLevel()); byte[] requestBody = new byte[] {42}; byte[] responseBody = new byte[] {24, 42}; - String url = "https://test.com/loggingHeadersAndBodyVerbose/" + logLevel; + String url = "https://test.com/loggingHeadersAndBodyVerbose/" + logOptions.getLogLevel(); HttpRequest request = new HttpRequest(HttpMethod.POST, url) .setBody(requestBody) + .setHeader(HttpHeaderName.AUTHORIZATION, "not-allowed-value") .setHeader(HttpHeaderName.X_MS_CLIENT_REQUEST_ID, "client-request-id"); HttpHeaders responseHeaders = new HttpHeaders() .set(HttpHeaderName.CONTENT_LENGTH, Integer.toString(responseBody.length)) + .set(HttpHeaderName.AUTHORIZATION, "not-allowed-value") .set(X_MS_REQUEST_ID, "server-request-id"); HttpPipeline pipeline = new HttpPipelineBuilder() - .policies(new RetryPolicy(), new HttpLoggingPolicy(new HttpLogOptions().setLogLevel(logLevel))) + .policies(new RetryPolicy(), new HttpLoggingPolicy(logOptions)) .httpClient(r -> Mono.just(new com.azure.core.http.MockHttpResponse(r, 200, responseHeaders, responseBody))) .build(); @@ -499,26 +500,27 @@ public void loggingHeadersAndBodyVerbose(HttpLogDetailLevel logLevel) { List messages = HttpLogMessage.fromString(logString); assertEquals(2, messages.size()); - expectedRequest.assertEqual(messages.get(0), logLevel, LogLevel.VERBOSE); - expectedResponse.assertEqual(messages.get(1), logLevel, LogLevel.VERBOSE); + expectedRequest.assertEqual(messages.get(0), logOptions, LogLevel.VERBOSE); + expectedResponse.assertEqual(messages.get(1), logOptions, LogLevel.VERBOSE); } @ParameterizedTest(name = "[{index}] {displayName}") - @EnumSource(value = HttpLogDetailLevel.class, mode = EnumSource.Mode.INCLUDE, - names = {"BASIC", "HEADERS", "BODY", "BODY_AND_HEADERS"}) - public void loggingIncludesRetryCountSync(HttpLogDetailLevel logLevel) { + @MethodSource("logOptionsSupplier") + public void loggingIncludesRetryCountSync(HttpLogOptions logOptions) { AtomicInteger requestCount = new AtomicInteger(); - String url = "https://test.com/loggingIncludesRetryCountSync/" + logLevel; + String url = "https://test.com/loggingIncludesRetryCountSync/" + logOptions.getLogLevel(); HttpRequest request = new HttpRequest(HttpMethod.GET, url) + .setHeader(HttpHeaderName.AUTHORIZATION, "not-allowed-value") .setHeader(HttpHeaderName.X_MS_CLIENT_REQUEST_ID, "client-request-id"); byte[] responseBody = new byte[] {24, 42}; HttpHeaders responseHeaders = new HttpHeaders() .set(HttpHeaderName.CONTENT_LENGTH, Integer.toString(responseBody.length)) + .set(HttpHeaderName.AUTHORIZATION, "not-allowed-value") .set(X_MS_REQUEST_ID, "server-request-id"); HttpPipeline pipeline = new HttpPipelineBuilder() - .policies(new RetryPolicy(), new HttpLoggingPolicy(new HttpLogOptions().setLogLevel(logLevel))) + .policies(new RetryPolicy(), new HttpLoggingPolicy(logOptions)) .httpClient(ignored -> (requestCount.getAndIncrement() == 0) ? Mono.error(new RuntimeException("Try again!")) : Mono.just(new com.azure.core.http.MockHttpResponse(ignored, 200, responseHeaders, responseBody))) @@ -546,37 +548,38 @@ public void loggingIncludesRetryCountSync(HttpLogDetailLevel logLevel) { assertEquals(4, messages.size(), logString); - expectedRetry1.assertEqual(messages.get(0), logLevel, LogLevel.INFORMATIONAL); + expectedRetry1.assertEqual(messages.get(0), logOptions, LogLevel.INFORMATIONAL); assertEquals("HTTP FAILED", messages.get(1).getMessage()); assertEquals("client-request-id", messages.get(1).getHeaders().get("x-ms-client-request-id")); assertEquals("Try again!", messages.get(1).getHeaders().get("exception")); - expectedRetry2.assertEqual(messages.get(2), logLevel, LogLevel.INFORMATIONAL); - expectedResponse.assertEqual(messages.get(3), logLevel, LogLevel.INFORMATIONAL); + expectedRetry2.assertEqual(messages.get(2), logOptions, LogLevel.INFORMATIONAL); + expectedResponse.assertEqual(messages.get(3), logOptions, LogLevel.INFORMATIONAL); assertArraysEqual(responseBody, content.toBytes()); } } @ParameterizedTest(name = "[{index}] {displayName}") - @EnumSource(value = HttpLogDetailLevel.class, mode = EnumSource.Mode.INCLUDE, - names = {"BASIC", "HEADERS", "BODY", "BODY_AND_HEADERS"}) - public void loggingHeadersAndBodyVerboseSync(HttpLogDetailLevel logLevel) { + @MethodSource("logOptionsSupplier") + public void loggingHeadersAndBodyVerboseSync(HttpLogOptions logOptions) { setupLogLevel(LogLevel.VERBOSE.getLogLevel()); byte[] requestBody = new byte[] {42}; byte[] responseBody = new byte[] {24, 42}; - String url = "https://test.com/loggingHeadersAndBodyVerboseSync/" + logLevel; + String url = "https://test.com/loggingHeadersAndBodyVerboseSync/" + logOptions.getLogLevel(); HttpRequest request = new HttpRequest(HttpMethod.POST, url) .setBody(requestBody) + .setHeader(HttpHeaderName.AUTHORIZATION, "not-allowed-value") .setHeader(HttpHeaderName.X_MS_CLIENT_REQUEST_ID, "client-request-id"); HttpHeaders responseHeaders = new HttpHeaders() .set(HttpHeaderName.CONTENT_LENGTH, Integer.toString(responseBody.length)) + .set(HttpHeaderName.AUTHORIZATION, "not-allowed-value") .set(X_MS_REQUEST_ID, "server-request-id"); HttpPipeline pipeline = new HttpPipelineBuilder() - .policies(new RetryPolicy(), new HttpLoggingPolicy(new HttpLogOptions().setLogLevel(logLevel))) + .policies(new RetryPolicy(), new HttpLoggingPolicy(logOptions)) .httpClient(r -> Mono.just(new com.azure.core.http.MockHttpResponse(r, 200, responseHeaders, responseBody))) .build(); @@ -599,11 +602,20 @@ public void loggingHeadersAndBodyVerboseSync(HttpLogDetailLevel logLevel) { assertEquals(2, messages.size(), logString); - expectedRequest.assertEqual(messages.get(0), logLevel, LogLevel.VERBOSE); - expectedResponse.assertEqual(messages.get(1), logLevel, LogLevel.VERBOSE); + expectedRequest.assertEqual(messages.get(0), logOptions, LogLevel.VERBOSE); + expectedResponse.assertEqual(messages.get(1), logOptions, LogLevel.VERBOSE); } } + private static Stream logOptionsSupplier() { + return Stream.of( + new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BASIC), + new HttpLogOptions().setLogLevel(HttpLogDetailLevel.HEADERS), + new HttpLogOptions().setLogLevel(HttpLogDetailLevel.HEADERS).disableRedactedHeaderLogging(true), + new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS), + new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS).disableRedactedHeaderLogging(true)); + } + private static Context getCallerMethodContext(String testMethodName) { return new Context("caller-method", HttpLoggingPolicyTests.class.getName() + "." + testMethodName); } @@ -784,7 +796,7 @@ public static List fromString(String logRecord) { return messages; } - void assertEqual(HttpLogMessage other, HttpLogDetailLevel httpLevel, LogLevel logLevel) { + void assertEqual(HttpLogMessage other, HttpLogOptions logOptions, LogLevel logLevel) { assertEquals(this.message, other.message); assertEquals(this.method, other.method); assertEquals(this.url, other.url); @@ -795,16 +807,25 @@ void assertEqual(HttpLogMessage other, HttpLogDetailLevel httpLevel, LogLevel lo assertNotNull(other.durationMs); } - if (httpLevel.shouldLogBody()) { + if (logOptions.getLogLevel().shouldLogBody()) { assertEquals(this.body, other.body); } - if (httpLevel.shouldLogHeaders() && logLevel == LogLevel.VERBOSE) { - assertEquals(this.headers.size(), other.headers.size()); - + if (logOptions.getLogLevel().shouldLogHeaders() && LogLevel.INFORMATIONAL.compareTo(logLevel) >= 0) { + int expectedHeaders = 0; + boolean expectRedactedHeaders = false; for (Map.Entry kvp : this.headers.entrySet()) { - assertEquals(kvp.getValue(), other.headers.get(kvp.getKey())); + boolean isAllowed = logOptions.getAllowedHeaderNames().contains(kvp.getKey()); + if (isAllowed) { + expectedHeaders++; + assertEquals(kvp.getValue(), other.headers.get(kvp.getKey())); + } else if (!logOptions.isRedactedHeaderLoggingDisabled()) { + expectRedactedHeaders = true; + assertTrue(other.headers.get("redactedHeaders").contains(kvp.getKey())); + } } + + assertEquals(expectedHeaders + (expectRedactedHeaders ? 1 : 0), other.headers.size()); } } }