Skip to content

Commit

Permalink
Change HttpLogOptions to be HttpInstrumentationOptions, update docs (#…
Browse files Browse the repository at this point in the history
…43780)

* simplified httplogopts

* HttpLogOptions -> HttpInstrumentationOptions, docs and logging cleanup

* rename get|setProvider to get|setTelemetryProvider and make it object
  • Loading branch information
lmolkova authored Jan 22, 2025
1 parent c939638 commit 5bc93af
Show file tree
Hide file tree
Showing 32 changed files with 717 additions and 552 deletions.
1 change: 0 additions & 1 deletion sdk/clientcore/core/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
<suppress files="io.clientcore.core.http.client.DefaultHttpClientBuilder.java" checks="com.azure.tools.checkstyle.checks.ServiceClientBuilderCheck" />
<suppress files="io.clientcore.core.http.client.implementation.InputStreamTimeoutResponseSubscriber.java" checks="com.azure.tools.checkstyle.checks.ThrowFromClientLoggerCheck" />
<suppress files="io.clientcore.core.http.pipeline.HttpInstrumentationPolicy.java" checks="com.azure.tools.checkstyle.checks.ThrowFromClientLoggerCheck" />
<suppress files="io.clientcore.core.http.pipeline.HttpLoggingPolicy.java" checks="com.azure.tools.checkstyle.checks.ThrowFromClientLoggerCheck" />
<suppress files="io.clientcore.core.implementation.MethodHandleReflectiveInvoker.java" checks="com.azure.tools.checkstyle.checks.ThrowFromClientLoggerCheck" />
<suppress files="io.clientcore.core.implementation.http.rest.LengthValidatingInputStream.java" checks="com.azure.tools.checkstyle.checks.ThrowFromClientLoggerCheck" />
<suppress files="io.clientcore.core.serialization.json.JsonReader.java" checks="com.azure.tools.checkstyle.checks.ThrowFromClientLoggerCheck" />
Expand Down
4 changes: 0 additions & 4 deletions sdk/clientcore/core/spotbugs-exclude.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,6 @@
<Bug pattern="DB_DUPLICATE_SWITCH_CLAUSES" />
<Class name="io.clientcore.core.serialization.xml.implementation.aalto.out.CharXmlWriter" />
</Match>
<Match>
<Bug pattern="DCN_NULLPOINTER_EXCEPTION" />
<Class name="io.clientcore.core.http.pipeline.HttpLoggingPolicy" />
</Match>
<Match>
<Bug pattern="DLS_DEAD_LOCAL_STORE" />
<Or>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package io.clientcore.core.http.models;

