diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpPipelineCallContext.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpPipelineCallContext.java index b52b3a0c2e64..362422b3ee85 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpPipelineCallContext.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/HttpPipelineCallContext.java @@ -3,13 +3,14 @@ package com.azure.core.http; +import com.azure.core.implementation.http.HttpPipelineCallContextHelper; import com.azure.core.util.Context; import java.util.Objects; import java.util.Optional; /** - * Type representing context local to a single http request and it's response. + * Represents the information used to make a single HTTP request. */ public final class HttpPipelineCallContext { private HttpRequest httpRequest; @@ -46,6 +47,10 @@ public final class HttpPipelineCallContext { this.data = data; } + static { + HttpPipelineCallContextHelper.setAccessor(HttpPipelineCallContext::getContext); + } + /** * Stores a key-value data in the context. * 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 39c50459e0c5..ae9d16d6506a 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 @@ -3,8 +3,8 @@ package com.azure.core.http.policy; -import com.azure.core.util.CoreUtils; import com.azure.core.util.ClientOptions; +import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; import java.util.Arrays; @@ -22,6 +22,10 @@ public class HttpLogOptions { private Set allowedHeaderNames; private Set allowedQueryParamNames; private boolean prettyPrintBody; + + private HttpRequestLogger requestLogger; + private HttpResponseLogger responseLogger; + private final ClientLogger logger = new ClientLogger(HttpLogOptions.class); private static final int MAX_APPLICATION_ID_LENGTH = 24; @@ -96,10 +100,9 @@ public Set getAllowedHeaderNames() { * *

* This method sets the provided header names to be the whitelisted header names which will be logged for all HTTP - * requests and responses, overwriting any previously configured headers, including the default set. Additionally, - * users can use {@link HttpLogOptions#addAllowedHeaderName(String)} or - * {@link HttpLogOptions#getAllowedHeaderNames()} to add or remove more headers names to the existing set of - * allowed header names. + * requests and responses, overwriting any previously configured headers. Additionally, users can use {@link + * HttpLogOptions#addAllowedHeaderName(String)} or {@link HttpLogOptions#getAllowedHeaderNames()} to add or remove + * more headers names to the existing set of allowed header names. *

* * @param allowedHeaderNames The list of whitelisted header names from the user. @@ -202,12 +205,60 @@ public boolean isPrettyPrintBody() { /** * Sets flag to allow pretty printing of message bodies. * - * @param prettyPrintBody If true, pretty prints message bodies when logging. If the detailLevel does not - * include body logging, this flag does nothing. + * @param prettyPrintBody If true, pretty prints message bodies when logging. If the detailLevel does not include + * body logging, this flag does nothing. * @return The updated HttpLogOptions object. */ public HttpLogOptions setPrettyPrintBody(boolean prettyPrintBody) { this.prettyPrintBody = prettyPrintBody; return this; } + + /** + * Gets the {@link HttpRequestLogger} that will be used to log HTTP requests. + *

+ * A default {@link HttpRequestLogger} will be used if one isn't supplied. + * + * @return The {@link HttpRequestLogger} that will be used to log HTTP requests. + */ + public HttpRequestLogger getRequestLogger() { + return requestLogger; + } + + /** + * Sets the {@link HttpRequestLogger} that will be used to log HTTP requests. + *

+ * A default {@link HttpRequestLogger} will be used if one isn't supplied. + * + * @param requestLogger The {@link HttpRequestLogger} that will be used to log HTTP requests. + * @return The updated HttpLogOptions object. + */ + public HttpLogOptions setRequestLogger(HttpRequestLogger requestLogger) { + this.requestLogger = requestLogger; + return this; + } + + /** + * Gets the {@link HttpResponseLogger} that will be used to log HTTP responses. + *

+ * A default {@link HttpResponseLogger} will be used if one isn't supplied. + * + * @return The {@link HttpResponseLogger} that will be used to log HTTP responses. + */ + public HttpResponseLogger getResponseLogger() { + return responseLogger; + } + + /** + * Sets the {@link HttpResponseLogger} that will be used to log HTTP responses. + *

+ * A default {@link HttpResponseLogger} will be used if one isn't supplied. + * + * @param responseLogger The {@link HttpResponseLogger} that will be used to log HTTP responses. + * @return The updated HttpLogOptions object. + */ + public HttpLogOptions setResponseLogger(HttpResponseLogger responseLogger) { + this.responseLogger = responseLogger; + return this; + } } 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 d6e3362852cf..3a9e76e24bea 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 @@ -10,6 +10,7 @@ import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; +import com.azure.core.implementation.http.HttpPipelineCallContextHelper; import com.azure.core.util.Context; import com.azure.core.util.CoreUtils; import com.azure.core.util.UrlBuilder; @@ -26,11 +27,10 @@ import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; +import java.time.Duration; import java.util.Collections; import java.util.Locale; -import java.util.Optional; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -41,11 +41,16 @@ public class HttpLoggingPolicy implements HttpPipelinePolicy { private static final int MAX_BODY_LOG_SIZE = 1024 * 16; private static final String REDACTED_PLACEHOLDER = "REDACTED"; + private final ClientLogger logger = new ClientLogger(HttpLoggingPolicy.class); + private final HttpLogDetailLevel httpLogDetailLevel; private final Set allowedHeaderNames; private final Set allowedQueryParameterNames; private final boolean prettyPrintBody; + private final HttpRequestLogger requestLogger; + private final HttpResponseLogger responseLogger; + /** * Key for {@link Context} to pass request retry count metadata for logging. */ @@ -62,6 +67,9 @@ public HttpLoggingPolicy(HttpLogOptions httpLogOptions) { this.allowedHeaderNames = Collections.emptySet(); this.allowedQueryParameterNames = Collections.emptySet(); this.prettyPrintBody = false; + + this.requestLogger = new DefaultHttpRequestLogger(); + this.responseLogger = new DefaultHttpResponseLogger(); } else { this.httpLogDetailLevel = httpLogOptions.getLogLevel(); this.allowedHeaderNames = httpLogOptions.getAllowedHeaderNames() @@ -73,6 +81,13 @@ public HttpLoggingPolicy(HttpLogOptions httpLogOptions) { .map(queryParamName -> queryParamName.toLowerCase(Locale.ROOT)) .collect(Collectors.toSet()); this.prettyPrintBody = httpLogOptions.isPrettyPrintBody(); + + this.requestLogger = (httpLogOptions.getRequestLogger() == null) + ? new DefaultHttpRequestLogger() + : httpLogOptions.getRequestLogger(); + this.responseLogger = (httpLogOptions.getResponseLogger() == null) + ? new DefaultHttpResponseLogger() + : httpLogOptions.getResponseLogger(); } } @@ -86,164 +101,204 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN final ClientLogger logger = new ClientLogger((String) context.getData("caller-method").orElse("")); final long startNs = System.nanoTime(); - return logRequest(logger, context.getHttpRequest(), context.getData(RETRY_COUNT_CONTEXT)) + return requestLogger.logRequest(logger, getRequestLoggingOptions(context)) .then(next.process()) - .flatMap(response -> logResponse(logger, response, startNs)) + .flatMap(response -> responseLogger.logResponse(logger, + getResponseLoggingOptions(response, startNs, context))) .doOnError(throwable -> logger.warning("<-- HTTP FAILED: ", throwable)); } - /* - * Logs the HTTP request. - * - * @param logger Logger used to log the request. - * @param request HTTP request being sent to Azure. - * @return A Mono which will emit the string to log. - */ - private Mono logRequest(final ClientLogger logger, final HttpRequest request, - final Optional optionalRetryCount) { + private HttpRequestLoggingContext getRequestLoggingOptions(HttpPipelineCallContext callContext) { + return new HttpRequestLoggingContext(callContext.getHttpRequest(), + HttpPipelineCallContextHelper.getContext(callContext), + getRequestRetryCount(HttpPipelineCallContextHelper.getContext(callContext), getHttpLoggingPolicyLogger())); + } - if (!logger.canLogAtLevel(LogLevel.INFORMATIONAL)) { - return Mono.empty(); - } + private HttpResponseLoggingContext getResponseLoggingOptions(HttpResponse httpResponse, long startNs, + HttpPipelineCallContext callContext) { + return new HttpResponseLoggingContext(httpResponse, Duration.ofNanos(System.nanoTime() - startNs), + HttpPipelineCallContextHelper.getContext(callContext), + getRequestRetryCount(HttpPipelineCallContextHelper.getContext(callContext), getHttpLoggingPolicyLogger())); + } - StringBuilder requestLogMessage = new StringBuilder(); - if (httpLogDetailLevel.shouldLogUrl()) { - requestLogMessage.append("--> ") - .append(request.getHttpMethod()) - .append(" ") - .append(getRedactedUrl(request.getUrl())) - .append(System.lineSeparator()); - - optionalRetryCount.ifPresent(o -> requestLogMessage.append("Try count: ") - .append(o) - .append(System.lineSeparator())); - } + private final class DefaultHttpRequestLogger implements HttpRequestLogger { + @Override + public Mono logRequest(ClientLogger logger, HttpRequestLoggingContext loggingOptions) { + final LogLevel logLevel = getLogLevel(loggingOptions); - addHeadersToLogMessage(logger, request.getHeaders(), requestLogMessage); + if (!logger.canLogAtLevel(logLevel)) { + return Mono.empty(); + } - if (!httpLogDetailLevel.shouldLogBody()) { - return logAndReturn(logger, requestLogMessage, null); - } + final HttpRequest request = loggingOptions.getHttpRequest(); + + StringBuilder requestLogMessage = new StringBuilder(); + if (httpLogDetailLevel.shouldLogUrl()) { + requestLogMessage.append("--> ") + .append(request.getHttpMethod()) + .append(" ") + .append(getRedactedUrl(request.getUrl(), allowedQueryParameterNames)) + .append(System.lineSeparator()); + + Integer retryCount = loggingOptions.getTryCount(); + if (retryCount != null) { + requestLogMessage.append("Try count: ") + .append(retryCount) + .append(System.lineSeparator()); + } + } + + if (httpLogDetailLevel.shouldLogHeaders() && logger.canLogAtLevel(LogLevel.VERBOSE)) { + addHeadersToLogMessage(allowedHeaderNames, request.getHeaders(), requestLogMessage); + } - if (request.getBody() == null) { - requestLogMessage.append("(empty body)") - .append(System.lineSeparator()) - .append("--> END ") - .append(request.getHttpMethod()) - .append(System.lineSeparator()); + if (!httpLogDetailLevel.shouldLogBody()) { + return logAndReturn(logger, logLevel, requestLogMessage, null); + } + + if (request.getBody() == null) { + requestLogMessage.append("(empty body)") + .append(System.lineSeparator()) + .append("--> END ") + .append(request.getHttpMethod()) + .append(System.lineSeparator()); + + return logAndReturn(logger, logLevel, requestLogMessage, null); + } - return logAndReturn(logger, requestLogMessage, null); + String contentType = request.getHeaders().getValue("Content-Type"); + long contentLength = getContentLength(logger, request.getHeaders()); + + if (shouldBodyBeLogged(contentType, contentLength)) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream((int) contentLength); + WritableByteChannel bodyContentChannel = Channels.newChannel(outputStream); + + // Add non-mutating operators to the data stream. + request.setBody( + request.getBody() + .flatMap(byteBuffer -> writeBufferToBodyStream(bodyContentChannel, byteBuffer)) + .doFinally(ignored -> { + requestLogMessage.append(contentLength) + .append("-byte body:") + .append(System.lineSeparator()) + .append(prettyPrintIfNeeded(logger, prettyPrintBody, contentType, + convertStreamToString(outputStream, logger))) + .append(System.lineSeparator()) + .append("--> END ") + .append(request.getHttpMethod()) + .append(System.lineSeparator()); + + logAndReturn(logger, logLevel, requestLogMessage, null); + })); + + return Mono.empty(); + } else { + requestLogMessage.append(contentLength) + .append("-byte body: (content not logged)") + .append(System.lineSeparator()) + .append("--> END ") + .append(request.getHttpMethod()) + .append(System.lineSeparator()); + + return logAndReturn(logger, logLevel, requestLogMessage, null); + } } + } - String contentType = request.getHeaders().getValue("Content-Type"); - long contentLength = getContentLength(logger, request.getHeaders()); + private final class DefaultHttpResponseLogger implements HttpResponseLogger { + @Override + public Mono logResponse(ClientLogger logger, HttpResponseLoggingContext loggingOptions) { + final LogLevel logLevel = getLogLevel(loggingOptions); + final HttpResponse response = loggingOptions.getHttpResponse(); - if (shouldBodyBeLogged(contentType, contentLength)) { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream((int) contentLength); - WritableByteChannel bodyContentChannel = Channels.newChannel(outputStream); + if (!logger.canLogAtLevel(logLevel)) { + return Mono.just(response); + } - // Add non-mutating operators to the data stream. - request.setBody( - request.getBody() + String contentLengthString = response.getHeaderValue("Content-Length"); + String bodySize = (CoreUtils.isNullOrEmpty(contentLengthString)) + ? "unknown-length body" + : contentLengthString + "-byte body"; + + StringBuilder responseLogMessage = new StringBuilder(); + if (httpLogDetailLevel.shouldLogUrl()) { + responseLogMessage.append("<-- ") + .append(response.getStatusCode()) + .append(" ") + .append(getRedactedUrl(response.getRequest().getUrl(), allowedQueryParameterNames)) + .append(" (") + .append(loggingOptions.getResponseDuration().toMillis()) + .append(" ms, ") + .append(bodySize) + .append(")") + .append(System.lineSeparator()); + } + + if (httpLogDetailLevel.shouldLogHeaders() && logger.canLogAtLevel(LogLevel.VERBOSE)) { + addHeadersToLogMessage(allowedHeaderNames, response.getHeaders(), responseLogMessage); + } + + if (!httpLogDetailLevel.shouldLogBody()) { + responseLogMessage.append("<-- END HTTP"); + return logAndReturn(logger, logLevel, responseLogMessage, response); + } + + String contentTypeHeader = response.getHeaderValue("Content-Type"); + long contentLength = getContentLength(logger, response.getHeaders()); + + if (shouldBodyBeLogged(contentTypeHeader, contentLength)) { + HttpResponse bufferedResponse = response.buffer(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream((int) contentLength); + WritableByteChannel bodyContentChannel = Channels.newChannel(outputStream); + return bufferedResponse.getBody() .flatMap(byteBuffer -> writeBufferToBodyStream(bodyContentChannel, byteBuffer)) .doFinally(ignored -> { - requestLogMessage.append(contentLength) - .append("-byte body:") + responseLogMessage.append("Response body:") .append(System.lineSeparator()) - .append(prettyPrintIfNeeded(logger, contentType, + .append(prettyPrintIfNeeded(logger, prettyPrintBody, contentTypeHeader, convertStreamToString(outputStream, logger))) .append(System.lineSeparator()) - .append("--> END ") - .append(request.getHttpMethod()) - .append(System.lineSeparator()); + .append("<-- END HTTP"); - logger.info(requestLogMessage.toString()); - })); + logAndReturn(logger, logLevel, responseLogMessage, response); + }).then(Mono.just(bufferedResponse)); + } else { + responseLogMessage.append("(body content not logged)") + .append(System.lineSeparator()) + .append("<-- END HTTP"); - return Mono.empty(); - } else { - requestLogMessage.append(contentLength) - .append("-byte body: (content not logged)") - .append(System.lineSeparator()) - .append("--> END ") - .append(request.getHttpMethod()) - .append(System.lineSeparator()); - - return logAndReturn(logger, requestLogMessage, null); + return logAndReturn(logger, logLevel, responseLogMessage, response); + } } } - /* - * Logs thr HTTP response. - * - * @param logger Logger used to log the response. - * @param response HTTP response returned from Azure. - * @param startNs Nanosecond representation of when the request was sent. - * @return A Mono containing the HTTP response. - */ - private Mono logResponse(final ClientLogger logger, final HttpResponse response, long startNs) { - if (!logger.canLogAtLevel(LogLevel.INFORMATIONAL)) { - return Mono.just(response); - } + private ClientLogger getHttpLoggingPolicyLogger() { + return this.logger; + } - long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); - - String contentLengthString = response.getHeaderValue("Content-Length"); - String bodySize = (CoreUtils.isNullOrEmpty(contentLengthString)) - ? "unknown-length body" - : contentLengthString + "-byte body"; - - StringBuilder responseLogMessage = new StringBuilder(); - if (httpLogDetailLevel.shouldLogUrl()) { - responseLogMessage.append("<-- ") - .append(response.getStatusCode()) - .append(" ") - .append(getRedactedUrl(response.getRequest().getUrl())) - .append(" (") - .append(tookMs) - .append(" ms, ") - .append(bodySize) - .append(")") - .append(System.lineSeparator()); - } + private static Mono logAndReturn(ClientLogger logger, LogLevel logLevel, StringBuilder logMessageBuilder, + T data) { + switch (logLevel) { + case VERBOSE: + logger.verbose(logMessageBuilder.toString()); + break; - addHeadersToLogMessage(logger, response.getHeaders(), responseLogMessage); + case INFORMATIONAL: + logger.info(logMessageBuilder.toString()); + break; - if (!httpLogDetailLevel.shouldLogBody()) { - responseLogMessage.append("<-- END HTTP"); - return logAndReturn(logger, responseLogMessage, response); - } + case WARNING: + logger.warning(logMessageBuilder.toString()); + break; - String contentTypeHeader = response.getHeaderValue("Content-Type"); - long contentLength = getContentLength(logger, response.getHeaders()); - - if (shouldBodyBeLogged(contentTypeHeader, contentLength)) { - HttpResponse bufferedResponse = response.buffer(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream((int) contentLength); - WritableByteChannel bodyContentChannel = Channels.newChannel(outputStream); - return bufferedResponse.getBody() - .flatMap(byteBuffer -> writeBufferToBodyStream(bodyContentChannel, byteBuffer)) - .doFinally(ignored -> { - responseLogMessage.append("Response body:") - .append(System.lineSeparator()) - .append(prettyPrintIfNeeded(logger, contentTypeHeader, - convertStreamToString(outputStream, logger))) - .append(System.lineSeparator()) - .append("<-- END HTTP"); - - logger.info(responseLogMessage.toString()); - }).then(Mono.just(bufferedResponse)); - } else { - responseLogMessage.append("(body content not logged)") - .append(System.lineSeparator()) - .append("<-- END HTTP"); + case ERROR: + logger.error(logMessageBuilder.toString()); + break; - return logAndReturn(logger, responseLogMessage, response); + default: + break; } - } - private Mono logAndReturn(ClientLogger logger, StringBuilder logMessageBuilder, T data) { - logger.info(logMessageBuilder.toString()); return Mono.justOrEmpty(data); } @@ -253,9 +308,9 @@ private Mono logAndReturn(ClientLogger logger, StringBuilder logMessageBu * @param url URL where the request is being sent. * @return A URL with query parameters redacted based on configurations in this policy. */ - private String getRedactedUrl(URL url) { + private static String getRedactedUrl(URL url, Set allowedQueryParameterNames) { return UrlBuilder.parse(url) - .setQuery(getAllowedQueryString(url.getQuery())) + .setQuery(getAllowedQueryString(url.getQuery(), allowedQueryParameterNames)) .toString(); } @@ -265,7 +320,7 @@ private String getRedactedUrl(URL url) { * @param queryString Query parameter string from the request URL. * @return A query parameter string redacted based on the configurations in this policy. */ - private String getAllowedQueryString(String queryString) { + private static String getAllowedQueryString(String queryString, Set allowedQueryParameterNames) { if (CoreUtils.isNullOrEmpty(queryString)) { return ""; } @@ -300,12 +355,7 @@ private String getAllowedQueryString(String queryString) { * @param sb StringBuilder that is generating the log message. * @param logLevel Log level the environment is configured to use. */ - private void addHeadersToLogMessage(ClientLogger logger, HttpHeaders headers, StringBuilder sb) { - // Either headers shouldn't be logged or the logging level isn't set to VERBOSE, don't add headers. - if (!httpLogDetailLevel.shouldLogHeaders() || !logger.canLogAtLevel(LogLevel.VERBOSE)) { - return; - } - + private static void addHeadersToLogMessage(Set allowedHeaderNames, HttpHeaders headers, StringBuilder sb) { for (HttpHeader header : headers) { String headerName = header.getName(); sb.append(headerName).append(":"); @@ -328,7 +378,8 @@ private void addHeadersToLogMessage(ClientLogger logger, HttpHeaders headers, St * @param body Body of the request or response. * @return The body pretty printed if it is JSON, otherwise the unmodified body. */ - private String prettyPrintIfNeeded(ClientLogger logger, String contentType, String body) { + private static String prettyPrintIfNeeded(ClientLogger logger, boolean prettyPrintBody, String contentType, + String body) { String result = body; if (prettyPrintBody && contentType != null && (contentType.startsWith(ContentType.APPLICATION_JSON) || contentType.startsWith("text/json"))) { @@ -349,7 +400,7 @@ private String prettyPrintIfNeeded(ClientLogger logger, String contentType, Stri * @param headers HTTP headers that are checked for containing Content-Length. * @return */ - private long getContentLength(ClientLogger logger, HttpHeaders headers) { + private static long getContentLength(ClientLogger logger, HttpHeaders headers) { long contentLength = 0; String contentLengthString = headers.getValue("Content-Length"); @@ -377,7 +428,7 @@ private long getContentLength(ClientLogger logger, HttpHeaders headers) { * @param contentLength Content-Length header represented as a numeric. * @return A flag indicating if the request or response body should be logged. */ - private boolean shouldBodyBeLogged(String contentTypeHeader, long contentLength) { + private static boolean shouldBodyBeLogged(String contentTypeHeader, long contentLength) { return !ContentType.APPLICATION_OCTET_STREAM.equalsIgnoreCase(contentTypeHeader) && contentLength != 0 && contentLength < MAX_BODY_LOG_SIZE; @@ -405,4 +456,24 @@ private static Mono writeBufferToBodyStream(WritableByteChannel chan return Mono.error(ex); } } + + /* + * Gets the request retry count to include in logging. + * + * If there is no value set or it isn't a valid number null will be returned indicating that retry count won't be + * logged. + */ + private static Integer getRequestRetryCount(Context context, ClientLogger logger) { + Object rawRetryCount = context.getData(RETRY_COUNT_CONTEXT).orElse(null); + if (rawRetryCount == null) { + return null; + } + + try { + return Integer.valueOf(rawRetryCount.toString()); + } catch (NumberFormatException ex) { + logger.warning("Could not parse the request retry count: '{}'.", rawRetryCount); + return null; + } + } } diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpRequestLogger.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpRequestLogger.java new file mode 100644 index 000000000000..5f7f845c0fd8 --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpRequestLogger.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.http.policy; + +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.logging.LogLevel; +import reactor.core.publisher.Mono; + +/** + * Manages logging HTTP requests in {@link HttpLoggingPolicy}. + */ +@FunctionalInterface +public interface HttpRequestLogger { + /** + * Gets the {@link LogLevel} used to log the HTTP request. + *

+ * By default this will return {@link LogLevel#INFORMATIONAL}. + * + * @param loggingOptions The information available during request logging. + * @return The {@link LogLevel} used to log the HTTP request. + */ + default LogLevel getLogLevel(HttpRequestLoggingContext loggingOptions) { + return LogLevel.INFORMATIONAL; + } + + /** + * Logs the HTTP request. + *

+ * To get the {@link LogLevel} used to log the HTTP request use {@link #getLogLevel(HttpRequestLoggingContext)}. + * + * @param logger The {@link ClientLogger} used to log the HTTP request. + * @param loggingOptions The information available during request logging. + * @return A reactive response that indicates that the HTTP request has been logged. + */ + Mono logRequest(ClientLogger logger, HttpRequestLoggingContext loggingOptions); +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpRequestLoggingContext.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpRequestLoggingContext.java new file mode 100644 index 000000000000..8b2fd8106ef1 --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpRequestLoggingContext.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.http.policy; + +import com.azure.core.http.HttpRequest; +import com.azure.core.util.Context; + +/** + * Options class containing information available during HTTP request logging. + */ +public final class HttpRequestLoggingContext { + private final HttpRequest httpRequest; + private final Context context; + private final Integer tryCount; + + HttpRequestLoggingContext(HttpRequest httpRequest, Context context, Integer tryCount) { + this.httpRequest = httpRequest; + this.context = context; + this.tryCount = tryCount; + } + + /** + * Gets the HTTP request being sent. + * + * @return The HTTP request. + */ + public HttpRequest getHttpRequest() { + return httpRequest; + } + + /** + * Gets the contextual information about the HTTP request. + * + * @return The contextual information. + */ + public Context getContext() { + return context; + } + + /** + * Gets the try count for the HTTP request. + * + * @return The HTTP request try count. + */ + public Integer getTryCount() { + return tryCount; + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpResponseLogger.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpResponseLogger.java new file mode 100644 index 000000000000..b71dbd9e1cce --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpResponseLogger.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.http.policy; + +import com.azure.core.http.HttpResponse; +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.logging.LogLevel; +import reactor.core.publisher.Mono; + +/** + * Manages logging HTTP responses in {@link HttpLoggingPolicy}. + */ +@FunctionalInterface +public interface HttpResponseLogger { + /** + * Gets the {@link LogLevel} used to log the HTTP response. + *

+ * By default this will return {@link LogLevel#INFORMATIONAL}. + * + * @param loggingOptions The information available during response logging. + * @return The {@link LogLevel} used to log the HTTP response. + */ + default LogLevel getLogLevel(HttpResponseLoggingContext loggingOptions) { + return LogLevel.INFORMATIONAL; + } + + /** + * Logs the HTTP response. + *

+ * To get the {@link LogLevel} used to log the HTTP response use {@link #getLogLevel(HttpResponseLoggingContext)}. + * + * @param logger The {@link ClientLogger} used to log the response. + * @param loggingOptions The information available during response logging. + * @return A reactive response that returns the HTTP response that was logged. + */ + Mono logResponse(ClientLogger logger, HttpResponseLoggingContext loggingOptions); +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpResponseLoggingContext.java b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpResponseLoggingContext.java new file mode 100644 index 000000000000..ac113230cbee --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpResponseLoggingContext.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.http.policy; + +import com.azure.core.http.HttpResponse; +import com.azure.core.util.Context; + +import java.time.Duration; + +/** + * Options class containing information available during HTTP response logging. + */ +public final class HttpResponseLoggingContext { + private final HttpResponse httpResponse; + private final Duration responseDuration; + private final Context context; + private final Integer tryCount; + + HttpResponseLoggingContext(HttpResponse httpResponse, Duration responseDuration, Context context, + Integer tryCount) { + this.httpResponse = httpResponse; + this.responseDuration = responseDuration; + this.context = context; + this.tryCount = tryCount; + } + + /** + * Gets the HTTP response being received. + * + * @return The HTTP response being received. + */ + public HttpResponse getHttpResponse() { + return httpResponse; + } + + /** + * Gets the duration between the HTTP request being sent and the HTTP response being received. + * + * @return The duration between the HTTP request being sent and the HTTP response being received. + */ + public Duration getResponseDuration() { + return responseDuration; + } + + /** + * Gets the contextual information about the HTTP response. + * + * @return The contextual information. + */ + public Context getContext() { + return context; + } + + /** + * Gets the try count for the HTTP request associated to the HTTP response. + * + * @return The HTTP request try count. + */ + public Integer getTryCount() { + return tryCount; + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/HttpPipelineCallContextHelper.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/HttpPipelineCallContextHelper.java new file mode 100644 index 000000000000..571a1bf9d3d3 --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/HttpPipelineCallContextHelper.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.implementation.http; + +import com.azure.core.http.HttpPipelineCallContext; +import com.azure.core.util.Context; + +/** + * Helper class to access private values of {@link HttpPipelineCallContext} across package boundaries. + */ +public final class HttpPipelineCallContextHelper { + private static HttpPipelineCallContextAccessor accessor; + + private HttpPipelineCallContextHelper() { } + + /** + * Type defining the methods to set the non-public properties of an {@link HttpPipelineCallContext} instance. + */ + public interface HttpPipelineCallContextAccessor { + Context getContext(HttpPipelineCallContext callContext); + } + + /** + * The method called from {@link HttpPipelineCallContext} to set it's accessor. + * + * @param callContextAccessor The accessor. + */ + public static void setAccessor(final HttpPipelineCallContextAccessor callContextAccessor) { + accessor = callContextAccessor; + } + + public static Context getContext(HttpPipelineCallContext callContext) { + return accessor.getContext(callContext); + } +}