* It propagates context to the downstream service following W3C Trace Context specification. *
- * The {@link HttpInstrumentationPolicy} should be added to the HTTP pipeline by client libraries. It should be added between - * {@link HttpRetryPolicy} and {@link HttpLoggingPolicy} so that it's executed on each try or redirect and logging happens + * The {@link HttpInstrumentationPolicy} should be added to the HTTP pipeline by client libraries. It should be added after + * {@link HttpRetryPolicy} and {@link HttpRedirectPolicy} so that it's executed on each try or redirect and logging happens * in the scope of the span. *
* The policy supports basic customizations using {@link InstrumentationOptions} and {@link HttpLogOptions}. @@ -62,8 +83,7 @@ * HttpPipeline pipeline = new HttpPipelineBuilder() * .policies( * new HttpRetryPolicy(), - * new HttpInstrumentationPolicy(instrumentationOptions, logOptions), - * new HttpLoggingPolicy(logOptions)) + * new HttpInstrumentationPolicy(instrumentationOptions, logOptions)) * .build(); * * @@ -81,8 +101,7 @@ * HttpPipeline pipeline = new HttpPipelineBuilder() * .policies( * new HttpRetryPolicy(), - * new HttpInstrumentationPolicy(instrumentationOptions, logOptions), - * new HttpLoggingPolicy(logOptions)) + * new HttpInstrumentationPolicy(instrumentationOptions, logOptions)) * .build(); * * @@ -93,9 +112,11 @@ *
*
* HttpPipelinePolicy enrichingPolicy = (request, next) -> {
- * Object span = request.getRequestOptions().getContext().get(TRACE_CONTEXT_KEY);
- * if (span instanceof Span) {
- * ((Span)span).setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID));
+ * Span span = request.getRequestOptions() == null
+ * ? Span.noop()
+ * : request.getRequestOptions().getInstrumentationContext().getSpan();
+ * if (span.isRecording()) {
+ * span.setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID));
* }
*
* return next.process();
@@ -105,8 +126,7 @@
* .policies(
* new HttpRetryPolicy(),
* new HttpInstrumentationPolicy(instrumentationOptions, logOptions),
- * enrichingPolicy,
- * new HttpLoggingPolicy(logOptions))
+ * enrichingPolicy)
* .build();
*
*
@@ -139,17 +159,19 @@ public final class HttpInstrumentationPolicy implements HttpPipelinePolicy {
LIBRARY_OPTIONS = libOptions;
}
- private static final String HTTP_REQUEST_METHOD = "http.request.method";
- private static final String HTTP_RESPONSE_STATUS_CODE = "http.response.status_code";
- private static final String SERVER_ADDRESS = "server.address";
- private static final String SERVER_PORT = "server.port";
- private static final String URL_FULL = "url.full";
- private static final String HTTP_REQUEST_RESEND_COUNT = "http.request.resend_count";
- private static final String USER_AGENT_ORIGINAL = "user_agent.original";
+ private static final int MAX_BODY_LOG_SIZE = 1024 * 16;
+ private static final String REDACTED_PLACEHOLDER = "REDACTED";
+
+ // request log level is low (verbose) since almost all request details are also
+ // captured on the response log.
+ private static final ClientLogger.LogLevel HTTP_REQUEST_LOG_LEVEL = ClientLogger.LogLevel.VERBOSE;
+ private static final ClientLogger.LogLevel HTTP_RESPONSE_LOG_LEVEL = ClientLogger.LogLevel.INFORMATIONAL;
private final Tracer tracer;
private final TraceContextPropagator traceContextPropagator;
private final Set allowedQueryParameterNames;
+ private final HttpLogOptions.HttpLogDetailLevel httpLogDetailLevel;
+ private final Set allowedHeaderNames;
/**
* Creates a new instrumentation policy.
@@ -162,7 +184,12 @@ public HttpInstrumentationPolicy(InstrumentationOptions> instrumentationOption
this.traceContextPropagator = instrumentation.getW3CTraceContextPropagator();
HttpLogOptions logOptionsToUse = logOptions == null ? DEFAULT_LOG_OPTIONS : logOptions;
- this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames();
+ this.httpLogDetailLevel = logOptionsToUse.getLogLevel();
+ this.allowedHeaderNames = logOptionsToUse.getAllowedHeaderNames();
+ this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames()
+ .stream()
+ .map(queryParamName -> queryParamName.toLowerCase(Locale.ROOT))
+ .collect(Collectors.toSet());
}
/**
@@ -171,40 +198,70 @@ public HttpInstrumentationPolicy(InstrumentationOptions> instrumentationOption
@SuppressWarnings("try")
@Override
public Response> process(HttpRequest request, HttpPipelineNextPolicy next) {
- if (!isTracingEnabled(request)) {
+ boolean isTracingEnabled = tracer.isEnabled();
+ if (!isTracingEnabled && httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
return next.process();
}
- String sanitizedUrl = getRedactedUri(request.getUri(), allowedQueryParameterNames);
- Span span = startHttpSpan(request, sanitizedUrl);
+ ClientLogger logger = getLogger(request);
+ final long startNs = System.nanoTime();
+ String redactedUrl = getRedactedUri(request.getUri(), allowedQueryParameterNames);
+ int tryCount = HttpRequestAccessHelper.getTryCount(request);
+ final long requestContentLength = getContentLength(logger, request.getBody(), request.getHeaders(), true);
+
+ InstrumentationContext instrumentationContext
+ = request.getRequestOptions() == null ? null : request.getRequestOptions().getInstrumentationContext();
+ Span span = Span.noop();
+ if (isTracingEnabled) {
+ if (request.getRequestOptions() == null || request.getRequestOptions() == RequestOptions.none()) {
+ request.setRequestOptions(new RequestOptions());
+ }
+
+ span = startHttpSpan(request, redactedUrl, instrumentationContext);
+ instrumentationContext = span.getInstrumentationContext();
+ request.getRequestOptions().setInstrumentationContext(instrumentationContext);
+ }
- if (request.getRequestOptions() == RequestOptions.none()) {
- request = request.setRequestOptions(new RequestOptions());
+ // even if tracing is disabled, we could have a valid context to propagate
+ // if it was provided by the application explicitly.
+ if (instrumentationContext != null && instrumentationContext.isValid()) {
+ traceContextPropagator.inject(instrumentationContext, request.getHeaders(), SETTER);
}
- Context context = request.getRequestOptions().getContext().put(TRACE_CONTEXT_KEY, span);
- request.getRequestOptions().setContext(context);
- propagateContext(context, request.getHeaders());
+ logRequest(logger, request, startNs, requestContentLength, redactedUrl, tryCount, instrumentationContext);
try (TracingScope scope = span.makeCurrent()) {
Response> response = next.process();
- addDetails(request, response, span);
+ if (response == null) {
+ LOGGER.atError()
+ .setInstrumentationContext(span.getInstrumentationContext())
+ .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod())
+ .addKeyValue(URL_FULL_KEY, redactedUrl)
+ .log(
+ "HTTP response is null and no exception is thrown. Please report it to the client library maintainers.");
+
+ return null;
+ }
+ addDetails(request, response, tryCount, span);
+ response = logResponse(logger, response, startNs, requestContentLength, redactedUrl, tryCount,
+ instrumentationContext);
span.end();
return response;
- } catch (Throwable t) {
+ } catch (RuntimeException t) {
span.end(unwrap(t));
- throw t;
+ // TODO (limolkova) test otel scope still covers this
+ throw logException(logger, request, null, t, startNs, null, requestContentLength, redactedUrl, tryCount,
+ instrumentationContext);
}
}
- private Span startHttpSpan(HttpRequest request, String sanitizedUrl) {
- SpanBuilder spanBuilder
- = tracer.spanBuilder(request.getHttpMethod().toString(), CLIENT, request.getRequestOptions())
- .setAttribute(HTTP_REQUEST_METHOD, request.getHttpMethod().toString())
- .setAttribute(URL_FULL, sanitizedUrl)
- .setAttribute(SERVER_ADDRESS, request.getUri().getHost());
+ private Span startHttpSpan(HttpRequest request, String sanitizedUrl, InstrumentationContext context) {
+ SpanBuilder spanBuilder = tracer.spanBuilder(request.getHttpMethod().toString(), CLIENT, context)
+ .setAttribute(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod().toString())
+ .setAttribute(URL_FULL_KEY, sanitizedUrl)
+ .setAttribute(SERVER_ADDRESS_KEY, request.getUri().getHost());
maybeSetServerPort(spanBuilder, request.getUri());
return spanBuilder.startSpan();
}
@@ -220,15 +277,15 @@ private Span startHttpSpan(HttpRequest request, String sanitizedUrl) {
private static void maybeSetServerPort(SpanBuilder spanBuilder, URI uri) {
int port = uri.getPort();
if (port != -1) {
- spanBuilder.setAttribute(SERVER_PORT, port);
+ spanBuilder.setAttribute(SERVER_PORT_KEY, port);
} else {
switch (uri.getScheme()) {
case "http":
- spanBuilder.setAttribute(SERVER_PORT, 80);
+ spanBuilder.setAttribute(SERVER_PORT_KEY, 80);
break;
case "https":
- spanBuilder.setAttribute(SERVER_PORT, 443);
+ spanBuilder.setAttribute(SERVER_PORT_KEY, 443);
break;
default:
@@ -237,21 +294,20 @@ private static void maybeSetServerPort(SpanBuilder spanBuilder, URI uri) {
}
}
- private void addDetails(HttpRequest request, Response> response, Span span) {
+ private void addDetails(HttpRequest request, Response> response, int tryCount, Span span) {
if (!span.isRecording()) {
return;
}
- span.setAttribute(HTTP_RESPONSE_STATUS_CODE, (long) response.getStatusCode());
+ span.setAttribute(HTTP_RESPONSE_STATUS_CODE_KEY, (long) response.getStatusCode());
- int tryCount = HttpRequestAccessHelper.getTryCount(request);
if (tryCount > 0) {
- span.setAttribute(HTTP_REQUEST_RESEND_COUNT, (long) tryCount);
+ span.setAttribute(HTTP_REQUEST_RESEND_COUNT_KEY, (long) tryCount);
}
String userAgent = request.getHeaders().getValue(HttpHeaderName.USER_AGENT);
if (userAgent != null) {
- span.setAttribute(USER_AGENT_ORIGINAL, userAgent);
+ span.setAttribute(USER_AGENT_ORIGINAL_KEY, userAgent);
}
if (response.getStatusCode() >= 400) {
@@ -260,29 +316,21 @@ private void addDetails(HttpRequest request, Response> response, Span span) {
// TODO (lmolkova) url.template and experimental features
}
- private boolean isTracingEnabled(HttpRequest httpRequest) {
- if (!tracer.isEnabled()) {
- return false;
- }
-
- Context context = httpRequest.getRequestOptions().getContext();
- Object disableTracing = context.get(DISABLE_TRACING_KEY);
- if (disableTracing instanceof Boolean) {
- return !((Boolean) disableTracing);
- }
-
- return true;
- }
-
- private Throwable unwrap(Throwable t) {
+ private static Throwable unwrap(Throwable t) {
while (t.getCause() != null) {
t = t.getCause();
}
return t;
}
- private void propagateContext(Context context, HttpHeaders headers) {
- traceContextPropagator.inject(context, headers, SETTER);
+ private ClientLogger getLogger(HttpRequest httpRequest) {
+ ClientLogger logger = null;
+
+ if (httpRequest.getRequestOptions() != null && httpRequest.getRequestOptions().getLogger() != null) {
+ logger = httpRequest.getRequestOptions().getLogger();
+ }
+
+ return logger == null ? LOGGER : logger;
}
private static Map getProperties(String propertiesFileName) {
@@ -303,4 +351,231 @@ private static Map getProperties(String propertiesFileName) {
return Collections.emptyMap();
}
+
+ private void logRequest(ClientLogger logger, HttpRequest request, long startNanoTime, long requestContentLength,
+ String redactedUrl, int tryCount, InstrumentationContext context) {
+ ClientLogger.LoggingEvent logBuilder = logger.atLevel(HTTP_REQUEST_LOG_LEVEL);
+ if (!logBuilder.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
+ return;
+ }
+
+ logBuilder.setEventName(HTTP_REQUEST_EVENT_NAME)
+ .setInstrumentationContext(context)
+ .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod())
+ .addKeyValue(URL_FULL_KEY, redactedUrl)
+ .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount)
+ .addKeyValue(HTTP_REQUEST_BODY_SIZE_KEY, requestContentLength);
+
+ addHeadersToLogMessage(request.getHeaders(), logBuilder);
+
+ if (httpLogDetailLevel.shouldLogBody() && canLogBody(request.getBody())) {
+ try {
+ BinaryData bufferedBody = request.getBody().toReplayableBinaryData();
+ request.setBody(bufferedBody);
+ logBuilder.addKeyValue(HTTP_REQUEST_BODY_CONTENT_KEY, bufferedBody.toString());
+ } catch (RuntimeException e) {
+ // we'll log exception at the appropriate level.
+ throw logException(logger, request, null, e, startNanoTime, null, requestContentLength, redactedUrl,
+ tryCount, context);
+ }
+ }
+
+ logBuilder.log();
+ }
+
+ private Response> logResponse(ClientLogger logger, Response> response, long startNanoTime,
+ long requestContentLength, String redactedUrl, int tryCount, InstrumentationContext context) {
+ ClientLogger.LoggingEvent logBuilder = logger.atLevel(HTTP_RESPONSE_LOG_LEVEL);
+ if (httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
+ return response;
+ }
+
+ long responseStartNanoTime = System.nanoTime();
+
+ // response may be disabled, but we still need to log the exception if an exception occurs during stream reading.
+ if (logBuilder.isEnabled()) {
+ logBuilder.setEventName(HTTP_RESPONSE_EVENT_NAME)
+ .setInstrumentationContext(context)
+ .addKeyValue(HTTP_REQUEST_METHOD_KEY, response.getRequest().getHttpMethod())
+ .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount)
+ .addKeyValue(URL_FULL_KEY, redactedUrl)
+ .addKeyValue(HTTP_REQUEST_TIME_TO_RESPONSE_KEY, getDurationMs(startNanoTime, responseStartNanoTime))
+ .addKeyValue(HTTP_RESPONSE_STATUS_CODE_KEY, response.getStatusCode())
+ .addKeyValue(HTTP_REQUEST_BODY_SIZE_KEY, requestContentLength)
+ .addKeyValue(HTTP_RESPONSE_BODY_SIZE_KEY,
+ getContentLength(logger, response.getBody(), response.getHeaders(), false));
+
+ addHeadersToLogMessage(response.getHeaders(), logBuilder);
+ }
+
+ if (httpLogDetailLevel.shouldLogBody() && canLogBody(response.getBody())) {
+ return new LoggingHttpResponse<>(response, content -> {
+ if (logBuilder.isEnabled()) {
+ logBuilder.addKeyValue(HTTP_RESPONSE_BODY_CONTENT_KEY, content.toString())
+ .addKeyValue(HTTP_REQUEST_DURATION_KEY, getDurationMs(startNanoTime, System.nanoTime()))
+ .log();
+ }
+ }, throwable -> logException(logger, response.getRequest(), response, throwable, startNanoTime,
+ responseStartNanoTime, requestContentLength, redactedUrl, tryCount, context));
+ }
+
+ if (logBuilder.isEnabled()) {
+ logBuilder.addKeyValue(HTTP_REQUEST_DURATION_KEY, getDurationMs(startNanoTime, System.nanoTime())).log();
+ }
+
+ return response;
+ }
+
+ private T logException(ClientLogger logger, HttpRequest request, Response> response,
+ T throwable, long startNanoTime, Long responseStartNanoTime, long requestContentLength, String redactedUrl,
+ int tryCount, InstrumentationContext context) {
+
+ ClientLogger.LoggingEvent log = logger.atLevel(ClientLogger.LogLevel.WARNING);
+ if (!log.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
+ return throwable;
+ }
+
+ log.setEventName(HTTP_RESPONSE_EVENT_NAME)
+ .setInstrumentationContext(context)
+ .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod())
+ .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount)
+ .addKeyValue(URL_FULL_KEY, redactedUrl)
+ .addKeyValue(HTTP_REQUEST_BODY_SIZE_KEY, requestContentLength)
+ .addKeyValue(HTTP_REQUEST_DURATION_KEY, getDurationMs(startNanoTime, System.nanoTime()));
+
+ if (response != null) {
+ addHeadersToLogMessage(response.getHeaders(), log);
+ log.addKeyValue(HTTP_RESPONSE_BODY_SIZE_KEY,
+ getContentLength(logger, response.getBody(), response.getHeaders(), false))
+ .addKeyValue(HTTP_RESPONSE_STATUS_CODE_KEY, response.getStatusCode());
+
+ if (responseStartNanoTime != null) {
+ log.addKeyValue(HTTP_REQUEST_TIME_TO_RESPONSE_KEY, getDurationMs(startNanoTime, responseStartNanoTime));
+ }
+ }
+
+ log.log(null, unwrap(throwable));
+ return throwable;
+ }
+
+ private double getDurationMs(long startNs, long endNs) {
+ return (endNs - startNs) / 1_000_000.0;
+ }
+
+ /**
+ * Determines if the request or response body should be logged.
+ *
+ * The request or response body is logged if the body is replayable, content length is known,
+ * isn't empty, and is less than 16KB in size.
+ *
+ * @param data The request or response body.
+ * @return A flag indicating if the request or response body should be logged.
+ */
+ private static boolean canLogBody(BinaryData data) {
+ // TODO (limolkova) we might want to filter out binary data, but
+ // if somebody enabled logging it - why not log it?
+ return data != null && data.getLength() != null && data.getLength() > 0 && data.getLength() < MAX_BODY_LOG_SIZE;
+ }
+
+ /**
+ * Adds HTTP headers into the StringBuilder that is generating the log message.
+ *
+ * @param headers HTTP headers on the request or response.
+ * @param logBuilder Log message builder.
+ */
+ private void addHeadersToLogMessage(HttpHeaders headers, ClientLogger.LoggingEvent logBuilder) {
+ if (httpLogDetailLevel.shouldLogHeaders()) {
+ for (HttpHeader header : headers) {
+ HttpHeaderName headerName = header.getName();
+ String headerValue = allowedHeaderNames.contains(headerName) ? header.getValue() : REDACTED_PLACEHOLDER;
+ logBuilder.addKeyValue(headerName.toString(), headerValue);
+ }
+ }
+ }
+
+ /**
+ * Attempts to get request or response body content length.
+ *
+ * If the body length is known, it will be returned.
+ * Otherwise, the method parses Content-Length header.
+ *
+ * @param logger Logger used to log a warning if the Content-Length header is an invalid number.
+ * @param body The request or response body object.
+ * @param headers HTTP headers that are checked for containing Content-Length.
+ * @return The numeric value of the Content-Length header or 0 if the header is not present or invalid.
+ */
+ private static long getContentLength(ClientLogger logger, BinaryData body, HttpHeaders headers, boolean isRequest) {
+ if (body == null) {
+ return 0;
+ }
+
+ if (body.getLength() != null) {
+ return body.getLength();
+ }
+
+ long contentLength = 0;
+
+ String contentLengthString = headers.getValue(HttpHeaderName.CONTENT_LENGTH);
+
+ if (isNullOrEmpty(contentLengthString)) {
+ return contentLength;
+ }
+
+ try {
+ contentLength = Long.parseLong(contentLengthString);
+ } catch (NumberFormatException e) {
+ logger.atVerbose()
+ .addKeyValue(
+ isRequest ? HTTP_REQUEST_HEADER_CONTENT_LENGTH_KEY : HTTP_RESPONSE_HEADER_CONTENT_LENGTH_KEY,
+ contentLengthString)
+ .log("Could not parse the HTTP header content-length", e);
+ }
+
+ return contentLength;
+ }
+
+ private static final class LoggingHttpResponse extends HttpResponse {
+ private final Consumer onContent;
+ private final Consumer onException;
+ private final BinaryData originalBody;
+ private BinaryData bufferedBody;
+
+ private LoggingHttpResponse(Response actualResponse, Consumer onContent,
+ Consumer onException) {
+ super(actualResponse.getRequest(), actualResponse.getStatusCode(), actualResponse.getHeaders(),
+ actualResponse.getValue());
+
+ this.onContent = onContent;
+ this.onException = onException;
+ this.originalBody = actualResponse.getBody();
+ }
+
+ @Override
+ public BinaryData getBody() {
+ if (bufferedBody != null) {
+ return bufferedBody;
+ }
+
+ try {
+ bufferedBody = originalBody.toReplayableBinaryData();
+ onContent.accept(bufferedBody);
+ return bufferedBody;
+ } catch (RuntimeException e) {
+ // we'll log exception at the appropriate level.
+ onException.accept(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (bufferedBody == null) {
+ getBody();
+ }
+ if (bufferedBody != null) {
+ bufferedBody.close();
+ }
+ originalBody.close();
+ }
+ }
}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java
deleted file mode 100644
index c43ebca9a3c3..000000000000
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java
+++ /dev/null
@@ -1,337 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-package io.clientcore.core.http.pipeline;
-
-import io.clientcore.core.http.models.HttpHeader;
-import io.clientcore.core.http.models.HttpHeaderName;
-import io.clientcore.core.http.models.HttpHeaders;
-import io.clientcore.core.http.models.HttpLogOptions;
-import io.clientcore.core.http.models.HttpRequest;
-import io.clientcore.core.http.models.HttpResponse;
-import io.clientcore.core.http.models.Response;
-import io.clientcore.core.implementation.http.HttpRequestAccessHelper;
-import io.clientcore.core.implementation.util.LoggingKeys;
-import io.clientcore.core.util.ClientLogger;
-import io.clientcore.core.util.binarydata.BinaryData;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-import static io.clientcore.core.http.models.HttpHeaderName.TRACEPARENT;
-import static io.clientcore.core.implementation.UrlRedactionUtil.getRedactedUri;
-import static io.clientcore.core.implementation.util.ImplUtils.isNullOrEmpty;
-
-/**
- * The pipeline policy that handles logging of HTTP requests and responses.
- */
-public class HttpLoggingPolicy implements HttpPipelinePolicy {
- private static final HttpLogOptions DEFAULT_HTTP_LOG_OPTIONS = new HttpLogOptions();
- private static final List ALWAYS_ALLOWED_HEADERS = Collections.singletonList(TRACEPARENT);
- private static final int MAX_BODY_LOG_SIZE = 1024 * 16;
- private static final String REDACTED_PLACEHOLDER = "REDACTED";
- private static final ClientLogger LOGGER = new ClientLogger(HttpLoggingPolicy.class);
- private final HttpLogOptions.HttpLogDetailLevel httpLogDetailLevel;
- private final Set allowedHeaderNames;
-
- private final Set allowedQueryParameterNames;
-
- private static final String HTTP_REQUEST_EVENT_NAME = "http.request";
- private static final String HTTP_RESPONSE_EVENT_NAME = "http.response";
-
- // request log level is low (verbose) since almost all request details are also
- // captured on the response log.
- private static final ClientLogger.LogLevel HTTP_REQUEST_LOG_LEVEL = ClientLogger.LogLevel.VERBOSE;
- private static final ClientLogger.LogLevel HTTP_RESPONSE_LOG_LEVEL = ClientLogger.LogLevel.INFORMATIONAL;
-
- /**
- * Creates an HttpLoggingPolicy with the given log configurations.
- *
- * @param httpLogOptions The HTTP logging configuration options.
- */
- public HttpLoggingPolicy(HttpLogOptions httpLogOptions) {
- HttpLogOptions logOptionsToUse = httpLogOptions == null ? DEFAULT_HTTP_LOG_OPTIONS : httpLogOptions;
- this.httpLogDetailLevel = logOptionsToUse.getLogLevel();
- this.allowedHeaderNames = logOptionsToUse.getAllowedHeaderNames();
- this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames()
- .stream()
- .map(queryParamName -> queryParamName.toLowerCase(Locale.ROOT))
- .collect(Collectors.toSet());
- }
-
- @Override
- public Response> process(HttpRequest httpRequest, HttpPipelineNextPolicy next) {
- // No logging will be performed, trigger a no-op.
- if (httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
- return next.process();
- }
-
- ClientLogger logger = getLogger(httpRequest);
-
- final long startNs = System.nanoTime();
- final String redactedUrl = getRedactedUri(httpRequest.getUri(), allowedQueryParameterNames);
- final int tryCount = HttpRequestAccessHelper.getTryCount(httpRequest);
- final long requestContentLength = httpRequest.getBody() == null
- ? 0
- : getContentLength(logger, httpRequest.getBody(), httpRequest.getHeaders());
-
- logRequest(logger, httpRequest, startNs, requestContentLength, redactedUrl, tryCount);
-
- try {
- Response> response = next.process();
-
- if (response == null) {
- LOGGER.atError()
- .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, httpRequest.getHttpMethod())
- .addKeyValue(LoggingKeys.URI_KEY, redactedUrl)
- .log(
- "HTTP response is null and no exception is thrown. Please report it to the client library maintainers.");
-
- return null;
- }
-
- return logResponse(logger, response, startNs, requestContentLength, redactedUrl, tryCount);
- } catch (RuntimeException e) {
- throw logException(logger, httpRequest, null, e, startNs, null, requestContentLength, redactedUrl,
- tryCount);
- }
- }
-
- private ClientLogger getLogger(HttpRequest request) {
- if (request.getRequestOptions() != null && request.getRequestOptions().getLogger() != null) {
- return request.getRequestOptions().getLogger();
- }
-
- return LOGGER;
- }
-
- private void logRequest(ClientLogger logger, HttpRequest request, long startNanoTime, long requestContentLength,
- String redactedUrl, int tryCount) {
- ClientLogger.LoggingEvent logBuilder = logger.atLevel(HTTP_REQUEST_LOG_LEVEL);
- if (!logBuilder.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
- return;
- }
-
- logBuilder.setEventName(HTTP_REQUEST_EVENT_NAME)
- .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, request.getHttpMethod())
- .addKeyValue(LoggingKeys.URI_KEY, redactedUrl)
- .addKeyValue(LoggingKeys.TRY_COUNT_KEY, tryCount)
- .addKeyValue(LoggingKeys.REQUEST_CONTENT_LENGTH_KEY, requestContentLength);
-
- addHeadersToLogMessage(request.getHeaders(), logBuilder);
-
- if (httpLogDetailLevel.shouldLogBody() && canLogBody(request.getBody())) {
- try {
- BinaryData bufferedBody = request.getBody().toReplayableBinaryData();
- request.setBody(bufferedBody);
- logBuilder.addKeyValue(LoggingKeys.BODY_KEY, bufferedBody.toString());
- } catch (RuntimeException e) {
- // we'll log exception at the appropriate level.
- throw logException(logger, request, null, e, startNanoTime, null, requestContentLength, redactedUrl,
- tryCount);
- }
- }
-
- logBuilder.log();
- }
-
- private Response> logResponse(ClientLogger logger, Response> response, long startNanoTime,
- long requestContentLength, String redactedUrl, int tryCount) {
- ClientLogger.LoggingEvent logBuilder = logger.atLevel(HTTP_RESPONSE_LOG_LEVEL);
- if (httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
- return response;
- }
-
- long responseStartNanoTime = System.nanoTime();
-
- // response may be disabled, but we still need to log the exception if an exception occurs during stream reading.
- if (logBuilder.isEnabled()) {
- logBuilder.setEventName(HTTP_RESPONSE_EVENT_NAME)
- .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, response.getRequest().getHttpMethod())
- .addKeyValue(LoggingKeys.TRY_COUNT_KEY, tryCount)
- .addKeyValue(LoggingKeys.URI_KEY, redactedUrl)
- .addKeyValue(LoggingKeys.TIME_TO_RESPONSE_MS_KEY, getDurationMs(startNanoTime, responseStartNanoTime))
- .addKeyValue(LoggingKeys.STATUS_CODE_KEY, response.getStatusCode())
- .addKeyValue(LoggingKeys.REQUEST_CONTENT_LENGTH_KEY, requestContentLength)
- .addKeyValue(LoggingKeys.RESPONSE_CONTENT_LENGTH_KEY,
- getContentLength(logger, response.getBody(), response.getHeaders()));
-
- addHeadersToLogMessage(response.getHeaders(), logBuilder);
- }
-
- if (httpLogDetailLevel.shouldLogBody() && canLogBody(response.getBody())) {
- return new LoggingHttpResponse<>(response, content -> {
- if (logBuilder.isEnabled()) {
- logBuilder.addKeyValue(LoggingKeys.BODY_KEY, content.toString())
- .addKeyValue(LoggingKeys.DURATION_MS_KEY, getDurationMs(startNanoTime, System.nanoTime()))
- .log();
- }
- }, throwable -> logException(logger, response.getRequest(), response, throwable, startNanoTime,
- responseStartNanoTime, requestContentLength, redactedUrl, tryCount));
- }
-
- if (logBuilder.isEnabled()) {
- logBuilder.addKeyValue(LoggingKeys.DURATION_MS_KEY, getDurationMs(startNanoTime, System.nanoTime())).log();
- }
-
- return response;
- }
-
- private T logException(ClientLogger logger, HttpRequest request, Response> response,
- T throwable, long startNanoTime, Long responseStartNanoTime, long requestContentLength, String redactedUrl,
- int tryCount) {
- ClientLogger.LoggingEvent logBuilder = logger.atLevel(ClientLogger.LogLevel.WARNING);
- if (!logBuilder.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
- return throwable;
- }
-
- logBuilder.setEventName(HTTP_RESPONSE_EVENT_NAME)
- .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, request.getHttpMethod())
- .addKeyValue(LoggingKeys.TRY_COUNT_KEY, tryCount)
- .addKeyValue(LoggingKeys.URI_KEY, redactedUrl)
- .addKeyValue(LoggingKeys.REQUEST_CONTENT_LENGTH_KEY, requestContentLength)
- .addKeyValue(LoggingKeys.DURATION_MS_KEY, getDurationMs(startNanoTime, System.nanoTime()));
-
- if (response != null) {
- addHeadersToLogMessage(response.getHeaders(), logBuilder);
- logBuilder
- .addKeyValue(LoggingKeys.RESPONSE_CONTENT_LENGTH_KEY,
- getContentLength(logger, response.getBody(), response.getHeaders()))
- .addKeyValue(LoggingKeys.STATUS_CODE_KEY, response.getStatusCode());
-
- if (responseStartNanoTime != null) {
- logBuilder.addKeyValue(LoggingKeys.TIME_TO_RESPONSE_MS_KEY,
- getDurationMs(startNanoTime, responseStartNanoTime));
- }
- }
-
- return logBuilder.log(null, throwable);
- }
-
- private double getDurationMs(long startNs, long endNs) {
- return (endNs - startNs) / 1_000_000.0;
- }
-
- /**
- * Determines if the request or response body should be logged.
- *
- * The request or response body is logged if the body is replayable, content length is known,
- * isn't empty, and is less than 16KB in size.
- *
- * @param data The request or response body.
- * @return A flag indicating if the request or response body should be logged.
- */
- private static boolean canLogBody(BinaryData data) {
- // TODO: limolkova - we might want to filter out binary data, but
- // if somebody enabled logging it - why not log it?
- return data != null && data.getLength() != null && data.getLength() > 0 && data.getLength() < MAX_BODY_LOG_SIZE;
- }
-
- /**
- * Adds HTTP headers into the StringBuilder that is generating the log message.
- *
- * @param headers HTTP headers on the request or response.
- * @param logBuilder Log message builder.
- */
- private void addHeadersToLogMessage(HttpHeaders headers, ClientLogger.LoggingEvent logBuilder) {
- if (httpLogDetailLevel.shouldLogHeaders()) {
- for (HttpHeader header : headers) {
- HttpHeaderName headerName = header.getName();
- String headerValue = allowedHeaderNames.contains(headerName) ? header.getValue() : REDACTED_PLACEHOLDER;
- logBuilder.addKeyValue(headerName.toString(), headerValue);
- }
- } else {
- for (HttpHeaderName headerName : ALWAYS_ALLOWED_HEADERS) {
- String headerValue = headers.getValue(headerName);
- if (headerValue != null) {
- logBuilder.addKeyValue(headerName.toString(), headerValue);
- }
- }
- }
- }
-
- /**
- * Attempts to get request or response body content length.
- *
- * If the body length is known, it will be returned.
- * Otherwise, the method parses Content-Length header.
- *
- * @param logger Logger used to log a warning if the Content-Length header is an invalid number.
- * @param body The request or response body object.
- * @param headers HTTP headers that are checked for containing Content-Length.
- * @return The numeric value of the Content-Length header or 0 if the header is not present or invalid.
- */
- private static long getContentLength(ClientLogger logger, BinaryData body, HttpHeaders headers) {
- if (body != null && body.getLength() != null) {
- return body.getLength();
- }
-
- long contentLength = 0;
-
- String contentLengthString = headers.getValue(HttpHeaderName.CONTENT_LENGTH);
-
- if (isNullOrEmpty(contentLengthString)) {
- return contentLength;
- }
-
- try {
- contentLength = Long.parseLong(contentLengthString);
- } catch (NumberFormatException | NullPointerException e) {
- logger.atVerbose()
- .addKeyValue("contentLength", contentLengthString)
- .log("Could not parse the HTTP header content-length", e);
- }
-
- return contentLength;
- }
-
- private static final class LoggingHttpResponse extends HttpResponse {
- private final Consumer onContent;
- private final Consumer onException;
- private final BinaryData originalBody;
- private BinaryData bufferedBody;
-
- private LoggingHttpResponse(Response actualResponse, Consumer onContent,
- Consumer onException) {
- super(actualResponse.getRequest(), actualResponse.getStatusCode(), actualResponse.getHeaders(),
- actualResponse.getValue());
-
- this.onContent = onContent;
- this.onException = onException;
- this.originalBody = actualResponse.getBody();
- }
-
- @Override
- public BinaryData getBody() {
- if (bufferedBody != null) {
- return bufferedBody;
- }
-
- try {
- bufferedBody = originalBody.toReplayableBinaryData();
- onContent.accept(bufferedBody);
- return bufferedBody;
- } catch (RuntimeException e) {
- // we'll log exception at the appropriate level.
- onException.accept(e);
- throw e;
- }
- }
-
- @Override
- public void close() throws IOException {
- if (bufferedBody == null) {
- getBody();
- }
- if (bufferedBody != null) {
- bufferedBody.close();
- }
- originalBody.close();
- }
- }
-}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpPipelineNextPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpPipelineNextPolicy.java
index 8b6856e1a5f1..ed070bd50991 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpPipelineNextPolicy.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpPipelineNextPolicy.java
@@ -5,7 +5,7 @@
import io.clientcore.core.http.models.Response;
import io.clientcore.core.implementation.http.HttpPipelineCallState;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.io.IOException;
import java.io.UncheckedIOException;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRedirectPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRedirectPolicy.java
index 6bdddc826cd9..fbd3e84b3171 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRedirectPolicy.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRedirectPolicy.java
@@ -8,18 +8,28 @@
import io.clientcore.core.http.models.HttpRedirectOptions;
import io.clientcore.core.http.models.HttpRequest;
import io.clientcore.core.http.models.Response;
-import io.clientcore.core.implementation.util.LoggingKeys;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.InstrumentationContext;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
+import static io.clientcore.core.implementation.UrlRedactionUtil.getRedactedUri;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_METHOD_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_RESEND_COUNT_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_HEADER_LOCATION_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_MAX_ATTEMPT_COUNT_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_WAS_LAST_ATTEMPT_KEY;
+import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.HTTP_REDIRECT_EVENT_NAME;
+
/**
* A {@link HttpPipelinePolicy} that redirects a {@link HttpRequest} when an HTTP Redirect is received as a
* {@link Response response}.
@@ -29,8 +39,6 @@ public final class HttpRedirectPolicy implements HttpPipelinePolicy {
private final int maxAttempts;
private final Predicate shouldRedirectCondition;
private static final int DEFAULT_MAX_REDIRECT_ATTEMPTS = 3;
- private static final String REDIRECT_URIS_KEY = "redirectUris";
- private static final String ORIGINATING_REQUEST_URI_KEY = "originatingRequestUri";
private static final EnumSet DEFAULT_REDIRECT_ALLOWED_METHODS
= EnumSet.of(HttpMethod.GET, HttpMethod.HEAD);
@@ -74,86 +82,68 @@ public HttpRedirectPolicy(HttpRedirectOptions redirectOptions) {
@Override
public Response> process(HttpRequest httpRequest, HttpPipelineNextPolicy next) {
// Reset the attemptedRedirectUris for each individual request.
- return attemptRedirect(next, 1, new LinkedHashSet<>());
+ InstrumentationContext instrumentationContext = httpRequest.getRequestOptions() == null
+ ? null
+ : httpRequest.getRequestOptions().getInstrumentationContext();
+
+ ClientLogger logger = getLogger(httpRequest);
+ return attemptRedirect(logger, next, 0, new LinkedHashSet<>(), instrumentationContext);
}
/**
* Function to process through the HTTP Response received in the pipeline and redirect sending the request with a
* new redirect URI.
*/
- private Response> attemptRedirect(final HttpPipelineNextPolicy next, final int redirectAttempt,
- LinkedHashSet attemptedRedirectUris) {
+ private Response> attemptRedirect(ClientLogger logger, final HttpPipelineNextPolicy next,
+ final int redirectAttempt, LinkedHashSet attemptedRedirectUris,
+ InstrumentationContext instrumentationContext) {
+
// Make sure the context is not modified during redirect, except for the URI
Response> response = next.clone().process();
HttpRequestRedirectCondition requestRedirectCondition
= new HttpRequestRedirectCondition(response, redirectAttempt, attemptedRedirectUris);
+
if ((shouldRedirectCondition != null && shouldRedirectCondition.test(requestRedirectCondition))
- || (shouldRedirectCondition == null && defaultShouldAttemptRedirect(requestRedirectCondition))) {
+ || (shouldRedirectCondition == null
+ && defaultShouldAttemptRedirect(logger, requestRedirectCondition, instrumentationContext))) {
createRedirectRequest(response);
- return attemptRedirect(next, redirectAttempt + 1, attemptedRedirectUris);
+ return attemptRedirect(logger, next, redirectAttempt + 1, attemptedRedirectUris, instrumentationContext);
}
return response;
}
- private boolean defaultShouldAttemptRedirect(HttpRequestRedirectCondition requestRedirectCondition) {
+ private boolean defaultShouldAttemptRedirect(ClientLogger logger,
+ HttpRequestRedirectCondition requestRedirectCondition, InstrumentationContext context) {
Response> response = requestRedirectCondition.getResponse();
int tryCount = requestRedirectCondition.getTryCount();
Set attemptedRedirectUris = requestRedirectCondition.getRedirectedUris();
String redirectUri = response.getHeaders().getValue(this.locationHeader);
- if (isValidRedirectStatusCode(response.getStatusCode())
- && isValidRedirectCount(tryCount)
- && isAllowedRedirectMethod(response.getRequest().getHttpMethod())
- && redirectUri != null
- && !alreadyAttemptedRedirectUri(redirectUri, attemptedRedirectUris)) {
-
- LOGGER.atVerbose()
- .addKeyValue(LoggingKeys.TRY_COUNT_KEY, tryCount)
- .addKeyValue(REDIRECT_URIS_KEY, attemptedRedirectUris::toString)
- .addKeyValue(ORIGINATING_REQUEST_URI_KEY, response.getRequest().getUri())
- .log("Redirecting.");
-
- attemptedRedirectUris.add(redirectUri);
-
- return true;
- }
-
- return false;
- }
+ if (isValidRedirectStatusCode(response.getStatusCode()) && redirectUri != null) {
+ HttpMethod method = response.getRequest().getHttpMethod();
+ if (tryCount >= this.maxAttempts - 1) {
+ logRedirect(logger, true, redirectUri, tryCount, method, "Redirect attempts have been exhausted.",
+ context);
+ return false;
+ }
- /**
- * Check if the attempt count of the redirect is less than the {@code maxAttempts}
- *
- * @param tryCount the try count for the HTTP request associated to the HTTP response.
- *
- * @return {@code true} if the {@code tryCount} is greater than the {@code maxAttempts}, {@code false} otherwise.
- */
- private boolean isValidRedirectCount(int tryCount) {
- if (tryCount >= this.maxAttempts) {
- LOGGER.atError().addKeyValue("maxAttempts", this.maxAttempts).log("Redirect attempts have been exhausted.");
+ if (!allowedRedirectHttpMethods.contains(response.getRequest().getHttpMethod())) {
+ logRedirect(logger, true, redirectUri, tryCount, method,
+ "Request redirection is not enabled for this HTTP method.", context);
+ return false;
+ }
- return false;
- }
+ if (attemptedRedirectUris.contains(redirectUri)) {
+ logRedirect(logger, true, redirectUri, tryCount, method,
+ "Request was redirected more than once to the same URI.", context);
+ return false;
+ }
- return true;
- }
+ logRedirect(logger, false, redirectUri, tryCount, method, null, context);
- /**
- * Check if the redirect uri provided in the response headers is already attempted.
- *
- * @param redirectUri the redirect uri provided in the response header.
- * @param attemptedRedirectUris the set containing a list of attempted redirect locations.
- *
- * @return {@code true} if the redirectUri provided in the response header is already being attempted for redirect,
- * {@code false} otherwise.
- */
- private boolean alreadyAttemptedRedirectUri(String redirectUri, Set attemptedRedirectUris) {
- if (attemptedRedirectUris.contains(redirectUri)) {
- LOGGER.atError()
- .addKeyValue(LoggingKeys.REDIRECT_URI_KEY, redirectUri)
- .log("Request was redirected more than once to the same URI.");
+ attemptedRedirectUris.add(redirectUri);
return true;
}
@@ -161,25 +151,6 @@ private boolean alreadyAttemptedRedirectUri(String redirectUri, Set atte
return false;
}
- /**
- * Check if the request http method is a valid redirect method.
- *
- * @param httpMethod the http method of the request.
- *
- * @return {@code true} if the request {@code httpMethod} is a valid http redirect method, {@code false} otherwise.
- */
- private boolean isAllowedRedirectMethod(HttpMethod httpMethod) {
- if (allowedRedirectHttpMethods.contains(httpMethod)) {
- return true;
- } else {
- LOGGER.atError()
- .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, httpMethod)
- .log("Request redirection is not enabled for this HTTP method.");
-
- return false;
- }
- }
-
/**
* Checks if the incoming request status code is a valid redirect status code.
*
@@ -205,6 +176,41 @@ private void createRedirectRequest(Response> redirectResponse) {
} catch (IOException e) {
throw LOGGER.logThrowableAsError(new UncheckedIOException(e));
}
+ }
+
+ private void logRedirect(ClientLogger logger, boolean lastAttempt, String redirectUri, int tryCount,
+ HttpMethod method, String message, InstrumentationContext context) {
+ ClientLogger.LoggingEvent log = lastAttempt ? logger.atWarning() : logger.atVerbose();
+ if (log.isEnabled()) {
+ log.addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount)
+ .addKeyValue(RETRY_MAX_ATTEMPT_COUNT_KEY, maxAttempts)
+ .addKeyValue(HTTP_REQUEST_METHOD_KEY, method)
+ .addKeyValue(HTTP_RESPONSE_HEADER_LOCATION_KEY, redactUri(redirectUri))
+ .addKeyValue(RETRY_WAS_LAST_ATTEMPT_KEY, lastAttempt)
+ .setEventName(HTTP_REDIRECT_EVENT_NAME)
+ .setInstrumentationContext(context)
+ .log(message);
+ }
+ }
+
+ private String redactUri(String location) {
+ URI uri;
+ try {
+ uri = URI.create(location);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ // TODO: make it configurable? Or don't log URL?
+ return getRedactedUri(uri, Collections.emptySet());
+ }
+
+ private ClientLogger getLogger(HttpRequest httpRequest) {
+ ClientLogger logger = null;
+
+ if (httpRequest.getRequestOptions() != null && httpRequest.getRequestOptions().getLogger() != null) {
+ logger = httpRequest.getRequestOptions().getLogger();
+ }
+ return logger == null ? LOGGER : logger;
}
}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRetryPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRetryPolicy.java
index 477e2119a0eb..398ae3857a39 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRetryPolicy.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRetryPolicy.java
@@ -9,8 +9,8 @@
import io.clientcore.core.http.models.Response;
import io.clientcore.core.implementation.http.HttpRequestAccessHelper;
import io.clientcore.core.implementation.util.ImplUtils;
-import io.clientcore.core.implementation.util.LoggingKeys;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.InstrumentationContext;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.configuration.Configuration;
import java.io.IOException;
@@ -26,6 +26,11 @@
import java.util.function.Predicate;
import java.util.function.Supplier;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_RESEND_COUNT_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_DELAY_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_WAS_LAST_ATTEMPT_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_MAX_ATTEMPT_COUNT_KEY;
+import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.HTTP_RETRY_EVENT_NAME;
import static io.clientcore.core.implementation.util.ImplUtils.isNullOrEmpty;
import static io.clientcore.core.util.configuration.Configuration.PROPERTY_REQUEST_RETRY_COUNT;
@@ -141,16 +146,24 @@ private Response> attempt(final HttpRequest httpRequest, final HttpPipelineNex
// It can be used by the policies during the process call.
HttpRequestAccessHelper.setTryCount(httpRequest, tryCount);
+ final InstrumentationContext instrumentationContext = httpRequest.getRequestOptions() == null
+ ? null
+ : httpRequest.getRequestOptions().getInstrumentationContext();
+
Response> response;
+ ClientLogger logger = getLogger(httpRequest);
try {
response = next.clone().process();
} catch (RuntimeException err) {
if (shouldRetryException(err, tryCount, suppressed)) {
- logRetryWithError(LOGGER.atVerbose(), tryCount, "Error resume.", err);
+
+ Duration delayDuration = calculateRetryDelay(tryCount);
+ logRetry(logger.atVerbose(), tryCount, delayDuration, err, false, instrumentationContext);
boolean interrupted = false;
- long millis = calculateRetryDelay(tryCount).toMillis();
+ long millis = delayDuration.toMillis();
+
if (millis > 0) {
try {
Thread.sleep(millis);
@@ -161,7 +174,7 @@ private Response> attempt(final HttpRequest httpRequest, final HttpPipelineNex
}
if (interrupted) {
- throw LOGGER.logThrowableAsError(err);
+ throw logger.logThrowableAsError(err);
}
List suppressedLocal = suppressed == null ? new LinkedList<>() : suppressed;
@@ -170,20 +183,20 @@ private Response> attempt(final HttpRequest httpRequest, final HttpPipelineNex
return attempt(httpRequest, next, tryCount + 1, suppressedLocal);
} else {
- logRetryWithError(LOGGER.atError(), tryCount, "Retry attempts have been exhausted.", err);
+ logRetry(logger.atWarning(), tryCount, null, err, true, instrumentationContext);
if (suppressed != null) {
suppressed.forEach(err::addSuppressed);
}
- throw LOGGER.logThrowableAsError(err);
+ throw logger.logThrowableAsError(err);
}
}
if (shouldRetryResponse(response, tryCount, suppressed)) {
final Duration delayDuration = determineDelayDuration(response, tryCount, delayFromHeaders);
- logRetry(tryCount, delayDuration);
+ logRetry(logger.atVerbose(), tryCount, delayDuration, null, false, instrumentationContext);
try {
response.close();
@@ -191,7 +204,7 @@ private Response> attempt(final HttpRequest httpRequest, final HttpPipelineNex
throw LOGGER.logThrowableAsError(new UncheckedIOException(e));
}
- long millis = calculateRetryDelay(tryCount).toMillis();
+ long millis = delayDuration.toMillis();
if (millis > 0) {
try {
Thread.sleep(millis);
@@ -203,9 +216,10 @@ private Response> attempt(final HttpRequest httpRequest, final HttpPipelineNex
return attempt(httpRequest, next, tryCount + 1, suppressed);
} else {
if (tryCount >= maxRetries) {
- logRetryExhausted(tryCount);
+ // TODO (limolkova): do we have better heuristic to determine if we're retrying because of error
+ // or because we got successful response?
+ logRetry(logger.atWarning(), tryCount, null, null, true, instrumentationContext);
}
-
return response;
}
}
@@ -269,20 +283,25 @@ private boolean shouldRetryException(Exception exception, int tryCount, ListOpenTelemetry semantic conventions.
+ *
+ * These keys unify how core logs HTTP requests, responses or anything
+ * else and simplify telemetry analysis.
+ *
+ * When reporting in client libraries, please do the best effort to stay consistent with these keys, but copy the value.
+ */
+public final class AttributeKeys {
+ // Standard attribute names (defined in OpenTelemetry semantic conventions)
+
+ /**
+ * A class of error the operation ended with such as a fully-qualified exception type or a domain-specific error code.
+ * error.type attribute
+ */
+ public static final String ERROR_TYPE_KEY = "error.type";
+
+ /**
+ * Exception message.
+ * exception.message attribute
+ */
+ public static final String EXCEPTION_MESSAGE_KEY = "exception.message";
+
+ /**
+ * Exception stacktrace.
+ * exception.stacktrace attribute
+ */
+ public static final String EXCEPTION_STACKTRACE_KEY = "exception.stacktrace";
+
+ /**
+ * Exception type.
+ * exception.type attribute
+ */
+ public static final String EXCEPTION_TYPE_KEY = "exception.type";
+
+ /**
+ * The name of the logging event.
+ * event.name attribute
+ */
+ public static final String EVENT_NAME_KEY = "event.name";
+
+ /**
+ * The HTTP request method.
+ * http.request.method attribute
+ */
+ public static final String HTTP_REQUEST_METHOD_KEY = "http.request.method";
+
+ /**
+ * The ordinal number of request resending attempt (for any reason, including redirects)
+ * The value starts with {@code 0} on the first try
+ * and should be an {@code int} number.
+ * http.request.resend_count attribute
+ */
+ public static final String HTTP_REQUEST_RESEND_COUNT_KEY = "http.request.resend_count";
+
+ /**
+ * The size of the request payload body in bytes. It usually matches the value of the Content-Length header.
+ * http.request.body.size attribute
+ */
+ public static final String HTTP_REQUEST_BODY_SIZE_KEY = "http.request.body.size";
+
+ /**
+ * The value of request content length header.
+ * http.request.header.content-length attribute
+ */
+ public static final String HTTP_REQUEST_HEADER_CONTENT_LENGTH_KEY = "http.request.header.content-length";
+
+ /**
+ * The value of request traceparent header.
+ * http.request.header.content-length attribute
+ */
+ public static final String HTTP_REQUEST_HEADER_TRACEPARENT_KEY = "http.request.header.traceparent";
+
+ /**
+ * The value of response content length header.
+ * http.response.header.content-length attribute
+ */
+ public static final String HTTP_RESPONSE_HEADER_CONTENT_LENGTH_KEY = "http.response.header.content-length";
+
+ /**
+ * The value of response location header indicating the URL to redirect to.
+ * http.response.header.location attribute
+ */
+ public static final String HTTP_RESPONSE_HEADER_LOCATION_KEY = "http.response.header.location";
+
+ /**
+ * The size of the response payload body in bytes. It usually matches the value of the Content-Length header.
+ * http.response.body.size attribute
+ */
+ public static final String HTTP_RESPONSE_BODY_SIZE_KEY = "http.response.body.size";
+
+ /**
+ * The HTTP response status code. The value should be a number.
+ * http.response.status_code attribute
+ */
+ public static final String HTTP_RESPONSE_STATUS_CODE_KEY = "http.response.status_code";
+
+ /**
+ * Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.
+ * server.address attribute
+ */
+ public static final String SERVER_ADDRESS_KEY = "server.address";
+
+ /**
+ * Server port number.
+ * server.port attribute
+ */
+ public static final String SERVER_PORT_KEY = "server.port";
+
+ /**
+ * The request user agent.
+ * user_agent.original attribute
+ */
+ public static final String USER_AGENT_ORIGINAL_KEY = "user_agent.original";
+
+ /**
+ * Absolute URL describing a network resource.
+ * url.full attribute
+ */
+ public static final String URL_FULL_KEY = "url.full";
+
+ // Custom attribute names, use with caution
+ /**
+ * Key representing duration of call in milliseconds, the value should be a number.
+ */
+ public static final String HTTP_REQUEST_TIME_TO_RESPONSE_KEY = "http.request.time_to_response";
+
+ /**
+ * Key representing duration of call in milliseconds, the value should be a number.
+ */
+ public static final String HTTP_REQUEST_DURATION_KEY = "http.request.duration";
+
+ /**
+ * Key representing request body. The value should be populated conditionally
+ * if populated at all.
+ */
+ public static final String HTTP_REQUEST_BODY_CONTENT_KEY = "http.request.body.content";
+
+ /**
+ * Key representing response body. The value should be populated conditionally
+ * if populated at all.
+ */
+ public static final String HTTP_RESPONSE_BODY_CONTENT_KEY = "http.request.body.content";
+
+ /**
+ * Key representing maximum number of redirects or retries. It's reported when the number of redirects or retries
+ * was exhausted.
+ */
+ public static final String RETRY_MAX_ATTEMPT_COUNT_KEY = "retry.max_attempt_count";
+
+ /**
+ * Key representing delay before next retry attempt in milliseconds. The value should be a number.
+ */
+ public static final String RETRY_DELAY_KEY = "retry.delay";
+
+ /**
+ * Key representing whether the retry jor redirect ust performed was the last attempt.
+ */
+ public static final String RETRY_WAS_LAST_ATTEMPT_KEY = "retry.was_last_attempt";
+
+ /**
+ * Key representing span id on logs.
+ */
+ public static final String SPAN_ID_KEY = "span.id";
+
+ /**
+ * Key representing parent span id on logs.
+ */
+ public static final String SPAN_PARENT_ID_KEY = "span.parent.id";
+
+ /**
+ * Key representing span name on logs.
+ */
+ public static final String SPAN_NAME_KEY = "span.name";
+
+ /**
+ * Key representing span kind on logs.
+ */
+ public static final String SPAN_KIND_KEY = "span.kind";
+
+ /**
+ * Key representing span duration (in milliseconds) on logs. The value should be a number.
+ */
+ public static final String SPAN_DURATION_KEY = "span.duration";
+
+ /**
+ * Key representing trace id on logs.
+ */
+ public static final String TRACE_ID_KEY = "trace.id";
+
+ private AttributeKeys() {
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DefaultLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultLogger.java
similarity index 96%
rename from sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DefaultLogger.java
rename to sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultLogger.java
index 18c3e58042d4..df717ff5a92b 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DefaultLogger.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultLogger.java
@@ -1,9 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-package io.clientcore.core.implementation.util;
+package io.clientcore.core.implementation.instrumentation;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.implementation.util.EnvironmentConfiguration;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.configuration.Configuration;
import java.io.PrintStream;
@@ -13,7 +14,7 @@
import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
-import static io.clientcore.core.util.ClientLogger.LogLevel;
+import static io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel;
/**
* This class is an internal implementation of slf4j logger.
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LoggingEventNames.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LoggingEventNames.java
new file mode 100644
index 000000000000..a3a3213e2186
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LoggingEventNames.java
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.implementation.instrumentation;
+
+/**
+ * This class contains the names of the logging events that are emitted by the client core.
+ */
+public class LoggingEventNames {
+ // HTTP logging event names. None of them are defined in otel semantic conventions.
+ /**
+ * Identifies event that is logged when an HTTP request is sent.
+ * Depending on configuration and implementation, this event may be logged when request headers are sent or when
+ * the request body is fully written.
+ */
+ public static final String HTTP_REQUEST_EVENT_NAME = "http.request";
+
+ /**
+ * Identifies event that is logged when an HTTP response is received.
+ * Depending on configuration and implementation, this event may be logged when response headers and status code
+ * are received or when the response body is fully read.
+ */
+ public static final String HTTP_RESPONSE_EVENT_NAME = "http.response";
+
+ /**
+ * Identifies event that is logged when an HTTP request is being redirected to another URL.
+ * The event describes whether the redirect will be followed or not along with redirect context.
+ */
+ public static final String HTTP_REDIRECT_EVENT_NAME = "http.redirect";
+
+ /**
+ * Identifies event that is logged after an HTTP request has failed and is considered to be retried.
+ * The event describes whether the retry will be performed or not.
+ */
+ public static final String HTTP_RETRY_EVENT_NAME = "http.retry";
+
+ // Other logging event names
+
+ /**
+ * Identifies event that is logged when a span is ended.
+ */
+ public static final String SPAN_ENDED_EVENT_NAME = "span.ended";
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Slf4jLoggerShim.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShim.java
similarity index 95%
rename from sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Slf4jLoggerShim.java
rename to sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShim.java
index 8ecd69e34f32..7d7074724d37 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Slf4jLoggerShim.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShim.java
@@ -1,16 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-package io.clientcore.core.implementation.util;
+package io.clientcore.core.implementation.instrumentation;
import io.clientcore.core.implementation.ReflectionUtils;
import io.clientcore.core.implementation.ReflectiveInvoker;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
-import static io.clientcore.core.util.ClientLogger.LogLevel.ERROR;
-import static io.clientcore.core.util.ClientLogger.LogLevel.INFORMATIONAL;
-import static io.clientcore.core.util.ClientLogger.LogLevel.VERBOSE;
-import static io.clientcore.core.util.ClientLogger.LogLevel.WARNING;
+import static io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel.ERROR;
+import static io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel.INFORMATIONAL;
+import static io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel.VERBOSE;
+import static io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel.WARNING;
public class Slf4jLoggerShim {
private static final DefaultLogger DEFAULT_LOGGER = new DefaultLogger(Slf4jLoggerShim.class);
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackContextPropagator.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackContextPropagator.java
new file mode 100644
index 000000000000..7af20884e802
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackContextPropagator.java
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.implementation.instrumentation.fallback;
+
+import io.clientcore.core.instrumentation.InstrumentationContext;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
+import io.clientcore.core.instrumentation.tracing.Span;
+import io.clientcore.core.instrumentation.tracing.TraceContextGetter;
+import io.clientcore.core.instrumentation.tracing.TraceContextPropagator;
+import io.clientcore.core.instrumentation.tracing.TraceContextSetter;
+
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_HEADER_TRACEPARENT_KEY;
+
+final class FallbackContextPropagator implements TraceContextPropagator {
+ private static final ClientLogger LOGGER = new ClientLogger(FallbackContextPropagator.class);
+ static final TraceContextPropagator W3C_TRACE_CONTEXT_PROPAGATOR = new FallbackContextPropagator();
+
+ private FallbackContextPropagator() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void inject(InstrumentationContext spanContext, C carrier, TraceContextSetter setter) {
+ if (spanContext.isValid()) {
+ setter.set(carrier, "traceparent",
+ "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-" + spanContext.getTraceFlags());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InstrumentationContext extract(InstrumentationContext context, C carrier, TraceContextGetter getter) {
+ String traceparent = getter.get(carrier, "traceparent");
+ if (traceparent != null) {
+ if (isValidTraceparent(traceparent)) {
+ String traceId = traceparent.substring(3, 35);
+ String spanId = traceparent.substring(36, 52);
+ String traceFlags = traceparent.substring(53, 55);
+ return new FallbackSpanContext(traceId, spanId, traceFlags, true, Span.noop());
+ } else {
+ LOGGER.atVerbose()
+ .addKeyValue(HTTP_REQUEST_HEADER_TRACEPARENT_KEY, traceparent)
+ .log("Invalid traceparent header");
+ }
+ }
+ return context == null ? FallbackSpanContext.INVALID : context;
+ }
+
+ /**
+ * Validates the traceparent header according to W3C Trace Context
+ *
+ * @param traceparent the traceparent header value
+ * @return true if the traceparent header is valid, false otherwise
+ */
+ private static boolean isValidTraceparent(String traceparent) {
+ if (traceparent == null || traceparent.length() != 55) {
+ return false;
+ }
+
+ // valid traceparent format: ---
+ // version - only 00 is supported
+ if (traceparent.charAt(0) != '0'
+ || traceparent.charAt(1) != '0'
+ || traceparent.charAt(2) != '-'
+ || traceparent.charAt(35) != '-'
+ || traceparent.charAt(52) != '-') {
+ return false;
+ }
+
+ // trace-id - 32 lower case hex characters, all 0 is invalid
+ boolean isAllZero = true;
+ for (int i = 3; i < 35; i++) {
+ char c = traceparent.charAt(i);
+ if (c < '0' || c > 'f' || (c > '9' && c < 'a')) {
+ return false;
+ }
+ if (c != '0') {
+ isAllZero = false;
+ }
+ }
+ if (isAllZero) {
+ return false;
+ }
+
+ // span-id - 16 lower case hex characters, all 0 is invalid
+ isAllZero = true;
+ for (int i = 36; i < 52; i++) {
+ char c = traceparent.charAt(i);
+ if (c < '0' || c > 'f' || (c > '9' && c < 'a')) {
+ return false;
+ }
+ if (c != '0') {
+ isAllZero = false;
+ }
+ }
+
+ if (isAllZero) {
+ return false;
+ }
+
+ // trace-flags - 2 lower case hex characters
+ for (int i = 53; i < 55; i++) {
+ char c = traceparent.charAt(i);
+ if (c < '0' || c > 'f' || (c > '9' && c < 'a')) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentation.java
new file mode 100644
index 000000000000..109398c9efbd
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentation.java
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.implementation.instrumentation.fallback;
+
+import io.clientcore.core.instrumentation.Instrumentation;
+import io.clientcore.core.instrumentation.InstrumentationContext;
+import io.clientcore.core.instrumentation.InstrumentationOptions;
+import io.clientcore.core.instrumentation.LibraryInstrumentationOptions;
+import io.clientcore.core.instrumentation.tracing.TraceContextPropagator;
+import io.clientcore.core.instrumentation.tracing.Tracer;
+
+/**
+ * Fallback implementation of {@link Instrumentation} which implements basic correlation and context propagation
+ * and, when enabled, records traces as logs.
+ */
+public class FallbackInstrumentation implements Instrumentation {
+ public static final FallbackInstrumentation DEFAULT_INSTANCE = new FallbackInstrumentation(null, null);
+
+ private final InstrumentationOptions> instrumentationOptions;
+ private final LibraryInstrumentationOptions libraryOptions;
+
+ /**
+ * Creates a new instance of {@link FallbackInstrumentation}.
+ * @param instrumentationOptions the application instrumentation options
+ * @param libraryOptions the library instrumentation options
+ */
+ public FallbackInstrumentation(InstrumentationOptions> instrumentationOptions,
+ LibraryInstrumentationOptions libraryOptions) {
+ this.instrumentationOptions = instrumentationOptions;
+ this.libraryOptions = libraryOptions;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Tracer getTracer() {
+ return new FallbackTracer(instrumentationOptions, libraryOptions);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public TraceContextPropagator getW3CTraceContextPropagator() {
+ return FallbackContextPropagator.W3C_TRACE_CONTEXT_PROPAGATOR;
+ }
+
+ /**
+ * Creates a new instance of {@link InstrumentationContext} from the given object.
+ * It recognizes {@link FallbackSpanContext}, {@link FallbackSpan}, and generic {@link InstrumentationContext}
+ * as a source and converts them to {@link FallbackSpanContext}.
+ * @param context the context object to convert
+ * @return the instance of {@link InstrumentationContext} which is invalid if the context is not recognized
+ * @param the type of the context object
+ */
+ public InstrumentationContext createInstrumentationContext(T context) {
+ if (context instanceof InstrumentationContext) {
+ return FallbackSpanContext.fromInstrumentationContext((InstrumentationContext) context);
+ } else if (context instanceof FallbackSpan) {
+ return ((FallbackSpan) context).getInstrumentationContext();
+ } else {
+ return FallbackSpanContext.INVALID;
+ }
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackScope.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackScope.java
new file mode 100644
index 000000000000..008b79ce6599
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackScope.java
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.implementation.instrumentation.fallback;
+
+import io.clientcore.core.instrumentation.logging.ClientLogger;
+import io.clientcore.core.instrumentation.tracing.Span;
+import io.clientcore.core.instrumentation.tracing.TracingScope;
+
+final class FallbackScope implements TracingScope {
+ private static final ClientLogger LOGGER = new ClientLogger(FallbackScope.class);
+ private static final ThreadLocal CURRENT_SPAN = new ThreadLocal<>();
+ private final FallbackSpan originalSpan;
+ private final FallbackSpan span;
+
+ FallbackScope(FallbackSpan span) {
+ this.originalSpan = CURRENT_SPAN.get();
+ this.span = span;
+ CURRENT_SPAN.set(span);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() {
+ if (CURRENT_SPAN.get() == span) {
+ CURRENT_SPAN.set(originalSpan);
+ } else {
+ LOGGER.atVerbose().log("Attempting to close scope that is not the current. Ignoring.");
+ }
+ }
+
+ static Span getCurrentSpan() {
+ FallbackSpan span = CURRENT_SPAN.get();
+ return span == null ? Span.noop() : span;
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpan.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpan.java
new file mode 100644
index 000000000000..33c420b31876
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpan.java
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.implementation.instrumentation.fallback;
+
+import io.clientcore.core.instrumentation.InstrumentationContext;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
+import io.clientcore.core.instrumentation.tracing.Span;
+import io.clientcore.core.instrumentation.tracing.TracingScope;
+
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.ERROR_TYPE_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_DURATION_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_ID_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.TRACE_ID_KEY;
+import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.SPAN_ENDED_EVENT_NAME;
+
+final class FallbackSpan implements Span {
+ private final ClientLogger.LoggingEvent log;
+ private final long startTime;
+ private final FallbackSpanContext spanContext;
+ private String errorType;
+
+ FallbackSpan(ClientLogger.LoggingEvent log, FallbackSpanContext parentSpanContext, boolean isRecording) {
+ this.log = log;
+ this.startTime = isRecording ? System.nanoTime() : 0;
+ this.spanContext = FallbackSpanContext.fromParent(parentSpanContext, isRecording, this);
+ if (log != null && log.isEnabled()) {
+ this.log.addKeyValue(TRACE_ID_KEY, spanContext.getTraceId())
+ .addKeyValue(SPAN_ID_KEY, spanContext.getSpanId());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Span setAttribute(String key, Object value) {
+ if (log != null) {
+ log.addKeyValue(key, value);
+ }
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Span setError(String errorType) {
+ this.errorType = errorType;
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void end() {
+ end(null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void end(Throwable error) {
+ if (log == null || !log.isEnabled()) {
+ return;
+ }
+
+ double durationMs = (System.nanoTime() - startTime) / 1_000_000.0;
+ log.addKeyValue(SPAN_DURATION_KEY, durationMs);
+ if (error != null || errorType != null) {
+ setAttribute(ERROR_TYPE_KEY, errorType != null ? errorType : error.getClass().getCanonicalName());
+ }
+
+ log.setEventName(SPAN_ENDED_EVENT_NAME);
+ log.log(null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isRecording() {
+ return log != null && log.isEnabled();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public TracingScope makeCurrent() {
+ return new FallbackScope(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InstrumentationContext getInstrumentationContext() {
+ return spanContext;
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanBuilder.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanBuilder.java
new file mode 100644
index 000000000000..b2e67a3a5abc
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanBuilder.java
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.implementation.instrumentation.fallback;
+
+import io.clientcore.core.instrumentation.InstrumentationContext;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
+import io.clientcore.core.instrumentation.tracing.Span;
+import io.clientcore.core.instrumentation.tracing.SpanBuilder;
+import io.clientcore.core.instrumentation.tracing.SpanKind;
+
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_KIND_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_NAME_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_PARENT_ID_KEY;
+
+final class FallbackSpanBuilder implements SpanBuilder {
+ static final FallbackSpanBuilder NOOP = new FallbackSpanBuilder();
+ private final ClientLogger.LoggingEvent log;
+ private final FallbackSpanContext parentSpanContext;
+
+ private FallbackSpanBuilder() {
+ this.log = null;
+ this.parentSpanContext = FallbackSpanContext.INVALID;
+ }
+
+ FallbackSpanBuilder(ClientLogger logger, String spanName, SpanKind spanKind,
+ InstrumentationContext instrumentationContext) {
+ this.parentSpanContext = FallbackSpanContext.fromInstrumentationContext(instrumentationContext);
+ this.log = logger.atVerbose();
+ if (log.isEnabled()) {
+ log.addKeyValue(SPAN_NAME_KEY, spanName).addKeyValue(SPAN_KIND_KEY, spanKind.name());
+ if (parentSpanContext.isValid()) {
+ log.addKeyValue(SPAN_PARENT_ID_KEY, parentSpanContext.getSpanId());
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SpanBuilder setAttribute(String key, Object value) {
+ if (log != null) {
+ log.addKeyValue(key, value);
+ }
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Span startSpan() {
+ if (log != null) {
+ return new FallbackSpan(log, parentSpanContext, log.isEnabled());
+ }
+
+ return Span.noop();
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanContext.java
new file mode 100644
index 000000000000..f5a588451d05
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanContext.java
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.implementation.instrumentation.fallback;
+
+import io.clientcore.core.instrumentation.InstrumentationContext;
+import io.clientcore.core.instrumentation.tracing.Span;
+
+import static io.clientcore.core.implementation.instrumentation.fallback.RandomIdUtils.INVALID_SPAN_ID;
+import static io.clientcore.core.implementation.instrumentation.fallback.RandomIdUtils.INVALID_TRACE_ID;
+import static io.clientcore.core.implementation.instrumentation.fallback.RandomIdUtils.generateSpanId;
+import static io.clientcore.core.implementation.instrumentation.fallback.RandomIdUtils.generateTraceId;
+
+final class FallbackSpanContext implements InstrumentationContext {
+ static final FallbackSpanContext INVALID
+ = new FallbackSpanContext(INVALID_TRACE_ID, INVALID_SPAN_ID, "00", false, Span.noop());
+ private final String traceId;
+ private final String spanId;
+ private final String traceFlags;
+ private final boolean isValid;
+ private final Span span;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getTraceId() {
+ return traceId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getSpanId() {
+ return spanId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isValid() {
+ return isValid;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Span getSpan() {
+ return this.span;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getTraceFlags() {
+ return traceFlags;
+ }
+
+ FallbackSpanContext(String traceId, String spanId, String traceFlags, boolean isValid, Span span) {
+ this.traceId = traceId;
+ this.spanId = spanId;
+ this.traceFlags = traceFlags;
+ this.isValid = isValid;
+ this.span = span;
+ }
+
+ static FallbackSpanContext fromParent(InstrumentationContext parent, boolean isSampled, FallbackSpan span) {
+ return parent.isValid()
+ ? new FallbackSpanContext(parent.getTraceId(), generateSpanId(), isSampled ? "01" : "00", true, span)
+ : new FallbackSpanContext(generateTraceId(), generateSpanId(), isSampled ? "01" : "00", true, span);
+ }
+
+ static FallbackSpanContext fromInstrumentationContext(InstrumentationContext instrumentationContext) {
+ if (instrumentationContext instanceof FallbackSpanContext) {
+ return (FallbackSpanContext) instrumentationContext;
+ }
+
+ if (instrumentationContext != null) {
+ return new FallbackSpanContext(instrumentationContext.getTraceId(), instrumentationContext.getSpanId(),
+ instrumentationContext.getTraceFlags(), instrumentationContext.isValid(),
+ instrumentationContext.getSpan());
+ }
+
+ Span currentSpan = FallbackScope.getCurrentSpan();
+ if (currentSpan != Span.noop()) {
+ return (FallbackSpanContext) currentSpan.getInstrumentationContext();
+ }
+
+ return INVALID;
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracer.java
new file mode 100644
index 000000000000..006d86ebeacf
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracer.java
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.implementation.instrumentation.fallback;
+
+import io.clientcore.core.instrumentation.InstrumentationContext;
+import io.clientcore.core.instrumentation.InstrumentationOptions;
+import io.clientcore.core.instrumentation.LibraryInstrumentationOptions;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
+import io.clientcore.core.instrumentation.tracing.SpanBuilder;
+import io.clientcore.core.instrumentation.tracing.SpanKind;
+import io.clientcore.core.instrumentation.tracing.Tracer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+final class FallbackTracer implements Tracer {
+ private static final ClientLogger LOGGER = new ClientLogger(FallbackTracer.class);
+ private final boolean isEnabled;
+ private final ClientLogger logger;
+
+ FallbackTracer(InstrumentationOptions> instrumentationOptions, LibraryInstrumentationOptions libraryOptions) {
+ // TODO (limolkova): do we need additional config to enable fallback tracing? Or maybe we enable it only if logs are enabled?
+ this.isEnabled = instrumentationOptions == null || instrumentationOptions.isTracingEnabled();
+ this.logger = isEnabled ? getLogger(instrumentationOptions, libraryOptions) : LOGGER;
+ }
+
+ private static ClientLogger getLogger(InstrumentationOptions> instrumentationOptions,
+ LibraryInstrumentationOptions libraryOptions) {
+ Object providedLogger = instrumentationOptions == null ? null : instrumentationOptions.getProvider();
+ if (providedLogger instanceof ClientLogger) {
+ return (ClientLogger) providedLogger;
+ }
+
+ Map libraryContext = new HashMap<>(2);
+ libraryContext.put("library.name", libraryOptions.getLibraryName());
+ libraryContext.put("library.version", libraryOptions.getLibraryVersion());
+
+ return new ClientLogger(libraryOptions.getLibraryName() + ".tracing", libraryContext);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, InstrumentationContext instrumentationContext) {
+ return isEnabled
+ ? new FallbackSpanBuilder(logger, spanName, spanKind, instrumentationContext)
+ : FallbackSpanBuilder.NOOP;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/RandomIdUtils.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/RandomIdUtils.java
new file mode 100644
index 000000000000..205f9b77be48
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/RandomIdUtils.java
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+// This code was copied from the OpenTelemetry Java SDK
+// https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RandomIdGenerator.java
+// and modified to fit the needs of the ClientCore library.
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.clientcore.core.implementation.instrumentation.fallback;
+
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+class RandomIdUtils {
+ public static final String INVALID_TRACE_ID = "00000000000000000000000000000000";
+ public static final String INVALID_SPAN_ID = "0000000000000000";
+
+ private static final int BYTE_BASE16 = 2;
+ private static final long INVALID_ID = 0;
+ private static final int TRACE_ID_HEX_LENGTH = 32;
+ private static final int SPAN_ID_HEX_LENGTH = 16;
+ private static final char[] ENCODING = buildEncodingArray();
+
+ public static String generateSpanId() {
+ long id;
+ do {
+ id = ThreadLocalRandom.current().nextLong();
+ } while (id == INVALID_ID);
+ return getSpanId(id);
+ }
+
+ public static String generateTraceId() {
+ Random random = ThreadLocalRandom.current();
+ long idHi = random.nextLong();
+ long idLo;
+ do {
+ idLo = random.nextLong();
+ } while (idLo == INVALID_ID);
+ return getTraceId(idHi, idLo);
+ }
+
+ private static String getSpanId(long id) {
+ if (id == 0) {
+ return INVALID_SPAN_ID;
+ }
+ char[] result = new char[SPAN_ID_HEX_LENGTH];
+ longToBase16String(id, result, 0);
+ return new String(result, 0, SPAN_ID_HEX_LENGTH);
+ }
+
+ private static String getTraceId(long traceIdLongHighPart, long traceIdLongLowPart) {
+ if (traceIdLongHighPart == 0 && traceIdLongLowPart == 0) {
+ return INVALID_TRACE_ID;
+ }
+ char[] chars = new char[TRACE_ID_HEX_LENGTH];
+ longToBase16String(traceIdLongHighPart, chars, 0);
+ longToBase16String(traceIdLongLowPart, chars, 16);
+ return new String(chars, 0, TRACE_ID_HEX_LENGTH);
+ }
+
+ private static void longToBase16String(long value, char[] dest, int destOffset) {
+ byteToBase16((byte) (value >> 56 & 0xFFL), dest, destOffset);
+ byteToBase16((byte) (value >> 48 & 0xFFL), dest, destOffset + BYTE_BASE16);
+ byteToBase16((byte) (value >> 40 & 0xFFL), dest, destOffset + 2 * BYTE_BASE16);
+ byteToBase16((byte) (value >> 32 & 0xFFL), dest, destOffset + 3 * BYTE_BASE16);
+ byteToBase16((byte) (value >> 24 & 0xFFL), dest, destOffset + 4 * BYTE_BASE16);
+ byteToBase16((byte) (value >> 16 & 0xFFL), dest, destOffset + 5 * BYTE_BASE16);
+ byteToBase16((byte) (value >> 8 & 0xFFL), dest, destOffset + 6 * BYTE_BASE16);
+ byteToBase16((byte) (value & 0xFFL), dest, destOffset + 7 * BYTE_BASE16);
+ }
+
+ private static void byteToBase16(byte value, char[] dest, int destOffset) {
+ int b = value & 0xFF;
+ dest[destOffset] = ENCODING[b];
+ dest[destOffset + 1] = ENCODING[b | 0x100];
+ }
+
+ private static char[] buildEncodingArray() {
+ String alphabet = "0123456789abcdef";
+ char[] encoding = new char[512];
+ for (int i = 0; i < 256; ++i) {
+ encoding[i] = alphabet.charAt(i >>> 4);
+ encoding[i | 0x100] = alphabet.charAt(i & 0xF);
+ }
+ return encoding;
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/package-info.java
new file mode 100644
index 000000000000..5dd2110ab7d7
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/package-info.java
@@ -0,0 +1,8 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+/**
+ * This package contains fallback implementation of {@link java.lang.instrument.Instrumentation}
+ * that implements basic distributed tracing.
+ */
+package io.clientcore.core.implementation.instrumentation.fallback;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/FallbackInvoker.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/FallbackInvoker.java
index 72d85b0da7fd..f3920928d277 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/FallbackInvoker.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/FallbackInvoker.java
@@ -4,7 +4,7 @@
package io.clientcore.core.implementation.instrumentation.otel;
import io.clientcore.core.implementation.ReflectiveInvoker;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
/**
* A wrapper around a {@link ReflectiveInvoker} that provides a fallback value if the invocation fails,
@@ -39,6 +39,7 @@ public FallbackInvoker(ReflectiveInvoker inner, Object fallback, ClientLogger lo
/**
* Invokes the inner invoker and returns the fallback value if the invocation fails.
+ *
* @return the result of the invocation or the fallback value
*/
public Object invoke() {
@@ -52,6 +53,7 @@ public Object invoke() {
/**
* Invokes the inner invoker and returns the fallback value if the invocation fails.
+ *
* @param argOrTarget the argument or target
* @return the result of the invocation or the fallback value
*/
@@ -66,6 +68,7 @@ public Object invoke(Object argOrTarget) {
/**
* Invokes the inner invoker and returns the fallback value if the invocation fails.
+ *
* @param argOrTarget the argument or target
* @param arg1 the first argument
* @return the result of the invocation or the fallback value
@@ -81,6 +84,7 @@ public Object invoke(Object argOrTarget, Object arg1) {
/**
* Invokes the inner invoker and returns the fallback value if the invocation fails.
+ *
* @param argOrTarget the argument or target
* @param arg1 the first argument
* @param arg2 the second argument
@@ -97,6 +101,7 @@ public Object invoke(Object argOrTarget, Object arg1, Object arg2) {
/**
* Invokes the inner invoker and returns the fallback value if the invocation fails.
+ *
* @param argOrTarget the argument or target
* @param arg1 the first argument
* @param arg2 the second argument
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelAttributeKey.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelAttributeKey.java
index 4e1b1932aa1c..d6facac4803a 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelAttributeKey.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelAttributeKey.java
@@ -4,7 +4,7 @@
package io.clientcore.core.implementation.instrumentation.otel;
import io.clientcore.core.implementation.ReflectiveInvoker;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.ATTRIBUTE_KEY_CLASS;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInitializer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInitializer.java
index 7b0e5eb1b2a2..3e9b02871bd6 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInitializer.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInitializer.java
@@ -3,7 +3,7 @@
package io.clientcore.core.implementation.instrumentation.otel;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
/**
* This class is used to initialize OpenTelemetry.
@@ -186,4 +186,5 @@ public static void runtimeError(ClientLogger logger, Throwable t) {
public static boolean isInitialized() {
return INSTANCE.initialized;
}
+
}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInstrumentation.java
index 6edbbb51a4eb..38783b3bf6bc 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInstrumentation.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInstrumentation.java
@@ -4,18 +4,24 @@
package io.clientcore.core.implementation.instrumentation.otel;
import io.clientcore.core.implementation.ReflectiveInvoker;
+import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpan;
+import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext;
import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelTraceContextPropagator;
import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelTracer;
+import io.clientcore.core.instrumentation.Instrumentation;
+import io.clientcore.core.instrumentation.InstrumentationContext;
import io.clientcore.core.instrumentation.LibraryInstrumentationOptions;
import io.clientcore.core.instrumentation.InstrumentationOptions;
-import io.clientcore.core.instrumentation.Instrumentation;
import io.clientcore.core.instrumentation.tracing.TraceContextPropagator;
import io.clientcore.core.instrumentation.tracing.Tracer;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker;
+import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.GLOBAL_OTEL_CLASS;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.OTEL_CLASS;
+import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CLASS;
+import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CONTEXT_CLASS;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACER_PROVIDER_CLASS;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.W3C_PROPAGATOR_CLASS;
@@ -60,6 +66,7 @@ public class OTelInstrumentation implements Instrumentation {
W3C_PROPAGATOR_INSTANCE = new OTelTraceContextPropagator(w3cPropagatorInstance);
}
+ public static final OTelInstrumentation DEFAULT_INSTANCE = new OTelInstrumentation(null, null);
private final Object otelInstance;
private final LibraryInstrumentationOptions libraryOptions;
@@ -111,6 +118,32 @@ public TraceContextPropagator getW3CTraceContextPropagator() {
return OTelInitializer.isInitialized() ? W3C_PROPAGATOR_INSTANCE : OTelTraceContextPropagator.NOOP;
}
+ /**
+ * Creates a new instance of {@link InstrumentationContext} from the given object.
+ * It recognizes {@code io.opentelemetry.api.trace.Span}, {@code io.opentelemetry.api.trace.SpanContext},
+ * {@code io.opentelemetry.context.Context} and generic {@link InstrumentationContext}
+ * as a source and converts them to {@link InstrumentationContext}.
+ * @param context the context object to convert
+ * @return the instance of {@link InstrumentationContext} which is invalid if the context is not recognized
+ * @param the type of the context object
+ */
+ public InstrumentationContext createInstrumentationContext(T context) {
+ if (context instanceof InstrumentationContext) {
+ return (InstrumentationContext) context;
+ } else if (context instanceof OTelSpan) {
+ return ((OTelSpan) context).getInstrumentationContext();
+ } else if (SPAN_CLASS.isInstance(context)) {
+ return OTelSpanContext.fromOTelSpan(context);
+ } else if (CONTEXT_CLASS.isInstance(context)) {
+ return OTelSpanContext.fromOTelContext(context);
+ } else if (SPAN_CONTEXT_CLASS.isInstance(context)) {
+ return new OTelSpanContext(context, null);
+ }
+
+ return OTelSpanContext.getInvalid();
+
+ }
+
private Object getOtelInstance() {
// not caching global to prevent caching instance that was not setup yet at the start time.
return otelInstance != null ? otelInstance : GET_GLOBAL_OTEL_INVOKER.invoke();
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/package-info.java
index ead18dbf6d52..d3c6f6ccd74e 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/package-info.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/package-info.java
@@ -2,6 +2,6 @@
// Licensed under the MIT License.
/**
- * This package contains the implementation of the OpenTelemetry telemetry provider.
+ * This package contains OpenTelemetry-based implementation of {@link java.lang.instrument.Instrumentation}
*/
package io.clientcore.core.implementation.instrumentation.otel;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelContext.java
index 1d7219bc52a8..bfe298ea4f13 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelContext.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelContext.java
@@ -6,9 +6,9 @@
import io.clientcore.core.implementation.ReflectiveInvoker;
import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker;
import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer;
+import io.clientcore.core.instrumentation.InstrumentationContext;
import io.clientcore.core.instrumentation.tracing.TracingScope;
-import io.clientcore.core.instrumentation.tracing.SpanKind;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS;
@@ -24,19 +24,19 @@ class OTelContext {
private static final FallbackInvoker GET_INVOKER;
// this context key will indicate if the span is created by client core
- // AND has client or internal kind (logical client operation)
// this is used to suppress multiple spans created for the same logical operation
// such as convenience API on top of protocol methods when both as instrumented.
// We might need to suppress logical server (consumer) spans in the future, but that
// was not necessary so far
- private static final Object HAS_CLIENT_SPAN_CONTEXT_KEY;
+ private static final Object CLIENT_CORE_SPAN_CONTEXT_KEY;
static {
ReflectiveInvoker currentInvoker = null;
ReflectiveInvoker makeCurrentInvoker = null;
ReflectiveInvoker withInvoker = null;
ReflectiveInvoker getInvoker = null;
- Object hasClientSpanContextKey = null;
+
+ Object clientCoreSpanContextKey = null;
Object rootContext = null;
if (OTelInitializer.isInitialized()) {
@@ -50,10 +50,11 @@ class OTelContext {
ReflectiveInvoker contextKeyNamedInvoker
= getMethodInvoker(CONTEXT_KEY_CLASS, CONTEXT_KEY_CLASS.getMethod("named", String.class));
- hasClientSpanContextKey = contextKeyNamedInvoker.invoke("client-core-call");
+ clientCoreSpanContextKey = contextKeyNamedInvoker.invoke("client-core-span");
ReflectiveInvoker rootInvoker = getMethodInvoker(CONTEXT_CLASS, CONTEXT_CLASS.getMethod("root"));
rootContext = rootInvoker.invoke();
+
} catch (Throwable t) {
OTelInitializer.initError(LOGGER, t);
}
@@ -63,7 +64,7 @@ class OTelContext {
MAKE_CURRENT_INVOKER = new FallbackInvoker(makeCurrentInvoker, NOOP_SCOPE, LOGGER);
WITH_INVOKER = new FallbackInvoker(withInvoker, LOGGER);
GET_INVOKER = new FallbackInvoker(getInvoker, LOGGER);
- HAS_CLIENT_SPAN_CONTEXT_KEY = hasClientSpanContextKey;
+ CLIENT_CORE_SPAN_CONTEXT_KEY = clientCoreSpanContextKey;
}
static Object getCurrent() {
@@ -79,21 +80,41 @@ static AutoCloseable makeCurrent(Object context) {
return (AutoCloseable) scope;
}
- static Object markCoreSpan(Object context, SpanKind spanKind) {
+ static Object markCoreSpan(Object context, OTelSpan span) {
assert CONTEXT_CLASS.isInstance(context);
- if (spanKind == SpanKind.CLIENT || spanKind == SpanKind.INTERNAL) {
- Object updatedContext = WITH_INVOKER.invoke(context, HAS_CLIENT_SPAN_CONTEXT_KEY, Boolean.TRUE);
- if (updatedContext != null) {
- return updatedContext;
- }
- }
- return context;
+ Object updatedContext = WITH_INVOKER.invoke(context, CLIENT_CORE_SPAN_CONTEXT_KEY, span);
+ return updatedContext == null ? context : updatedContext;
}
- static boolean hasClientCoreSpan(Object context) {
+ static OTelSpan getClientCoreSpan(Object context) {
assert CONTEXT_CLASS.isInstance(context);
- Object flag = GET_INVOKER.invoke(context, HAS_CLIENT_SPAN_CONTEXT_KEY);
- assert flag == null || flag instanceof Boolean;
- return Boolean.TRUE.equals(flag);
+ Object clientCoreSpan = GET_INVOKER.invoke(context, CLIENT_CORE_SPAN_CONTEXT_KEY);
+ assert clientCoreSpan == null || clientCoreSpan instanceof OTelSpan;
+ return (OTelSpan) clientCoreSpan;
+ }
+
+ /**
+ * Get the OpenTelemetry context from the given context.
+ *
+ * @param context the context
+ * @return the OpenTelemetry context
+ */
+ static Object fromInstrumentationContext(InstrumentationContext context) {
+ if (context instanceof OTelSpanContext) {
+ Object otelContext = ((OTelSpanContext) context).getOtelContext();
+ if (otelContext != null) {
+ return otelContext;
+ }
+ }
+
+ Object currentContext = CURRENT_INVOKER.invoke();
+ if (context != null) {
+ Object spanContext = OTelSpanContext.toOTelSpanContext(context);
+ Object span = OTelSpan.wrapSpanContext(spanContext);
+
+ return OTelSpan.storeInContext(span, currentContext);
+ }
+
+ return currentContext;
}
}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpan.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpan.java
index 52a31d0f42e3..48ca484fee58 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpan.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpan.java
@@ -7,14 +7,16 @@
import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker;
import io.clientcore.core.implementation.instrumentation.otel.OTelAttributeKey;
import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer;
+import io.clientcore.core.instrumentation.InstrumentationContext;
import io.clientcore.core.instrumentation.tracing.TracingScope;
import io.clientcore.core.instrumentation.tracing.Span;
import io.clientcore.core.instrumentation.tracing.SpanKind;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.util.Objects;
import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.ERROR_TYPE_KEY;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.ATTRIBUTE_KEY_CLASS;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CLASS;
@@ -28,6 +30,8 @@
*/
public class OTelSpan implements Span {
private static final ClientLogger LOGGER = new ClientLogger(OTelSpan.class);
+ static final OTelSpan NOOP_SPAN;
+ private static final Object ERROR_TYPE_ATTRIBUTE_KEY;
private static final TracingScope NOOP_SCOPE = () -> {
};
private static final FallbackInvoker SET_ATTRIBUTE_INVOKER;
@@ -42,7 +46,9 @@ public class OTelSpan implements Span {
private final Object otelSpan;
private final Object otelContext;
private final boolean isRecording;
+ private final SpanKind spanKind;
private String errorType;
+ private OTelSpanContext spanContext;
static {
ReflectiveInvoker setAttributeInvoker = null;
@@ -55,6 +61,8 @@ public class OTelSpan implements Span {
ReflectiveInvoker wrapInvoker = null;
Object errorStatusCode = null;
+ OTelSpan noopSpan = null;
+ Object errorTypeAttributeKey = null;
if (OTelInitializer.isInitialized()) {
try {
@@ -75,6 +83,14 @@ public class OTelSpan implements Span {
wrapInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("wrap", SPAN_CONTEXT_CLASS));
errorStatusCode = STATUS_CODE_CLASS.getField("ERROR").get(null);
+
+ ReflectiveInvoker getInvalidInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("getInvalid"));
+
+ Object invalidSpan = getInvalidInvoker.invoke();
+ Object rootContext = OTelContext.getCurrent();
+
+ noopSpan = new OTelSpan(invalidSpan, rootContext);
+ errorTypeAttributeKey = OTelAttributeKey.getKey(ERROR_TYPE_KEY, "");
} catch (Throwable t) {
OTelInitializer.initError(LOGGER, t);
}
@@ -88,16 +104,26 @@ public class OTelSpan implements Span {
STORE_IN_CONTEXT_INVOKER = new FallbackInvoker(storeInContextInvoker, LOGGER);
FROM_CONTEXT_INVOKER = new FallbackInvoker(fromContextInvoker, LOGGER);
WRAP_INVOKER = new FallbackInvoker(wrapInvoker, LOGGER);
+ NOOP_SPAN = noopSpan;
ERROR_STATUS_CODE = errorStatusCode;
+ ERROR_TYPE_ATTRIBUTE_KEY = errorTypeAttributeKey;
}
OTelSpan(Object otelSpan, Object otelParentContext, SpanKind spanKind) {
this.otelSpan = otelSpan;
this.isRecording = otelSpan != null && (boolean) IS_RECORDING_INVOKER.invoke(otelSpan);
+ this.spanKind = spanKind;
Object contextWithSpan = otelSpan != null ? storeInContext(otelSpan, otelParentContext) : otelParentContext;
- this.otelContext = markCoreSpan(contextWithSpan, spanKind);
+ this.otelContext = markCoreSpan(contextWithSpan, this);
+ }
+
+ private OTelSpan(Object otelSpan, Object otelContext) {
+ this.otelSpan = otelSpan;
+ this.isRecording = false;
+ this.spanKind = null;
+ this.otelContext = otelContext;
}
/**
@@ -139,17 +165,6 @@ public void end() {
endSpan(null);
}
- /**
- * Gets span context.
- *
- * @return the span context.
- */
- public OTelSpanContext getSpanContext() {
- return isInitialized()
- ? new OTelSpanContext(GET_SPAN_CONTEXT_INVOKER.invoke(otelSpan))
- : OTelSpanContext.getInvalid();
- }
-
/**
* {@inheritDoc}
*/
@@ -166,29 +181,85 @@ public TracingScope makeCurrent() {
return isInitialized() ? wrapOTelScope(OTelContext.makeCurrent(otelContext)) : NOOP_SCOPE;
}
+ /**
+ * {@inheritDoc}
+ */
+ public InstrumentationContext getInstrumentationContext() {
+ if (spanContext != null) {
+ return spanContext;
+ }
+
+ spanContext = isInitialized()
+ ? new OTelSpanContext(GET_SPAN_CONTEXT_INVOKER.invoke(otelSpan), otelContext)
+ : OTelSpanContext.getInvalid();
+
+ return spanContext;
+ }
+
+ SpanKind getSpanKind() {
+ return spanKind;
+ }
+
+ Object getOtelSpan() {
+ return otelSpan;
+ }
+
+ static OTelSpan createPropagatingSpan(OTelSpanContext spanContext) {
+ Object span = wrapSpanContext(spanContext.getOtelSpanContext());
+ return new OTelSpan(span, spanContext.getOtelContext());
+ }
+
static Object createPropagatingSpan(Object otelContext) {
assert CONTEXT_CLASS.isInstance(otelContext);
- Object span = FROM_CONTEXT_INVOKER.invoke(otelContext);
- assert SPAN_CLASS.isInstance(span);
+ Object span = fromOTelContext(otelContext);
Object spanContext = GET_SPAN_CONTEXT_INVOKER.invoke(span);
assert SPAN_CONTEXT_CLASS.isInstance(spanContext);
- Object propagatingSpan = WRAP_INVOKER.invoke(spanContext);
+ return wrapSpanContext(spanContext);
+ }
+
+ static Object fromOTelContext(Object otelContext) {
+ assert CONTEXT_CLASS.isInstance(otelContext);
+
+ Object span = FROM_CONTEXT_INVOKER.invoke(otelContext);
+ assert SPAN_CLASS.isInstance(span);
+
+ return span;
+ }
+
+ static Object wrapSpanContext(Object otelSpanContext) {
+ assert SPAN_CONTEXT_CLASS.isInstance(otelSpanContext);
+
+ Object propagatingSpan = WRAP_INVOKER.invoke(otelSpanContext);
assert SPAN_CLASS.isInstance(propagatingSpan);
return propagatingSpan;
}
- Object getOtelContext() {
- return otelContext;
+ static Object getSpanContext(Object otelSpan) {
+ assert SPAN_CLASS.isInstance(otelSpan);
+
+ Object spanContext = GET_SPAN_CONTEXT_INVOKER.invoke(otelSpan);
+ assert SPAN_CONTEXT_CLASS.isInstance(spanContext);
+
+ return spanContext;
+ }
+
+ static Object storeInContext(Object otelSpan, Object otelContext) {
+ Object updatedContext = STORE_IN_CONTEXT_INVOKER.invoke(otelSpan, otelContext);
+
+ return updatedContext != null ? updatedContext : otelContext;
}
private void endSpan(Throwable throwable) {
if (isInitialized()) {
if (errorType != null || throwable != null) {
- setAttribute("error.type", errorType != null ? errorType : throwable.getClass().getCanonicalName());
+
+ String errorTypeStr = errorType != null ? errorType : throwable.getClass().getCanonicalName();
+ SET_ATTRIBUTE_INVOKER.invoke(otelSpan, ERROR_TYPE_ATTRIBUTE_KEY, errorTypeStr);
+
SET_STATUS_INVOKER.invoke(otelSpan, ERROR_STATUS_CODE,
throwable == null ? null : throwable.getMessage());
}
@@ -207,11 +278,6 @@ private static TracingScope wrapOTelScope(AutoCloseable otelScope) {
};
}
- private static Object storeInContext(Object otelSpan, Object otelContext) {
- Object updatedContext = STORE_IN_CONTEXT_INVOKER.invoke(otelSpan, otelContext);
- return updatedContext != null ? updatedContext : otelContext;
- }
-
private boolean isInitialized() {
return otelSpan != null && OTelInitializer.isInitialized();
}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanBuilder.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanBuilder.java
index ccd5e25dfb1a..0dcf0e53e8a5 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanBuilder.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanBuilder.java
@@ -7,12 +7,12 @@
import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker;
import io.clientcore.core.implementation.instrumentation.LibraryInstrumentationOptionsAccessHelper;
import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer;
+import io.clientcore.core.instrumentation.InstrumentationContext;
import io.clientcore.core.instrumentation.LibraryInstrumentationOptions;
import io.clientcore.core.instrumentation.tracing.Span;
import io.clientcore.core.instrumentation.tracing.SpanBuilder;
import io.clientcore.core.instrumentation.tracing.SpanKind;
-import io.clientcore.core.util.ClientLogger;
-import io.clientcore.core.util.Context;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker;
import static io.clientcore.core.implementation.instrumentation.otel.OTelAttributeKey.castAttributeValue;
@@ -21,17 +21,15 @@
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_BUILDER_CLASS;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_KIND_CLASS;
-import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelUtils.getOTelContext;
/**
* OpenTelemetry implementation of {@link SpanBuilder}.
*/
public class OTelSpanBuilder implements SpanBuilder {
static final OTelSpanBuilder NOOP
- = new OTelSpanBuilder(null, SpanKind.INTERNAL, Context.none(), new LibraryInstrumentationOptions("noop"));
+ = new OTelSpanBuilder(null, SpanKind.INTERNAL, null, new LibraryInstrumentationOptions("noop"));
private static final ClientLogger LOGGER = new ClientLogger(OTelSpanBuilder.class);
- private static final OTelSpan NOOP_SPAN;
private static final FallbackInvoker SET_PARENT_INVOKER;
private static final FallbackInvoker SET_ATTRIBUTE_INVOKER;
private static final FallbackInvoker SET_SPAN_KIND_INVOKER;
@@ -45,7 +43,7 @@ public class OTelSpanBuilder implements SpanBuilder {
private final Object otelSpanBuilder;
private final boolean suppressNestedSpans;
private final SpanKind spanKind;
- private final Context context;
+ private final InstrumentationContext context;
static {
ReflectiveInvoker setParentInvoker = null;
@@ -58,7 +56,6 @@ public class OTelSpanBuilder implements SpanBuilder {
Object clientKind = null;
Object producerKind = null;
Object consumerKind = null;
- OTelSpan noopSpan = null;
if (OTelInitializer.isInitialized()) {
try {
@@ -78,18 +75,15 @@ public class OTelSpanBuilder implements SpanBuilder {
clientKind = SPAN_KIND_CLASS.getField("CLIENT").get(null);
producerKind = SPAN_KIND_CLASS.getField("PRODUCER").get(null);
consumerKind = SPAN_KIND_CLASS.getField("CONSUMER").get(null);
-
- noopSpan = new OTelSpan(null, OTelContext.getCurrent(), SpanKind.INTERNAL);
} catch (Throwable t) {
OTelInitializer.initError(LOGGER, t);
}
}
- NOOP_SPAN = noopSpan;
SET_PARENT_INVOKER = new FallbackInvoker(setParentInvoker, LOGGER);
SET_ATTRIBUTE_INVOKER = new FallbackInvoker(setAttributeInvoker, LOGGER);
SET_SPAN_KIND_INVOKER = new FallbackInvoker(setSpanKindInvoker, LOGGER);
- START_SPAN_INVOKER = new FallbackInvoker(startSpanInvoker, NOOP_SPAN, LOGGER);
+ START_SPAN_INVOKER = new FallbackInvoker(startSpanInvoker, OTelSpan.NOOP_SPAN.getOtelSpan(), LOGGER);
INTERNAL_KIND = internalKind;
SERVER_KIND = serverKind;
CLIENT_KIND = clientKind;
@@ -98,7 +92,7 @@ public class OTelSpanBuilder implements SpanBuilder {
}
- OTelSpanBuilder(Object otelSpanBuilder, SpanKind kind, Context parent,
+ OTelSpanBuilder(Object otelSpanBuilder, SpanKind kind, InstrumentationContext parent,
LibraryInstrumentationOptions libraryOptions) {
this.otelSpanBuilder = otelSpanBuilder;
this.suppressNestedSpans = libraryOptions == null
@@ -125,7 +119,7 @@ public SpanBuilder setAttribute(String key, Object value) {
@Override
public Span startSpan() {
if (isInitialized()) {
- Object otelParentContext = getOTelContext(context);
+ Object otelParentContext = OTelContext.fromInstrumentationContext(context);
SET_PARENT_INVOKER.invoke(otelSpanBuilder, otelParentContext);
SET_SPAN_KIND_INVOKER.invoke(otelSpanBuilder, toOtelSpanKind(spanKind));
Object otelSpan = shouldSuppress(otelParentContext)
@@ -136,13 +130,16 @@ public Span startSpan() {
}
}
- return NOOP_SPAN;
+ return OTelSpan.NOOP_SPAN;
}
private boolean shouldSuppress(Object parentContext) {
- return suppressNestedSpans
- && (this.spanKind == SpanKind.CLIENT || this.spanKind == SpanKind.INTERNAL)
- && OTelContext.hasClientCoreSpan(parentContext);
+ if (suppressNestedSpans && (this.spanKind == SpanKind.CLIENT || this.spanKind == SpanKind.INTERNAL)) {
+ OTelSpan span = OTelContext.getClientCoreSpan(parentContext);
+ return span != null && (span.getSpanKind() == SpanKind.INTERNAL || span.getSpanKind() == SpanKind.CLIENT);
+ }
+
+ return false;
}
private Object toOtelSpanKind(SpanKind spanKind) {
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanContext.java
index 9433464c2281..83370ede4105 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanContext.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanContext.java
@@ -6,15 +6,19 @@
import io.clientcore.core.implementation.ReflectiveInvoker;
import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker;
import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.InstrumentationContext;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
+import io.clientcore.core.instrumentation.tracing.Span;
import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CONTEXT_CLASS;
+import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACE_FLAGS_CLASS;
+import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACE_STATE_CLASS;
/**
* Wrapper around OpenTelemetry SpanContext.
*/
-public class OTelSpanContext {
+public class OTelSpanContext implements InstrumentationContext {
public static final Object INVALID_OTEL_SPAN_CONTEXT;
private static final String INVALID_TRACE_ID = "00000000000000000000000000000000";
private static final String INVALID_SPAN_ID = "0000000000000000";
@@ -24,12 +28,22 @@ public class OTelSpanContext {
private static final FallbackInvoker GET_SPAN_ID_INVOKER;
private static final FallbackInvoker GET_TRACE_ID_INVOKER;
private static final FallbackInvoker GET_TRACE_FLAGS_INVOKER;
+ private static final FallbackInvoker IS_VALID_INVOKER;
+ private static final FallbackInvoker CREATE_INVOKER;
private final Object otelSpanContext;
+ private final Object otelContext;
+ private String traceId;
+ private String spanId;
+ private String traceFlags;
+ private Boolean isValid;
+
static {
ReflectiveInvoker getSpanIdInvoker = null;
ReflectiveInvoker getTraceIdInvoker = null;
ReflectiveInvoker getTraceFlagsInvoker = null;
+ ReflectiveInvoker isValidInvoker = null;
+ ReflectiveInvoker createInvoker = null;
Object invalidInstance = null;
@@ -43,58 +57,160 @@ public class OTelSpanContext {
= getMethodInvoker(SPAN_CONTEXT_CLASS, SPAN_CONTEXT_CLASS.getMethod("getInvalid"));
invalidInstance = getInvalidInvoker.invoke();
+ isValidInvoker = getMethodInvoker(SPAN_CONTEXT_CLASS, SPAN_CONTEXT_CLASS.getMethod("isValid"));
+ createInvoker = getMethodInvoker(SPAN_CONTEXT_CLASS, SPAN_CONTEXT_CLASS.getMethod("create",
+ String.class, String.class, TRACE_FLAGS_CLASS, TRACE_STATE_CLASS));
} catch (Throwable t) {
OTelInitializer.initError(LOGGER, t);
}
}
INVALID_OTEL_SPAN_CONTEXT = invalidInstance;
- INVALID = new OTelSpanContext(invalidInstance);
+ INVALID = new OTelSpanContext(invalidInstance, null);
+ IS_VALID_INVOKER = new FallbackInvoker(isValidInvoker, false, LOGGER);
GET_SPAN_ID_INVOKER = new FallbackInvoker(getSpanIdInvoker, INVALID_SPAN_ID, LOGGER);
GET_TRACE_ID_INVOKER = new FallbackInvoker(getTraceIdInvoker, INVALID_TRACE_ID, LOGGER);
GET_TRACE_FLAGS_INVOKER = new FallbackInvoker(getTraceFlagsInvoker, INVALID_TRACE_FLAGS, LOGGER);
+ CREATE_INVOKER = new FallbackInvoker(createInvoker, INVALID_OTEL_SPAN_CONTEXT, LOGGER);
}
- OTelSpanContext(Object otelSpanContext) {
+ /**
+ * Creates a new instance of {@link OTelSpanContext} from an OpenTelemetry {@code SpanContext}.
+ *
+ * @param otelSpanContext the instance of OpenTelemetry {@code io.opentelemetry.api.trace.SpanContext}
+ * @param otelContext the instance of OpenTelemetry {@code io.opentelemetry.context.Context}.
+ * It is used to propagate additional information within the process along with {@link InstrumentationContext}.
+ */
+ public OTelSpanContext(Object otelSpanContext, Object otelContext) {
this.otelSpanContext = otelSpanContext;
+ this.otelContext = otelContext;
+ }
+
+ /**
+ * Creates a new instance of {@link OTelSpanContext} from an OpenTelemetry {@code io.opentelemetry.context.Context}.
+ * @param otelContext the instance of OpenTelemetry {@code io.opentelemetry.context.Context}
+ *
+ * @return the instance of {@link OTelSpanContext}
+ */
+ public static OTelSpanContext fromOTelContext(Object otelContext) {
+ if (otelContext == null) {
+ return INVALID;
+ }
+ Object otelSpan = OTelSpan.fromOTelContext(otelContext);
+ Object otelSpanContext = OTelSpan.getSpanContext(otelSpan);
+ return new OTelSpanContext(otelSpanContext, otelContext);
+ }
+
+ /**
+ * Creates a new instance of {@link OTelSpanContext} from an OpenTelemetry {@code io.opentelemetry.api.trace.Span}.
+ * @param otelSpan the instance of OpenTelemetry {@code io.opentelemetry.api.trace.Span}
+ * @return the instance of {@link OTelSpanContext}
+ */
+ public static OTelSpanContext fromOTelSpan(Object otelSpan) {
+ Object otelSpanContext = OTelSpan.getSpanContext(otelSpan);
+ Object otelContext = OTelSpan.storeInContext(otelSpan, OTelContext.getCurrent());
+
+ return new OTelSpanContext(otelSpanContext, otelContext);
}
- static OTelSpanContext getInvalid() {
+ /**
+ * Returns an invalid instance of {@link OTelSpanContext}.
+ * @return the instance of {@link OTelSpanContext}
+ */
+ public static OTelSpanContext getInvalid() {
return INVALID;
}
/**
- * Gets trace id.
- *
- * @return the trace id.
+ * {@inheritDoc}
*/
public String getTraceId() {
- return isInitialized() ? (String) GET_TRACE_ID_INVOKER.invoke(otelSpanContext) : INVALID_TRACE_ID;
+ if (traceId != null) {
+ return traceId;
+ }
+
+ traceId = isInitialized() ? (String) GET_TRACE_ID_INVOKER.invoke(otelSpanContext) : INVALID_TRACE_ID;
+ return traceId;
}
/**
- * Gets span id.
- *
- * @return the span id.
+ * {@inheritDoc}
*/
public String getSpanId() {
- return isInitialized() ? (String) GET_SPAN_ID_INVOKER.invoke(otelSpanContext) : INVALID_SPAN_ID;
+ if (spanId != null) {
+ return spanId;
+ }
+
+ spanId = isInitialized() ? (String) GET_SPAN_ID_INVOKER.invoke(otelSpanContext) : INVALID_SPAN_ID;
+ return spanId;
}
/**
- * Gets trace flags.
- *
- * @return the trace flags.
+ * {@inheritDoc}
*/
+ @Override
public String getTraceFlags() {
+ if (traceFlags != null) {
+ return traceFlags;
+ }
+
+ if (isInitialized()) {
+ Object traceFlagsObj = GET_TRACE_FLAGS_INVOKER.invoke(otelSpanContext);
+ if (traceFlagsObj != null) {
+ traceFlags = traceFlagsObj.toString();
+ }
+ } else {
+ traceFlags = INVALID_TRACE_FLAGS;
+ }
+
+ return traceFlags;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isValid() {
+ if (isValid != null) {
+ return isValid;
+ }
+
+ isValid = isInitialized() && (Boolean) IS_VALID_INVOKER.invoke(otelSpanContext);
+ return isValid;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Span getSpan() {
if (isInitialized()) {
- Object traceFlags = GET_TRACE_FLAGS_INVOKER.invoke(otelSpanContext);
- if (traceFlags != null) {
- return traceFlags.toString();
+ if (otelContext != null) {
+ OTelSpan coreSpan = OTelContext.getClientCoreSpan(otelContext);
+ if (coreSpan != null) {
+ return coreSpan;
+ }
}
+
+ return OTelSpan.createPropagatingSpan(this);
+ }
+ return Span.noop();
+ }
+
+ Object getOtelContext() {
+ return otelContext;
+ }
+
+ Object getOtelSpanContext() {
+ return otelSpanContext;
+ }
+
+ static Object toOTelSpanContext(InstrumentationContext context) {
+ if (context instanceof OTelSpanContext) {
+ return ((OTelSpanContext) context).otelSpanContext;
}
- return INVALID_TRACE_FLAGS;
+ return CREATE_INVOKER.invoke(context.getTraceId(), context.getTraceFlags(), null);
}
private boolean isInitialized() {
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java
index eb673edc9fa6..c4d328545879 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java
@@ -6,11 +6,11 @@
import io.clientcore.core.implementation.ReflectiveInvoker;
import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker;
import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer;
+import io.clientcore.core.instrumentation.InstrumentationContext;
import io.clientcore.core.instrumentation.tracing.TraceContextGetter;
import io.clientcore.core.instrumentation.tracing.TraceContextPropagator;
import io.clientcore.core.instrumentation.tracing.TraceContextSetter;
-import io.clientcore.core.util.ClientLogger;
-import io.clientcore.core.util.Context;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
@@ -22,8 +22,6 @@
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TEXT_MAP_GETTER_CLASS;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TEXT_MAP_PROPAGATOR_CLASS;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TEXT_MAP_SETTER_CLASS;
-import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelUtils.getOTelContext;
-import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY;
/**
* OpenTelemetry implementation of {@link TraceContextPropagator}.
@@ -69,9 +67,10 @@ public OTelTraceContextPropagator(Object otelPropagator) {
* {@inheritDoc}
*/
@Override
- public void inject(Context context, C carrier, TraceContextSetter setter) {
+ public void inject(InstrumentationContext context, C carrier, TraceContextSetter setter) {
if (isInitialized()) {
- INJECT_INVOKER.invoke(otelPropagator, getOTelContext(context), carrier, Setter.toOTelSetter(setter));
+ INJECT_INVOKER.invoke(otelPropagator, OTelContext.fromInstrumentationContext(context), carrier,
+ Setter.toOTelSetter(setter));
}
}
@@ -79,12 +78,12 @@ public void inject(Context context, C carrier, TraceContextSetter setter)
* {@inheritDoc}
*/
@Override
- public Context extract(Context context, C carrier, TraceContextGetter getter) {
+ public InstrumentationContext extract(InstrumentationContext context, C carrier, TraceContextGetter getter) {
if (isInitialized()) {
- Object updatedContext
- = EXTRACT_INVOKER.invoke(otelPropagator, getOTelContext(context), carrier, Getter.toOTelGetter(getter));
+ Object updatedContext = EXTRACT_INVOKER.invoke(otelPropagator,
+ OTelContext.fromInstrumentationContext(context), carrier, Getter.toOTelGetter(getter));
if (updatedContext != null) {
- return context.put(TRACE_CONTEXT_KEY, updatedContext);
+ return OTelSpanContext.fromOTelContext(updatedContext);
}
}
return context;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTracer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTracer.java
index 15948ab3005b..cecaba652175 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTracer.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTracer.java
@@ -3,16 +3,15 @@
package io.clientcore.core.implementation.instrumentation.otel.tracing;
-import io.clientcore.core.http.models.RequestOptions;
import io.clientcore.core.implementation.ReflectiveInvoker;
import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker;
import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer;
+import io.clientcore.core.instrumentation.InstrumentationContext;
import io.clientcore.core.instrumentation.LibraryInstrumentationOptions;
import io.clientcore.core.instrumentation.tracing.SpanBuilder;
import io.clientcore.core.instrumentation.tracing.SpanKind;
import io.clientcore.core.instrumentation.tracing.Tracer;
-import io.clientcore.core.util.ClientLogger;
-import io.clientcore.core.util.Context;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker;
import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACER_BUILDER_CLASS;
@@ -94,12 +93,11 @@ public OTelTracer(Object otelTracerProvider, LibraryInstrumentationOptions libra
* {@inheritDoc}
*/
@Override
- public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, RequestOptions options) {
+ public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, InstrumentationContext parentContext) {
if (isEnabled()) {
Object otelSpanBuilder = SPAN_BUILDER_INVOKER.invoke(otelTracer, spanName);
if (otelSpanBuilder != null) {
- Context parent = options == null ? Context.none() : options.getContext();
- return new OTelSpanBuilder(otelSpanBuilder, spanKind, parent, libraryOptions);
+ return new OTelSpanBuilder(otelSpanBuilder, spanKind, parentContext, libraryOptions);
}
}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelUtils.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelUtils.java
deleted file mode 100644
index 1857512b82b4..000000000000
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelUtils.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-package io.clientcore.core.implementation.instrumentation.otel.tracing;
-
-import io.clientcore.core.instrumentation.Instrumentation;
-import io.clientcore.core.util.ClientLogger;
-import io.clientcore.core.util.Context;
-
-import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS;
-
-/**
- * Utility class for OpenTelemetry.
- */
-public final class OTelUtils {
- private static final ClientLogger LOGGER = new ClientLogger(OTelUtils.class);
-
- /**
- * Get the OpenTelemetry context from the given context.
- *
- * @param context the context
- * @return the OpenTelemetry context
- */
- public static Object getOTelContext(Context context) {
- Object parent = context.get(Instrumentation.TRACE_CONTEXT_KEY);
- if (CONTEXT_CLASS.isInstance(parent)) {
- return parent;
- } else if (parent instanceof OTelSpan) {
- return ((OTelSpan) parent).getOtelContext();
- } else if (parent != null) {
- LOGGER.atVerbose()
- .addKeyValue("expectedType", CONTEXT_CLASS.getName())
- .addKeyValue("actualType", parent.getClass().getName())
- .log("Context does not contain an OpenTelemetry context. Ignoring it.");
- }
-
- return OTelContext.getCurrent();
- }
-
- private OTelUtils() {
- }
-}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DateTimeRfc1123.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DateTimeRfc1123.java
index 6e656d9043ee..247dbf5577a5 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DateTimeRfc1123.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DateTimeRfc1123.java
@@ -3,7 +3,7 @@
package io.clientcore.core.implementation.util;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/ImplUtils.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/ImplUtils.java
index 880fbfd78f79..dd88ef33f2bd 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/ImplUtils.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/ImplUtils.java
@@ -5,7 +5,7 @@
import io.clientcore.core.http.models.HttpHeaderName;
import io.clientcore.core.http.models.HttpHeaders;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.configuration.Configuration;
import java.io.FileOutputStream;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/InternalContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/InternalContext.java
index 217a020f04f0..a0f0a10d89e4 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/InternalContext.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/InternalContext.java
@@ -2,7 +2,7 @@
// Licensed under the MIT License.
package io.clientcore.core.implementation.util;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.Context;
import java.util.Map;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/JsonSerializer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/JsonSerializer.java
index 7c776106b794..b9db02e6a314 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/JsonSerializer.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/JsonSerializer.java
@@ -8,7 +8,7 @@
import io.clientcore.core.serialization.json.JsonReader;
import io.clientcore.core.serialization.json.JsonSerializable;
import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.serializer.ObjectSerializer;
import io.clientcore.core.util.serializer.SerializationFormat;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/LoggingKeys.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/LoggingKeys.java
deleted file mode 100644
index 568799979296..000000000000
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/LoggingKeys.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-package io.clientcore.core.implementation.util;
-
-/**
- * Constants used as keys in semantic logging. Logging keys unify how core logs HTTP requests, responses or anything
- * else and simplify log analysis.
- *
- * When logging in client libraries, please do the best effort to stay consistent with these keys, but copy the value.
- */
-public final class LoggingKeys {
- private LoggingKeys() {
-
- }
-
- /**
- * Key representing HTTP method.
- */
- public static final String HTTP_METHOD_KEY = "method";
-
- /**
- * Key representing try count, the value starts with {@code 0} on the first try
- * and should be an {@code int} number.
- */
- public static final String TRY_COUNT_KEY = "tryCount";
-
- /**
- * Key representing time in milliseconds from request start to the moment response (headers and response code) were received,
- * the value should be a number.
- *
- * Depending on the implementation and content type, this time may include time to receive the body.
- */
- public static final String TIME_TO_RESPONSE_MS_KEY = "timeToResponseMs";
-
- /**
- * Key representing duration of call in milliseconds, the value should be a number.
- *
- * This time represents the most accurate duration that logging policy can record.
- *
- * If exception was thrown, this time represents time to exception.
- * If response was received and body logging is disabled, it represents time to get the response (headers and status code).
- * If response was received and body logging is enabled, it represents time-to-last-byte (or, if response was closed before
- * body was fully received, time to closure).
- */
- public static final String DURATION_MS_KEY = "durationMs";
-
- /**
- * Key representing URI request was redirected to.
- */
- public static final String REDIRECT_URI_KEY = "redirectUri";
-
- /**
- * Key representing request URI.
- */
- public static final String URI_KEY = "uri";
-
- /**
- * Key representing request body content length.
- */
- public static final String REQUEST_CONTENT_LENGTH_KEY = "requestContentLength";
-
- /**
- * Key representing response body content length.
- */
- public static final String RESPONSE_CONTENT_LENGTH_KEY = "responseContentLength";
-
- /**
- * Key representing request body. The value should be populated conditionally
- * if populated at all.
- */
- public static final String BODY_KEY = "body";
-
- /**
- * Key representing response status code. The value should be a number.
- */
- public static final String STATUS_CODE_KEY = "statusCode";
-}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Providers.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Providers.java
index 5e8abe009477..51806137e62e 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Providers.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Providers.java
@@ -2,7 +2,7 @@
// Licensed under the MIT License.
package io.clientcore.core.implementation.util;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.util.HashMap;
import java.util.Iterator;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/SliceInputStream.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/SliceInputStream.java
index e602cb8e2d73..d2d94091cd4d 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/SliceInputStream.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/SliceInputStream.java
@@ -3,7 +3,7 @@
package io.clientcore.core.implementation.util;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.io.IOException;
import java.io.InputStream;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/StreamUtil.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/StreamUtil.java
index fed2ba45d15f..bcf0be2856e4 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/StreamUtil.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/StreamUtil.java
@@ -3,7 +3,7 @@
package io.clientcore.core.implementation.util;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.io.IOException;
import java.io.InputStream;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/XmlSerializer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/XmlSerializer.java
index 4580213e72a7..b50c7d89550f 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/XmlSerializer.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/XmlSerializer.java
@@ -7,7 +7,7 @@
import io.clientcore.core.serialization.xml.XmlReader;
import io.clientcore.core.serialization.xml.XmlSerializable;
import io.clientcore.core.serialization.xml.XmlWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.serializer.ObjectSerializer;
import io.clientcore.core.util.serializer.SerializationFormat;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/Instrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/Instrumentation.java
index a5ca91ac6e09..c1de07959f73 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/Instrumentation.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/Instrumentation.java
@@ -3,6 +3,7 @@
package io.clientcore.core.instrumentation;
+import io.clientcore.core.implementation.instrumentation.fallback.FallbackInstrumentation;
import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer;
import io.clientcore.core.implementation.instrumentation.otel.OTelInstrumentation;
import io.clientcore.core.instrumentation.tracing.TraceContextPropagator;
@@ -10,27 +11,10 @@
import java.util.Objects;
-import static io.clientcore.core.instrumentation.NoopInstrumentation.NOOP_PROVIDER;
-
/**
* A container that can resolve observability provider and its components. Only OpenTelemetry is supported.
- *
- *
This interface is intended to be used by client libraries. Application developers
- * should use OpenTelemetry API directly
*/
public interface Instrumentation {
- /**
- * The key used to disable tracing on a per-request basis.
- * To disable tracing, set this key to {@code true} on the request context.
- */
- String DISABLE_TRACING_KEY = "disable-tracing";
-
- /**
- * The key used to set the parent trace context explicitly.
- * To set the trace context, set this key to a value of {@code io.opentelemetry.context.Context}.
- */
- String TRACE_CONTEXT_KEY = "trace-context";
-
/**
* Gets the tracer.
*
@@ -60,6 +44,9 @@ public interface Instrumentation {
/**
* Gets the implementation of W3C Trace Context propagator.
*
+ *
This method is intended to be used by client libraries. Application developers
+ * should use OpenTelemetry API directly
+ *
* @return The context propagator.
*/
TraceContextPropagator getW3CTraceContextPropagator();
@@ -67,6 +54,9 @@ public interface Instrumentation {
/**
* Gets the singleton instance of the resolved telemetry provider.
*
+ * This method is intended to be used by client libraries. Application developers
+ * should use OpenTelemetry API directly
+ *
* @param applicationOptions Telemetry collection options provided by the application.
* @param libraryOptions Library-specific telemetry collection options.
* @return The instance of telemetry provider implementation.
@@ -77,7 +67,60 @@ static Instrumentation create(InstrumentationOptions> applicationOptions,
if (OTelInitializer.isInitialized()) {
return new OTelInstrumentation(applicationOptions, libraryOptions);
} else {
- return NOOP_PROVIDER;
+ return new FallbackInstrumentation(applicationOptions, libraryOptions);
+ }
+ }
+
+ /**
+ * Retrieves the instrumentation context from the given context. The type of the context is determined by the
+ * instrumentation implementation.
+ *
+ * When using OpenTelemetry, the context can be a {@code io.opentelemetry.api.trace.Span}, {@code io.opentelemetry.api.trace.SpanContext},
+ * {@code io.opentelemetry.context.Context} or any implementation of {@link InstrumentationContext}.
+ *
+ *
+ *
+ * SampleClient client = new SampleClientBuilder().build();
+ *
+ * RequestOptions options = new RequestOptions()
+ * .setInstrumentationContext(new MyInstrumentationContext("e4eaaaf2d48f4bf3b299a8a2a2a77ad7", "5e0c63257de34c56"));
+ *
+ * // run on another thread
+ * client.clientCall(options);
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Tracer tracer = GlobalOpenTelemetry.getTracer("sample");
+ * Span span = tracer.spanBuilder("my-operation")
+ * .startSpan();
+ * SampleClient client = new SampleClientBuilder().build();
+ *
+ * // Propagating context implicitly is preferred way in synchronous code.
+ * // However, in asynchronous code, context may need to be propagated explicitly using RequestOptions
+ * // and explicit io.clientcore.core.util.Context.
+ *
+ * RequestOptions options = new RequestOptions()
+ * .setInstrumentationContext(Instrumentation.createInstrumentationContext(span));
+ *
+ * // run on another thread - all telemetry will be correlated with the span created above
+ * client.clientCall(options);
+ *
+ *
+ *
+ *
+ * @param context the context to retrieve the instrumentation context from.
+ * @return the instrumentation context.
+ * @param the type of the context.
+ */
+ static InstrumentationContext createInstrumentationContext(T context) {
+ if (OTelInitializer.isInitialized()) {
+ return OTelInstrumentation.DEFAULT_INSTANCE.createInstrumentationContext(context);
+ } else {
+ return FallbackInstrumentation.DEFAULT_INSTANCE.createInstrumentationContext(context);
}
}
}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationContext.java
new file mode 100644
index 000000000000..173b3d7110e5
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationContext.java
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.instrumentation;
+
+import io.clientcore.core.instrumentation.tracing.Span;
+
+/**
+ * The instrumentation context that is used to correlate telemetry data.
+ * It's created along with the {@link Span} by
+ * the client libraries and is propagated through SDK code.
+ *
+ * It must provide access to W3C Trace Context
+ * properties. Implementations may use it to propagate additional information within the process.
+ *
+ * @see Instrumentation#createInstrumentationContext(Object)
+ */
+public interface InstrumentationContext {
+ /**
+ * Gets the trace id - 32-char long hex-encoded string that identifies end-to-end operation.
+ * @return the trace id.
+ */
+ String getTraceId();
+
+ /**
+ * Gets the span id - 16-char hex-encoded string that identifies span - an individual
+ * operation within a trace.
+ * @return the span id.
+ */
+ String getSpanId();
+
+ /**
+ * Gets the trace flags - 2-char hex-encoded string that represents trace flags.
+ * Flag with value "01" indicates that the span is sampled, "00" indicates that it is
+ * not sampled.
+ * @return the trace flags.
+ */
+ String getTraceFlags();
+
+ /**
+ * Checks if the context is valid - i.e. if it can be propagated.
+ * Invalid contexts are ignored by the instrumentation.
+ *
+ * @return true if the context is valid, false otherwise.
+ */
+ boolean isValid();
+
+ /**
+ * Gets the span that is associated with this context. If there is no span associated with this context,
+ * returns a no-op span.
+ *
+ * @return the span.
+ */
+ Span getSpan();
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/NoopInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/NoopInstrumentation.java
deleted file mode 100644
index 53205494031c..000000000000
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/NoopInstrumentation.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-package io.clientcore.core.instrumentation;
-
-import io.clientcore.core.instrumentation.tracing.Span;
-import io.clientcore.core.instrumentation.tracing.SpanBuilder;
-import io.clientcore.core.instrumentation.tracing.TraceContextGetter;
-import io.clientcore.core.instrumentation.tracing.TraceContextPropagator;
-import io.clientcore.core.instrumentation.tracing.TraceContextSetter;
-import io.clientcore.core.instrumentation.tracing.Tracer;
-import io.clientcore.core.instrumentation.tracing.TracingScope;
-import io.clientcore.core.util.Context;
-
-class NoopInstrumentation implements Instrumentation {
- static final Instrumentation NOOP_PROVIDER = new NoopInstrumentation();
-
- @Override
- public Tracer getTracer() {
- return NOOP_TRACER;
- }
-
- @Override
- public TraceContextPropagator getW3CTraceContextPropagator() {
- return NOOP_CONTEXT_PROPAGATOR;
- }
-
- private static final Span NOOP_SPAN = new Span() {
- @Override
- public Span setAttribute(String key, Object value) {
- return this;
- }
-
- @Override
- public Span setError(String errorType) {
- return this;
- }
-
- @Override
- public void end() {
- }
-
- @Override
- public void end(Throwable error) {
- }
-
- @Override
- public boolean isRecording() {
- return false;
- }
-
- @Override
- public TracingScope makeCurrent() {
- return NOOP_SCOPE;
- }
- };
-
- private static final SpanBuilder NOOP_SPAN_BUILDER = new SpanBuilder() {
- @Override
- public SpanBuilder setAttribute(String key, Object value) {
- return this;
- }
-
- @Override
- public Span startSpan() {
- return NOOP_SPAN;
- }
- };
-
- private static final TracingScope NOOP_SCOPE = () -> {
- };
- private static final Tracer NOOP_TRACER = (name, kind, ctx) -> NOOP_SPAN_BUILDER;
-
- private static final TraceContextPropagator NOOP_CONTEXT_PROPAGATOR = new TraceContextPropagator() {
-
- @Override
- public void inject(Context context, C carrier, TraceContextSetter setter) {
-
- }
-
- @Override
- public Context extract(Context context, C carrier, TraceContextGetter getter) {
- return context;
- }
- };
-}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/ClientLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java
similarity index 88%
rename from sdk/clientcore/core/src/main/java/io/clientcore/core/util/ClientLogger.java
rename to sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java
index 6fea3c48010d..5974d6708f06 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/ClientLogger.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java
@@ -1,12 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-package io.clientcore.core.util;
+package io.clientcore.core.instrumentation.logging;
import io.clientcore.core.annotation.Metadata;
import io.clientcore.core.implementation.AccessibleByteArrayOutputStream;
-import io.clientcore.core.implementation.util.DefaultLogger;
-import io.clientcore.core.implementation.util.Slf4jLoggerShim;
+import io.clientcore.core.implementation.instrumentation.Slf4jLoggerShim;
+import io.clientcore.core.implementation.instrumentation.DefaultLogger;
+import io.clientcore.core.instrumentation.InstrumentationContext;
import io.clientcore.core.serialization.json.JsonWriter;
import io.clientcore.core.serialization.json.implementation.DefaultJsonWriter;
import io.clientcore.core.util.configuration.Configuration;
@@ -23,6 +24,12 @@
import java.util.function.Supplier;
import static io.clientcore.core.annotation.TypeConditions.FLUENT;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.EVENT_NAME_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.EXCEPTION_MESSAGE_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.EXCEPTION_STACKTRACE_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.EXCEPTION_TYPE_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_ID_KEY;
+import static io.clientcore.core.implementation.instrumentation.AttributeKeys.TRACE_ID_KEY;
/**
* This is a fluent logger helper class that wraps an SLF4J Logger (if available) or a default implementation of the
@@ -60,8 +67,20 @@ public ClientLogger(Class> clazz) {
* @throws RuntimeException when logging configuration is invalid depending on SLF4J implementation.
*/
public ClientLogger(String className) {
+ this(className, null);
+ }
+
+ /**
+ * Retrieves a logger for the passed class name.
+ *
+ * @param className Class name creating the logger.
+ * @param context Context to be populated on every log record written with this logger.
+ * Objects are serialized with {@code toString()} method.
+ * @throws RuntimeException when logging configuration is invalid depending on SLF4J implementation.
+ */
+ public ClientLogger(String className, Map context) {
logger = new Slf4jLoggerShim(getClassPathFromClassName(className));
- globalContext = null;
+ globalContext = context == null ? null : Collections.unmodifiableMap(context);
}
/**
@@ -288,6 +307,7 @@ public static final class LoggingEvent {
private final boolean isEnabled;
private Map keyValuePairs;
private String eventName;
+ private InstrumentationContext context = null;
/**
* Creates {@code LoggingEvent} for provided level and {@link ClientLogger}.
@@ -430,6 +450,18 @@ public LoggingEvent addKeyValue(String key, Supplier valueSupplier) {
return this;
}
+ /**
+ * Sets operation context on the log event being created.
+ * It's used to correlate logs between each other and with other telemetry items.
+ *
+ * @param context operation context.
+ * @return The updated {@code LoggingEventBuilder} object.
+ */
+ public LoggingEvent setInstrumentationContext(InstrumentationContext context) {
+ this.context = context;
+ return this;
+ }
+
/**
* Sets the event name for the current log event. The event name is used to query all logs
* that describe the same event. It must not contain any dynamic parts.
@@ -444,6 +476,7 @@ public LoggingEvent setEventName(String eventName) {
/**
* Logs event annotated with context.
+ * Logs event with context.
*/
public void log() {
log(null);
@@ -473,12 +506,12 @@ public T log(String message, T throwable) {
if (this.isEnabled) {
boolean isDebugEnabled = logger.canLogAtLevel(LogLevel.VERBOSE);
if (throwable != null) {
- addKeyValueInternal("exception.type", throwable.getClass().getCanonicalName());
- addKeyValueInternal("exception.message", throwable.getMessage());
+ addKeyValueInternal(EXCEPTION_TYPE_KEY, throwable.getClass().getCanonicalName());
+ addKeyValueInternal(EXCEPTION_MESSAGE_KEY, throwable.getMessage());
if (isDebugEnabled) {
StringBuilder stackTrace = new StringBuilder();
DefaultLogger.appendThrowable(stackTrace, throwable);
- addKeyValue("exception.stacktrace", stackTrace.toString());
+ addKeyValue(EXCEPTION_STACKTRACE_KEY, stackTrace.toString());
}
}
logger.performLogging(level, getMessageWithContext(message), isDebugEnabled ? throwable : null);
@@ -487,16 +520,27 @@ public T log(String message, T throwable) {
}
private String getMessageWithContext(String message) {
- if (message == null) {
- message = "";
+ if (this.context != null && this.context.isValid()) {
+ // TODO (limolkova) we can set context from implicit current span
+ // we should also support OTel as a logging provider and avoid adding redundant
+ // traceId and spanId to the logs
+
+ addKeyValue(TRACE_ID_KEY, context.getTraceId());
+ addKeyValue(SPAN_ID_KEY, context.getSpanId());
}
int pairsCount
= (keyValuePairs == null ? 0 : keyValuePairs.size()) + (globalPairs == null ? 0 : globalPairs.size());
- int speculatedSize = 20 + pairsCount * 20 + message.length();
+
+ int messageLength = message == null ? 0 : message.length();
+ int speculatedSize = 20 + pairsCount * 20 + messageLength;
try (AccessibleByteArrayOutputStream outputStream = new AccessibleByteArrayOutputStream(speculatedSize);
JsonWriter jsonWriter = DefaultJsonWriter.toStream(outputStream, null)) {
- jsonWriter.writeStartObject().writeStringField("message", message);
+ jsonWriter.writeStartObject();
+
+ if (message != null) {
+ jsonWriter.writeStringField("message", message);
+ }
if (globalPairs != null) {
for (Map.Entry kvp : globalPairs.entrySet()) {
@@ -511,7 +555,7 @@ private String getMessageWithContext(String message) {
}
if (eventName != null) {
- jsonWriter.writeStringField("event.name", eventName);
+ jsonWriter.writeStringField(EVENT_NAME_KEY, eventName);
}
jsonWriter.writeEndObject().flush();
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/package-info.java
new file mode 100644
index 000000000000..1887eaa4ca51
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/package-info.java
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+/**
+ * Package containing core logging primitives to be used by client libraries.
+ *
+ *
+ * Classes in this package are intended to be used by client libraries only. Application developers
+ * should use SLF4J or another logging facade directly
+ */
+package io.clientcore.core.instrumentation.logging;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/package-info.java
index 61f58befec5a..63fa95b9eba6 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/package-info.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/package-info.java
@@ -97,9 +97,9 @@
* // and explicit io.clientcore.core.util.Context.
*
* RequestOptions options = new RequestOptions()
- * .setContext(io.clientcore.core.util.Context.of(TRACE_CONTEXT_KEY, Context.current().with(span)));
+ * .setInstrumentationContext(Instrumentation.createInstrumentationContext(span));
*
- * // run on another thread
+ * // run on another thread - all telemetry will be correlated with the span created above
* client.clientCall(options);
*
*
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopInstrumentationContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopInstrumentationContext.java
new file mode 100644
index 000000000000..98938a193094
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopInstrumentationContext.java
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.instrumentation.tracing;
+
+import io.clientcore.core.instrumentation.InstrumentationContext;
+
+final class NoopInstrumentationContext implements InstrumentationContext {
+ public static final NoopInstrumentationContext INSTANCE = new NoopInstrumentationContext();
+
+ private NoopInstrumentationContext() {
+ }
+
+ @Override
+ public String getTraceId() {
+ return null;
+ }
+
+ @Override
+ public String getSpanId() {
+ return null;
+ }
+
+ @Override
+ public String getTraceFlags() {
+ return null;
+ }
+
+ @Override
+ public boolean isValid() {
+ return false;
+ }
+
+ @Override
+ public Span getSpan() {
+ return Span.noop();
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopSpan.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopSpan.java
new file mode 100644
index 000000000000..8ec976b911b7
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopSpan.java
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.instrumentation.tracing;
+
+import io.clientcore.core.instrumentation.InstrumentationContext;
+
+final class NoopSpan implements Span {
+ static final NoopSpan INSTANCE = new NoopSpan();
+
+ private NoopSpan() {
+ }
+
+ private static final TracingScope NOOP_SCOPE = () -> {
+ };
+
+ @Override
+ public Span setAttribute(String key, Object value) {
+ return this;
+ }
+
+ @Override
+ public Span setError(String errorType) {
+ return this;
+ }
+
+ @Override
+ public void end(Throwable throwable) {
+
+ }
+
+ @Override
+ public void end() {
+
+ }
+
+ @Override
+ public boolean isRecording() {
+ return false;
+ }
+
+ @Override
+ public TracingScope makeCurrent() {
+ return NOOP_SCOPE;
+ }
+
+ @Override
+ public InstrumentationContext getInstrumentationContext() {
+ return NoopInstrumentationContext.INSTANCE;
+ }
+}
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Span.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Span.java
index 1a4ea4143515..29c587b8ed10 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Span.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Span.java
@@ -3,6 +3,8 @@
package io.clientcore.core.instrumentation.tracing;
+import io.clientcore.core.instrumentation.InstrumentationContext;
+
/**
* A {@code Span} represents a single operation within a trace. Spans can be nested to form a trace tree.
* This interface is intended to be used by client libraries only. Application developers should use OpenTelemetry API directly
@@ -76,4 +78,19 @@ public interface Span { * @return The {@link TracingScope} object. */ TracingScope makeCurrent(); + + /** + * Gets the instrumentation context that is used to correlate telemetry data. + * + * @return The instrumentation context. + */ + InstrumentationContext getInstrumentationContext(); + + /** + * Returns a no-op span. + * @return A no-op span. + */ + static Span noop() { + return NoopSpan.INSTANCE; + } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextPropagator.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextPropagator.java index d5bffecb6039..6eb49d4fab2f 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextPropagator.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextPropagator.java @@ -3,7 +3,7 @@ package io.clientcore.core.instrumentation.tracing; -import io.clientcore.core.util.Context; +import io.clientcore.core.instrumentation.InstrumentationContext; /** * A {@link TraceContextPropagator} injects and extracts tracing context from a carrier, @@ -19,7 +19,7 @@ public interface TraceContextPropagator { * @param setter The setter to use to inject the context into the carrier. * @param
*
- * Span span = tracer.spanBuilder("{operationName}", SpanKind.CLIENT, requestOptions)
+ * Span span = tracer.spanBuilder("{operationName}", SpanKind.CLIENT, null)
* .startSpan();
*
* // we'll propagate context implicitly using span.makeCurrent() as shown later.
* // Libraries that write async code should propagate context explicitly in addition to implicit propagation.
* if (tracer.isEnabled()) {
- * requestOptions.putContext(TRACE_CONTEXT_KEY, span);
+ * if (requestOptions == null) {
+ * requestOptions = new RequestOptions();
+ * }
+ * requestOptions.setInstrumentationContext(span.getInstrumentationContext());
* }
*
* try (TracingScope scope = span.makeCurrent()) {
@@ -47,7 +50,7 @@ public interface Tracer {
*
*
*
- * Span sendSpan = tracer.spanBuilder("send {queue-name}", SpanKind.PRODUCER, requestOptions)
+ * Span sendSpan = tracer.spanBuilder("send {queue-name}", SpanKind.PRODUCER, null)
* // Some of the attributes should be provided at the start time (as documented in semantic conventions) -
* // they can be used by client apps to sample spans.
* .setAttribute("messaging.system", "servicebus")
@@ -73,10 +76,10 @@ public interface Tracer {
*
* @param spanName The name of the span.
* @param spanKind The kind of the span.
- * @param requestOptions The request options.
+ * @param instrumentationContext The parent context.
* @return The span builder.
*/
- SpanBuilder spanBuilder(String spanName, SpanKind spanKind, RequestOptions requestOptions);
+ SpanBuilder spanBuilder(String spanName, SpanKind spanKind, InstrumentationContext instrumentationContext);
/**
* Checks if the tracer is enabled.
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/json/models/JsonNumber.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/json/models/JsonNumber.java
index 0a1c9b266c32..050184c4b42e 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/json/models/JsonNumber.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/json/models/JsonNumber.java
@@ -6,7 +6,7 @@
import io.clientcore.core.serialization.json.JsonReader;
import io.clientcore.core.serialization.json.JsonToken;
import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.io.IOException;
import java.math.BigDecimal;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/xml/XmlReader.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/xml/XmlReader.java
index 613be6a2a80d..ce0023ca023a 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/xml/XmlReader.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/xml/XmlReader.java
@@ -4,7 +4,7 @@
package io.clientcore.core.serialization.xml;
import io.clientcore.core.serialization.xml.implementation.aalto.stax.InputFactoryImpl;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Context.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Context.java
index 1282962f25df..b126a11d0839 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Context.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Context.java
@@ -5,6 +5,7 @@
import io.clientcore.core.annotation.Metadata;
import io.clientcore.core.implementation.util.InternalContext;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.util.Map;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/SharedExecutorService.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/SharedExecutorService.java
index bffb558bc42b..5f9680e2063e 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/SharedExecutorService.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/SharedExecutorService.java
@@ -7,6 +7,7 @@
import io.clientcore.core.implementation.ReflectiveInvoker;
import io.clientcore.core.implementation.util.EnvironmentConfiguration;
import io.clientcore.core.implementation.util.ImplUtils;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.time.Duration;
import java.util.Collection;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/auth/CompositeChallengeHandler.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/auth/CompositeChallengeHandler.java
index 5eca550da569..2dfd99822358 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/auth/CompositeChallengeHandler.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/auth/CompositeChallengeHandler.java
@@ -4,7 +4,7 @@
import io.clientcore.core.http.models.HttpRequest;
import io.clientcore.core.http.models.Response;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.util.List;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/FileBinaryData.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/FileBinaryData.java
index 1b6093c2441a..4b3dfe28af94 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/FileBinaryData.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/FileBinaryData.java
@@ -5,7 +5,7 @@
import io.clientcore.core.implementation.util.SliceInputStream;
import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.serializer.ObjectSerializer;
import java.io.BufferedInputStream;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/InputStreamBinaryData.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/InputStreamBinaryData.java
index 47db439ae074..ee30aea3f0dd 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/InputStreamBinaryData.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/InputStreamBinaryData.java
@@ -8,7 +8,7 @@
import io.clientcore.core.implementation.util.IterableOfByteBuffersInputStream;
import io.clientcore.core.implementation.util.StreamUtil;
import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.serializer.ObjectSerializer;
import java.io.IOException;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/ListByteBufferBinaryData.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/ListByteBufferBinaryData.java
index 1c227a8f8e91..4938c053e0fd 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/ListByteBufferBinaryData.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/ListByteBufferBinaryData.java
@@ -6,7 +6,7 @@
import io.clientcore.core.implementation.util.ImplUtils;
import io.clientcore.core.implementation.util.IterableOfByteBuffersInputStream;
import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.serializer.ObjectSerializer;
import java.io.IOException;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/SerializableBinaryData.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/SerializableBinaryData.java
index 09174ee28559..3853ceb5da31 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/SerializableBinaryData.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/SerializableBinaryData.java
@@ -4,7 +4,7 @@
package io.clientcore.core.util.binarydata;
import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.serializer.ObjectSerializer;
import java.io.ByteArrayInputStream;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/Configuration.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/Configuration.java
index 8b2878c73e94..6e749a54c3c9 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/Configuration.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/Configuration.java
@@ -7,7 +7,7 @@
import io.clientcore.core.http.client.HttpClientProvider;
import io.clientcore.core.implementation.util.EnvironmentConfiguration;
import io.clientcore.core.implementation.util.ImplUtils;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.util.Collections;
import java.util.HashMap;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/ConfigurationBuilder.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/ConfigurationBuilder.java
index 03e34eba45ae..20f487aed48d 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/ConfigurationBuilder.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/ConfigurationBuilder.java
@@ -5,7 +5,7 @@
import io.clientcore.core.annotation.Metadata;
import io.clientcore.core.implementation.util.EnvironmentConfiguration;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.util.Collections;
import java.util.HashMap;
diff --git a/sdk/clientcore/core/src/main/java/module-info.java b/sdk/clientcore/core/src/main/java/module-info.java
index 36127c7b6cd6..dee608a9a5b6 100644
--- a/sdk/clientcore/core/src/main/java/module-info.java
+++ b/sdk/clientcore/core/src/main/java/module-info.java
@@ -28,6 +28,7 @@
exports io.clientcore.core.util.auth;
exports io.clientcore.core.instrumentation;
exports io.clientcore.core.instrumentation.tracing;
+ exports io.clientcore.core.instrumentation.logging;
uses io.clientcore.core.http.client.HttpClientProvider;
diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java
new file mode 100644
index 000000000000..11779e0bbf1b
--- /dev/null
+++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.instrumentation;
+
+import io.clientcore.core.http.models.HttpMethod;
+import io.clientcore.core.http.models.HttpRequest;
+import io.clientcore.core.http.models.RequestOptions;
+import io.clientcore.core.http.models.Response;
+import io.clientcore.core.http.pipeline.HttpInstrumentationPolicy;
+import io.clientcore.core.http.pipeline.HttpPipeline;
+import io.clientcore.core.http.pipeline.HttpPipelineBuilder;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
+import io.clientcore.core.instrumentation.tracing.Span;
+import io.clientcore.core.instrumentation.tracing.SpanKind;
+import io.clientcore.core.instrumentation.tracing.TracingScope;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+/**
+ * Application developers that don't have OpenTelemetry on the classpath
+ * can take advantage of basic distributed tracing providing log correlation.
+ *
+ * Instrumented client libraries start a span for each client call and
+ * propagate the span context to the HTTP pipeline that creates
+ * a new span for each HTTP request.
+ *
+ * All logs emitted by the client library are automatically correlated
+ * with the encompassing span contexts.
+ *
+ * Span context is propagated to the endpoint using W3C Trace Context format.
+ *
+ * You can also receive logs describing generated spans by enabling {library-name}.tracing
+ * logger at INFO level.
+ *
+ * You can pass custom correlation IDs (following W3C Trace Context format) in {@link RequestOptions}.
+ */
+public class TelemetryJavaDocCodeSnippets {
+
+ /**
+ * To get basic distributed tracing, just use client libraries as usual.
+ */
+ public void fallbackTracing() {
+ // BEGIN: io.clientcore.core.telemetry.fallback.tracing
+
+ SampleClient client = new SampleClientBuilder().build();
+ client.clientCall();
+
+ // END: io.clientcore.core.telemetry.fallback.tracing
+ }
+
+ /**
+ * You can pass custom logger to the client library to receive spans from
+ * this library in the logs.
+ */
+ public void useCustomLogger() {
+ // BEGIN: io.clientcore.core.telemetry.usecustomlogger
+
+ ClientLogger logger = new ClientLogger("sample-client-traces");
+
+ InstrumentationOptions instrumentationOptions = new InstrumentationOptions()
+ .setProvider(logger);
+
+ SampleClient client = new SampleClientBuilder().instrumentationOptions(instrumentationOptions).build();
+ client.clientCall();
+
+ // END: io.clientcore.core.telemetry.usecustomlogger
+ }
+
+ /**
+ * This code snippet shows how to disable distributed tracing
+ * for a specific instance of client.
+ */
+ public void disableDistributedTracing() {
+ // BEGIN: io.clientcore.core.telemetry.fallback.disabledistributedtracing
+
+ InstrumentationOptions> instrumentationOptions = new InstrumentationOptions<>()
+ .setTracingEnabled(false);
+
+ SampleClient client = new SampleClientBuilder().instrumentationOptions(instrumentationOptions).build();
+ client.clientCall();
+
+ // END: io.clientcore.core.telemetry.fallback.disabledistributedtracing
+ }
+
+ /**
+ * This code snippet shows how to assign custom traceId and spanId to the client call.
+ */
+ public void correlationWithExplicitContext() {
+ // BEGIN: io.clientcore.core.telemetry.fallback.correlationwithexplicitcontext
+
+ SampleClient client = new SampleClientBuilder().build();
+
+ RequestOptions options = new RequestOptions()
+ .setInstrumentationContext(new MyInstrumentationContext("e4eaaaf2d48f4bf3b299a8a2a2a77ad7", "5e0c63257de34c56"));
+
+ // run on another thread
+ client.clientCall(options);
+
+ // END: io.clientcore.core.telemetry.fallback.correlationwithexplicitcontext
+ }
+
+ static class MyInstrumentationContext implements InstrumentationContext {
+ private final String traceId;
+ private final String spanId;
+
+ MyInstrumentationContext(String traceId, String spanId) {
+ this.traceId = traceId;
+ this.spanId = spanId;
+ }
+
+ @Override
+ public String getTraceId() {
+ return traceId;
+ }
+
+ @Override
+ public String getSpanId() {
+ return spanId;
+ }
+
+ @Override
+ public String getTraceFlags() {
+ return "00";
+ }
+
+ @Override
+ public boolean isValid() {
+ return true;
+ }
+
+ @Override
+ public Span getSpan() {
+ return Span.noop();
+ }
+ }
+
+ static class SampleClientBuilder {
+ private InstrumentationOptions> instrumentationOptions;
+ // TODO (limolkova): do we need InstrumnetationTrait?
+ public SampleClientBuilder instrumentationOptions(InstrumentationOptions> instrumentationOptions) {
+ this.instrumentationOptions = instrumentationOptions;
+ return this;
+ }
+
+ public SampleClient build() {
+ return new SampleClient(instrumentationOptions, new HttpPipelineBuilder()
+ .policies(new HttpInstrumentationPolicy(instrumentationOptions, null))
+ .build());
+ }
+ }
+
+ static class SampleClient {
+ private final static LibraryInstrumentationOptions LIBRARY_OPTIONS = new LibraryInstrumentationOptions("sample");
+ private final HttpPipeline httpPipeline;
+ private final io.clientcore.core.instrumentation.tracing.Tracer tracer;
+
+ SampleClient(InstrumentationOptions> instrumentationOptions, HttpPipeline httpPipeline) {
+ this.httpPipeline = httpPipeline;
+ this.tracer = Instrumentation.create(instrumentationOptions, LIBRARY_OPTIONS).getTracer();
+ }
+
+ public void clientCall() {
+ this.clientCall(null);
+ }
+
+ @SuppressWarnings("try")
+ public void clientCall(RequestOptions options) {
+ Span span = tracer.spanBuilder("clientCall", SpanKind.CLIENT, options.getInstrumentationContext())
+ .startSpan();
+
+ if (options == null) {
+ options = new RequestOptions();
+ }
+
+ options.setInstrumentationContext(span.getInstrumentationContext());
+
+ try (TracingScope scope = span.makeCurrent()) {
+ Response> response = httpPipeline.send(new HttpRequest(HttpMethod.GET, "https://example.com"));
+ response.close();
+ span.end();
+ } catch (Throwable t) {
+ span.end(t);
+
+ if (t instanceof IOException) {
+ throw new UncheckedIOException((IOException) t);
+ } else if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ } else {
+ throw new RuntimeException(t);
+ }
+ }
+ }
+ }
+}
diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java
index 0fc62912cf21..5d294a02fa72 100644
--- a/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java
+++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java
@@ -7,7 +7,6 @@
import io.clientcore.core.http.models.HttpLogOptions;
import io.clientcore.core.http.models.RequestOptions;
import io.clientcore.core.http.pipeline.HttpInstrumentationPolicy;
-import io.clientcore.core.http.pipeline.HttpLoggingPolicy;
import io.clientcore.core.http.pipeline.HttpPipeline;
import io.clientcore.core.http.pipeline.HttpPipelineBuilder;
import io.clientcore.core.http.pipeline.HttpPipelinePolicy;
@@ -17,8 +16,6 @@
import io.clientcore.core.instrumentation.tracing.Tracer;
import io.clientcore.core.instrumentation.tracing.TracingScope;
-import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY;
-
/**
* THESE CODE SNIPPETS ARE INTENDED FOR CLIENT LIBRARY DEVELOPERS ONLY.
*
@@ -50,6 +47,7 @@ public void createTracer() {
/**
* This example shows minimal distributed tracing instrumentation.
*/
+ @SuppressWarnings("try")
public void traceCall() {
Tracer tracer = Instrumentation.create(null, LIBRARY_OPTIONS).getTracer();
@@ -57,13 +55,16 @@ public void traceCall() {
// BEGIN: io.clientcore.core.telemetry.tracing.tracecall
- Span span = tracer.spanBuilder("{operationName}", SpanKind.CLIENT, requestOptions)
+ Span span = tracer.spanBuilder("{operationName}", SpanKind.CLIENT, null)
.startSpan();
// we'll propagate context implicitly using span.makeCurrent() as shown later.
// Libraries that write async code should propagate context explicitly in addition to implicit propagation.
if (tracer.isEnabled()) {
- requestOptions.putContext(TRACE_CONTEXT_KEY, span);
+ if (requestOptions == null) {
+ requestOptions = new RequestOptions();
+ }
+ requestOptions.setInstrumentationContext(span.getInstrumentationContext());
}
try (TracingScope scope = span.makeCurrent()) {
@@ -83,6 +84,7 @@ public void traceCall() {
/**
* This example shows full distributed tracing instrumentation that adds attributes.
*/
+ @SuppressWarnings("try")
public void traceWithAttributes() {
Tracer tracer = Instrumentation.create(null, LIBRARY_OPTIONS).getTracer();
@@ -90,7 +92,7 @@ public void traceWithAttributes() {
// BEGIN: io.clientcore.core.telemetry.tracing.tracewithattributes
- Span sendSpan = tracer.spanBuilder("send {queue-name}", SpanKind.PRODUCER, requestOptions)
+ Span sendSpan = tracer.spanBuilder("send {queue-name}", SpanKind.PRODUCER, null)
// Some of the attributes should be provided at the start time (as documented in semantic conventions) -
// they can be used by client apps to sample spans.
.setAttribute("messaging.system", "servicebus")
@@ -123,8 +125,7 @@ public void configureInstrumentationPolicy() {
HttpPipeline pipeline = new HttpPipelineBuilder()
.policies(
new HttpRetryPolicy(),
- new HttpInstrumentationPolicy(instrumentationOptions, logOptions),
- new HttpLoggingPolicy(logOptions))
+ new HttpInstrumentationPolicy(instrumentationOptions, logOptions))
.build();
// END: io.clientcore.core.telemetry.tracing.instrumentationpolicy
@@ -143,8 +144,7 @@ public void customizeInstrumentationPolicy() {
HttpPipeline pipeline = new HttpPipelineBuilder()
.policies(
new HttpRetryPolicy(),
- new HttpInstrumentationPolicy(instrumentationOptions, logOptions),
- new HttpLoggingPolicy(logOptions))
+ new HttpInstrumentationPolicy(instrumentationOptions, logOptions))
.build();
// END: io.clientcore.core.telemetry.tracing.customizeinstrumentationpolicy
@@ -157,9 +157,11 @@ public void enrichInstrumentationPolicySpans() {
// BEGIN: io.clientcore.core.telemetry.tracing.enrichhttpspans
HttpPipelinePolicy enrichingPolicy = (request, next) -> {
- Object span = request.getRequestOptions().getContext().get(TRACE_CONTEXT_KEY);
- if (span instanceof Span) {
- ((Span)span).setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID));
+ Span span = request.getRequestOptions() == null
+ ? Span.noop()
+ : request.getRequestOptions().getInstrumentationContext().getSpan();
+ if (span.isRecording()) {
+ span.setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID));
}
return next.process();
@@ -169,8 +171,7 @@ public void enrichInstrumentationPolicySpans() {
.policies(
new HttpRetryPolicy(),
new HttpInstrumentationPolicy(instrumentationOptions, logOptions),
- enrichingPolicy,
- new HttpLoggingPolicy(logOptions))
+ enrichingPolicy)
.build();
diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/BinaryDataJavaDocCodeSnippet.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/BinaryDataJavaDocCodeSnippet.java
index f727a2f34161..69fb89b4ed97 100644
--- a/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/BinaryDataJavaDocCodeSnippet.java
+++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/BinaryDataJavaDocCodeSnippet.java
@@ -3,7 +3,7 @@
package io.clientcore.core.models;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import io.clientcore.core.util.binarydata.BinaryData;
import io.clientcore.core.implementation.util.JsonSerializer;
import io.clientcore.core.util.serializer.ObjectSerializer;
diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/ContextJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/ContextJavaDocCodeSnippets.java
index 5d38ddaa7ed6..6b48c5e194de 100644
--- a/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/ContextJavaDocCodeSnippets.java
+++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/ContextJavaDocCodeSnippets.java
@@ -18,11 +18,8 @@ public void constructContextObject() {
// Create an empty context having no data
Context emptyContext = Context.none();
- // OpenTelemetry context can be optionally passed using PARENT_TRACE_CONTEXT_KEY
- // when OpenTelemetry context is not provided explicitly, ambient
- // io.opentelemetry.context.Context.current() is used
-
- // Context contextWithSpan = new Context(PARENT_TRACE_CONTEXT_KEY, openTelemetryContext);
+ // Create a context with one key value pair
+ Context contextWithOnePair = Context.of("key", "value");
// END: io.clientcore.core.util.context#object-object
}
@@ -31,17 +28,15 @@ public void constructContextObject() {
*/
public void putToContext() {
// BEGIN: io.clientcore.core.util.context.addData#object-object
- // Users can pass parent trace context information and additional metadata to attach to spans created by SDKs
// using the io.clientcore.core.util.Context object.
+
+ Context originalContext = Context.none();
+
final String hostNameValue = "host-name-value";
final String entityPathValue = "entity-path-value";
- // TraceContext represents a tracing solution context type - io.opentelemetry.context.Context for OpenTelemetry.
- final TraceContext parentContext = TraceContext.root();
- Context parentSpanContext = Context.of("PARENT_TRACE_CONTEXT_KEY", parentContext);
-
// Add a new key value pair to the existing context object.
- Context updatedContext = parentSpanContext.put("HOST_NAME_KEY", hostNameValue)
+ Context updatedContext = originalContext.put("HOST_NAME_KEY", hostNameValue)
.put("ENTITY_PATH_KEY", entityPathValue);
// Both key values found on the same updated context object
diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/ClientLoggerJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/ClientLoggerJavaDocCodeSnippets.java
index 9c3c9a7d9538..d3d64fa2097a 100644
--- a/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/ClientLoggerJavaDocCodeSnippets.java
+++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/ClientLoggerJavaDocCodeSnippets.java
@@ -4,6 +4,7 @@
package io.clientcore.core.util;
import io.clientcore.core.http.models.Response;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
import java.io.File;
import java.io.IOException;
diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationLoggingTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationLoggingTests.java
new file mode 100644
index 000000000000..f66fbf73974c
--- /dev/null
+++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationLoggingTests.java
@@ -0,0 +1,973 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.http.pipeline;
+
+import io.clientcore.core.http.MockHttpResponse;
+import io.clientcore.core.http.models.HttpHeader;
+import io.clientcore.core.http.models.HttpHeaderName;
+import io.clientcore.core.http.models.HttpHeaders;
+import io.clientcore.core.http.models.HttpLogOptions;
+import io.clientcore.core.http.models.HttpMethod;
+import io.clientcore.core.http.models.HttpRequest;
+import io.clientcore.core.http.models.HttpRetryOptions;
+import io.clientcore.core.http.models.RequestOptions;
+import io.clientcore.core.http.models.Response;
+import io.clientcore.core.implementation.AccessibleByteArrayOutputStream;
+import io.clientcore.core.implementation.http.HttpRequestAccessHelper;
+import io.clientcore.core.instrumentation.InstrumentationContext;
+import io.clientcore.core.instrumentation.InstrumentationOptions;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
+import io.clientcore.core.util.binarydata.BinaryData;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import static io.clientcore.core.http.models.HttpHeaderName.TRACEPARENT;
+import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.createInstrumentationContext;
+import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.createRandomInstrumentationContext;
+import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.parseLogMessages;
+import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.setupLogLevelAndGetLogger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Execution(ExecutionMode.SAME_THREAD)
+public class HttpInstrumentationLoggingTests {
+ private static final String URI = "https://example.com?param=value&api-version=42";
+ private static final String REDACTED_URI = "https://example.com?param=REDACTED&api-version=42";
+ private static final Set DEFAULT_ALLOWED_QUERY_PARAMS = new HttpLogOptions().getAllowedQueryParamNames();
+ private static final Set DEFAULT_ALLOWED_HEADERS = new HttpLogOptions().getAllowedHeaderNames();
+ private static final HttpHeaderName CUSTOM_REQUEST_ID = HttpHeaderName.fromString("custom-request-id");
+ private static final InstrumentationOptions> DEFAULT_INSTRUMENTATION_OPTIONS = null;
+
+ private final AccessibleByteArrayOutputStream logCaptureStream;
+
+ public HttpInstrumentationLoggingTests() {
+ this.logCaptureStream = new AccessibleByteArrayOutputStream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("disabledHttpLoggingSource")
+ public void testDisabledHttpLogging(ClientLogger.LogLevel logLevel, HttpLogOptions.HttpLogDetailLevel httpLogLevel)
+ throws IOException {
+ ClientLogger logger = setupLogLevelAndGetLogger(logLevel, logCaptureStream);
+
+ HttpPipeline pipeline
+ = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, new HttpLogOptions().setLogLevel(httpLogLevel));
+ HttpRequest request = new HttpRequest(HttpMethod.GET, URI);
+ request.setRequestOptions(new RequestOptions().setLogger(logger));
+
+ pipeline.send(request).close();
+
+ assertEquals(0, parseLogMessages(logCaptureStream).size());
+ }
+
+ public static Stream disabledHttpLoggingSource() {
+ return Stream.of(Arguments.of(ClientLogger.LogLevel.VERBOSE, HttpLogOptions.HttpLogDetailLevel.NONE),
+ Arguments.of(ClientLogger.LogLevel.WARNING, HttpLogOptions.HttpLogDetailLevel.BASIC),
+ Arguments.of(ClientLogger.LogLevel.WARNING, HttpLogOptions.HttpLogDetailLevel.HEADERS),
+ Arguments.of(ClientLogger.LogLevel.WARNING, HttpLogOptions.HttpLogDetailLevel.BODY),
+ Arguments.of(ClientLogger.LogLevel.WARNING, HttpLogOptions.HttpLogDetailLevel.BODY_AND_HEADERS));
+ }
+
+ @ParameterizedTest
+ @MethodSource("allowQueryParamSource")
+ public void testBasicHttpLogging(Set allowedParams, String expectedUri) throws IOException {
+ ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream);
+ HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC)
+ .setAllowedQueryParamNames(allowedParams);
+
+ HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options);
+
+ HttpRequest request = createRequest(HttpMethod.GET, URI, logger);
+ Response> response = pipeline.send(request);
+ response.close();
+
+ List