import io.clientcore.core.instrumentation.InstrumentationOptions;
import io.clientcore.core.util.configuration.Configuration;
import io.clientcore.core.util.configuration.ConfigurationProperty;
import io.clientcore.core.util.configuration.ConfigurationPropertyBuilder;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
* Configuration options for HTTP instrumentation.
* <p>
* The instrumentation emits distributed traces following <a href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md">OpenTelemetry HTTP semantic conventions</a>
* and, when enabled, detailed HTTP logs.
* <p>
* The following information is recorded on distributed traces:
* <ul>
* <li>Request method, URI. The URI is sanitized based on allowed query parameters configurable with {@link #setAllowedQueryParamNames(Set)} and {@link #addAllowedQueryParamName(String)}</li>
* <li>Response status code</li>
* <li>Error details if the request fails</li>
* <li>Time it takes to receive response</li>
* <li>Correlation identifiers</li>
* </ul>
*
The following information is recorded on detailed HTTP logs:
* <ul>
* <li>Request method, URI, and body size. URI is sanitized based on allowed query parameters configurable with {@link #setAllowedQueryParamNames(Set)} and {@link #addAllowedQueryParamName(String)}</li>
* <li>Response status code and body size</li>
* <li>Request and response headers from allow-list configured via {@link #setAllowedHeaderNames(Set)} and {@link #addAllowedHeaderName(HttpHeaderName)}.</li>
* <li>Error details if the request fails</li>
* <li>Time it takes to receive response</li>
* <li>Correlation identifiers</li>
* <li>When content logging is enabled via {@link HttpLogDetailLevel#BODY_AND_HEADERS}: request and response body, and time-to-last-byte</li>
* </ul>
*
* Client libraries auto-discover global OpenTelemetry SDK instance configured by the java agent or
* in the application code. Just create a client instance as usual as shown in the following code snippet:
*
* <p><strong>Clients auto-discover global OpenTelemetry</strong></p>
*
* <!-- src_embed io.clientcore.core.telemetry.useglobalopentelemetry -->
* <pre>
*
* AutoConfiguredOpenTelemetrySdk.initialize&#40;&#41;;
*
* SampleClient client = new SampleClientBuilder&#40;&#41;.build&#40;&#41;;
*
* &#47;&#47; this call will be traced using OpenTelemetry SDK initialized globally
* client.clientCall&#40;&#41;;
*
* </pre>
* <!-- end io.clientcore.core.telemetry.useglobalopentelemetry -->
* <p>
*
* Alternatively, application developers can pass OpenTelemetry SDK instance explicitly to the client libraries.
*
* <p><strong>Pass configured OpenTelemetry instance explicitly</strong></p>
*
* <!-- src_embed io.clientcore.core.telemetry.useexplicitopentelemetry -->
* <pre>
*
* OpenTelemetry openTelemetry = AutoConfiguredOpenTelemetrySdk.initialize&#40;&#41;.getOpenTelemetrySdk&#40;&#41;;
* HttpInstrumentationOptions instrumentationOptions = new HttpInstrumentationOptions&#40;&#41;
* .setTelemetryProvider&#40;openTelemetry&#41;;
*
* SampleClient client = new SampleClientBuilder&#40;&#41;.instrumentationOptions&#40;instrumentationOptions&#41;.build&#40;&#41;;
*
* &#47;&#47; this call will be traced using OpenTelemetry SDK provided explicitly
* client.clientCall&#40;&#41;;
*
* </pre>
* <!-- end io.clientcore.core.telemetry.useexplicitopentelemetry -->
*/
public final class HttpInstrumentationOptions extends InstrumentationOptions {
private HttpLogDetailLevel logDetailLevel;
private boolean isRedactedHeaderNamesLoggingEnabled;
private Set<HttpHeaderName> allowedHeaderNames;
private Set<String> allowedQueryParamNames;
private static final List<HttpHeaderName> DEFAULT_HEADERS_ALLOWLIST
= Arrays.asList(HttpHeaderName.TRACEPARENT, HttpHeaderName.ACCEPT, HttpHeaderName.CACHE_CONTROL,
HttpHeaderName.CONNECTION, HttpHeaderName.CONTENT_LENGTH, HttpHeaderName.CONTENT_TYPE, HttpHeaderName.DATE,
HttpHeaderName.ETAG, HttpHeaderName.EXPIRES, HttpHeaderName.IF_MATCH, HttpHeaderName.IF_MODIFIED_SINCE,
HttpHeaderName.IF_NONE_MATCH, HttpHeaderName.IF_UNMODIFIED_SINCE, HttpHeaderName.LAST_MODIFIED,
HttpHeaderName.PRAGMA, HttpHeaderName.RETRY_AFTER, HttpHeaderName.SERVER, HttpHeaderName.TRANSFER_ENCODING,
HttpHeaderName.USER_AGENT, HttpHeaderName.WWW_AUTHENTICATE);

static final HttpLogDetailLevel ENVIRONMENT_HTTP_LOG_DETAIL_LEVEL
= HttpLogDetailLevel.fromConfiguration(Configuration.getGlobalConfiguration());
private static final List<String> DEFAULT_QUERY_PARAMS_ALLOWLIST = Collections.singletonList("api-version");

/**
* Creates a new instance using default options:
* <ul>
* <li>Detailed HTTP logging is disabled.</li>
* <li>Distributed tracing is enabled.</li>
* </ul>
*/
public HttpInstrumentationOptions() {
super();
logDetailLevel = ENVIRONMENT_HTTP_LOG_DETAIL_LEVEL;
isRedactedHeaderNamesLoggingEnabled = true;
allowedHeaderNames = new HashSet<>(DEFAULT_HEADERS_ALLOWLIST);
allowedQueryParamNames = new HashSet<>(DEFAULT_QUERY_PARAMS_ALLOWLIST);
}

/**
* Gets the level of detail for HTTP request logs. Default is {@link HttpLogDetailLevel#NONE}.
* <p>
* When HTTP logging is disabled, basic information about the request and response is still recorded
* on distributed tracing spans.
*
* @return The {@link HttpLogDetailLevel}.
*/
public HttpLogDetailLevel getHttpLogLevel() {
return logDetailLevel;
}

/**
* Flag indicating whether HTTP request and response header values are added to the logs
* when their name is not explicitly allowed via {@link HttpInstrumentationOptions#setAllowedHeaderNames(Set)} or
* {@link HttpInstrumentationOptions#addAllowedHeaderName(HttpHeaderName)}.
* True by default.
*
* @return True if redacted header names logging is enabled, false otherwise.
*/
public boolean isRedactedHeaderNamesLoggingEnabled() {
return isRedactedHeaderNamesLoggingEnabled;
}

/**
* Enables or disables logging of redacted header names.
* @param redactedHeaderNamesLoggingEnabled True to enable logging of redacted header names, false otherwise.
* Default is true.
* @return The updated {@link HttpInstrumentationOptions} object.
*/
public HttpInstrumentationOptions setRedactedHeaderNamesLoggingEnabled(boolean redactedHeaderNamesLoggingEnabled) {
isRedactedHeaderNamesLoggingEnabled = redactedHeaderNamesLoggingEnabled;
return this;
}

/**
* Sets the level of detail for HTTP request logs.
* Default is {@link HttpLogDetailLevel#NONE}.
*
* @param logDetailLevel The {@link HttpLogDetailLevel}.
*
* @return The updated {@link HttpInstrumentationOptions} object.
*/
public HttpInstrumentationOptions setHttpLogLevel(HttpLogDetailLevel logDetailLevel) {
this.logDetailLevel = logDetailLevel;
return this;
}

/**
* Gets the allowed headers that should be logged when they appear on the request or response.
*
* @return The list of allowed headers.
*/
public Set<HttpHeaderName> getAllowedHeaderNames() {
return Collections.unmodifiableSet(allowedHeaderNames);
}

/**
* Sets the given allowed headers that should be logged.
* Note: headers are not recorded on traces.
*
* <p>
* This method sets the provided header names to be the allowed header names which will be logged for all HTTP
* requests and responses, overwriting any previously configured headers. Additionally, users can use
* {@link HttpInstrumentationOptions#addAllowedHeaderName(HttpHeaderName)} or {@link HttpInstrumentationOptions#getAllowedHeaderNames()} to add or
* remove more headers names to the existing set of allowed header names.
* </p>
*
* @param allowedHeaderNames The list of allowed header names from the user.
*
* @return The updated HttpLogOptions object.
*/
public HttpInstrumentationOptions setAllowedHeaderNames(final Set<HttpHeaderName> allowedHeaderNames) {
this.allowedHeaderNames = allowedHeaderNames == null ? new HashSet<>() : new HashSet<>(allowedHeaderNames);

return this;
}

/**
* Sets the given allowed header to the default header set that should be logged when they appear on the request or response.
* <p>
* Note: headers are not recorded on traces.
*
* @param allowedHeaderName The allowed header name from the user.
*
* @return The updated HttpLogOptions object.
*
* @throws NullPointerException If {@code allowedHeaderName} is {@code null}.
*/
public HttpInstrumentationOptions addAllowedHeaderName(final HttpHeaderName allowedHeaderName) {
Objects.requireNonNull(allowedHeaderName);
this.allowedHeaderNames.add(allowedHeaderName);

return this;
}

/**
* Gets the allowed query parameters.
*
* @return The list of allowed query parameters.
*/
public Set<String> getAllowedQueryParamNames() {
return Collections.unmodifiableSet(allowedQueryParamNames);
}

/**
* Sets the given allowed query params to be recorded on logs and traces.
*
* @param allowedQueryParamNames The list of allowed query params from the user.
*
* @return The updated {@code allowedQueryParamName} object.
*/
public HttpInstrumentationOptions setAllowedQueryParamNames(final Set<String> allowedQueryParamNames) {
this.allowedQueryParamNames
= allowedQueryParamNames == null ? new HashSet<>() : new HashSet<>(allowedQueryParamNames);

return this;
}

/**
* Sets the given allowed query param that can be recorded on logs and traces.
*
* @param allowedQueryParamName The allowed query param name from the user.
*
* @return The updated {@link HttpInstrumentationOptions} object.
*
* @throws NullPointerException If {@code allowedQueryParamName} is {@code null}.
*/
public HttpInstrumentationOptions addAllowedQueryParamName(final String allowedQueryParamName) {
this.allowedQueryParamNames.add(allowedQueryParamName);
return this;
}

@Override
public HttpInstrumentationOptions setTracingEnabled(boolean isTracingEnabled) {
super.setTracingEnabled(isTracingEnabled);
return this;
}

@Override
public HttpInstrumentationOptions setTelemetryProvider(Object telemetryProvider) {
super.setTelemetryProvider(telemetryProvider);
return this;
}

/**
* The level of detail for HTTP request logs.
*/
public enum HttpLogDetailLevel {
/**
* HTTP logging is turned off.
*/
NONE,

/**
* Enables logging the following information on detailed HTTP logs
* <ul>
* <li>Request method, URI, and body size. URI is sanitized based on allowed query parameters configurable with {@link #setAllowedQueryParamNames(Set)} and {@link #addAllowedQueryParamName(String)}</li>
* <li>Response status code and body size</li>
* <li>Request and response headers from allow-list configured via {@link #setAllowedHeaderNames(Set)} and {@link #addAllowedHeaderName(HttpHeaderName)}.</li>
* <li>Error details if the request fails</li>
* <li>Time it takes to receive response</li>
* <li>Correlation identifiers</li>
* </ul>
*/
HEADERS,

/**
* Enables logging the following information on detailed HTTP logs
* <ul>
* <li>Request method, URI, and body size. URI is sanitized based on allowed query parameters configurable with {@link #setAllowedQueryParamNames(Set)} and {@link #addAllowedQueryParamName(String)}</li>
* <li>Response status code and body size</li>
* <li>Error details if the request fails</li>
* <li>Time it takes to receive response</li>
* <li>Correlation identifiers</li>
* <li>Request and response bodies</li>
* <li>Time-to-last-byte</li>
* </ul>
*
* <p>
* The request and response body will be buffered into memory even if it is never consumed by an application, possibly impacting
* performance.
* <p>
* Body is not logged (and not buffered) for requests and responses where the content length is not known or greater than 16KB.
*/
BODY,

/**
* Enables logging everything in {@link #HEADERS} and {@link #BODY}.
*
* <p>
* The request and response body will be buffered into memory even if it is never consumed by an application, possibly impacting
* performance.
* <p>
* Body is not logged (and not buffered) for requests and responses where the content length is not known or greater than 16KB.
*/
BODY_AND_HEADERS;

private static final String HEADERS_VALUE = "headers";
private static final String BODY_VALUE = "body";
private static final String BODY_AND_HEADERS_VALUE = "body_and_headers";

private static final ConfigurationProperty<String> HTTP_LOG_DETAIL_LEVEL
= ConfigurationPropertyBuilder.ofString("http.log.detail.level")
.shared(true)
.environmentVariableName(Configuration.PROPERTY_HTTP_LOG_DETAIL_LEVEL)
.defaultValue("none")
.build();

static HttpLogDetailLevel fromConfiguration(Configuration configuration) {
String detailLevel = configuration.get(HTTP_LOG_DETAIL_LEVEL);

HttpLogDetailLevel logDetailLevel;

if (HEADERS_VALUE.equalsIgnoreCase(detailLevel)) {
logDetailLevel = HEADERS;
} else if (BODY_VALUE.equalsIgnoreCase(detailLevel)) {
logDetailLevel = BODY;
} else if (BODY_AND_HEADERS_VALUE.equalsIgnoreCase(detailLevel)) {
logDetailLevel = BODY_AND_HEADERS;
} else {
logDetailLevel = NONE;
}

return logDetailLevel;
}
}
}
Loading

0 comments on commit 5bc93af

Please sign in to comment.