From 06b56d98d48a7f27e247df90f03914982138d85e Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Wed, 1 Jan 2025 10:39:05 -0800 Subject: [PATCH 01/11] log correlation and log-based tracing --- .../core/http/models/HttpLogOptions.java | 6 +- .../pipeline/HttpInstrumentationPolicy.java | 1 + .../http/pipeline/HttpRedirectPolicy.java | 35 +- .../DefaultInstrumentation.java | 329 ++++++++++++++++++ .../instrumentation/otel/FallbackInvoker.java | 17 +- .../instrumentation/otel/OTelInitializer.java | 1 + .../otel/OTelInstrumentation.java | 2 +- .../otel/tracing/OTelSpan.java | 5 + .../core/instrumentation/Instrumentation.java | 5 +- .../io/clientcore/core/util/ClientLogger.java | 52 ++- .../HttpInstrumentationPolicyNoopTests.java | 17 +- .../instrumentation/SuppressionTests.java | 1 - 12 files changed, 430 insertions(+), 41 deletions(-) create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpLogOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpLogOptions.java index 1814586f3779d..54b83990e8aae 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpLogOptions.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpLogOptions.java @@ -22,9 +22,9 @@ public final class HttpLogOptions { private Set allowedHeaderNames; private Set allowedQueryParamNames; private static final List DEFAULT_HEADERS_ALLOWLIST - = Arrays.asList(HttpHeaderName.TRACEPARENT, HttpHeaderName.ACCEPT, HttpHeaderName.CACHE_CONTROL, - HttpHeaderName.CONNECTION, HttpHeaderName.CONTENT_LENGTH, HttpHeaderName.CONTENT_TYPE, HttpHeaderName.DATE, - HttpHeaderName.ETAG, HttpHeaderName.EXPIRES, HttpHeaderName.IF_MATCH, HttpHeaderName.IF_MODIFIED_SINCE, + = Arrays.asList(HttpHeaderName.ACCEPT, HttpHeaderName.CACHE_CONTROL, HttpHeaderName.CONNECTION, + HttpHeaderName.CONTENT_LENGTH, HttpHeaderName.CONTENT_TYPE, HttpHeaderName.DATE, HttpHeaderName.ETAG, + HttpHeaderName.EXPIRES, HttpHeaderName.IF_MATCH, HttpHeaderName.IF_MODIFIED_SINCE, HttpHeaderName.IF_NONE_MATCH, HttpHeaderName.IF_UNMODIFIED_SINCE, HttpHeaderName.LAST_MODIFIED, HttpHeaderName.PRAGMA, HttpHeaderName.RETRY_AFTER, HttpHeaderName.SERVER, HttpHeaderName.TRANSFER_ENCODING, HttpHeaderName.USER_AGENT, HttpHeaderName.WWW_AUTHENTICATE); diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java index bcc9370c62ca3..e4b0c46e48b1c 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java @@ -183,6 +183,7 @@ public Response process(HttpRequest request, HttpPipelineNextPolicy next) { } Context context = request.getRequestOptions().getContext().put(TRACE_CONTEXT_KEY, span); + request.getRequestOptions().setContext(context); propagateContext(context, request.getHeaders()); 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 6bdddc826cd98..ab73242ac7b8a 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 @@ -10,6 +10,7 @@ import io.clientcore.core.http.models.Response; import io.clientcore.core.implementation.util.LoggingKeys; import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.util.Context; import java.io.IOException; import java.io.UncheckedIOException; @@ -74,7 +75,7 @@ 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<>()); + return attemptRedirect(next, 1, new LinkedHashSet<>(), httpRequest.getRequestOptions().getContext()); } /** @@ -82,37 +83,40 @@ public Response process(HttpRequest httpRequest, HttpPipelineNextPolicy next) * new redirect URI. */ private Response attemptRedirect(final HttpPipelineNextPolicy next, final int redirectAttempt, - LinkedHashSet attemptedRedirectUris) { + LinkedHashSet attemptedRedirectUris, Context context) { + // 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(requestRedirectCondition, context))) { createRedirectRequest(response); - return attemptRedirect(next, redirectAttempt + 1, attemptedRedirectUris); + return attemptRedirect(next, redirectAttempt + 1, attemptedRedirectUris, context); } return response; } - private boolean defaultShouldAttemptRedirect(HttpRequestRedirectCondition requestRedirectCondition) { + private boolean defaultShouldAttemptRedirect(HttpRequestRedirectCondition requestRedirectCondition, + Context 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()) + && isValidRedirectCount(tryCount, context) + && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), context) && redirectUri != null - && !alreadyAttemptedRedirectUri(redirectUri, attemptedRedirectUris)) { + && !alreadyAttemptedRedirectUri(redirectUri, attemptedRedirectUris, context)) { LOGGER.atVerbose() .addKeyValue(LoggingKeys.TRY_COUNT_KEY, tryCount) .addKeyValue(REDIRECT_URIS_KEY, attemptedRedirectUris::toString) .addKeyValue(ORIGINATING_REQUEST_URI_KEY, response.getRequest().getUri()) + .setContext(context) .log("Redirecting."); attemptedRedirectUris.add(redirectUri); @@ -130,9 +134,12 @@ && isAllowedRedirectMethod(response.getRequest().getHttpMethod()) * * @return {@code true} if the {@code tryCount} is greater than the {@code maxAttempts}, {@code false} otherwise. */ - private boolean isValidRedirectCount(int tryCount) { + private boolean isValidRedirectCount(int tryCount, Context context) { if (tryCount >= this.maxAttempts) { - LOGGER.atError().addKeyValue("maxAttempts", this.maxAttempts).log("Redirect attempts have been exhausted."); + LOGGER.atError() + .addKeyValue("maxAttempts", this.maxAttempts) + .setContext(context) + .log("Redirect attempts have been exhausted."); return false; } @@ -149,10 +156,12 @@ private boolean isValidRedirectCount(int tryCount) { * @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) { + private boolean alreadyAttemptedRedirectUri(String redirectUri, Set attemptedRedirectUris, + Context context) { if (attemptedRedirectUris.contains(redirectUri)) { LOGGER.atError() .addKeyValue(LoggingKeys.REDIRECT_URI_KEY, redirectUri) + .setContext(context) .log("Request was redirected more than once to the same URI."); return true; @@ -168,11 +177,12 @@ private boolean alreadyAttemptedRedirectUri(String redirectUri, Set atte * * @return {@code true} if the request {@code httpMethod} is a valid http redirect method, {@code false} otherwise. */ - private boolean isAllowedRedirectMethod(HttpMethod httpMethod) { + private boolean isAllowedRedirectMethod(HttpMethod httpMethod, Context context) { if (allowedRedirectHttpMethods.contains(httpMethod)) { return true; } else { LOGGER.atError() + .setContext(context) .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, httpMethod) .log("Request redirection is not enabled for this HTTP method."); @@ -205,6 +215,5 @@ private void createRedirectRequest(Response redirectResponse) { } catch (IOException e) { throw LOGGER.logThrowableAsError(new UncheckedIOException(e)); } - } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java new file mode 100644 index 0000000000000..5f2d02e7120fc --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation; + +import io.clientcore.core.http.models.RequestOptions; +import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; +import io.clientcore.core.instrumentation.Instrumentation; +import io.clientcore.core.instrumentation.InstrumentationOptions; +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.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.ClientLogger; +import io.clientcore.core.util.Context; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class DefaultInstrumentation implements Instrumentation { + private static final String INVALID_TRACE_ID = "00000000000000000000000000000000"; + private static final String INVALID_SPAN_ID = "0000000000000000"; + + private final InstrumentationOptions instrumentationOptions; + private final LibraryInstrumentationOptions libraryOptions; + + public DefaultInstrumentation(InstrumentationOptions instrumentationOptions, + LibraryInstrumentationOptions libraryOptions) { + this.instrumentationOptions = instrumentationOptions; + this.libraryOptions = libraryOptions; + } + + @Override + public Tracer getTracer() { + return new DefaultTracer(instrumentationOptions, libraryOptions); + } + + @Override + public TraceContextPropagator getW3CTraceContextPropagator() { + return DefaultContextPropagator.W3C_TRACE_CONTEXT_PROPAGATOR; + } + + public static ClientLogger.LoggingEvent enrichLog(ClientLogger.LoggingEvent log, Context context) { + if (OTelInitializer.isInitialized()) { + return log; + } + + DefaultSpan span = DefaultSpan.fromContextOrCurrent(context); + if (span == null) { + return log; + } + + return log.addKeyValue("trace.id", span.getSpanContext().getTraceId()) + .addKeyValue("span.id", span.getSpanContext().getSpanId()); + } + + private static final class DefaultTracer implements Tracer { + private final boolean isEnabled; + private final ClientLogger logger; + + DefaultTracer(InstrumentationOptions instrumentationOptions, LibraryInstrumentationOptions libraryOptions) { + this.isEnabled = instrumentationOptions.isTracingEnabled(); // TODO: probably need additional config for log-based tracing + Map libraryContext = new HashMap<>(2); + libraryContext.put("library.version", libraryOptions.getLibraryVersion()); + libraryContext.put("library.instrumentation.schema_url", libraryOptions.getSchemaUrl()); + + this.logger = new ClientLogger(libraryOptions.getLibraryName() + ".tracing", libraryContext); + } + + @Override + public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, RequestOptions requestOptions) { + return new DefaultSpanBuilder(this.logger, spanName, spanKind, requestOptions); + } + + @Override + public boolean isEnabled() { + return isEnabled; + } + } + + private static final class DefaultSpanBuilder implements SpanBuilder { + private final ClientLogger.LoggingEvent log; + private final boolean isRecording; + private final DefaultSpanContext spanContext; + + DefaultSpanBuilder(ClientLogger logger, String spanName, SpanKind spanKind, RequestOptions requestOptions) { + isRecording = logger.canLogAtLevel(ClientLogger.LogLevel.INFORMATIONAL); + DefaultSpanContext parentSpanContext = requestOptions == null + ? DefaultSpanContext.INVALID + : DefaultSpanContext.fromContext(requestOptions.getContext()); + spanContext = DefaultSpanContext.fromParent(parentSpanContext, isRecording); + this.log = logger.atInfo() + .addKeyValue("span.trace_id", spanContext.getTraceId()) + .addKeyValue("span.id", spanContext.getSpanId()) + .addKeyValue("span.parent.id", parentSpanContext.getSpanId()) + .addKeyValue("span.name", spanName) + .addKeyValue("span.kind", spanKind.name()); + } + + @Override + public SpanBuilder setAttribute(String key, Object value) { + this.log.addKeyValue(key, value); + return this; + } + + @Override + public Span startSpan() { + return new DefaultSpan(log, spanContext, isRecording); + } + } + + private static final class DefaultSpan implements Span { + private final ClientLogger.LoggingEvent log; + private final long startTime; + private final boolean isRecording; + private final DefaultSpanContext spanContext; + private String errorType; + + DefaultSpan(ClientLogger.LoggingEvent log, DefaultSpanContext spanContext, boolean isRecording) { + this.log = log; + this.startTime = System.nanoTime(); + this.spanContext = spanContext; + this.isRecording = isRecording; + } + + DefaultSpan(DefaultSpanContext spanContext) { + this.spanContext = spanContext; + this.isRecording = false; + this.log = null; + this.startTime = 0; + } + + @Override + public Span setAttribute(String key, Object value) { + if (log != null) { + log.addKeyValue(key, value); + } + return this; + } + + @Override + public Span setError(String errorType) { + this.errorType = errorType; + return this; + } + + @Override + public void end() { + end(null); + } + + @Override + public void end(Throwable error) { + if (log == null) { + return; + } + + if (isRecording) { + double durationMs = (System.nanoTime() - startTime) / 1_000_000.0; + log.addKeyValue("span.duration.ms", durationMs); + if (error != null || errorType != null) { + setAttribute("error.type", errorType != null ? errorType : error.getClass().getCanonicalName()); + } + } + + if (error != null) { + log.log("span ended", error); + } else { + log.log("span ended"); + } + } + + @Override + public boolean isRecording() { + return isRecording; + } + + @Override + public TracingScope makeCurrent() { + return new DefaultScope(this); + } + + public DefaultSpanContext getSpanContext() { + return spanContext; + } + + public static DefaultSpan fromContextOrCurrent(Context context) { + if (context != null) { + Object span = context.get(TRACE_CONTEXT_KEY); + if (span instanceof DefaultSpan) { + return (DefaultSpan) span; + } + + if (span != null) { + return null; + } + } + + return DefaultScope.getCurrent(); + } + }; + + private static final class DefaultScope implements TracingScope { + private final static ThreadLocal CURRENT_SPAN = new ThreadLocal<>(); + private final DefaultSpan originalSpan; + + DefaultScope(DefaultSpan span) { + this.originalSpan = CURRENT_SPAN.get(); + CURRENT_SPAN.set(span); + } + + @Override + public void close() { + CURRENT_SPAN.set(originalSpan); + } + + static DefaultSpan getCurrent() { + return CURRENT_SPAN.get(); + } + } + + private static final class DefaultContextPropagator implements TraceContextPropagator { + static final TraceContextPropagator W3C_TRACE_CONTEXT_PROPAGATOR = new DefaultContextPropagator(); + + private DefaultContextPropagator() { + } + + @Override + public void inject(Context context, C carrier, TraceContextSetter setter) { + DefaultSpanContext spanContext = DefaultSpanContext.fromContext(context); + if (spanContext.isValid()) { + setter.set(carrier, "traceparent", "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + + "-" + spanContext.getTraceFlags()); + } + } + + @Override + public Context extract(Context 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); + DefaultSpanContext spanContext = new DefaultSpanContext(traceId, spanId, traceFlags); + return context.put(TRACE_CONTEXT_KEY, new DefaultSpan(spanContext)); + } else { + // TODO log + } + } + return context; + } + + private static boolean isValidTraceparent(String traceparent) { + // TODO: add more validation + return traceparent.startsWith("00-") && traceparent.length() == 55; + } + }; + + private static final class DefaultSpanContext { + static final DefaultSpanContext INVALID = new DefaultSpanContext(); + private final String traceId; + private final String spanId; + private final String traceFlags; + private final boolean isValid; + + String getTraceId() { + return traceId; + } + + String getSpanId() { + return spanId; + } + + String getTraceFlags() { + return traceFlags; + } + + boolean isValid() { + return isValid; + } + + DefaultSpanContext() { + this.traceId = INVALID_TRACE_ID; + this.spanId = INVALID_SPAN_ID; + this.traceFlags = "00"; + this.isValid = false; + } + + DefaultSpanContext(String traceId, String spanId, String traceFlags) { + this.traceId = traceId; + this.spanId = spanId; + this.traceFlags = traceFlags; + this.isValid = true; + } + + static DefaultSpanContext fromParent(DefaultSpanContext parent, boolean isSampled) { + return parent.isValid() + ? new DefaultSpanContext(parent.traceId, getRandomId(16), isSampled ? "01" : "00") + : new DefaultSpanContext(getRandomId(32), getRandomId(16), isSampled ? "01" : "00"); + } + + static DefaultSpanContext fromContext(Context context) { + Object span = context.get(TRACE_CONTEXT_KEY); + if (span instanceof DefaultSpan) { + return ((DefaultSpan) span).getSpanContext(); + } else if (span != null) { + // TODO log + } + + return INVALID; + } + + /** + * Generates random id with given length up to 32 chars. + */ + private static String getRandomId(int length) { + // TODO: copy impl from OTel + UUID uuid = UUID.randomUUID(); + return uuid.toString().replace("-", "").substring(32 - length); + } + }; +} 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 72d85b0da7fda..a41a7c96ee398 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 @@ -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,8 +68,9 @@ 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 + * @param arg1 the first argument * @return the result of the invocation or the fallback value */ public Object invoke(Object argOrTarget, Object arg1) { @@ -81,9 +84,10 @@ 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 + * @param arg1 the first argument + * @param arg2 the second argument * @return the result of the invocation or the fallback value */ public Object invoke(Object argOrTarget, Object arg1, Object arg2) { @@ -97,10 +101,11 @@ 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 - * @param arg3 the third argument + * @param arg1 the first argument + * @param arg2 the second argument + * @param arg3 the third argument * @return the result of the invocation or the fallback value */ public Object invoke(Object argOrTarget, Object arg1, Object arg2, Object arg3) { 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 7b0e5eb1b2a28..932f086277b4e 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 @@ -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 6edbbb51a4eb6..e5cf4fe2088bf 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 @@ -6,9 +6,9 @@ import io.clientcore.core.implementation.ReflectiveInvoker; 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.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; 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 52a31d0f42e3b..4958dc8ddc8cd 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 @@ -38,6 +38,7 @@ public class OTelSpan implements Span { private static final FallbackInvoker STORE_IN_CONTEXT_INVOKER; private static final FallbackInvoker FROM_CONTEXT_INVOKER; private static final FallbackInvoker WRAP_INVOKER; + private static final FallbackInvoker CURRENT_INVOKER; private static final Object ERROR_STATUS_CODE; private final Object otelSpan; private final Object otelContext; @@ -53,6 +54,7 @@ public class OTelSpan implements Span { ReflectiveInvoker storeInContextInvoker = null; ReflectiveInvoker fromContextInvoker = null; ReflectiveInvoker wrapInvoker = null; + ReflectiveInvoker currentInvoker = null; Object errorStatusCode = null; @@ -75,6 +77,8 @@ public class OTelSpan implements Span { wrapInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("wrap", SPAN_CONTEXT_CLASS)); errorStatusCode = STATUS_CODE_CLASS.getField("ERROR").get(null); + + currentInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("current")); } catch (Throwable t) { OTelInitializer.initError(LOGGER, t); } @@ -88,6 +92,7 @@ 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); + CURRENT_INVOKER = new FallbackInvoker(currentInvoker, LOGGER); ERROR_STATUS_CODE = errorStatusCode; } 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 a5ca91ac6e094..74d3f06252c07 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.DefaultInstrumentation; import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; import io.clientcore.core.implementation.instrumentation.otel.OTelInstrumentation; import io.clientcore.core.instrumentation.tracing.TraceContextPropagator; @@ -10,8 +11,6 @@ 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. * @@ -77,7 +76,7 @@ static Instrumentation create(InstrumentationOptions applicationOptions, if (OTelInitializer.isInitialized()) { return new OTelInstrumentation(applicationOptions, libraryOptions); } else { - return NOOP_PROVIDER; + return new DefaultInstrumentation(applicationOptions, libraryOptions); } } } 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/util/ClientLogger.java index 6fea3c48010da..7cd37cc89964e 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/ClientLogger.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/ClientLogger.java @@ -5,6 +5,7 @@ import io.clientcore.core.annotation.Metadata; import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; +import io.clientcore.core.implementation.instrumentation.DefaultInstrumentation; import io.clientcore.core.implementation.util.DefaultLogger; import io.clientcore.core.implementation.util.Slf4jLoggerShim; import io.clientcore.core.serialization.json.JsonWriter; @@ -60,8 +61,19 @@ 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. + * @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 +300,7 @@ public static final class LoggingEvent { private final boolean isEnabled; private Map keyValuePairs; private String eventName; + private Context context; /** * Creates {@code LoggingEvent} for provided level and {@link ClientLogger}. @@ -430,6 +443,18 @@ public LoggingEvent addKeyValue(String key, Supplier valueSupplier) { return this; } + public LoggingEvent setContext(Context context) { + this.context = context; + return this; + } + + public LoggingEvent setCause(Throwable throwable) { + if (this.isEnabled) { + setThrowableInternal(throwable, logger.canLogAtLevel(LogLevel.VERBOSE)); + } + 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. @@ -472,25 +497,32 @@ public void log(String message) { 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()); - if (isDebugEnabled) { - StringBuilder stackTrace = new StringBuilder(); - DefaultLogger.appendThrowable(stackTrace, throwable); - addKeyValue("exception.stacktrace", stackTrace.toString()); - } - } + setThrowableInternal(throwable, isDebugEnabled); + // TODO: we should not trace-id/span-id (by default) when otel is enabled? logger.performLogging(level, getMessageWithContext(message), isDebugEnabled ? throwable : null); } return throwable; } + private void setThrowableInternal(Throwable throwable, boolean isDebugEnabled) { + if (throwable != null) { + addKeyValueInternal("exception.type", throwable.getClass().getCanonicalName()); + addKeyValueInternal("exception.message", throwable.getMessage()); + if (isDebugEnabled) { + StringBuilder stackTrace = new StringBuilder(); + DefaultLogger.appendThrowable(stackTrace, throwable); + addKeyValue("exception.stacktrace", stackTrace.toString()); + } + } + } + private String getMessageWithContext(String message) { if (message == null) { message = ""; } + DefaultInstrumentation.enrichLog(this, context); + int pairsCount = (keyValuePairs == null ? 0 : keyValuePairs.size()) + (globalPairs == null ? 0 : globalPairs.size()); int speculatedSize = 20 + pairsCount * 20 + message.length(); diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyNoopTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyNoopTests.java index 38f13b79d5a40..e247d70163f20 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyNoopTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyNoopTests.java @@ -5,6 +5,7 @@ import io.clientcore.core.http.MockHttpResponse; import io.clientcore.core.http.models.HttpHeaderName; +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.Response; @@ -24,12 +25,16 @@ public class HttpInstrumentationPolicyNoopTests { private static final InstrumentationOptions OPTIONS = new InstrumentationOptions<>(); + private static final HttpLogOptions ENABLED_HTTP_LOG_OPTIONS + = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS); private static final HttpHeaderName TRACESTATE = HttpHeaderName.fromString("tracestate"); @ParameterizedTest @ValueSource(ints = { 200, 201, 206, 302, 400, 404, 500, 503 }) public void simpleRequestTracingDisabled(int statusCode) throws IOException { - HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(OPTIONS, null)) + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(new HttpInstrumentationPolicy(OPTIONS, ENABLED_HTTP_LOG_OPTIONS), + new HttpLoggingPolicy(ENABLED_HTTP_LOG_OPTIONS)) .httpClient(request -> new MockHttpResponse(request, statusCode)) .build(); @@ -45,9 +50,13 @@ public void simpleRequestTracingDisabled(int statusCode) throws IOException { public void exceptionTracingDisabled() { SocketException exception = new SocketException("test exception"); HttpPipeline pipeline - = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(OPTIONS, null)).httpClient(request -> { - throw exception; - }).build(); + = new HttpPipelineBuilder() + .policies(new HttpInstrumentationPolicy(OPTIONS, ENABLED_HTTP_LOG_OPTIONS), + new HttpLoggingPolicy(ENABLED_HTTP_LOG_OPTIONS)) + .httpClient(request -> { + throw exception; + }) + .build(); assertThrows(UncheckedIOException.class, () -> pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost/")).close()); diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java index 9316de90b0442..ad13df52bab81 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java @@ -317,7 +317,6 @@ static class SampleClient { public void protocolMethod(RequestOptions options) { Span span = tracer.spanBuilder("protocolMethod", INTERNAL, options).startSpan(); - // TODO (limolkova): should we have addContext(k, v) on options? options.putContext(TRACE_CONTEXT_KEY, span); try (TracingScope scope = span.makeCurrent()) { From 82819ce1a55a36b484515ff661279ff2d67ceb91 Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Thu, 2 Jan 2025 10:49:40 -0800 Subject: [PATCH 02/11] a lot of changes --- .../core/checkstyle-suppressions.xml | 9 +- sdk/clientcore/core/spotbugs-exclude.xml | 5 +- .../core/credential/KeyCredential.java | 2 +- .../core/http/client/DefaultHttpClient.java | 2 +- .../http/client/DefaultHttpClientBuilder.java | 2 +- .../implementation/HeaderFilteringMap.java | 2 +- .../client/implementation/JdkHttpRequest.java | 2 +- .../client/implementation/JdkHttpUtils.java | 2 +- .../core/http/models/HttpRedirectOptions.java | 2 +- .../core/http/models/HttpRequest.java | 2 +- .../core/http/models/HttpRetryOptions.java | 2 +- .../core/http/models/ProxyOptions.java | 2 +- .../core/http/models/RequestOptions.java | 2 +- .../pipeline/HttpInstrumentationPolicy.java | 554 ++++++++++++++++-- .../http/pipeline/HttpPipelineNextPolicy.java | 2 +- .../http/pipeline/HttpRedirectPolicy.java | 21 +- .../core/http/pipeline/HttpRequestLogger.java | 37 ++ .../http/pipeline/HttpResponseLogger.java | 57 ++ .../core/http/pipeline/HttpRetryPolicy.java | 13 +- .../http/pipeline/KeyCredentialPolicy.java | 2 +- .../ReflectionSerializable.java | 2 +- .../core/implementation/ReflectionUtils.java | 2 +- .../http/HttpPipelineCallState.java | 2 +- .../rest/LengthValidatingInputStream.java | 2 +- .../http/rest/PercentEscaper.java | 2 +- .../http/rest/ResponseConstructorsCache.java | 2 +- .../ResponseExceptionConstructorCache.java | 2 +- .../http/rest/RestProxyImpl.java | 2 +- .../http/rest/SwaggerMethodParser.java | 2 +- .../http/serializer/CompositeSerializer.java | 2 +- .../serializer/HttpResponseBodyDecoder.java | 2 +- .../instrumentation/AttributeKeys.java | 72 +++ .../DefaultInstrumentation.java | 13 +- .../DefaultLogger.java | 7 +- ...aryInstrumentationOptionsAccessHelper.java | 1 + .../Slf4jLoggerShim.java | 15 +- .../instrumentation/otel/FallbackInvoker.java | 2 +- .../otel/OTelAttributeKey.java | 2 +- .../instrumentation/otel/OTelInitializer.java | 4 +- .../otel/OTelInstrumentation.java | 2 +- .../otel/tracing/OTelContext.java | 2 +- .../otel/tracing/OTelSpan.java | 2 +- .../otel/tracing/OTelSpanBuilder.java | 2 +- .../otel/tracing/OTelSpanContext.java | 2 +- .../tracing/OTelTraceContextPropagator.java | 2 +- .../otel/tracing/OTelTracer.java | 2 +- .../otel/tracing/OTelUtils.java | 2 +- .../implementation/util/DateTimeRfc1123.java | 2 +- .../core/implementation/util/ImplUtils.java | 2 +- .../implementation/util/InternalContext.java | 2 +- .../implementation/util/JsonSerializer.java | 2 +- .../core/implementation/util/Providers.java | 2 +- .../implementation/util/SliceInputStream.java | 2 +- .../core/implementation/util/StreamUtil.java | 2 +- .../implementation/util/XmlSerializer.java | 2 +- .../instrumentation/NoopInstrumentation.java | 86 --- .../logging}/ClientLogger.java | 22 +- .../core/instrumentation/tracing/Span.java | 41 ++ .../serialization/json/models/JsonNumber.java | 2 +- .../core/serialization/xml/XmlReader.java | 2 +- .../java/io/clientcore/core/util/Context.java | 1 + .../core/util/SharedExecutorService.java | 1 + .../util/auth/CompositeChallengeHandler.java | 2 +- .../core/util/binarydata/FileBinaryData.java | 2 +- .../binarydata/InputStreamBinaryData.java | 2 +- .../binarydata/ListByteBufferBinaryData.java | 2 +- .../binarydata/SerializableBinaryData.java | 2 +- .../util/configuration/Configuration.java | 2 +- .../configuration/ConfigurationBuilder.java | 2 +- .../core/src/main/java/module-info.java | 1 + ...rLibraryDevelopersJavaDocCodeSnippets.java | 12 +- .../models/BinaryDataJavaDocCodeSnippet.java | 2 +- .../util/ClientLoggerJavaDocCodeSnippets.java | 1 + ...ttpInstrumentationPolicyDefaultTests.java} | 37 +- ...nseConstructorsCacheLambdaMetaFactory.java | 2 +- ...ResponseConstructorsNoCacheReflection.java | 2 +- .../tracing/DefaultInstrumentationTests.java | 78 +++ .../tracing/DefaultTracerTests.java | 4 + .../logging}/ClientLoggerTests.java | 6 +- .../core/shared/HttpClientTests.java | 6 +- .../http/okhttp3/OkHttpHttpClient.java | 2 +- .../http/okhttp3/OkHttpHttpClientBuilder.java | 2 +- .../OkHttpInputStreamRequestBody.java | 2 +- .../implementation/ProxyAuthenticator.java | 2 +- .../io/clientcore/http/stress/HttpGet.java | 3 +- .../io/clientcore/http/stress/HttpPatch.java | 2 +- .../src/main/java/module-info.java | 1 + .../TelemetryJavaDocCodeSnippets.java | 1 + .../Slf4jLoggerShimIT.java | 4 +- .../ClientLoggerSlf4JTests.java | 5 +- 90 files changed, 980 insertions(+), 256 deletions(-) create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRequestLogger.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpResponseLogger.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java rename sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/{util => instrumentation}/DefaultLogger.java (96%) rename sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/{util => instrumentation}/Slf4jLoggerShim.java (95%) delete mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/NoopInstrumentation.java rename sdk/clientcore/core/src/main/java/io/clientcore/core/{util => instrumentation/logging}/ClientLogger.java (97%) rename sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/{HttpInstrumentationPolicyNoopTests.java => HttpInstrumentationPolicyDefaultTests.java} (65%) create mode 100644 sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java create mode 100644 sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultTracerTests.java rename sdk/clientcore/core/src/test/java/io/clientcore/core/{util => instrumentation/logging}/ClientLoggerTests.java (99%) rename sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/{util => instrumentation}/Slf4jLoggerShimIT.java (96%) rename sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/{util => instrumentation}/ClientLoggerSlf4JTests.java (53%) diff --git a/sdk/clientcore/core/checkstyle-suppressions.xml b/sdk/clientcore/core/checkstyle-suppressions.xml index e81feb3773645..88e7a72d28fef 100644 --- a/sdk/clientcore/core/checkstyle-suppressions.xml +++ b/sdk/clientcore/core/checkstyle-suppressions.xml @@ -6,20 +6,19 @@ - + - + - + - - + diff --git a/sdk/clientcore/core/spotbugs-exclude.xml b/sdk/clientcore/core/spotbugs-exclude.xml index 2235ca95cf865..6d3d22b6e5d55 100644 --- a/sdk/clientcore/core/spotbugs-exclude.xml +++ b/sdk/clientcore/core/spotbugs-exclude.xml @@ -71,7 +71,8 @@ - + + @@ -165,7 +166,7 @@ - + diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/credential/KeyCredential.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/credential/KeyCredential.java index 7fd0aed22ec24..3f1ccc8a8e84b 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/credential/KeyCredential.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/credential/KeyCredential.java @@ -3,7 +3,7 @@ package io.clientcore.core.credential; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.util.Objects; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/DefaultHttpClient.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/DefaultHttpClient.java index 2ae30c0bcd2e4..d408eb0dab410 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/DefaultHttpClient.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/DefaultHttpClient.java @@ -13,7 +13,7 @@ import io.clientcore.core.http.models.Response; import io.clientcore.core.http.models.ResponseBodyMode; import io.clientcore.core.http.models.ServerSentEventListener; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.ServerSentEventUtils; import io.clientcore.core.util.ServerSentResult; import io.clientcore.core.util.binarydata.BinaryData; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/DefaultHttpClientBuilder.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/DefaultHttpClientBuilder.java index 3a4263ac2c334..2a0a2dc99b1a4 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/DefaultHttpClientBuilder.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/DefaultHttpClientBuilder.java @@ -5,7 +5,7 @@ import io.clientcore.core.http.client.implementation.JdkHttpClientProxySelector; import io.clientcore.core.http.models.ProxyOptions; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.SharedExecutorService; import io.clientcore.core.util.configuration.Configuration; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/HeaderFilteringMap.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/HeaderFilteringMap.java index 8158c66e9f9d7..9e0916d4aa9ad 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/HeaderFilteringMap.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/HeaderFilteringMap.java @@ -3,7 +3,7 @@ package io.clientcore.core.http.client.implementation; import io.clientcore.core.http.models.HttpHeaders; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.util.AbstractMap; import java.util.List; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/JdkHttpRequest.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/JdkHttpRequest.java index 7fb033ea3501a..8f5769b399200 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/JdkHttpRequest.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/JdkHttpRequest.java @@ -3,7 +3,7 @@ package io.clientcore.core.http.client.implementation; import io.clientcore.core.http.models.HttpMethod; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.net.URI; import java.net.http.HttpClient; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/JdkHttpUtils.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/JdkHttpUtils.java index 154bac4f63a22..3ad9cb165f13e 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/JdkHttpUtils.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/client/implementation/JdkHttpUtils.java @@ -4,7 +4,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.SharedExecutorService; import io.clientcore.core.util.configuration.Configuration; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRedirectOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRedirectOptions.java index b1bb668f78dc5..9769a05ec5fdb 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRedirectOptions.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRedirectOptions.java @@ -4,7 +4,7 @@ package io.clientcore.core.http.models; import io.clientcore.core.http.pipeline.HttpRequestRedirectCondition; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.util.EnumSet; import java.util.function.Predicate; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRequest.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRequest.java index 68c471617fabe..c94e3f4e8d71d 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRequest.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRequest.java @@ -5,7 +5,7 @@ import io.clientcore.core.annotation.Metadata; import io.clientcore.core.implementation.http.HttpRequestAccessHelper; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.binarydata.BinaryData; import java.net.URI; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRetryOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRetryOptions.java index 7362d6b178094..5f1d30cc4e53d 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRetryOptions.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpRetryOptions.java @@ -4,7 +4,7 @@ package io.clientcore.core.http.models; import io.clientcore.core.http.pipeline.HttpRequestRetryCondition; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.time.Duration; import java.util.Objects; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/ProxyOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/ProxyOptions.java index 8c676bfb72237..0678ff4c3ea0d 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/ProxyOptions.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/ProxyOptions.java @@ -3,7 +3,7 @@ package io.clientcore.core.http.models; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.auth.BasicChallengeHandler; import io.clientcore.core.util.auth.ChallengeHandler; import io.clientcore.core.util.auth.DigestChallengeHandler; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/RequestOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/RequestOptions.java index 16095265d6a82..2edf35df9354f 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/RequestOptions.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/RequestOptions.java @@ -6,7 +6,7 @@ import io.clientcore.core.http.annotation.QueryParam; import io.clientcore.core.http.client.HttpClient; import io.clientcore.core.implementation.http.rest.UriEscapers; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.Context; import io.clientcore.core.util.binarydata.BinaryData; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java index e4b0c46e48b1c..ee53f055614d1 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java @@ -3,13 +3,16 @@ 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.RequestOptions; import io.clientcore.core.http.models.Response; import io.clientcore.core.implementation.http.HttpRequestAccessHelper; +import io.clientcore.core.implementation.http.HttpResponseAccessHelper; import io.clientcore.core.implementation.instrumentation.LibraryInstrumentationOptionsAccessHelper; import io.clientcore.core.instrumentation.Instrumentation; import io.clientcore.core.instrumentation.LibraryInstrumentationOptions; @@ -20,19 +23,37 @@ 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.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.serialization.json.JsonWriter; import io.clientcore.core.util.Context; +import io.clientcore.core.util.binarydata.BinaryData; +import io.clientcore.core.util.serializer.ObjectSerializer; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; import java.util.Collections; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.net.URI; import static io.clientcore.core.implementation.UrlRedactionUtil.getRedactedUri; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_SIZE_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_DURATION_KEY; +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_REQUEST_TIME_TO_HEADERS_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_BODY_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_BODY_SIZE_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_STATUS_CODE_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.URL_FULL_KEY; +import static io.clientcore.core.implementation.util.ImplUtils.isNullOrEmpty; import static io.clientcore.core.instrumentation.Instrumentation.DISABLE_TRACING_KEY; import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY; import static io.clientcore.core.instrumentation.tracing.SpanKind.CLIENT; @@ -44,8 +65,8 @@ *

* 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 (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(); * * @@ -105,8 +124,7 @@ * .policies( * new HttpRetryPolicy(), * new HttpInstrumentationPolicy(instrumentationOptions, logOptions), - * enrichingPolicy, - * new HttpLoggingPolicy(logOptions)) + * enrichingPolicy) * .build(); * * @@ -139,17 +157,21 @@ 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 String HTTP_REQUEST_EVENT_NAME = "http.request"; + private static final String HTTP_RESPONSE_EVENT_NAME = "http.response"; + private static final int MAX_BODY_LOG_SIZE = 1024 * 16; + private static final String REDACTED_PLACEHOLDER = "REDACTED"; private final Tracer tracer; private final TraceContextPropagator traceContextPropagator; private final Set allowedQueryParameterNames; + private final HttpLogOptions.HttpLogDetailLevel httpLogDetailLevel; + private final HttpRequestLogger requestLogger; + private final HttpResponseLogger responseLogger; + private final Set allowedHeaderNames; /** * Creates a new instrumentation policy. @@ -163,6 +185,15 @@ public HttpInstrumentationPolicy(InstrumentationOptions instrumentationOption HttpLogOptions logOptionsToUse = logOptions == null ? DEFAULT_LOG_OPTIONS : logOptions; this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames(); + this.httpLogDetailLevel = logOptionsToUse.getLogLevel(); + + this.requestLogger = logOptionsToUse.getRequestLogger() == null + ? new DefaultHttpRequestLogger() + : logOptionsToUse.getRequestLogger(); + this.responseLogger = logOptionsToUse.getResponseLogger() == null + ? new DefaultHttpResponseLogger() + : logOptionsToUse.getResponseLogger(); + this.allowedHeaderNames = logOptionsToUse.getAllowedHeaderNames(); } /** @@ -171,43 +202,53 @@ public HttpInstrumentationPolicy(InstrumentationOptions instrumentationOption @SuppressWarnings("try") @Override public Response process(HttpRequest request, HttpPipelineNextPolicy next) { - if (!isTracingEnabled(request)) { + boolean isTracingEnabled = isTracingEnabled(request); + if (!isTracingEnabled && httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { return next.process(); } String sanitizedUrl = getRedactedUri(request.getUri(), allowedQueryParameterNames); - Span span = startHttpSpan(request, sanitizedUrl); + int tryCount = HttpRequestAccessHelper.getTryCount(request); - if (request.getRequestOptions() == RequestOptions.none()) { - request = request.setRequestOptions(new RequestOptions()); - } + Span span = startHttpSpan(request, sanitizedUrl); - Context context = request.getRequestOptions().getContext().put(TRACE_CONTEXT_KEY, span); + traceContextPropagator.inject(request.getRequestOptions().getContext(), request.getHeaders(), SETTER); - request.getRequestOptions().setContext(context); - propagateContext(context, request.getHeaders()); + ClientLogger logger = getLogger(request); + final long startNs = System.nanoTime(); + requestLogger.logRequest(logger, request, sanitizedUrl, tryCount); try (TracingScope scope = span.makeCurrent()) { Response response = next.process(); - addDetails(request, response, span); - - span.end(); + addDetails(request, response, tryCount, span); + instrumentResponse((HttpResponse) response, span, logger, startNs, sanitizedUrl, tryCount); return response; } catch (Throwable t) { + responseLogger.logException(logger, request, null, t, startNs, null, sanitizedUrl, tryCount); span.end(unwrap(t)); throw t; } } private Span startHttpSpan(HttpRequest request, String sanitizedUrl) { + if (!isTracingEnabled(request)) { + return Span.noop(); + } + + if (request.getRequestOptions() == null || request.getRequestOptions() == RequestOptions.none()) { + request.setRequestOptions(new RequestOptions()); + } + SpanBuilder spanBuilder = tracer.spanBuilder(request.getHttpMethod().toString(), CLIENT, request.getRequestOptions()) - .setAttribute(HTTP_REQUEST_METHOD, request.getHttpMethod().toString()) - .setAttribute(URL_FULL, sanitizedUrl) + .setAttribute(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod().toString()) + .setAttribute(URL_FULL_KEY, sanitizedUrl) .setAttribute(SERVER_ADDRESS, request.getUri().getHost()); maybeSetServerPort(spanBuilder, request.getUri()); - return spanBuilder.startSpan(); + Span span = spanBuilder.startSpan(); + request.getRequestOptions().putContext(TRACE_CONTEXT_KEY, span); + return span; } /** @@ -238,16 +279,15 @@ 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); @@ -266,6 +306,10 @@ private boolean isTracingEnabled(HttpRequest httpRequest) { return false; } + if (httpRequest.getRequestOptions() == null) { + return true; + } + Context context = httpRequest.getRequestOptions().getContext(); Object disableTracing = context.get(DISABLE_TRACING_KEY); if (disableTracing instanceof Boolean) { @@ -275,15 +319,200 @@ private boolean isTracingEnabled(HttpRequest httpRequest) { 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 void setContext(HttpRequest request, Context context) { + + } + + private ClientLogger getLogger(HttpRequest httpRequest) { + ClientLogger logger = null; + + if (httpRequest.getRequestOptions() != null) { + logger = httpRequest.getRequestOptions().getLogger(); + } + + return logger == null ? LOGGER : logger; + } + + private final class DefaultHttpRequestLogger implements HttpRequestLogger { + @Override + public void logRequest(ClientLogger logger, HttpRequest request, String redactedUrl, int tryCount) { + ClientLogger.LoggingEvent log = logger.atLevel(getLogLevel(request)); + if (!log.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { + return; + } + + log.setEventName(HTTP_REQUEST_EVENT_NAME) + .setContext(request.getRequestOptions().getContext()) + .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod()) + .addKeyValue(URL_FULL_KEY, redactedUrl) + .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount); + + addHeadersToLogMessage(request.getHeaders(), log); + + long contentLength = request.getBody() == null ? 0 : getContentLength(request.getHeaders()); + log.addKeyValue(HTTP_REQUEST_BODY_SIZE_KEY, contentLength); + + if (httpLogDetailLevel.shouldLogBody() && canLogBody(request.getBody())) { + log.addKeyValue(HTTP_REQUEST_BODY_KEY, request.getBody().toString()); + } + + log.log(); + } + } + + private final class DefaultHttpResponseLogger implements HttpResponseLogger { + @Override + public void logResponse(ClientLogger logger, Response response, long startNanoTime, long headersNanoTime, + String redactedUrl, int tryCount) { + ClientLogger.LoggingEvent log = logger.atLevel(getLogLevel(response)); + if (!log.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { + return; + } + + long contentLength = getContentLength(response.getHeaders()); + + log.setEventName(HTTP_RESPONSE_EVENT_NAME) + .setContext(response.getRequest().getRequestOptions().getContext()) + .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_HEADERS_KEY, getDurationMs(startNanoTime, headersNanoTime)) + .addKeyValue(HTTP_RESPONSE_STATUS_CODE_KEY, response.getStatusCode()) + .addKeyValue(HTTP_RESPONSE_BODY_SIZE_KEY, contentLength); + + addHeadersToLogMessage(response.getHeaders(), log); + + if (httpLogDetailLevel.shouldLogBody() && canLogBody(response.getBody())) { + // logResponse is called after body is requested and buffered, so it's safe to call getBody().toString() here. + // not catching exception here, because it's caught in InstrumentedResponse.getBody() and will + String content = response.getBody().toString(); + log.addKeyValue(HTTP_RESPONSE_BODY_KEY, content) + .addKeyValue(HTTP_REQUEST_DURATION_KEY, getDurationMs(startNanoTime, System.nanoTime())); + } + log.log(); + } + + @Override + public void logException(ClientLogger logger, HttpRequest request, Response response, Throwable throwable, + long startNanoTime, Long headersNanoTime, String redactedUrl, int tryCount) { + ClientLogger.LoggingEvent log = logger.atWarning(); + if (!log.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { + return; + } + + log.setEventName(HTTP_RESPONSE_EVENT_NAME) + .setContext(request.getRequestOptions().getContext()) + .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod()) + .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount) + .addKeyValue(URL_FULL_KEY, redactedUrl); + + // exception could happen before response code and headers were received + // or after that. So we may or may not have a response object. + if (response != null) { + log.addKeyValue(HTTP_RESPONSE_STATUS_CODE_KEY, response.getStatusCode()); + addHeadersToLogMessage(response.getHeaders(), log); + + if (headersNanoTime != null) { + log.addKeyValue(HTTP_REQUEST_TIME_TO_HEADERS_KEY, + getDurationMs(startNanoTime, headersNanoTime)); + } + } + + // not logging body - there was an exception + log.log(null, throwable); + } + } + + private double getDurationMs(long startNs, long endNs) { + return (endNs - startNs) / 1_000_000.0; + } + + /* + * Adds HTTP headers into the StringBuilder that is generating the log message. + * + * @param headers HTTP headers on the request or response. + * @param sb StringBuilder that is generating the log message. + * @param logLevel Log level the environment is configured to use. + */ + private void addHeadersToLogMessage(HttpHeaders headers, ClientLogger.LoggingEvent log) { + if (httpLogDetailLevel.shouldLogHeaders()) { + for (HttpHeader header : headers) { + HttpHeaderName headerName = header.getName(); + String headerValue = allowedHeaderNames.contains(headerName) ? header.getValue() : REDACTED_PLACEHOLDER; + log.addKeyValue(headerName.toString(), headerValue); + } + } + } + + /* + * Attempts to retrieve and parse the Content-Length header into a numeric representation. + * + * @param logger Logger used to log a warning if the Content-Length header is an invalid number. + * @param headers HTTP headers that are checked for containing Content-Length. + * @return + */ + private static long getContentLength(HttpHeaders headers) { + 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; + } + + /** + * Determines if the request or response body should be logged. + * + *

The request or response body is logged if the Content-Type is not "application/octet-stream" and the body + * 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) { + return data != null && data.isReplayable() && data.getLength() != null && data.getLength() < MAX_BODY_LOG_SIZE; + } + + private void instrumentResponse(HttpResponse actualResponse, Span span, ClientLogger logger, long startNs, + String sanitizedUrl, int tryCount) { + long timeToHeadersNs = System.nanoTime(); + /*if (!httpLogDetailLevel.shouldLogBody()) { + responseLogger.logResponse(logger, actualResponse, startNs, timeToHeadersNs, sanitizedUrl, tryCount); + + if (!span.isRecording()) { + span.end(); + return; + } + }*/ + + HttpResponseAccessHelper.setBody(actualResponse, new InstrumentedBinaryData(actualResponse.getBody(), error -> { + if (error == null) { + responseLogger.logResponse(logger, actualResponse, startNs, timeToHeadersNs, sanitizedUrl, tryCount); + span.end(); + } else { + responseLogger.logException(logger, actualResponse.getRequest(), actualResponse, error, startNs, + timeToHeadersNs, sanitizedUrl, tryCount); + span.end(unwrap(error)); + } + })); } private static Map getProperties(String propertiesFileName) { @@ -304,4 +533,261 @@ private static Map getProperties(String propertiesFileName) { return Collections.emptyMap(); } + + private static class InstrumentedBinaryData extends BinaryData { + private final BinaryData inner; + private final Consumer onComplete; + private boolean reported; + + InstrumentedBinaryData(BinaryData inner, Consumer onComplete) { + this.inner = inner; + this.onComplete = onComplete; + } + + @Override + public byte[] toBytes() { + try { + byte[] bytes = inner.toBytes(); + report(); + return bytes; + } catch (RuntimeException t) { + report(t); + throw t; + } + } + + @Override + public String toString() { + try { + String str = inner.toString(); + report(); + return str; + } catch (RuntimeException t) { + onComplete.accept(t); + throw t; + } + } + + @Override + public T toObject(Type type, ObjectSerializer serializer) throws IOException { + try { + T value = inner.toObject(type, serializer); + report(); + return value; + } catch (RuntimeException t) { + report(t); + throw t; + } + } + + @Override + public InputStream toStream() { + InputStream stream = inner.toStream(); + return new InputStream() { + @Override + public int read() throws IOException { + try { + int read = stream.read(); + if (read == -1) { + report(); + } + return read; + } catch (IOException | RuntimeException e) { + report(e); + throw e; + } + } + + @Override + public int read(byte[] b) throws IOException { + try { + int read = stream.read(b); + if (read == -1) { + report(); + } + return read; + } catch (IOException | RuntimeException e) { + report(e); + throw e; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + try { + int read = stream.read(b, off, len); + if (read == -1) { + report(); + } + return read; + } catch (IOException | RuntimeException e) { + report(e); + throw e; + } + } + + @Override + public byte[] readAllBytes() throws IOException { + try { + byte[] bytes = stream.readAllBytes(); + report(); + return bytes; + } catch (IOException | RuntimeException e) { + report(e); + throw e; + } + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + try { + int read = stream.readNBytes(b, off, len); + if (read < len) { + report(); + } + return read; + } catch (IOException | RuntimeException e) { + report(e); + throw e; + } + } + + @Override + public byte[] readNBytes(int len) throws IOException { + try { + byte[] bytes = stream.readNBytes(len); + if (bytes.length < len) { + report(); + } + return bytes; + } catch (IOException | RuntimeException e) { + report(e); + throw e; + } + } + + @Override + public void reset() throws IOException { + stream.reset(); + } + + @Override + public long skip(long n) throws IOException { + try { + return stream.skip(n); + } catch (IOException | RuntimeException e) { + report(e); + throw e; + } + } + + @Override + public void skipNBytes(long n) throws IOException { + try { + stream.skipNBytes(n); + } catch (IOException | RuntimeException e) { + report(e); + throw e; + } + } + + @Override + public int available() throws IOException { + return stream.available(); + } + + @Override + public void mark(int readlimit) { + stream.mark(readlimit); + } + + @Override + public boolean markSupported() { + return stream.markSupported(); + } + + @Override + public void close() throws IOException { + try { + stream.close(); + report(); + } catch (IOException e) { + report(e); + throw e; + } + } + + @Override + public long transferTo(OutputStream out) throws IOException { + try { + long transferred = stream.transferTo(out); + report(); + return transferred; + } catch (IOException | RuntimeException e) { + report(e); + throw e; + } + } + }; + } + + @Override + public void writeTo(JsonWriter jsonWriter) throws IOException { + try { + inner.writeTo(jsonWriter); + report(); + } catch (RuntimeException t) { + report(t); + throw t; + } + } + + @Override + public ByteBuffer toByteBuffer() { + try { + ByteBuffer bb = inner.toByteBuffer(); + report(); + return bb; + } catch (RuntimeException t) { + report(t); + throw t; + } + } + + @Override + public Long getLength() { + return inner.getLength(); + } + + @Override + public boolean isReplayable() { + return inner.isReplayable(); + } + + @Override + public BinaryData toReplayableBinaryData() { + if (inner.isReplayable()) { + return this; + } + + return new InstrumentedBinaryData(inner.toReplayableBinaryData(), onComplete); + } + + @Override + public void close() throws IOException { + inner.close(); + report(); + } + + private void report() { + report(null); + } + + private void report(Throwable t) { + if (!reported) { + onComplete.accept(t); + reported = true; + } + } + } } 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 8b6856e1a5f1f..ed070bd50991c 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 ab73242ac7b8a..18da7b6a964ea 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,8 +8,8 @@ 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.implementation.instrumentation.AttributeKeys; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.Context; import java.io.IOException; @@ -21,6 +21,9 @@ import java.util.Set; import java.util.function.Predicate; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_RESEND_COUNT_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.URL_FULL_KEY; + /** * A {@link HttpPipelinePolicy} that redirects a {@link HttpRequest} when an HTTP Redirect is received as a * {@link Response response}. @@ -113,11 +116,13 @@ && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), context) && !alreadyAttemptedRedirectUri(redirectUri, attemptedRedirectUris, context)) { LOGGER.atVerbose() - .addKeyValue(LoggingKeys.TRY_COUNT_KEY, tryCount) - .addKeyValue(REDIRECT_URIS_KEY, attemptedRedirectUris::toString) - .addKeyValue(ORIGINATING_REQUEST_URI_KEY, response.getRequest().getUri()) + .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount - 1) + .addKeyValue("url.full.from", response.getRequest().getUri()) + .addKeyValue("url.full.to", redirectUri) + .addKeyValue("url.full.all", attemptedRedirectUris::toString) + .setEventName("http.request.redirect") .setContext(context) - .log("Redirecting."); + .log(); attemptedRedirectUris.add(redirectUri); @@ -160,7 +165,7 @@ private boolean alreadyAttemptedRedirectUri(String redirectUri, Set atte Context context) { if (attemptedRedirectUris.contains(redirectUri)) { LOGGER.atError() - .addKeyValue(LoggingKeys.REDIRECT_URI_KEY, redirectUri) + .addKeyValue(URL_FULL_KEY, redirectUri) .setContext(context) .log("Request was redirected more than once to the same URI."); @@ -183,7 +188,7 @@ private boolean isAllowedRedirectMethod(HttpMethod httpMethod, Context context) } else { LOGGER.atError() .setContext(context) - .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, httpMethod) + .addKeyValue(AttributeKeys.HTTP_REQUEST_METHOD_KEY, httpMethod) .log("Request redirection is not enabled for this HTTP method."); return false; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRequestLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRequestLogger.java new file mode 100644 index 0000000000000..2c678e9550a73 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRequestLogger.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.http.pipeline; + +import io.clientcore.core.http.models.HttpRequest; +import io.clientcore.core.instrumentation.logging.ClientLogger; + +/** + * Manages logging HTTP requests in {@link HttpInstrumentationPolicy}. + */ +@FunctionalInterface +public interface HttpRequestLogger { + /** + * Gets the {@link ClientLogger.LogLevel} used to log the HTTP request. + *

+ * By default, this will return {@link ClientLogger.LogLevel#INFORMATIONAL}. + * + * @param request The request being logged. + * @return The {@link ClientLogger.LogLevel} used to log the HTTP request. + */ + default ClientLogger.LogLevel getLogLevel(HttpRequest request) { + return ClientLogger.LogLevel.VERBOSE; + } + + /** + * Logs the HTTP request. + *

+ * To get the {@link ClientLogger.LogLevel} used to log the HTTP request use {@link #getLogLevel(HttpRequest)}. + * + * @param logger The {@link ClientLogger} used to log the HTTP request. + * @param request The request being logged. + * @param redactedUrl The redacted URL of the request. + * @param tryCount The number of times the request has been attempted. + */ + void logRequest(ClientLogger logger, HttpRequest request, String redactedUrl, int tryCount); +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpResponseLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpResponseLogger.java new file mode 100644 index 0000000000000..7ebf8e4a3fb17 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpResponseLogger.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.http.pipeline; + +import io.clientcore.core.http.models.HttpRequest; +import io.clientcore.core.http.models.Response; +import io.clientcore.core.instrumentation.logging.ClientLogger; + +/** + * Manages logging HTTP responses in {@link HttpInstrumentationPolicy}. + */ +public interface HttpResponseLogger { + /** + * Gets the {@link ClientLogger.LogLevel} used to log the HTTP response. + *

+ * By default, this will return {@link ClientLogger.LogLevel#INFORMATIONAL}. + * + * @param response The response being logged. + * @return The {@link ClientLogger.LogLevel} used to log the HTTP response. + */ + default ClientLogger.LogLevel getLogLevel(Response response) { + return ClientLogger.LogLevel.INFORMATIONAL; + } + + /** + * Logs the HTTP response. + *

+ * To get the {@link ClientLogger.LogLevel} used to log the HTTP response use {@link #getLogLevel(Response)} . + * + * @param logger The {@link ClientLogger} used to log the response. + * @param response The response being logged. + * @param startNanoTime The start time of the HTTP call captured via {@link System#nanoTime()}. + * @param headersNanoTime The time when headers were received captured via {@link System#nanoTime()}. {@code null} if headers were not received. + * @param redactedUrl The sanitized URL of the HTTP call. + * @param tryCount The try count of the HTTP call starting from 0. + */ + void logResponse(ClientLogger logger, Response response, long startNanoTime, long headersNanoTime, + String redactedUrl, int tryCount); + + /** + * Logs the HTTP request exception. If logging request body is enabled, it captures + *

+ * To get the {@link ClientLogger.LogLevel} used to log the HTTP response use {@link #getLogLevel(Response)} . + * + * @param logger The {@link ClientLogger} used to log the response. + * @param request The request instance. + * @param response The response being logged or {@code null} if exception was thrown before response was received. + * @param throwable The exception that was thrown. + * @param startNanoTime The start time of the HTTP call captured via {@link System#nanoTime()}. + * @param headersNanoTime The time when headers were received captured via {@link System#nanoTime()}. {@code null} if headers were not received. + * @param sanitizedUrl The sanitized URL of the HTTP call. + * @param tryCount The try count of the HTTP call starting from 0. + */ + void logException(ClientLogger logger, HttpRequest request, Response response, Throwable throwable, + long startNanoTime, Long headersNanoTime, String sanitizedUrl, int tryCount); +} 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 477e2119a0eb6..3ac4c51d580ff 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,7 @@ 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.logging.ClientLogger; import io.clientcore.core.util.configuration.Configuration; import java.io.IOException; @@ -26,6 +25,8 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_DURATION_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_RESEND_COUNT_KEY; import static io.clientcore.core.implementation.util.ImplUtils.isNullOrEmpty; import static io.clientcore.core.util.configuration.Configuration.PROPERTY_REQUEST_RETRY_COUNT; @@ -271,18 +272,18 @@ 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 { + private AttributeKeys() { + + } + + /** + * Key representing HTTP method. + */ + public static final String HTTP_REQUEST_METHOD_KEY = "http.request.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 HTTP_REQUEST_RESEND_COUNT_KEY = "http.request.resend_count"; + + /** + * Key representing duration of call in milliseconds, the value should be a number. + */ + public static final String HTTP_REQUEST_TIME_TO_HEADERS_KEY = "http.request.time_to_headers"; + + /** + * 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 URI. + */ + public static final String URL_FULL_KEY = "url.full"; + + /** + * Key representing request body content length. + */ + public static final String HTTP_REQUEST_BODY_SIZE_KEY = "http.request.body.size"; + + /** + * Key representing request body content length. + */ + public static final String HTTP_RESPONSE_BODY_SIZE_KEY = "http.response.body.size"; + + /** + * Key representing request body. The value should be populated conditionally + * if populated at all. + */ + public static final String HTTP_REQUEST_BODY_KEY = "http.request.body"; + + /** + * Key representing response body. The value should be populated conditionally + * if populated at all. + */ + public static final String HTTP_RESPONSE_BODY_KEY = "http.request.body"; + + /** + * Key representing response status code. The value should be a number. + */ + public static final String HTTP_RESPONSE_STATUS_CODE_KEY = "http.response.status_code"; +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java index 5f2d02e7120fc..9fb874b10280a 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java @@ -16,7 +16,7 @@ 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.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.Context; import java.util.HashMap; @@ -60,17 +60,22 @@ public static ClientLogger.LoggingEvent enrichLog(ClientLogger.LoggingEvent log, .addKeyValue("span.id", span.getSpanContext().getSpanId()); } - private static final class DefaultTracer implements Tracer { + public static final class DefaultTracer implements Tracer { private final boolean isEnabled; private final ClientLogger logger; DefaultTracer(InstrumentationOptions instrumentationOptions, LibraryInstrumentationOptions libraryOptions) { - this.isEnabled = instrumentationOptions.isTracingEnabled(); // TODO: probably need additional config for log-based tracing + this.isEnabled = instrumentationOptions == null || instrumentationOptions.isTracingEnabled(); // TODO: probably need additional config for log-based tracing Map libraryContext = new HashMap<>(2); libraryContext.put("library.version", libraryOptions.getLibraryVersion()); libraryContext.put("library.instrumentation.schema_url", libraryOptions.getSchemaUrl()); - this.logger = new ClientLogger(libraryOptions.getLibraryName() + ".tracing", libraryContext); + Object providedLogger = instrumentationOptions == null ? null : instrumentationOptions.getProvider(); + if (providedLogger instanceof ClientLogger) { + this.logger = (ClientLogger) providedLogger; + } else { + this.logger = new ClientLogger(libraryOptions.getLibraryName() + ".tracing", libraryContext); + } } @Override 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 18c3e58042d49..df717ff5a92be 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/LibraryInstrumentationOptionsAccessHelper.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LibraryInstrumentationOptionsAccessHelper.java index 21409d97d9c9e..63a699e01a78c 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LibraryInstrumentationOptionsAccessHelper.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LibraryInstrumentationOptionsAccessHelper.java @@ -60,4 +60,5 @@ public static void setAccessor(LibraryInstrumentationOptionsAccessor accessor) { private LibraryInstrumentationOptionsAccessHelper() { } + } 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 8ecd69e34f32d..e802d6ebf0199 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,13 @@ -// 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/otel/FallbackInvoker.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/FallbackInvoker.java index a41a7c96ee398..629717a0c5fe1 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, 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 4e1b1932aa1ca..d6facac4803a9 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 932f086277b4e..f6af8ae1ad380 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. @@ -184,7 +184,7 @@ public static void runtimeError(ClientLogger logger, Throwable t) { * @return true if OTel is initialized successfully, false otherwise */ public static boolean isInitialized() { - return INSTANCE.initialized; + return INSTANCE == null || 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 e5cf4fe2088bf..a42f002d7f9a9 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 @@ -11,7 +11,7 @@ import io.clientcore.core.instrumentation.InstrumentationOptions; 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.GLOBAL_OTEL_CLASS; 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 1d7219bc52a83..6fff4b20133a9 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 @@ -8,7 +8,7 @@ import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; 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; 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 4958dc8ddc8cd..178a408117896 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 @@ -10,7 +10,7 @@ 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; 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 ccd5e25dfb1ad..9d53d6f4ffa48 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 @@ -11,7 +11,7 @@ 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.instrumentation.logging.ClientLogger; import io.clientcore.core.util.Context; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; 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 9433464c2281f..170e05108e20f 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,7 +6,7 @@ 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.logging.ClientLogger; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CONTEXT_CLASS; 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 eb673edc9fa63..0b06d2c3cf744 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 @@ -9,7 +9,7 @@ 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.instrumentation.logging.ClientLogger; import io.clientcore.core.util.Context; import java.lang.reflect.InvocationHandler; 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 15948ab3005b9..a34f087cd0adb 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 @@ -11,7 +11,7 @@ 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.instrumentation.logging.ClientLogger; import io.clientcore.core.util.Context; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; 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 index 1857512b82b42..750a08c8957ad 100644 --- 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 @@ -4,7 +4,7 @@ package io.clientcore.core.implementation.instrumentation.otel.tracing; import io.clientcore.core.instrumentation.Instrumentation; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.Context; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS; 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 6e656d9043ee5..247dbf5577a5f 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 880fbfd78f79a..dd88ef33f2bdd 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 217a020f04f05..a0f0a10d89e4e 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 7c776106b7949..b9db02e6a3149 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/Providers.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Providers.java index 5e8abe009477c..51806137e62ec 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 e602cb8e2d738..d2d94091cd4d2 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 fed2ba45d15fb..bcf0be2856e45 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 4580213e72a79..b50c7d89550fb 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/NoopInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/NoopInstrumentation.java deleted file mode 100644 index 53205494031c4..0000000000000 --- 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 97% 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 7cd37cc89964e..5f6407ae0ad8b 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,15 +1,16 @@ // 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.instrumentation.DefaultInstrumentation; -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.serialization.json.JsonWriter; import io.clientcore.core.serialization.json.implementation.DefaultJsonWriter; +import io.clientcore.core.util.Context; import io.clientcore.core.util.configuration.Configuration; import java.io.IOException; @@ -443,18 +444,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 setContext(Context context) { this.context = context; return this; } - public LoggingEvent setCause(Throwable throwable) { - if (this.isEnabled) { - setThrowableInternal(throwable, logger.canLogAtLevel(LogLevel.VERBOSE)); - } - 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. @@ -469,6 +470,7 @@ public LoggingEvent setEventName(String eventName) { /** * Logs event annotated with context. + * Logs event with context. */ public void log() { log(null); 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 1a4ea4143515c..f3a7125d23ec2 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 @@ -76,4 +76,45 @@ public interface Span { * @return The {@link TracingScope} object. */ TracingScope makeCurrent(); + + /** + * Returns a no-op span. + * @return A no-op span. + */ + static Span noop() { + return new Span() { + private final static 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; + } + }; + } } 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 0a1c9b266c329..050184c4b42e8 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 613be6a2a80d8..ce0023ca023ae 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 1282962f25df9..b126a11d0839f 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 bffb558bc42b0..5f9680e2063e8 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 5eca550da5693..2dfd998223584 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 1b6093c2441a8..4b3dfe28af947 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 47db439ae0745..ee30aea3f0dd0 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 1c227a8f8e919..4938c053e0fdc 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 09174ee28559d..3853ceb5da318 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 8b2878c73e94b..6e749a54c3c92 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 03e34eba45aea..20f487aed48d2 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 36127c7b6cd65..dee608a9a5b6e 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/TracingForLibraryDevelopersJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java index 0fc62912cf21f..5060f164aea06 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; @@ -50,6 +49,7 @@ public void createTracer() { /** * This example shows minimal distributed tracing instrumentation. */ + @SuppressWarnings("try") public void traceCall() { Tracer tracer = Instrumentation.create(null, LIBRARY_OPTIONS).getTracer(); @@ -83,6 +83,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(); @@ -123,8 +124,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 +143,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 @@ -169,8 +168,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 f727a2f341611..69fb89b4ed975 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/util/ClientLoggerJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/ClientLoggerJavaDocCodeSnippets.java index 9c3c9a7d95384..d3d64fa2097a6 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/HttpInstrumentationPolicyNoopTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyDefaultTests.java similarity index 65% rename from sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyNoopTests.java rename to sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyDefaultTests.java index e247d70163f20..754b60b9fc463 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyNoopTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyDefaultTests.java @@ -20,39 +20,54 @@ import static io.clientcore.core.http.models.HttpHeaderName.TRACEPARENT; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -public class HttpInstrumentationPolicyNoopTests { +public class HttpInstrumentationPolicyDefaultTests { private static final InstrumentationOptions OPTIONS = new InstrumentationOptions<>(); + private static final InstrumentationOptions DISABLED_TRACING_OPTIONS + = new InstrumentationOptions<>().setTracingEnabled(false); private static final HttpLogOptions ENABLED_HTTP_LOG_OPTIONS = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS); private static final HttpHeaderName TRACESTATE = HttpHeaderName.fromString("tracestate"); - @ParameterizedTest - @ValueSource(ints = { 200, 201, 206, 302, 400, 404, 500, 503 }) - public void simpleRequestTracingDisabled(int statusCode) throws IOException { + @Test + public void simpleRequestTracingDisabled() throws IOException { HttpPipeline pipeline = new HttpPipelineBuilder() - .policies(new HttpInstrumentationPolicy(OPTIONS, ENABLED_HTTP_LOG_OPTIONS), - new HttpLoggingPolicy(ENABLED_HTTP_LOG_OPTIONS)) - .httpClient(request -> new MockHttpResponse(request, statusCode)) + .policies(new HttpInstrumentationPolicy(DISABLED_TRACING_OPTIONS, ENABLED_HTTP_LOG_OPTIONS)) + .httpClient(request -> new MockHttpResponse(request, 200)) .build(); // should not throw try (Response response = pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost/"))) { - assertEquals(statusCode, response.getStatusCode()); + assertEquals(200, response.getStatusCode()); assertNull(response.getRequest().getHeaders().get(TRACESTATE)); assertNull(response.getRequest().getHeaders().get(TRACEPARENT)); } } + @ParameterizedTest + @ValueSource(ints = { 200, 201, 206, 302, 400, 404, 500, 503 }) + public void simpleRequestTracingEnabled(int statusCode) throws IOException { + HttpPipeline pipeline + = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(OPTIONS, ENABLED_HTTP_LOG_OPTIONS)) + .httpClient(request -> new MockHttpResponse(request, statusCode)) + .build(); + + // should not throw + try (Response response = pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost/"))) { + assertEquals(statusCode, response.getStatusCode()); + assertNull(response.getRequest().getHeaders().get(TRACESTATE)); + assertNotNull(response.getRequest().getHeaders().get(TRACEPARENT)); + } + } + @Test public void exceptionTracingDisabled() { SocketException exception = new SocketException("test exception"); HttpPipeline pipeline - = new HttpPipelineBuilder() - .policies(new HttpInstrumentationPolicy(OPTIONS, ENABLED_HTTP_LOG_OPTIONS), - new HttpLoggingPolicy(ENABLED_HTTP_LOG_OPTIONS)) + = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(OPTIONS, ENABLED_HTTP_LOG_OPTIONS)) .httpClient(request -> { throw exception; }) diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/ResponseConstructorsCacheLambdaMetaFactory.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/ResponseConstructorsCacheLambdaMetaFactory.java index b07be1215b503..66ed63f2613b4 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/ResponseConstructorsCacheLambdaMetaFactory.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/ResponseConstructorsCacheLambdaMetaFactory.java @@ -6,7 +6,7 @@ import io.clientcore.core.http.models.HttpHeaders; 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.lang.invoke.LambdaMetafactory; import java.lang.invoke.MethodHandle; diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/ResponseConstructorsNoCacheReflection.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/ResponseConstructorsNoCacheReflection.java index d0d3cf13b1caf..1a459531da8af 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/ResponseConstructorsNoCacheReflection.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/http/rest/ResponseConstructorsNoCacheReflection.java @@ -6,7 +6,7 @@ import io.clientcore.core.http.models.HttpHeaders; 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.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java new file mode 100644 index 0000000000000..792ac367e2ece --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.tracing; + +import io.clientcore.core.http.models.RequestOptions; +import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; +import io.clientcore.core.implementation.instrumentation.DefaultInstrumentation; +import io.clientcore.core.implementation.instrumentation.DefaultLogger; +import io.clientcore.core.instrumentation.Instrumentation; +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.Span; +import io.clientcore.core.instrumentation.tracing.SpanKind; +import io.clientcore.core.instrumentation.tracing.Tracer; +import org.junit.jupiter.api.Test; +import org.openjdk.jmh.annotations.Setup; + +import java.io.PrintStream; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DefaultInstrumentationTests { + private static final LibraryInstrumentationOptions DEFAULT_LIB_OPTIONS + = new LibraryInstrumentationOptions("test-library"); + private final AccessibleByteArrayOutputStream logCaptureStream; + + public DefaultInstrumentationTests() { + logCaptureStream = new AccessibleByteArrayOutputStream(); + } + + @Test + public void createTracer() { + Tracer tracer = Instrumentation.create(null, DEFAULT_LIB_OPTIONS).getTracer(); + assertTrue(tracer.isEnabled()); + + Span span = tracer.spanBuilder("test-span", SpanKind.INTERNAL, RequestOptions.none()).startSpan(); + + assertFalse(span.isRecording()); + assertInstanceOf(DefaultInstrumentation.DefaultSpan.class, span); + } + + @Test + public void createTracerTracingDisabled() { + InstrumentationOptions options = new InstrumentationOptions<>().setTracingEnabled(false); + + Tracer tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); + assertFalse(tracer.isEnabled()); + + // should not throw + Span span = tracer.spanBuilder("test-span", SpanKind.INTERNAL, RequestOptions.none()).startSpan(); + + assertNotNull(span); + assertFalse(span.isRecording()); + } + + @Test + public void createTracerBadArguments() { + InstrumentationOptions options = new InstrumentationOptions<>().setProvider("this is not a valid provider"); + + assertThrows(NullPointerException.class, () -> Instrumentation.create(options, null).getTracer()); + } + + private ClientLogger setupLogLevelAndGetLogger(ClientLogger.LogLevel logLevelToSet, + Map globalContext) { + DefaultLogger logger + = new DefaultLogger(ClientLogger.class.getName(), new PrintStream(logCaptureStream), logLevelToSet); + + return new ClientLogger(ClientLogger.class); + } + +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultTracerTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultTracerTests.java new file mode 100644 index 0000000000000..f93c445db2de9 --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultTracerTests.java @@ -0,0 +1,4 @@ +package io.clientcore.core.implementation.instrumentation.tracing; + +public class DefaultTracerTests { +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/util/ClientLoggerTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java similarity index 99% rename from sdk/clientcore/core/src/test/java/io/clientcore/core/util/ClientLoggerTests.java rename to sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java index 7375c46827ed7..1a4d389e38788 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/util/ClientLoggerTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java @@ -1,14 +1,14 @@ // 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.implementation.AccessibleByteArrayOutputStream; -import io.clientcore.core.implementation.util.DefaultLogger; +import io.clientcore.core.implementation.instrumentation.DefaultLogger; import io.clientcore.core.serialization.json.JsonOptions; import io.clientcore.core.serialization.json.JsonProviders; import io.clientcore.core.serialization.json.JsonReader; -import io.clientcore.core.util.ClientLogger.LogLevel; +import io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/shared/HttpClientTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/shared/HttpClientTests.java index d1d86484eb1fa..5fab3a5472d60 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/shared/HttpClientTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/shared/HttpClientTests.java @@ -26,11 +26,11 @@ import io.clientcore.core.http.models.ResponseBodyMode; import io.clientcore.core.http.models.ServerSentEvent; import io.clientcore.core.http.models.ServerSentEventListener; -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.HttpInstrumentationPolicy; import io.clientcore.core.implementation.util.UriBuilder; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.Context; import io.clientcore.core.util.binarydata.BinaryData; import io.clientcore.core.util.binarydata.ByteArrayBinaryData; @@ -1492,7 +1492,7 @@ public void binaryDataUploadTest() throws Exception { // Order in which policies applied will be the order in which they added to builder final HttpPipeline httpPipeline = new HttpPipelineBuilder().httpClient(httpClient) - .policies(new HttpLoggingPolicy( + .policies(new HttpInstrumentationPolicy(null, new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY_AND_HEADERS))) .build(); diff --git a/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/OkHttpHttpClient.java b/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/OkHttpHttpClient.java index 4610150d72c29..ce3fb8f922831 100644 --- a/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/OkHttpHttpClient.java +++ b/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/OkHttpHttpClient.java @@ -13,7 +13,7 @@ import io.clientcore.core.http.models.Response; import io.clientcore.core.http.models.ResponseBodyMode; import io.clientcore.core.http.models.ServerSentEventListener; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.ServerSentEventUtils; import io.clientcore.core.util.ServerSentResult; import io.clientcore.core.util.binarydata.BinaryData; diff --git a/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/OkHttpHttpClientBuilder.java b/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/OkHttpHttpClientBuilder.java index 234e86372b1de..0735f8eadf906 100644 --- a/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/OkHttpHttpClientBuilder.java +++ b/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/OkHttpHttpClientBuilder.java @@ -5,7 +5,7 @@ import io.clientcore.core.http.client.HttpClient; import io.clientcore.core.http.models.ProxyOptions; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.SharedExecutorService; import io.clientcore.core.util.auth.ChallengeHandler; import io.clientcore.core.util.configuration.Configuration; diff --git a/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/implementation/OkHttpInputStreamRequestBody.java b/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/implementation/OkHttpInputStreamRequestBody.java index b643c31afa081..5e657ba966dcd 100644 --- a/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/implementation/OkHttpInputStreamRequestBody.java +++ b/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/implementation/OkHttpInputStreamRequestBody.java @@ -3,7 +3,7 @@ package io.clientcore.http.okhttp3.implementation; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.binarydata.InputStreamBinaryData; import okhttp3.MediaType; import okio.BufferedSink; diff --git a/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/implementation/ProxyAuthenticator.java b/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/implementation/ProxyAuthenticator.java index 2eddd2cc5857f..0fbc68e8b4d4d 100644 --- a/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/implementation/ProxyAuthenticator.java +++ b/sdk/clientcore/http-okhttp3/src/main/java/io/clientcore/http/okhttp3/implementation/ProxyAuthenticator.java @@ -7,7 +7,7 @@ import io.clientcore.core.http.models.HttpMethod; import io.clientcore.core.http.models.HttpRequest; import io.clientcore.core.http.models.HttpResponse; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.auth.AuthUtils; import io.clientcore.core.util.auth.ChallengeHandler; import io.clientcore.core.util.binarydata.BinaryData; diff --git a/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/HttpGet.java b/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/HttpGet.java index 3953b16994189..b2e34e29c1825 100644 --- a/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/HttpGet.java +++ b/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/HttpGet.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.lang.instrument.Instrumentation; import java.net.URI; import java.net.URISyntaxException; import java.time.Instant; @@ -152,7 +153,7 @@ private HttpPipelineBuilder getPipelineBuilder() { HttpLogOptions logOptions = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS); HttpPipelineBuilder builder - = new HttpPipelineBuilder().policies(new HttpRetryPolicy(), new HttpLoggingPolicy(logOptions)); + = new HttpPipelineBuilder().policies(new HttpRetryPolicy(), new InstrumentationPolicy(null, logOptions)); if (options.getHttpClient() == PerfStressOptions.HttpClientType.OKHTTP) { builder.httpClient(new OkHttpHttpClientProvider().getSharedInstance()); diff --git a/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/HttpPatch.java b/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/HttpPatch.java index 12b2fa87ac3fb..95ebc444fbe1c 100644 --- a/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/HttpPatch.java +++ b/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/HttpPatch.java @@ -90,7 +90,7 @@ private HttpPipelineBuilder getPipelineBuilder() { HttpLogOptions logOptions = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS); HttpPipelineBuilder builder - = new HttpPipelineBuilder().policies(new HttpRetryPolicy(), new HttpLoggingPolicy(logOptions)); + = new HttpPipelineBuilder().policies(new HttpRetryPolicy(), new InstrumentationPolicy(null, logOptions)); if (options.getHttpClient() == PerfStressOptions.HttpClientType.OKHTTP) { builder.httpClient(new OkHttpHttpClientProvider().getSharedInstance()); diff --git a/sdk/clientcore/http-stress/src/main/java/module-info.java b/sdk/clientcore/http-stress/src/main/java/module-info.java index c7ad143213cec..aad37b1fa3e6a 100644 --- a/sdk/clientcore/http-stress/src/main/java/module-info.java +++ b/sdk/clientcore/http-stress/src/main/java/module-info.java @@ -20,6 +20,7 @@ requires io.opentelemetry.sdk.autoconfigure; requires io.opentelemetry.sdk.autoconfigure.spi; requires io.opentelemetry.sdk.trace; + requires java.instrument; exports io.clientcore.http.stress; exports io.clientcore.http.stress.util; diff --git a/sdk/clientcore/optional-dependency-tests/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java b/sdk/clientcore/optional-dependency-tests/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java index 59f8010182319..d62907e43843a 100644 --- a/sdk/clientcore/optional-dependency-tests/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java +++ b/sdk/clientcore/optional-dependency-tests/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java @@ -104,6 +104,7 @@ public void disableDistributedTracing() { * client library with spans from application code * using current context. */ + @SuppressWarnings("try") public void correlationWithImplicitContext() { // BEGIN: io.clientcore.core.telemetry.correlationwithimplicitcontext diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/util/Slf4jLoggerShimIT.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java similarity index 96% rename from sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/util/Slf4jLoggerShimIT.java rename to sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java index 0ef00ce83e8d5..982a861085b0c 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/util/Slf4jLoggerShimIT.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java @@ -1,8 +1,8 @@ // 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.instrumentation.logging.ClientLogger; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/util/ClientLoggerSlf4JTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ClientLoggerSlf4JTests.java similarity index 53% rename from sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/util/ClientLoggerSlf4JTests.java rename to sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ClientLoggerSlf4JTests.java index 81ec9c97058e7..558b76125c680 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/util/ClientLoggerSlf4JTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ClientLoggerSlf4JTests.java @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package io.clientcore.core.util; +package io.clientcore.core.instrumentation; + +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLoggerTests; /** * Tests for {@link ClientLogger}. From eba834330fa3a76384e40c17278d30fdab494201 Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Sat, 4 Jan 2025 11:37:06 -0800 Subject: [PATCH 03/11] draft complete --- .../core/http/models/RequestOptions.java | 31 + .../pipeline/HttpInstrumentationPolicy.java | 630 ++++++------------ .../core/http/pipeline/HttpLoggingPolicy.java | 2 +- .../http/pipeline/HttpRedirectPolicy.java | 8 +- .../instrumentation/AttributeKeys.java | 2 +- .../DefaultInstrumentation.java | 165 +++-- .../instrumentation/Slf4jLoggerShim.java | 3 + .../instrumentation/otel/FallbackInvoker.java | 12 +- .../otel/OTelInstrumentation.java | 22 + .../otel/tracing/OTelContext.java | 60 +- .../otel/tracing/OTelSpan.java | 92 ++- .../otel/tracing/OTelSpanBuilder.java | 30 +- .../otel/tracing/OTelSpanContext.java | 115 +++- .../tracing/OTelTraceContextPropagator.java | 13 +- .../otel/tracing/OTelTracer.java | 6 +- .../otel/tracing/OTelUtils.java | 42 -- .../core/instrumentation/Instrumentation.java | 10 + .../InstrumentationContext.java | 11 + .../instrumentation/logging/ClientLogger.java | 12 +- .../instrumentation/logging/package-info.java | 11 + .../tracing/NoopInstrumentationContext.java | 35 + .../instrumentation/tracing/NoopSpan.java | 46 ++ .../core/instrumentation/tracing/Span.java | 39 +- .../tracing/TraceContextPropagator.java | 5 +- .../core/instrumentation/tracing/Tracer.java | 5 +- .../core/src/main/java/module-info.java | 2 +- ...rLibraryDevelopersJavaDocCodeSnippets.java | 10 +- .../tracing/DefaultInstrumentationTests.java | 8 +- .../tracing/DefaultTracerTests.java | 4 - .../logging}/HttpLoggingPolicyTests.java | 4 +- .../HttpInstrumentationPolicyTests.java | 20 +- .../instrumentation/Slf4jLoggerShimIT.java | 12 +- .../ContextPropagationTests.java | 51 +- .../instrumentation/SuppressionTests.java | 43 +- .../core/instrumentation/TracerTests.java | 14 +- 35 files changed, 798 insertions(+), 777 deletions(-) delete mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelUtils.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationContext.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/package-info.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopInstrumentationContext.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopSpan.java delete mode 100644 sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultTracerTests.java rename sdk/clientcore/core/src/test/java/io/clientcore/core/{util => instrumentation/logging}/HttpLoggingPolicyTests.java (99%) diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/RequestOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/RequestOptions.java index 2edf35df9354f..19f9bd7e1ca53 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/RequestOptions.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/RequestOptions.java @@ -6,6 +6,7 @@ import io.clientcore.core.http.annotation.QueryParam; import io.clientcore.core.http.client.HttpClient; import io.clientcore.core.implementation.http.rest.UriEscapers; +import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.Context; import io.clientcore.core.util.binarydata.BinaryData; @@ -119,6 +120,7 @@ public final class RequestOptions { private ResponseBodyMode responseBodyMode; private boolean locked; private ClientLogger logger; + private InstrumentationContext instrumentationContext; /** * Creates a new instance of {@link RequestOptions}. @@ -408,4 +410,33 @@ private RequestOptions lock() { public static RequestOptions none() { return NONE; } + + /** + * Gets the {@link InstrumentationContext} used to instrument the request. + * + * @return The {@link InstrumentationContext} used to instrument the request. + */ + public InstrumentationContext getInstrumentationContext() { + return instrumentationContext; + } + + /** + * Sets the {@link InstrumentationContext} used to instrument the request. + * + * @param instrumentationContext The {@link InstrumentationContext} used to instrument the request. + * + * @return The updated {@link RequestOptions} object. + * + * @throws IllegalStateException if this instance is obtained by calling {@link RequestOptions#none()}. + */ + public RequestOptions setInstrumentationContext(InstrumentationContext instrumentationContext) { + if (locked) { + throw LOGGER.logThrowableAsError(new IllegalStateException( + "This instance of RequestOptions is immutable. Cannot set instrumentation context.")); + } + + this.instrumentationContext = instrumentationContext; + + return this; + } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java index ee53f055614d1..7871fd4e8bb7d 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java @@ -12,9 +12,10 @@ import io.clientcore.core.http.models.RequestOptions; import io.clientcore.core.http.models.Response; import io.clientcore.core.implementation.http.HttpRequestAccessHelper; -import io.clientcore.core.implementation.http.HttpResponseAccessHelper; import io.clientcore.core.implementation.instrumentation.LibraryInstrumentationOptionsAccessHelper; +import io.clientcore.core.implementation.util.LoggingKeys; 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.tracing.SpanBuilder; @@ -24,17 +25,13 @@ import io.clientcore.core.instrumentation.tracing.TraceContextSetter; import io.clientcore.core.instrumentation.tracing.Tracer; import io.clientcore.core.instrumentation.logging.ClientLogger; -import io.clientcore.core.serialization.json.JsonWriter; import io.clientcore.core.util.Context; import io.clientcore.core.util.binarydata.BinaryData; -import io.clientcore.core.util.serializer.ObjectSerializer; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.Type; -import java.nio.ByteBuffer; import java.util.Collections; +import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; @@ -42,20 +39,20 @@ import java.util.stream.Collectors; import java.net.URI; +import static io.clientcore.core.http.models.HttpHeaderName.TRACEPARENT; import static io.clientcore.core.implementation.UrlRedactionUtil.getRedactedUri; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_SIZE_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_DURATION_KEY; 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_REQUEST_TIME_TO_HEADERS_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_TIME_TO_RESPONSE_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_BODY_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_BODY_SIZE_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_STATUS_CODE_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.URL_FULL_KEY; import static io.clientcore.core.implementation.util.ImplUtils.isNullOrEmpty; import static io.clientcore.core.instrumentation.Instrumentation.DISABLE_TRACING_KEY; -import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY; import static io.clientcore.core.instrumentation.tracing.SpanKind.CLIENT; /** @@ -134,7 +131,8 @@ */ public final class HttpInstrumentationPolicy implements HttpPipelinePolicy { private static final ClientLogger LOGGER = new ClientLogger(HttpInstrumentationPolicy.class); - private static final HttpLogOptions DEFAULT_LOG_OPTIONS = new HttpLogOptions(); + private static final Set ALWAYS_ALLOWED_HEADERS = Set.of(TRACEPARENT); + private static final HttpLogOptions DEFAULT_HTTP_LOG_OPTIONS = new HttpLogOptions(); private static final String LIBRARY_NAME; private static final String LIBRARY_VERSION; private static final LibraryInstrumentationOptions LIBRARY_OPTIONS; @@ -165,12 +163,15 @@ public final class HttpInstrumentationPolicy implements HttpPipelinePolicy { 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 HttpRequestLogger requestLogger; - private final HttpResponseLogger responseLogger; private final Set allowedHeaderNames; /** @@ -183,17 +184,13 @@ public HttpInstrumentationPolicy(InstrumentationOptions instrumentationOption this.tracer = instrumentation.getTracer(); this.traceContextPropagator = instrumentation.getW3CTraceContextPropagator(); - HttpLogOptions logOptionsToUse = logOptions == null ? DEFAULT_LOG_OPTIONS : logOptions; - this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames(); + HttpLogOptions logOptionsToUse = logOptions == null ? DEFAULT_HTTP_LOG_OPTIONS : logOptions; this.httpLogDetailLevel = logOptionsToUse.getLogLevel(); - - this.requestLogger = logOptionsToUse.getRequestLogger() == null - ? new DefaultHttpRequestLogger() - : logOptionsToUse.getRequestLogger(); - this.responseLogger = logOptionsToUse.getResponseLogger() == null - ? new DefaultHttpResponseLogger() - : logOptionsToUse.getResponseLogger(); this.allowedHeaderNames = logOptionsToUse.getAllowedHeaderNames(); + this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames() + .stream() + .map(queryParamName -> queryParamName.toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()); } /** @@ -207,48 +204,59 @@ public Response process(HttpRequest request, HttpPipelineNextPolicy next) { return next.process(); } - String sanitizedUrl = getRedactedUri(request.getUri(), allowedQueryParameterNames); + 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()); - Span span = startHttpSpan(request, sanitizedUrl); - - traceContextPropagator.inject(request.getRequestOptions().getContext(), request.getHeaders(), SETTER); + Span span = Span.noop(); + if (isTracingEnabled(request)) { + span = startHttpSpan(request, redactedUrl); + request.getRequestOptions().setInstrumentationContext(span.getInstrumentationContext()); + traceContextPropagator.inject(span.getInstrumentationContext(), request.getHeaders(), SETTER); + } - ClientLogger logger = getLogger(request); - final long startNs = System.nanoTime(); - requestLogger.logRequest(logger, request, sanitizedUrl, tryCount); + logRequest(logger, request, startNs, requestContentLength, redactedUrl, tryCount, span.getInstrumentationContext()); try (TracingScope scope = span.makeCurrent()) { Response response = next.process(); + if (response == null) { + LOGGER.atError() + .setContext(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); - instrumentResponse((HttpResponse) response, span, logger, startNs, sanitizedUrl, tryCount); + response = logResponse(logger, response, startNs, requestContentLength, redactedUrl, tryCount, span.getInstrumentationContext()); + span.end(); return response; - } catch (Throwable t) { - responseLogger.logException(logger, request, null, t, startNs, null, sanitizedUrl, tryCount); + } catch (RuntimeException t) { + var ex = logException(logger, request, null, t, startNs, null, requestContentLength, redactedUrl, tryCount, span.getInstrumentationContext()); span.end(unwrap(t)); - throw t; + throw ex; } } private Span startHttpSpan(HttpRequest request, String sanitizedUrl) { - if (!isTracingEnabled(request)) { - return Span.noop(); - } - if (request.getRequestOptions() == null || request.getRequestOptions() == RequestOptions.none()) { request.setRequestOptions(new RequestOptions()); } + InstrumentationContext context = request.getRequestOptions().getInstrumentationContext(); + SpanBuilder spanBuilder - = tracer.spanBuilder(request.getHttpMethod().toString(), CLIENT, request.getRequestOptions()) + = tracer.spanBuilder(request.getHttpMethod().toString(), CLIENT, context) .setAttribute(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod().toString()) .setAttribute(URL_FULL_KEY, sanitizedUrl) .setAttribute(SERVER_ADDRESS, request.getUri().getHost()); maybeSetServerPort(spanBuilder, request.getUri()); - Span span = spanBuilder.startSpan(); - request.getRequestOptions().putContext(TRACE_CONTEXT_KEY, span); - return span; + return spanBuilder.startSpan(); } /** @@ -326,10 +334,6 @@ private static Throwable unwrap(Throwable t) { return t; } - private void setContext(HttpRequest request, Context context) { - - } - private ClientLogger getLogger(HttpRequest httpRequest) { ClientLogger logger = null; @@ -340,125 +344,195 @@ private ClientLogger getLogger(HttpRequest httpRequest) { return logger == null ? LOGGER : logger; } - private final class DefaultHttpRequestLogger implements HttpRequestLogger { - @Override - public void logRequest(ClientLogger logger, HttpRequest request, String redactedUrl, int tryCount) { - ClientLogger.LoggingEvent log = logger.atLevel(getLogLevel(request)); - if (!log.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { - return; + private static Map getProperties(String propertiesFileName) { + try (InputStream inputStream + = HttpInstrumentationPolicy.class.getClassLoader().getResourceAsStream(propertiesFileName)) { + if (inputStream != null) { + Properties properties = new Properties(); + properties.load(inputStream); + return Collections.unmodifiableMap(properties.entrySet() + .stream() + .collect(Collectors.toMap(entry -> (String) entry.getKey(), entry -> (String) entry.getValue()))); } + } catch (IOException ex) { + LOGGER.atWarning() + .addKeyValue("propertiesFileName", propertiesFileName) + .log("Failed to read properties.", ex); + } - log.setEventName(HTTP_REQUEST_EVENT_NAME) - .setContext(request.getRequestOptions().getContext()) - .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod()) - .addKeyValue(URL_FULL_KEY, redactedUrl) - .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount); + return Collections.emptyMap(); + } - addHeadersToLogMessage(request.getHeaders(), log); + 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; + } - long contentLength = request.getBody() == null ? 0 : getContentLength(request.getHeaders()); - log.addKeyValue(HTTP_REQUEST_BODY_SIZE_KEY, contentLength); + logBuilder + .setEventName(HTTP_REQUEST_EVENT_NAME) + .setContext(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); - if (httpLogDetailLevel.shouldLogBody() && canLogBody(request.getBody())) { - log.addKeyValue(HTTP_REQUEST_BODY_KEY, request.getBody().toString()); - } + addHeadersToLogMessage(request.getHeaders(), logBuilder); - log.log(); + if (httpLogDetailLevel.shouldLogBody() && canLogBody(request.getBody())) { + try { + BinaryData bufferedBody = request.getBody().toReplayableBinaryData(); + request.setBody(bufferedBody); + logBuilder.addKeyValue(HTTP_REQUEST_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, context); + } } + + logBuilder.log(); } - private final class DefaultHttpResponseLogger implements HttpResponseLogger { - @Override - public void logResponse(ClientLogger logger, Response response, long startNanoTime, long headersNanoTime, - String redactedUrl, int tryCount) { - ClientLogger.LoggingEvent log = logger.atLevel(getLogLevel(response)); - if (!log.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { - return; - } + 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 contentLength = getContentLength(response.getHeaders()); + long responseStartNanoTime = System.nanoTime(); - log.setEventName(HTTP_RESPONSE_EVENT_NAME) - .setContext(response.getRequest().getRequestOptions().getContext()) + // 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) + .setContext(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_HEADERS_KEY, getDurationMs(startNanoTime, headersNanoTime)) + .addKeyValue(HTTP_REQUEST_TIME_TO_RESPONSE_KEY, getDurationMs(startNanoTime, responseStartNanoTime)) .addKeyValue(HTTP_RESPONSE_STATUS_CODE_KEY, response.getStatusCode()) - .addKeyValue(HTTP_RESPONSE_BODY_SIZE_KEY, contentLength); + .addKeyValue(HTTP_REQUEST_BODY_SIZE_KEY, requestContentLength) + .addKeyValue(HTTP_RESPONSE_BODY_SIZE_KEY, + getContentLength(logger, response.getBody(), response.getHeaders())); - addHeadersToLogMessage(response.getHeaders(), log); + addHeadersToLogMessage(response.getHeaders(), logBuilder); + } - if (httpLogDetailLevel.shouldLogBody() && canLogBody(response.getBody())) { - // logResponse is called after body is requested and buffered, so it's safe to call getBody().toString() here. - // not catching exception here, because it's caught in InstrumentedResponse.getBody() and will - String content = response.getBody().toString(); - log.addKeyValue(HTTP_RESPONSE_BODY_KEY, content) - .addKeyValue(HTTP_REQUEST_DURATION_KEY, getDurationMs(startNanoTime, System.nanoTime())); - } - log.log(); + if (httpLogDetailLevel.shouldLogBody() && canLogBody(response.getBody())) { + return new LoggingHttpResponse<>(response, content -> { + if (logBuilder.isEnabled()) { + logBuilder.addKeyValue(HTTP_RESPONSE_BODY_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)); } - @Override - public void logException(ClientLogger logger, HttpRequest request, Response response, Throwable throwable, - long startNanoTime, Long headersNanoTime, String redactedUrl, int tryCount) { - ClientLogger.LoggingEvent log = logger.atWarning(); - if (!log.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { - return; - } + 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) { - log.setEventName(HTTP_RESPONSE_EVENT_NAME) - .setContext(request.getRequestOptions().getContext()) + ClientLogger.LoggingEvent log = logger.atLevel(ClientLogger.LogLevel.WARNING); + if (!log.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { + return throwable; + } + + log.setEventName(HTTP_RESPONSE_EVENT_NAME) + .setContext(context) .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod()) .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount) - .addKeyValue(URL_FULL_KEY, redactedUrl); - - // exception could happen before response code and headers were received - // or after that. So we may or may not have a response object. - if (response != null) { - log.addKeyValue(HTTP_RESPONSE_STATUS_CODE_KEY, response.getStatusCode()); - addHeadersToLogMessage(response.getHeaders(), log); + .addKeyValue(URL_FULL_KEY, redactedUrl) + .addKeyValue(HTTP_REQUEST_BODY_SIZE_KEY, requestContentLength) + .addKeyValue(HTTP_REQUEST_DURATION_KEY, getDurationMs(startNanoTime, System.nanoTime())); - if (headersNanoTime != null) { - log.addKeyValue(HTTP_REQUEST_TIME_TO_HEADERS_KEY, - getDurationMs(startNanoTime, headersNanoTime)); - } + if (response != null) { + addHeadersToLogMessage(response.getHeaders(), log); + log + .addKeyValue(HTTP_RESPONSE_BODY_SIZE_KEY, + getContentLength(logger, response.getBody(), response.getHeaders())) + .addKeyValue(HTTP_RESPONSE_STATUS_CODE_KEY, response.getStatusCode()); + + if (responseStartNanoTime != null) { + log.addKeyValue(HTTP_REQUEST_TIME_TO_RESPONSE_KEY, + getDurationMs(startNanoTime, responseStartNanoTime)); } - - // not logging body - there was an exception - log.log(null, throwable); } + + return log.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 sb StringBuilder that is generating the log message. - * @param logLevel Log level the environment is configured to use. + * @param logBuilder Log message builder. */ - private void addHeadersToLogMessage(HttpHeaders headers, ClientLogger.LoggingEvent log) { + 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; - log.addKeyValue(headerName.toString(), headerValue); + 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 retrieve and parse the Content-Length header into a numeric representation. + /** + * 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 + * @return The numeric value of the Content-Length header or 0 if the header is not present or invalid. */ - private static long getContentLength(HttpHeaders headers) { + private static long getContentLength(ClientLogger logger, BinaryData body, HttpHeaders headers) { + if (body == null) { + return 0; + } + + if (body.getLength() != null) { + return body.getLength(); + } + long contentLength = 0; String contentLengthString = headers.getValue(HttpHeaderName.CONTENT_LENGTH); @@ -470,7 +544,7 @@ private static long getContentLength(HttpHeaders headers) { try { contentLength = Long.parseLong(contentLengthString); } catch (NumberFormatException | NullPointerException e) { - LOGGER.atVerbose() + logger.atVerbose() .addKeyValue("contentLength", contentLengthString) .log("Could not parse the HTTP header content-length", e); } @@ -478,316 +552,48 @@ private static long getContentLength(HttpHeaders headers) { return contentLength; } - /** - * Determines if the request or response body should be logged. - * - *

The request or response body is logged if the Content-Type is not "application/octet-stream" and the body - * 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) { - return data != null && data.isReplayable() && data.getLength() != null && data.getLength() < MAX_BODY_LOG_SIZE; - } - - private void instrumentResponse(HttpResponse actualResponse, Span span, ClientLogger logger, long startNs, - String sanitizedUrl, int tryCount) { - long timeToHeadersNs = System.nanoTime(); - /*if (!httpLogDetailLevel.shouldLogBody()) { - responseLogger.logResponse(logger, actualResponse, startNs, timeToHeadersNs, sanitizedUrl, tryCount); - - if (!span.isRecording()) { - span.end(); - return; - } - }*/ - - HttpResponseAccessHelper.setBody(actualResponse, new InstrumentedBinaryData(actualResponse.getBody(), error -> { - if (error == null) { - responseLogger.logResponse(logger, actualResponse, startNs, timeToHeadersNs, sanitizedUrl, tryCount); - span.end(); - } else { - responseLogger.logException(logger, actualResponse.getRequest(), actualResponse, error, startNs, - timeToHeadersNs, sanitizedUrl, tryCount); - span.end(unwrap(error)); - } - })); - } - - private static Map getProperties(String propertiesFileName) { - try (InputStream inputStream - = HttpInstrumentationPolicy.class.getClassLoader().getResourceAsStream(propertiesFileName)) { - if (inputStream != null) { - Properties properties = new Properties(); - properties.load(inputStream); - return Collections.unmodifiableMap(properties.entrySet() - .stream() - .collect(Collectors.toMap(entry -> (String) entry.getKey(), entry -> (String) entry.getValue()))); - } - } catch (IOException ex) { - LOGGER.atWarning() - .addKeyValue("propertiesFileName", propertiesFileName) - .log("Failed to read properties.", ex); - } - - return Collections.emptyMap(); - } - - private static class InstrumentedBinaryData extends BinaryData { - private final BinaryData inner; - private final Consumer onComplete; - private boolean reported; - - InstrumentedBinaryData(BinaryData inner, Consumer onComplete) { - this.inner = inner; - this.onComplete = onComplete; - } - - @Override - public byte[] toBytes() { - try { - byte[] bytes = inner.toBytes(); - report(); - return bytes; - } catch (RuntimeException t) { - report(t); - throw t; - } - } - - @Override - public String toString() { - try { - String str = inner.toString(); - report(); - return str; - } catch (RuntimeException t) { - onComplete.accept(t); - throw t; - } - } - - @Override - public T toObject(Type type, ObjectSerializer serializer) throws IOException { - try { - T value = inner.toObject(type, serializer); - report(); - return value; - } catch (RuntimeException t) { - report(t); - throw t; - } - } - - @Override - public InputStream toStream() { - InputStream stream = inner.toStream(); - return new InputStream() { - @Override - public int read() throws IOException { - try { - int read = stream.read(); - if (read == -1) { - report(); - } - return read; - } catch (IOException | RuntimeException e) { - report(e); - throw e; - } - } - - @Override - public int read(byte[] b) throws IOException { - try { - int read = stream.read(b); - if (read == -1) { - report(); - } - return read; - } catch (IOException | RuntimeException e) { - report(e); - throw e; - } - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - try { - int read = stream.read(b, off, len); - if (read == -1) { - report(); - } - return read; - } catch (IOException | RuntimeException e) { - report(e); - throw e; - } - } - - @Override - public byte[] readAllBytes() throws IOException { - try { - byte[] bytes = stream.readAllBytes(); - report(); - return bytes; - } catch (IOException | RuntimeException e) { - report(e); - throw e; - } - } - - @Override - public int readNBytes(byte[] b, int off, int len) throws IOException { - try { - int read = stream.readNBytes(b, off, len); - if (read < len) { - report(); - } - return read; - } catch (IOException | RuntimeException e) { - report(e); - throw e; - } - } - - @Override - public byte[] readNBytes(int len) throws IOException { - try { - byte[] bytes = stream.readNBytes(len); - if (bytes.length < len) { - report(); - } - return bytes; - } catch (IOException | RuntimeException e) { - report(e); - throw e; - } - } + private static final class LoggingHttpResponse extends HttpResponse { + private final Consumer onContent; + private final Consumer onException; + private final BinaryData originalBody; + private BinaryData bufferedBody; - @Override - public void reset() throws IOException { - stream.reset(); - } - - @Override - public long skip(long n) throws IOException { - try { - return stream.skip(n); - } catch (IOException | RuntimeException e) { - report(e); - throw e; - } - } - - @Override - public void skipNBytes(long n) throws IOException { - try { - stream.skipNBytes(n); - } catch (IOException | RuntimeException e) { - report(e); - throw e; - } - } - - @Override - public int available() throws IOException { - return stream.available(); - } - - @Override - public void mark(int readlimit) { - stream.mark(readlimit); - } - - @Override - public boolean markSupported() { - return stream.markSupported(); - } + private LoggingHttpResponse(Response actualResponse, Consumer onContent, + Consumer onException) { + super(actualResponse.getRequest(), actualResponse.getStatusCode(), actualResponse.getHeaders(), + actualResponse.getValue()); - @Override - public void close() throws IOException { - try { - stream.close(); - report(); - } catch (IOException e) { - report(e); - throw e; - } - } - - @Override - public long transferTo(OutputStream out) throws IOException { - try { - long transferred = stream.transferTo(out); - report(); - return transferred; - } catch (IOException | RuntimeException e) { - report(e); - throw e; - } - } - }; + this.onContent = onContent; + this.onException = onException; + this.originalBody = actualResponse.getBody(); } @Override - public void writeTo(JsonWriter jsonWriter) throws IOException { - try { - inner.writeTo(jsonWriter); - report(); - } catch (RuntimeException t) { - report(t); - throw t; + public BinaryData getBody() { + if (bufferedBody != null) { + return bufferedBody; } - } - @Override - public ByteBuffer toByteBuffer() { try { - ByteBuffer bb = inner.toByteBuffer(); - report(); - return bb; - } catch (RuntimeException t) { - report(t); - throw t; - } - } - - @Override - public Long getLength() { - return inner.getLength(); - } - - @Override - public boolean isReplayable() { - return inner.isReplayable(); - } - - @Override - public BinaryData toReplayableBinaryData() { - if (inner.isReplayable()) { - return this; + bufferedBody = originalBody.toReplayableBinaryData(); + onContent.accept(bufferedBody); + return bufferedBody; + } catch (RuntimeException e) { + // we'll log exception at the appropriate level. + onException.accept(e); + throw e; } - - return new InstrumentedBinaryData(inner.toReplayableBinaryData(), onComplete); } @Override public void close() throws IOException { - inner.close(); - report(); - } - - private void report() { - report(null); - } - - private void report(Throwable t) { - if (!reported) { - onComplete.accept(t); - reported = true; + 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 index c43ebca9a3c37..9a891f67073aa 100644 --- 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 @@ -12,7 +12,7 @@ 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.instrumentation.logging.ClientLogger; import io.clientcore.core.util.binarydata.BinaryData; import java.io.IOException; 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 18da7b6a964ea..8fb3120d2f7d1 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 @@ -121,7 +121,7 @@ && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), context) .addKeyValue("url.full.to", redirectUri) .addKeyValue("url.full.all", attemptedRedirectUris::toString) .setEventName("http.request.redirect") - .setContext(context) + //.setContext(context) .log(); attemptedRedirectUris.add(redirectUri); @@ -143,7 +143,7 @@ private boolean isValidRedirectCount(int tryCount, Context context) { if (tryCount >= this.maxAttempts) { LOGGER.atError() .addKeyValue("maxAttempts", this.maxAttempts) - .setContext(context) + //.setContext(context) .log("Redirect attempts have been exhausted."); return false; @@ -166,7 +166,7 @@ private boolean alreadyAttemptedRedirectUri(String redirectUri, Set atte if (attemptedRedirectUris.contains(redirectUri)) { LOGGER.atError() .addKeyValue(URL_FULL_KEY, redirectUri) - .setContext(context) + //.setContext(context) .log("Request was redirected more than once to the same URI."); return true; @@ -187,7 +187,7 @@ private boolean isAllowedRedirectMethod(HttpMethod httpMethod, Context context) return true; } else { LOGGER.atError() - .setContext(context) + //.setContext(context) .addKeyValue(AttributeKeys.HTTP_REQUEST_METHOD_KEY, httpMethod) .log("Request redirection is not enabled for this HTTP method."); diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java index 195516891ef4f..5551d955ecf3f 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java @@ -31,7 +31,7 @@ private AttributeKeys() { /** * Key representing duration of call in milliseconds, the value should be a number. */ - public static final String HTTP_REQUEST_TIME_TO_HEADERS_KEY = "http.request.time_to_headers"; + 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. diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java index 9fb874b10280a..de86ee17f015e 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java @@ -3,9 +3,8 @@ package io.clientcore.core.implementation.instrumentation; -import io.clientcore.core.http.models.RequestOptions; -import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; 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.Span; @@ -17,70 +16,82 @@ import io.clientcore.core.instrumentation.tracing.Tracer; import io.clientcore.core.instrumentation.tracing.TracingScope; import io.clientcore.core.instrumentation.logging.ClientLogger; -import io.clientcore.core.util.Context; import java.util.HashMap; import java.util.Map; import java.util.UUID; +/** + * Default implementation of {@link Instrumentation} which implements correlation and context propagation + * and records traces as logs. + */ public class DefaultInstrumentation implements Instrumentation { + public static final DefaultInstrumentation DEFAULT_INSTANCE = new DefaultInstrumentation(null, null); private static final String INVALID_TRACE_ID = "00000000000000000000000000000000"; private static final String INVALID_SPAN_ID = "0000000000000000"; private final InstrumentationOptions instrumentationOptions; private final LibraryInstrumentationOptions libraryOptions; + /** + * Creates a new instance of {@link DefaultInstrumentation}. + * @param instrumentationOptions the application instrumentation options + * @param libraryOptions the library instrumentation options + */ public DefaultInstrumentation(InstrumentationOptions instrumentationOptions, LibraryInstrumentationOptions libraryOptions) { this.instrumentationOptions = instrumentationOptions; this.libraryOptions = libraryOptions; } + /** + * {@inheritDoc} + */ @Override public Tracer getTracer() { return new DefaultTracer(instrumentationOptions, libraryOptions); } + /** + * {@inheritDoc} + */ @Override public TraceContextPropagator getW3CTraceContextPropagator() { return DefaultContextPropagator.W3C_TRACE_CONTEXT_PROPAGATOR; } - public static ClientLogger.LoggingEvent enrichLog(ClientLogger.LoggingEvent log, Context context) { - if (OTelInitializer.isInitialized()) { - return log; + public InstrumentationContext createInstrumentationContext(T context) { + if (context instanceof DefaultSpanContext) { + return (DefaultSpanContext) context; + } else if (context instanceof DefaultSpan) { + return ((DefaultSpan) context).spanContext; + } else { + return DefaultSpanContext.INVALID; } - - DefaultSpan span = DefaultSpan.fromContextOrCurrent(context); - if (span == null) { - return log; - } - - return log.addKeyValue("trace.id", span.getSpanContext().getTraceId()) - .addKeyValue("span.id", span.getSpanContext().getSpanId()); } - public static final class DefaultTracer implements Tracer { + static final class DefaultTracer implements Tracer { private final boolean isEnabled; private final ClientLogger logger; DefaultTracer(InstrumentationOptions instrumentationOptions, LibraryInstrumentationOptions libraryOptions) { this.isEnabled = instrumentationOptions == null || instrumentationOptions.isTracingEnabled(); // TODO: probably need additional config for log-based tracing - Map libraryContext = new HashMap<>(2); - libraryContext.put("library.version", libraryOptions.getLibraryVersion()); - libraryContext.put("library.instrumentation.schema_url", libraryOptions.getSchemaUrl()); Object providedLogger = instrumentationOptions == null ? null : instrumentationOptions.getProvider(); if (providedLogger instanceof ClientLogger) { this.logger = (ClientLogger) providedLogger; } else { + Map libraryContext = new HashMap<>(2); + libraryContext.put("library.version", libraryOptions.getLibraryVersion()); + libraryContext.put("library.instrumentation.schema_url", libraryOptions.getSchemaUrl()); + this.logger = new ClientLogger(libraryOptions.getLibraryName() + ".tracing", libraryContext); } } @Override - public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, RequestOptions requestOptions) { - return new DefaultSpanBuilder(this.logger, spanName, spanKind, requestOptions); + public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, InstrumentationContext instrumentationContext) { + return new DefaultSpanBuilder(this.logger, spanName, spanKind, instrumentationContext); } @Override @@ -92,17 +103,14 @@ public boolean isEnabled() { private static final class DefaultSpanBuilder implements SpanBuilder { private final ClientLogger.LoggingEvent log; private final boolean isRecording; - private final DefaultSpanContext spanContext; + private final DefaultSpanContext parentSpanContext; - DefaultSpanBuilder(ClientLogger logger, String spanName, SpanKind spanKind, RequestOptions requestOptions) { + DefaultSpanBuilder(ClientLogger logger, String spanName, SpanKind spanKind, InstrumentationContext instrumentationContext) { isRecording = logger.canLogAtLevel(ClientLogger.LogLevel.INFORMATIONAL); - DefaultSpanContext parentSpanContext = requestOptions == null - ? DefaultSpanContext.INVALID - : DefaultSpanContext.fromContext(requestOptions.getContext()); - spanContext = DefaultSpanContext.fromParent(parentSpanContext, isRecording); + DefaultSpanContext parentSpanContext = instrumentationContext instanceof DefaultSpanContext + ? (DefaultSpanContext) instrumentationContext : DefaultSpanContext.INVALID; + this.parentSpanContext = parentSpanContext; this.log = logger.atInfo() - .addKeyValue("span.trace_id", spanContext.getTraceId()) - .addKeyValue("span.id", spanContext.getSpanId()) .addKeyValue("span.parent.id", parentSpanContext.getSpanId()) .addKeyValue("span.name", spanName) .addKeyValue("span.kind", spanKind.name()); @@ -116,7 +124,7 @@ public SpanBuilder setAttribute(String key, Object value) { @Override public Span startSpan() { - return new DefaultSpan(log, spanContext, isRecording); + return new DefaultSpan(log, parentSpanContext, isRecording); } } @@ -127,18 +135,20 @@ private static final class DefaultSpan implements Span { private final DefaultSpanContext spanContext; private String errorType; - DefaultSpan(ClientLogger.LoggingEvent log, DefaultSpanContext spanContext, boolean isRecording) { + DefaultSpan(ClientLogger.LoggingEvent log, DefaultSpanContext parentSpanContext, boolean isRecording) { this.log = log; this.startTime = System.nanoTime(); - this.spanContext = spanContext; this.isRecording = isRecording; + this.spanContext = DefaultSpanContext.create(parentSpanContext, isRecording, this); + if (log != null) { + this.log + .addKeyValue("trace.id", spanContext.getTraceId()) + .addKeyValue("span.id", spanContext.getSpanId()); + } } - DefaultSpan(DefaultSpanContext spanContext) { - this.spanContext = spanContext; - this.isRecording = false; - this.log = null; - this.startTime = 0; + DefaultSpan(DefaultSpanContext parentSpanContext) { + this(null, parentSpanContext, false); } @Override @@ -191,28 +201,14 @@ public TracingScope makeCurrent() { return new DefaultScope(this); } - public DefaultSpanContext getSpanContext() { + @Override + public InstrumentationContext getInstrumentationContext() { return spanContext; } - - public static DefaultSpan fromContextOrCurrent(Context context) { - if (context != null) { - Object span = context.get(TRACE_CONTEXT_KEY); - if (span instanceof DefaultSpan) { - return (DefaultSpan) span; - } - - if (span != null) { - return null; - } - } - - return DefaultScope.getCurrent(); - } - }; + } private static final class DefaultScope implements TracingScope { - private final static ThreadLocal CURRENT_SPAN = new ThreadLocal<>(); + private static final ThreadLocal CURRENT_SPAN = new ThreadLocal<>(); private final DefaultSpan originalSpan; DefaultScope(DefaultSpan span) { @@ -224,10 +220,6 @@ private static final class DefaultScope implements TracingScope { public void close() { CURRENT_SPAN.set(originalSpan); } - - static DefaultSpan getCurrent() { - return CURRENT_SPAN.get(); - } } private static final class DefaultContextPropagator implements TraceContextPropagator { @@ -237,8 +229,7 @@ private DefaultContextPropagator() { } @Override - public void inject(Context context, C carrier, TraceContextSetter setter) { - DefaultSpanContext spanContext = DefaultSpanContext.fromContext(context); + public void inject(InstrumentationContext spanContext, C carrier, TraceContextSetter setter) { if (spanContext.isValid()) { setter.set(carrier, "traceparent", "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-" + spanContext.getTraceFlags()); @@ -246,15 +237,14 @@ public void inject(Context context, C carrier, TraceContextSetter setter) } @Override - public Context extract(Context context, C carrier, TraceContextGetter getter) { + 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); - DefaultSpanContext spanContext = new DefaultSpanContext(traceId, spanId, traceFlags); - return context.put(TRACE_CONTEXT_KEY, new DefaultSpan(spanContext)); + return new DefaultSpanContext(traceId, spanId, traceFlags, Span.noop()); } else { // TODO log } @@ -266,29 +256,39 @@ private static boolean isValidTraceparent(String traceparent) { // TODO: add more validation return traceparent.startsWith("00-") && traceparent.length() == 55; } - }; + } - private static final class DefaultSpanContext { + private static final class DefaultSpanContext implements InstrumentationContext { static final DefaultSpanContext INVALID = new DefaultSpanContext(); private final String traceId; private final String spanId; private final String traceFlags; private final boolean isValid; + private final Span span; - String getTraceId() { + @Override + public String getTraceId() { return traceId; } - String getSpanId() { + @Override + public String getSpanId() { return spanId; } - String getTraceFlags() { - return traceFlags; + @Override + public boolean isValid() { + return isValid; } - boolean isValid() { - return isValid; + @Override + public Span getSpan() { + return this.span; + } + + @Override + public String getTraceFlags() { + return traceFlags; } DefaultSpanContext() { @@ -296,30 +296,21 @@ boolean isValid() { this.spanId = INVALID_SPAN_ID; this.traceFlags = "00"; this.isValid = false; + this.span = Span.noop(); } - DefaultSpanContext(String traceId, String spanId, String traceFlags) { + DefaultSpanContext(String traceId, String spanId, String traceFlags, Span span) { this.traceId = traceId; this.spanId = spanId; this.traceFlags = traceFlags; this.isValid = true; + this.span = span; } - static DefaultSpanContext fromParent(DefaultSpanContext parent, boolean isSampled) { + static DefaultSpanContext create(DefaultSpanContext parent, boolean isSampled, DefaultSpan span) { return parent.isValid() - ? new DefaultSpanContext(parent.traceId, getRandomId(16), isSampled ? "01" : "00") - : new DefaultSpanContext(getRandomId(32), getRandomId(16), isSampled ? "01" : "00"); - } - - static DefaultSpanContext fromContext(Context context) { - Object span = context.get(TRACE_CONTEXT_KEY); - if (span instanceof DefaultSpan) { - return ((DefaultSpan) span).getSpanContext(); - } else if (span != null) { - // TODO log - } - - return INVALID; + ? new DefaultSpanContext(parent.traceId, getRandomId(16), isSampled ? "01" : "00", span) + : new DefaultSpanContext(getRandomId(32), getRandomId(16), isSampled ? "01" : "00", span); } /** @@ -330,5 +321,5 @@ private static String getRandomId(int length) { UUID uuid = UUID.randomUUID(); return uuid.toString().replace("-", "").substring(32 - length); } - }; + } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShim.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShim.java index e802d6ebf0199..7d7074724d378 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShim.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShim.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package io.clientcore.core.implementation.instrumentation; import io.clientcore.core.implementation.ReflectionUtils; 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 629717a0c5fe1..f3920928d2773 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 @@ -70,7 +70,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 + * @param arg1 the first argument * @return the result of the invocation or the fallback value */ public Object invoke(Object argOrTarget, Object arg1) { @@ -86,8 +86,8 @@ 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 + * @param arg1 the first argument + * @param arg2 the second argument * @return the result of the invocation or the fallback value */ public Object invoke(Object argOrTarget, Object arg1, Object arg2) { @@ -103,9 +103,9 @@ 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 - * @param arg3 the third argument + * @param arg1 the first argument + * @param arg2 the second argument + * @param arg3 the third argument * @return the result of the invocation or the fallback value */ public Object invoke(Object argOrTarget, Object arg1, Object arg2, Object arg3) { 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 a42f002d7f9a9..28922d87f9ca1 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,9 +4,12 @@ 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.tracing.TraceContextPropagator; @@ -14,8 +17,11 @@ 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; @@ -23,6 +29,7 @@ * A {@link Instrumentation} implementation that uses OpenTelemetry. */ public class OTelInstrumentation implements Instrumentation { + public static final OTelInstrumentation DEFAULT_INSTANCE = new OTelInstrumentation(null, null); private static final FallbackInvoker GET_PROVIDER_INVOKER; private static final FallbackInvoker GET_GLOBAL_OTEL_INVOKER; @@ -111,6 +118,21 @@ public TraceContextPropagator getW3CTraceContextPropagator() { return OTelInitializer.isInitialized() ? W3C_PROPAGATOR_INSTANCE : OTelTraceContextPropagator.NOOP; } + public InstrumentationContext createInstrumentationContext(T context) { + if (context instanceof InstrumentationContext) { + return (InstrumentationContext) context; + } 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/tracing/OTelContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelContext.java index 6fff4b20133a9..a648c065f8344 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,8 +6,8 @@ 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.instrumentation.logging.ClientLogger; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; @@ -16,6 +16,7 @@ class OTelContext { private static final ClientLogger LOGGER = new ClientLogger(OTelContext.class); + private static final Object ROOT_CONTEXT; private static final TracingScope NOOP_SCOPE = () -> { }; private static final FallbackInvoker CURRENT_INVOKER; @@ -23,20 +24,23 @@ class OTelContext { private static final FallbackInvoker WITH_INVOKER; 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()) { @@ -51,9 +55,12 @@ class OTelContext { = 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 +70,9 @@ 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; + //HAS_CLIENT_SPAN_CONTEXT_KEY = hasClientSpanContextKey; + CLIENT_CORE_SPAN_CONTEXT_KEY = clientCoreSpanContextKey; + ROOT_CONTEXT = rootContext; } static Object getCurrent() { @@ -72,6 +81,10 @@ static Object getCurrent() { return currentContext; } + static Object getRoot() { + return ROOT_CONTEXT; + } + static AutoCloseable makeCurrent(Object context) { assert CONTEXT_CLASS.isInstance(context); Object scope = MAKE_CURRENT_INVOKER.invoke(context); @@ -79,21 +92,38 @@ 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 && ((OTelSpanContext) context).getOtelContext() != null) { + return ((OTelSpanContext) context).getOtelContext(); + } + + 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 178a408117896..97c83a475be88 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,6 +7,7 @@ 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; @@ -22,12 +23,14 @@ import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.STATUS_CODE_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelContext.markCoreSpan; import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext.INVALID_OTEL_SPAN_CONTEXT; +import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext.fromOTelSpan; /** * OpenTelemetry implementation of {@link Span}. */ public class OTelSpan implements Span { private static final ClientLogger LOGGER = new ClientLogger(OTelSpan.class); + static final OTelSpan NOOP_SPAN; private static final TracingScope NOOP_SCOPE = () -> { }; private static final FallbackInvoker SET_ATTRIBUTE_INVOKER; @@ -43,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; @@ -57,6 +62,7 @@ public class OTelSpan implements Span { ReflectiveInvoker currentInvoker = null; Object errorStatusCode = null; + OTelSpan noopSpan = null; if (OTelInitializer.isInitialized()) { try { @@ -79,6 +85,12 @@ public class OTelSpan implements Span { errorStatusCode = STATUS_CODE_CLASS.getField("ERROR").get(null); currentInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("current")); + ReflectiveInvoker getInvalidInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("getInvalid")); + + Object invalidSpan = getInvalidInvoker.invoke(); + Object rootContext = OTelContext.getCurrent(); + + noopSpan = new OTelSpan(invalidSpan, rootContext); } catch (Throwable t) { OTelInitializer.initError(LOGGER, t); } @@ -93,6 +105,7 @@ public class OTelSpan implements Span { FROM_CONTEXT_INVOKER = new FallbackInvoker(fromContextInvoker, LOGGER); WRAP_INVOKER = new FallbackInvoker(wrapInvoker, LOGGER); CURRENT_INVOKER = new FallbackInvoker(currentInvoker, LOGGER); + NOOP_SPAN = noopSpan; ERROR_STATUS_CODE = errorStatusCode; } @@ -100,9 +113,17 @@ public class OTelSpan implements Span { 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; } /** @@ -144,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} */ @@ -174,20 +184,66 @@ public TracingScope makeCurrent() { 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 OTelSpan createPropagatingSpan(OTelSpanContext spanContext) { + Object span = wrapSpanContext(spanContext.getOtelContext()); + return new OTelSpan(span, spanContext.getOtelContext(), SpanKind.INTERNAL); + } + + public 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; + } + + Object getOtelSpan() { + return otelSpan; + } + + SpanKind getSpanKind() { + return spanKind; + } + + /** + * {@inheritDoc} + */ + public InstrumentationContext getInstrumentationContext() { + if (spanContext != null) { + return spanContext; + } + + spanContext = isInitialized() ? new OTelSpanContext(GET_SPAN_CONTEXT_INVOKER.invoke(otelSpan), otelContext) + : OTelSpanContext.getInvalid(); + + return spanContext; } private void endSpan(Throwable throwable) { @@ -212,7 +268,7 @@ private static TracingScope wrapOTelScope(AutoCloseable otelScope) { }; } - private static Object storeInContext(Object otelSpan, Object otelContext) { + static Object storeInContext(Object otelSpan, Object otelContext) { Object updatedContext = STORE_IN_CONTEXT_INVOKER.invoke(otelSpan, otelContext); return updatedContext != null ? updatedContext : otelContext; } 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 9d53d6f4ffa48..c74a9745b24a6 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.instrumentation.logging.ClientLogger; -import io.clientcore.core.util.Context; 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,17 @@ 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 170e05108e20f..388c9ca2ff71e 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.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,121 @@ 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) { + public OTelSpanContext(Object otelSpanContext, Object otelContext) { + assert otelSpanContext == null || SPAN_CONTEXT_CLASS.isInstance(otelSpanContext); + assert otelContext == null || OTelInitializer.CONTEXT_CLASS.isInstance(otelContext); + this.otelSpanContext = otelSpanContext; + this.otelContext = otelContext; + } + + static Object toOTelSpanContext(InstrumentationContext context) { + if (context instanceof OTelSpanContext) { + return ((OTelSpanContext) context).otelSpanContext; + } + + return CREATE_INVOKER.invoke(context.getTraceId(), context.getTraceFlags(), null); + } + + public static OTelSpanContext fromOTelContext(Object otelContext) { + if (otelContext == null) { + otelContext = OTelContext.getCurrent(); + } + Object otelSpan = OTelSpan.fromOTelContext(otelContext); + Object otelSpanContext = OTelSpan.getSpanContext(otelSpan); + return new OTelSpanContext(otelSpanContext, otelContext); + } + + public static OTelSpanContext fromOTelSpan(Object otelSpan) { + Object otelSpanContext = OTelSpan.getSpanContext(otelSpan); + return new OTelSpanContext(otelSpanContext, null); // TODO -this is not correct } - static OTelSpanContext getInvalid() { + 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 traceFlags = GET_TRACE_FLAGS_INVOKER.invoke(otelSpanContext); - if (traceFlags != null) { - return traceFlags.toString(); + Object traceFlagsObj = GET_TRACE_FLAGS_INVOKER.invoke(otelSpanContext); + if (traceFlagsObj != null) { + traceFlags = traceFlagsObj.toString(); } + } else { + traceFlags = INVALID_TRACE_FLAGS; } - return 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; + } + + @Override + public Span getSpan() { + return isInitialized() && otelContext != null ? OTelContext.getClientCoreSpan(otelContext) : OTelSpan.createPropagatingSpan(this); + } + + Object getOtelContext() { + return otelContext; } 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 0b06d2c3cf744..505ddbc8c481f 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,6 +6,7 @@ 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; @@ -22,8 +23,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 +68,9 @@ 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)); + = 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 a34f087cd0adb..a3d9a88ae5dc7 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 @@ -7,6 +7,7 @@ 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; @@ -94,12 +95,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 750a08c8957ad..0000000000000 --- 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.instrumentation.logging.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/instrumentation/Instrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/Instrumentation.java index 74d3f06252c07..45556807bd425 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 @@ -6,8 +6,10 @@ import io.clientcore.core.implementation.instrumentation.DefaultInstrumentation; import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; import io.clientcore.core.implementation.instrumentation.otel.OTelInstrumentation; +import io.clientcore.core.instrumentation.tracing.Span; import io.clientcore.core.instrumentation.tracing.TraceContextPropagator; import io.clientcore.core.instrumentation.tracing.Tracer; +import io.clientcore.core.util.Context; import java.util.Objects; @@ -79,4 +81,12 @@ static Instrumentation create(InstrumentationOptions applicationOptions, return new DefaultInstrumentation(applicationOptions, libraryOptions); } } + + static InstrumentationContext createInstrumentationContext(T context) { + if (OTelInitializer.isInitialized()) { + return OTelInstrumentation.DEFAULT_INSTANCE.createInstrumentationContext(context); + } else { + return DefaultInstrumentation.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 0000000000000..56b425e5c9e61 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationContext.java @@ -0,0 +1,11 @@ +package io.clientcore.core.instrumentation; + +import io.clientcore.core.instrumentation.tracing.Span; + +public interface InstrumentationContext { + String getTraceId(); + String getSpanId(); + String getTraceFlags(); + boolean isValid(); + Span getSpan(); +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java index 5f6407ae0ad8b..c27405b3e3836 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java @@ -5,12 +5,11 @@ import io.clientcore.core.annotation.Metadata; import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; -import io.clientcore.core.implementation.instrumentation.DefaultInstrumentation; 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.Context; import io.clientcore.core.util.configuration.Configuration; import java.io.IOException; @@ -301,7 +300,7 @@ public static final class LoggingEvent { private final boolean isEnabled; private Map keyValuePairs; private String eventName; - private Context context; + private InstrumentationContext context = null; /** * Creates {@code LoggingEvent} for provided level and {@link ClientLogger}. @@ -451,7 +450,7 @@ public LoggingEvent addKeyValue(String key, Supplier valueSupplier) { * @param context operation context. * @return The updated {@code LoggingEventBuilder} object. */ - public LoggingEvent setContext(Context context) { + public LoggingEvent setContext(InstrumentationContext context) { this.context = context; return this; } @@ -523,7 +522,10 @@ private String getMessageWithContext(String message) { message = ""; } - DefaultInstrumentation.enrichLog(this, context); + if (this.context != null && this.context.isValid()) { + addKeyValue("trace.id", context.getTraceId()); + addKeyValue("span.id", context.getSpanId()); + } int pairsCount = (keyValuePairs == null ? 0 : keyValuePairs.size()) + (globalPairs == null ? 0 : globalPairs.size()); 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 0000000000000..1887eaa4ca514 --- /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/tracing/NoopInstrumentationContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopInstrumentationContext.java new file mode 100644 index 0000000000000..3c3ff6f7d0fbd --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopInstrumentationContext.java @@ -0,0 +1,35 @@ +package io.clientcore.core.instrumentation.tracing; + +import io.clientcore.core.instrumentation.InstrumentationContext; + +public 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 0000000000000..9c07770f56a70 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopSpan.java @@ -0,0 +1,46 @@ +package io.clientcore.core.instrumentation.tracing; + +import io.clientcore.core.instrumentation.InstrumentationContext; + +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 f3a7125d23ec2..c3ef8607f7122 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

@@ -77,44 +79,13 @@ public interface Span { */ TracingScope makeCurrent(); + InstrumentationContext getInstrumentationContext(); + /** * Returns a no-op span. * @return A no-op span. */ static Span noop() { - return new Span() { - private final static 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; - } - }; + 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 d5bffecb60399..7189b8b9a5a84 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,6 +3,7 @@ package io.clientcore.core.instrumentation.tracing; +import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.util.Context; /** @@ -19,7 +20,7 @@ public interface TraceContextPropagator { * @param setter The setter to use to inject the context into the carrier. * @param The type of the carrier. */ - void inject(Context context, C carrier, TraceContextSetter setter); + void inject(InstrumentationContext context, C carrier, TraceContextSetter setter); /** * Extracts the context from the carrier. @@ -31,5 +32,5 @@ public interface TraceContextPropagator { * * @return The extracted context. */ - Context extract(Context context, C carrier, TraceContextGetter getter); + InstrumentationContext extract(InstrumentationContext context, C carrier, TraceContextGetter getter); } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Tracer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Tracer.java index 3223d411e3589..5c98c0df5b03f 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Tracer.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Tracer.java @@ -4,6 +4,7 @@ package io.clientcore.core.instrumentation.tracing; import io.clientcore.core.http.models.RequestOptions; +import io.clientcore.core.instrumentation.InstrumentationContext; /** * Represents a tracer - a component that creates spans. @@ -73,10 +74,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/module-info.java b/sdk/clientcore/core/src/main/java/module-info.java index dee608a9a5b6e..025ef8b177a02 100644 --- a/sdk/clientcore/core/src/main/java/module-info.java +++ b/sdk/clientcore/core/src/main/java/module-info.java @@ -5,9 +5,9 @@ * This module provides core functionality for the Java SDK. */ module io.clientcore.core { - requires transitive java.xml; requires java.net.http; + requires java.desktop; // public API surface area exports io.clientcore.core.annotation; 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 5060f164aea06..5471609b93fbc 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 @@ -57,7 +57,7 @@ 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. @@ -91,7 +91,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") @@ -156,9 +156,9 @@ 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 = Span.noop();//Instrumentation.fromContext(request.getRequestOptions().getContext()); + if (span.isRecording()) { + span.setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID)); } return next.process(); diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java index 792ac367e2ece..0dbe364bb6ae2 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java @@ -5,7 +5,6 @@ import io.clientcore.core.http.models.RequestOptions; import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; -import io.clientcore.core.implementation.instrumentation.DefaultInstrumentation; import io.clientcore.core.implementation.instrumentation.DefaultLogger; import io.clientcore.core.instrumentation.Instrumentation; import io.clientcore.core.instrumentation.InstrumentationOptions; @@ -15,13 +14,11 @@ import io.clientcore.core.instrumentation.tracing.SpanKind; import io.clientcore.core.instrumentation.tracing.Tracer; import org.junit.jupiter.api.Test; -import org.openjdk.jmh.annotations.Setup; import java.io.PrintStream; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -40,10 +37,9 @@ public void createTracer() { Tracer tracer = Instrumentation.create(null, DEFAULT_LIB_OPTIONS).getTracer(); assertTrue(tracer.isEnabled()); - Span span = tracer.spanBuilder("test-span", SpanKind.INTERNAL, RequestOptions.none()).startSpan(); + Span span = tracer.spanBuilder("test-span", SpanKind.INTERNAL, null).startSpan(); assertFalse(span.isRecording()); - assertInstanceOf(DefaultInstrumentation.DefaultSpan.class, span); } @Test @@ -54,7 +50,7 @@ public void createTracerTracingDisabled() { assertFalse(tracer.isEnabled()); // should not throw - Span span = tracer.spanBuilder("test-span", SpanKind.INTERNAL, RequestOptions.none()).startSpan(); + Span span = tracer.spanBuilder("test-span", SpanKind.INTERNAL, null).startSpan(); assertNotNull(span); assertFalse(span.isRecording()); diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultTracerTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultTracerTests.java deleted file mode 100644 index f93c445db2de9..0000000000000 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultTracerTests.java +++ /dev/null @@ -1,4 +0,0 @@ -package io.clientcore.core.implementation.instrumentation.tracing; - -public class DefaultTracerTests { -} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/util/HttpLoggingPolicyTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/HttpLoggingPolicyTests.java similarity index 99% rename from sdk/clientcore/core/src/test/java/io/clientcore/core/util/HttpLoggingPolicyTests.java rename to sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/HttpLoggingPolicyTests.java index 0a936e35a6f22..bbc8faf201a2e 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/util/HttpLoggingPolicyTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/HttpLoggingPolicyTests.java @@ -2,7 +2,7 @@ // Licensed under the MIT License. // we want to access package-private ClientLogger constructor -package io.clientcore.core.util; +package io.clientcore.core.instrumentation.logging; import io.clientcore.core.http.MockHttpResponse; import io.clientcore.core.http.models.HttpHeader; @@ -18,7 +18,7 @@ import io.clientcore.core.http.pipeline.HttpPipelineBuilder; import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; import io.clientcore.core.implementation.http.HttpRequestAccessHelper; -import io.clientcore.core.implementation.util.DefaultLogger; +import io.clientcore.core.implementation.instrumentation.DefaultLogger; import io.clientcore.core.serialization.json.JsonOptions; import io.clientcore.core.serialization.json.JsonProviders; import io.clientcore.core.serialization.json.JsonReader; diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java index f08b4c8411c22..41b8b11f4848d 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java @@ -9,9 +9,8 @@ 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.implementation.instrumentation.otel.tracing.OTelSpan; -import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext; 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.opentelemetry.api.OpenTelemetry; @@ -361,10 +360,9 @@ public void enrichSpans() throws IOException { HttpInstrumentationPolicy httpInstrumentationPolicy = new HttpInstrumentationPolicy(otelOptions, logOptions); HttpPipelinePolicy enrichingPolicy = (request, next) -> { - Object span = request.getRequestOptions().getContext().get(TRACE_CONTEXT_KEY); - if (span instanceof io.clientcore.core.instrumentation.tracing.Span) { - ((io.clientcore.core.instrumentation.tracing.Span) span).setAttribute("custom.request.id", - request.getHeaders().getValue(CUSTOM_REQUEST_ID)); + io.clientcore.core.instrumentation.tracing.Span span = request.getRequestOptions().getInstrumentationContext().getSpan(); + if (span.isRecording()) { + span.setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID)); } return next.process(); @@ -423,8 +421,8 @@ public void explicitParent() throws IOException { .httpClient(request -> new MockHttpResponse(request, 200)) .build(); - RequestOptions requestOptions = new RequestOptions().putContext(TRACE_CONTEXT_KEY, - io.opentelemetry.context.Context.current().with(testSpan)); + RequestOptions requestOptions = new RequestOptions(); + requestOptions.setInstrumentationContext(Instrumentation.createInstrumentationContext(testSpan)); pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost:8080/path/to/resource?query=param") .setRequestOptions(requestOptions)).close(); @@ -466,9 +464,9 @@ public void explicitLibraryCallParent() throws IOException { RequestOptions requestOptions = new RequestOptions(); io.clientcore.core.instrumentation.tracing.Span parent - = tracer.spanBuilder("parent", INTERNAL, requestOptions).startSpan(); + = tracer.spanBuilder("parent", INTERNAL, null).startSpan(); - requestOptions.putContext(TRACE_CONTEXT_KEY, parent); + requestOptions.setInstrumentationContext(parent.getInstrumentationContext()); HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) .httpClient(request -> new MockHttpResponse(request, 200)) @@ -485,7 +483,7 @@ public void explicitLibraryCallParent() throws IOException { SpanData httpSpan = exporter.getFinishedSpanItems().get(0); assertHttpSpan(httpSpan, HttpMethod.GET, "https://localhost:8080/path/to/resource?query=REDACTED", 200); - OTelSpanContext parentContext = ((OTelSpan) parent).getSpanContext(); + InstrumentationContext parentContext = parent.getInstrumentationContext(); assertEquals(parentContext.getSpanId(), httpSpan.getParentSpanContext().getSpanId()); assertEquals(parentContext.getTraceId(), httpSpan.getSpanContext().getTraceId()); } diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java index 982a861085b0c..846563a04176d 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java @@ -29,24 +29,24 @@ public class Slf4jLoggerShimIT { @BeforeEach public void setupLogLevels() { slf4jLoggerShimITLogLevel = System - .setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.util.Slf4jLoggerShimIT", "debug"); + .setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShimIT", "debug"); slf4jLoggerShimLogLevel - = System.setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.util.Slf4jLoggerShim", "info"); + = System.setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShim", "info"); } @AfterEach public void resetLogLevels() { if (slf4jLoggerShimITLogLevel == null) { - System.clearProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.util.Slf4jLoggerShimIT"); + System.clearProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShimIT"); } else { - System.setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.util.Slf4jLoggerShimIT", + System.setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShimIT", slf4jLoggerShimITLogLevel); } if (slf4jLoggerShimLogLevel == null) { - System.clearProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.util.Slf4jLoggerShim"); + System.clearProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShim"); } else { - System.setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.util.Slf4jLoggerShim", + System.setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShim", slf4jLoggerShimLogLevel); } } diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java index 4a61f5717dcf4..5188f76479b49 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java @@ -3,13 +3,10 @@ package io.clientcore.core.instrumentation; -import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpan; -import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext; 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.Tracer; -import io.clientcore.core.util.Context; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; @@ -28,11 +25,9 @@ import java.util.HashMap; import java.util.Map; -import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY; import static io.clientcore.core.instrumentation.tracing.SpanKind.INTERNAL; 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.assertTrue; @@ -82,7 +77,7 @@ public void testInject() { Span span = tracer.spanBuilder("test-span", INTERNAL, null).startSpan(); Map carrier = new HashMap<>(); - contextPropagator.inject(Context.of(TRACE_CONTEXT_KEY, span), carrier, Map::put); + contextPropagator.inject(span.getInstrumentationContext(), carrier, Map::put); assertEquals(getTraceparent(span), carrier.get("traceparent")); assertEquals(1, carrier.size()); @@ -94,7 +89,7 @@ public void testInjectReplaces() { Map carrier = new HashMap<>(); carrier.put("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); - contextPropagator.inject(Context.of(TRACE_CONTEXT_KEY, span), carrier, Map::put); + contextPropagator.inject(span.getInstrumentationContext(), carrier, Map::put); assertEquals(getTraceparent(span), carrier.get("traceparent")); assertEquals(1, carrier.size()); @@ -103,7 +98,7 @@ public void testInjectReplaces() { @Test public void testInjectNoContext() { Map carrier = new HashMap<>(); - contextPropagator.inject(Context.none(), carrier, Map::put); + contextPropagator.inject(null, carrier, Map::put); assertNull(carrier.get("traceparent")); assertNull(carrier.get("tracestate")); @@ -116,11 +111,8 @@ public void testInjectWithTracestate() { SpanContext otelSpanContext = SpanContext.create(IdGenerator.random().generateTraceId(), IdGenerator.random().generateSpanId(), TraceFlags.getSampled(), traceState); - io.opentelemetry.context.Context otelContext - = io.opentelemetry.context.Context.root().with(io.opentelemetry.api.trace.Span.wrap(otelSpanContext)); - Map carrier = new HashMap<>(); - contextPropagator.inject(Context.of(TRACE_CONTEXT_KEY, otelContext), carrier, Map::put); + contextPropagator.inject(Instrumentation.createInstrumentationContext(otelSpanContext), carrier, Map::put); assertEquals(getTraceparent(otelSpanContext), carrier.get("traceparent")); assertEquals("k2=v2,k1=v1", carrier.get("tracestate")); @@ -133,29 +125,19 @@ public void testExtract(boolean isSampled) { Map carrier = new HashMap<>(); carrier.put("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-" + (isSampled ? "01" : "00")); - Context updated = contextPropagator.extract(Context.none(), carrier, GETTER); + InstrumentationContext extracted = contextPropagator.extract(null, carrier, GETTER); - assertInstanceOf(io.opentelemetry.context.Context.class, updated.get(TRACE_CONTEXT_KEY)); - io.opentelemetry.context.Context otelContext - = (io.opentelemetry.context.Context) updated.get(TRACE_CONTEXT_KEY); - SpanContext extracted = io.opentelemetry.api.trace.Span.fromContext(otelContext).getSpanContext(); assertTrue(extracted.isValid()); assertEquals("0af7651916cd43dd8448eb211c80319c", extracted.getTraceId()); assertEquals("b7ad6b7169203331", extracted.getSpanId()); - assertEquals(isSampled, extracted.isSampled()); + assertEquals((isSampled ? "01" : "00"), extracted.getTraceFlags()); } @Test public void testExtractEmpty() { Map carrier = new HashMap<>(); - Context updated = contextPropagator.extract(Context.none(), carrier, GETTER); - - assertInstanceOf(io.opentelemetry.context.Context.class, updated.get(TRACE_CONTEXT_KEY)); - - io.opentelemetry.context.Context otelContext - = (io.opentelemetry.context.Context) updated.get(TRACE_CONTEXT_KEY); - SpanContext extracted = io.opentelemetry.api.trace.Span.fromContext(otelContext).getSpanContext(); + InstrumentationContext extracted = contextPropagator.extract(null, carrier, GETTER); assertFalse(extracted.isValid()); } @@ -164,18 +146,13 @@ public void testExtractInvalid() { Map carrier = new HashMap<>(); carrier.put("traceparent", "00-traceId-spanId-01"); - Context updated = contextPropagator.extract(Context.none(), carrier, GETTER); - - assertInstanceOf(io.opentelemetry.context.Context.class, updated.get(TRACE_CONTEXT_KEY)); - - io.opentelemetry.context.Context otelContext - = (io.opentelemetry.context.Context) updated.get(TRACE_CONTEXT_KEY); - assertFalse(io.opentelemetry.api.trace.Span.fromContext(otelContext).getSpanContext().isValid()); + InstrumentationContext extracted = contextPropagator.extract(null, carrier, GETTER); + assertFalse(extracted.isValid()); } @Test public void testExtractPreservesContext() { - Map carrier = new HashMap<>(); + /*Map carrier = new HashMap<>(); carrier.put("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); Context original = Context.of("key", "value"); @@ -185,15 +162,15 @@ public void testExtractPreservesContext() { = (io.opentelemetry.context.Context) updated.get(TRACE_CONTEXT_KEY); assertTrue(io.opentelemetry.api.trace.Span.fromContext(otelContext).getSpanContext().isValid()); - assertEquals("value", updated.get("key")); + assertEquals("value", updated.get("key"));*/ } private String getTraceparent(Span span) { - OTelSpanContext spanContext = ((OTelSpan) span).getSpanContext(); - return "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-01"; + InstrumentationContext spanContext = span.getInstrumentationContext(); + return "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-" + spanContext.getTraceFlags(); } private String getTraceparent(SpanContext spanContext) { - return "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-01"; + return "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-" + spanContext.getTraceFlags().asHex(); } } diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java index ad13df52bab81..62dbef3cb8330 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java @@ -10,8 +10,6 @@ import io.clientcore.core.http.models.Response; import io.clientcore.core.http.pipeline.HttpPipeline; import io.clientcore.core.http.pipeline.HttpPipelineBuilder; -import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpan; -import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext; import io.clientcore.core.instrumentation.tracing.Span; import io.clientcore.core.instrumentation.tracing.SpanKind; import io.clientcore.core.instrumentation.tracing.Tracer; @@ -33,7 +31,6 @@ import java.io.IOException; import java.util.stream.Stream; -import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY; import static io.clientcore.core.instrumentation.tracing.SpanKind.CLIENT; import static io.clientcore.core.instrumentation.tracing.SpanKind.INTERNAL; import static io.clientcore.core.instrumentation.tracing.SpanKind.PRODUCER; @@ -104,11 +101,11 @@ public void testDisabledSuppression() { .getTracer(); RequestOptions options = new RequestOptions(); - Span outerSpan = outerTracer.spanBuilder("outerSpan", CLIENT, options).startSpan(); + Span outerSpan = outerTracer.spanBuilder("outerSpan", CLIENT, options.getInstrumentationContext()).startSpan(); - options.putContext(TRACE_CONTEXT_KEY, outerSpan); + options.setInstrumentationContext(outerSpan.getInstrumentationContext()); - Span innerSpan = innerTracer.spanBuilder("innerSpan", CLIENT, options).startSpan(); + Span innerSpan = innerTracer.spanBuilder("innerSpan", CLIENT, options.getInstrumentationContext()).startSpan(); innerSpan.end(); outerSpan.end(); @@ -129,10 +126,10 @@ public void disabledSuppressionDoesNotAffectChildren() { Tracer innerTracer = tracer; RequestOptions options = new RequestOptions(); - Span outerSpan = outerTracer.spanBuilder("outerSpan", CLIENT, options).startSpan(); + Span outerSpan = outerTracer.spanBuilder("outerSpan", CLIENT, options.getInstrumentationContext()).startSpan(); - options.putContext(TRACE_CONTEXT_KEY, outerSpan); - Span innerSpan = innerTracer.spanBuilder("innerSpan", CLIENT, options).startSpan(); + options.setInstrumentationContext(outerSpan.getInstrumentationContext()); + Span innerSpan = innerTracer.spanBuilder("innerSpan", CLIENT, options.getInstrumentationContext()).startSpan(); innerSpan.end(); outerSpan.end(); @@ -162,13 +159,13 @@ public void multipleLayers() { RequestOptions options = new RequestOptions(); - Span outer = tracer.spanBuilder("outer", CLIENT, options).startSpan(); - options.putContext(TRACE_CONTEXT_KEY, outer); + Span outer = tracer.spanBuilder("outer", PRODUCER, options.getInstrumentationContext()).startSpan(); + options.setInstrumentationContext(outer.getInstrumentationContext()); - Span inner = tracer.spanBuilder("inner", PRODUCER, options).startSpan(); - options.putContext(TRACE_CONTEXT_KEY, inner); + Span inner = tracer.spanBuilder("inner", CLIENT, options.getInstrumentationContext()).startSpan(); + options.setInstrumentationContext(inner.getInstrumentationContext()); - Span suppressed = tracer.spanBuilder("suppressed", CLIENT, options).startSpan(); + Span suppressed = tracer.spanBuilder("suppressed", CLIENT, options.getInstrumentationContext()).startSpan(); suppressed.end(); inner.end(); outer.end(); @@ -189,12 +186,12 @@ public void multipleLayers() { public void testSuppressionExplicitContext(SpanKind outerKind, SpanKind innerKind, int expectedSpanCount) { RequestOptions options = new RequestOptions(); Span outerSpan - = tracer.spanBuilder("outerSpan", outerKind, options).setAttribute("key", "valueOuter").startSpan(); + = tracer.spanBuilder("outerSpan", outerKind, options.getInstrumentationContext()).setAttribute("key", "valueOuter").startSpan(); - options.putContext(TRACE_CONTEXT_KEY, outerSpan); + options.setInstrumentationContext(outerSpan.getInstrumentationContext()); Span innerSpan - = tracer.spanBuilder("innerSpan", innerKind, options).setAttribute("key", "valueInner").startSpan(); + = tracer.spanBuilder("innerSpan", innerKind, options.getInstrumentationContext()).setAttribute("key", "valueInner").startSpan(); // sanity check - this should not throw innerSpan.setAttribute("anotherKey", "anotherValue"); @@ -282,8 +279,8 @@ private static void assertIsParentOf(SpanData parent, SpanData child) { } private static void assertSpanContextEquals(Span first, Span second) { - OTelSpanContext firstContext = ((OTelSpan) first).getSpanContext(); - OTelSpanContext secondContext = ((OTelSpan) second).getSpanContext(); + InstrumentationContext firstContext = first.getInstrumentationContext(); + InstrumentationContext secondContext = second.getInstrumentationContext(); assertEquals(firstContext.getTraceId(), secondContext.getTraceId()); assertEquals(firstContext.getSpanId(), secondContext.getSpanId()); assertSame(firstContext.getTraceFlags(), secondContext.getTraceFlags()); @@ -315,9 +312,9 @@ static class SampleClient { @SuppressWarnings("try") public void protocolMethod(RequestOptions options) { - Span span = tracer.spanBuilder("protocolMethod", INTERNAL, options).startSpan(); + Span span = tracer.spanBuilder("protocolMethod", INTERNAL, options.getInstrumentationContext()).startSpan(); - options.putContext(TRACE_CONTEXT_KEY, span); + options.setInstrumentationContext(span.getInstrumentationContext()); try (TracingScope scope = span.makeCurrent()) { Response response = pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost")); @@ -333,9 +330,9 @@ public void protocolMethod(RequestOptions options) { @SuppressWarnings("try") public void convenienceMethod(RequestOptions options) { - Span span = tracer.spanBuilder("convenienceMethod", INTERNAL, options).startSpan(); + Span span = tracer.spanBuilder("convenienceMethod", INTERNAL, options.getInstrumentationContext()).startSpan(); - options.putContext(TRACE_CONTEXT_KEY, span); + options.setInstrumentationContext(span.getInstrumentationContext()); try (TracingScope scope = span.makeCurrent()) { protocolMethod(options); diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java index 0c229702c142b..f8b00eb60aec9 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java @@ -3,13 +3,14 @@ package io.clientcore.core.instrumentation; -import io.clientcore.core.http.models.RequestOptions; +import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext; import io.clientcore.core.instrumentation.tracing.Span; import io.clientcore.core.instrumentation.tracing.SpanKind; import io.clientcore.core.instrumentation.tracing.Tracer; import io.clientcore.core.instrumentation.tracing.TracingScope; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.Context; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.trace.SdkTracerProvider; @@ -25,10 +26,8 @@ import java.io.IOException; import java.util.stream.Stream; -import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY; import static io.clientcore.core.instrumentation.tracing.SpanKind.INTERNAL; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class TracerTests { @@ -176,9 +175,8 @@ public void explicitParent() throws Exception { io.opentelemetry.api.trace.Tracer otelTracer = otelOptions.getProvider().getTracer("test"); io.opentelemetry.api.trace.Span parent = otelTracer.spanBuilder("parent").startSpan(); - RequestOptions requestOptions = new RequestOptions().putContext(TRACE_CONTEXT_KEY, - parent.storeInContext(io.opentelemetry.context.Context.current())); - Span child = tracer.spanBuilder("child", INTERNAL, requestOptions).startSpan(); + Span child = tracer.spanBuilder("child", INTERNAL, OTelSpanContext.fromOTelContext(Context.root().with(parent))) + .startSpan(); child.end(); parent.end(); @@ -193,7 +191,7 @@ public void explicitParent() throws Exception { @Test public void explicitParentWrongType() { - RequestOptions requestOptions + /*RequestOptions requestOptions = new RequestOptions().putContext(TRACE_CONTEXT_KEY, "This is not a valid trace context"); Span child = tracer.spanBuilder("child", INTERNAL, requestOptions).startSpan(); child.end(); @@ -202,7 +200,7 @@ public void explicitParentWrongType() { SpanData childData = exporter.getFinishedSpanItems().get(0); assertEquals("child", childData.getName()); - assertFalse(childData.getParentSpanContext().isValid()); + assertFalse(childData.getParentSpanContext().isValid());*/ } public static Stream kindSource() { From 12a88936e903de6586cd768031ea662440c2bc3a Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Sat, 4 Jan 2025 20:29:47 -0800 Subject: [PATCH 04/11] almost ready --- sdk/clientcore/core/pom.xml | 1 + sdk/clientcore/core/spotbugs-exclude.xml | 22 +- .../pipeline/HttpInstrumentationPolicy.java | 57 +- .../http/pipeline/HttpRedirectPolicy.java | 36 +- .../core/http/pipeline/HttpRetryPolicy.java | 32 +- .../instrumentation/AttributeKeys.java | 37 +- .../DefaultInstrumentation.java | 325 --------- .../fallback/FallbackContextPropagator.java | 115 +++ .../fallback/FallbackInstrumentation.java | 67 ++ .../fallback/FallbackScope.java | 38 + .../fallback/FallbackSpan.java | 102 +++ .../fallback/FallbackSpanBuilder.java | 56 ++ .../fallback/FallbackSpanContext.java | 102 +++ .../fallback/FallbackTracer.java | 58 ++ .../fallback/RandomIdUtils.java | 90 +++ .../fallback/package-info.java | 8 + .../otel/OTelInstrumentation.java | 13 +- .../instrumentation/otel/package-info.java | 2 +- .../otel/tracing/OTelContext.java | 19 +- .../otel/tracing/OTelSpan.java | 76 +- .../otel/tracing/OTelSpanBuilder.java | 3 +- .../otel/tracing/OTelSpanContext.java | 71 +- .../tracing/OTelTraceContextPropagator.java | 8 +- .../otel/tracing/OTelTracer.java | 2 - .../core/instrumentation/Instrumentation.java | 47 +- .../InstrumentationContext.java | 44 ++ .../instrumentation/logging/ClientLogger.java | 1 + .../tracing/NoopInstrumentationContext.java | 5 +- .../instrumentation/tracing/NoopSpan.java | 7 +- .../core/instrumentation/tracing/Span.java | 5 + .../tracing/TraceContextPropagator.java | 1 - .../core/instrumentation/tracing/Tracer.java | 10 +- .../TelemetryJavaDocCodeSnippets.java | 196 ++++++ ...rLibraryDevelopersJavaDocCodeSnippets.java | 11 +- ...tpInstrumentationPolicyFallbackTests.java} | 2 +- ...HttpInstrumentationPolicyLoggingTests.java | 664 ++++++++++++++++++ .../FallbackInstrumentationTests.java | 568 +++++++++++++++ .../fallback/FallbackTracingBenchmarks.java | 113 +++ .../tracing/DefaultInstrumentationTests.java | 74 -- .../logging/ClientLoggerTests.java | 35 + .../logging/InstrumentationTestUtils.java | 116 +++ .../optional-dependency-tests/pom.xml | 6 + .../HttpInstrumentationPolicyTests.java | 4 +- .../instrumentation/Slf4jLoggerShimIT.java | 14 +- .../ContextPropagationTests.java | 9 +- .../instrumentation/InstrumentationTests.java | 114 +++ .../instrumentation/SuppressionTests.java | 13 +- .../core/instrumentation/TracerTests.java | 4 +- 48 files changed, 2803 insertions(+), 600 deletions(-) delete mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultInstrumentation.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackContextPropagator.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentation.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackScope.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpan.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanBuilder.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanContext.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracer.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/RandomIdUtils.java create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/package-info.java create mode 100644 sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java rename sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/{HttpInstrumentationPolicyDefaultTests.java => HttpInstrumentationPolicyFallbackTests.java} (98%) create mode 100644 sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java create mode 100644 sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java create mode 100644 sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracingBenchmarks.java delete mode 100644 sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java create mode 100644 sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/InstrumentationTestUtils.java diff --git a/sdk/clientcore/core/pom.xml b/sdk/clientcore/core/pom.xml index 25d191c6d96aa..a145023a8871f 100644 --- a/sdk/clientcore/core/pom.xml +++ b/sdk/clientcore/core/pom.xml @@ -64,6 +64,7 @@ --add-exports io.clientcore.core/io.clientcore.core.shared=ALL-UNNAMED --add-exports io.clientcore.core/io.clientcore.core.implementation=ALL-UNNAMED + --add-exports io.clientcore.core/io.clientcore.core.implementation.instrumentation.fallback=ALL-UNNAMED diff --git a/sdk/clientcore/core/spotbugs-exclude.xml b/sdk/clientcore/core/spotbugs-exclude.xml index 6d3d22b6e5d55..2661e212a909c 100644 --- a/sdk/clientcore/core/spotbugs-exclude.xml +++ b/sdk/clientcore/core/spotbugs-exclude.xml @@ -122,8 +122,8 @@ - - + + @@ -168,7 +168,7 @@ - +
@@ -238,13 +238,14 @@ + - + @@ -260,6 +261,7 @@ + @@ -400,4 +402,16 @@ + + + + + + + + + + + + diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java index 7871fd4e8bb7d..29a3b3f6404ec 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java @@ -13,7 +13,6 @@ import io.clientcore.core.http.models.Response; import io.clientcore.core.implementation.http.HttpRequestAccessHelper; import io.clientcore.core.implementation.instrumentation.LibraryInstrumentationOptionsAccessHelper; -import io.clientcore.core.implementation.util.LoggingKeys; import io.clientcore.core.instrumentation.Instrumentation; import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.instrumentation.LibraryInstrumentationOptions; @@ -39,7 +38,6 @@ import java.util.stream.Collectors; import java.net.URI; -import static io.clientcore.core.http.models.HttpHeaderName.TRACEPARENT; import static io.clientcore.core.implementation.UrlRedactionUtil.getRedactedUri; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_SIZE_KEY; @@ -109,9 +107,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();
@@ -131,7 +131,6 @@
  */
 public final class HttpInstrumentationPolicy implements HttpPipelinePolicy {
     private static final ClientLogger LOGGER = new ClientLogger(HttpInstrumentationPolicy.class);
-    private static final Set ALWAYS_ALLOWED_HEADERS = Set.of(TRACEPARENT);
     private static final HttpLogOptions DEFAULT_HTTP_LOG_OPTIONS = new HttpLogOptions();
     private static final String LIBRARY_NAME;
     private static final String LIBRARY_VERSION;
@@ -210,14 +209,17 @@ public Response process(HttpRequest request, HttpPipelineNextPolicy next) {
         int tryCount = HttpRequestAccessHelper.getTryCount(request);
         final long requestContentLength = getContentLength(logger, request.getBody(), request.getHeaders());
 
+        InstrumentationContext context
+            = request.getRequestOptions() == null ? null : request.getRequestOptions().getInstrumentationContext();
         Span span = Span.noop();
         if (isTracingEnabled(request)) {
-            span = startHttpSpan(request, redactedUrl);
-            request.getRequestOptions().setInstrumentationContext(span.getInstrumentationContext());
+            span = startHttpSpan(request, redactedUrl, context);
+            context = span.getInstrumentationContext();
+            request.getRequestOptions().setInstrumentationContext(context);
             traceContextPropagator.inject(span.getInstrumentationContext(), request.getHeaders(), SETTER);
         }
 
-        logRequest(logger, request, startNs, requestContentLength, redactedUrl, tryCount, span.getInstrumentationContext());
+        logRequest(logger, request, startNs, requestContentLength, redactedUrl, tryCount, context);
 
         try (TracingScope scope = span.makeCurrent()) {
             Response response = next.process();
@@ -227,34 +229,33 @@ public Response process(HttpRequest request, HttpPipelineNextPolicy next) {
                     .setContext(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.");
+                    .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, span.getInstrumentationContext());
+            response = logResponse(logger, response, startNs, requestContentLength, redactedUrl, tryCount, context);
             span.end();
             return response;
         } catch (RuntimeException t) {
-            var ex = logException(logger, request, null, t, startNs, null, requestContentLength, redactedUrl, tryCount, span.getInstrumentationContext());
+            var ex = logException(logger, request, null, t, startNs, null, requestContentLength, redactedUrl, tryCount,
+                context);
             span.end(unwrap(t));
             throw ex;
         }
     }
 
-    private Span startHttpSpan(HttpRequest request, String sanitizedUrl) {
+    private Span startHttpSpan(HttpRequest request, String sanitizedUrl, InstrumentationContext context) {
         if (request.getRequestOptions() == null || request.getRequestOptions() == RequestOptions.none()) {
             request.setRequestOptions(new RequestOptions());
         }
 
-        InstrumentationContext context = request.getRequestOptions().getInstrumentationContext();
-
-        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, request.getUri().getHost());
+        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, request.getUri().getHost());
         maybeSetServerPort(spanBuilder, request.getUri());
         return spanBuilder.startSpan();
     }
@@ -337,7 +338,7 @@ private static Throwable unwrap(Throwable t) {
     private ClientLogger getLogger(HttpRequest httpRequest) {
         ClientLogger logger = null;
 
-        if (httpRequest.getRequestOptions() != null) {
+        if (httpRequest.getRequestOptions() != null && httpRequest.getRequestOptions().getLogger() != null) {
             logger = httpRequest.getRequestOptions().getLogger();
         }
 
@@ -370,8 +371,7 @@ private void logRequest(ClientLogger logger, HttpRequest request, long startNano
             return;
         }
 
-        logBuilder
-            .setEventName(HTTP_REQUEST_EVENT_NAME)
+        logBuilder.setEventName(HTTP_REQUEST_EVENT_NAME)
             .setContext(context)
             .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod())
             .addKeyValue(URL_FULL_KEY, redactedUrl)
@@ -503,13 +503,6 @@ private void addHeadersToLogMessage(HttpHeaders headers, ClientLogger.LoggingEve
                 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);
-                }
-            }
         }
     }
 
@@ -543,7 +536,7 @@ private static long getContentLength(ClientLogger logger, BinaryData body, HttpH
 
         try {
             contentLength = Long.parseLong(contentLengthString);
-        } catch (NumberFormatException | NullPointerException e) {
+        } catch (NumberFormatException e) {
             logger.atVerbose()
                 .addKeyValue("contentLength", contentLengthString)
                 .log("Could not parse the HTTP header content-length", e);
@@ -559,7 +552,7 @@ private static final class LoggingHttpResponse extends HttpResponse {
         private BinaryData bufferedBody;
 
         private LoggingHttpResponse(Response actualResponse, Consumer onContent,
-                                    Consumer onException) {
+            Consumer onException) {
             super(actualResponse.getRequest(), actualResponse.getStatusCode(), actualResponse.getHeaders(),
                 actualResponse.getValue());
 
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 8fb3120d2f7d1..c729cdf14c34e 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
@@ -9,8 +9,8 @@
 import io.clientcore.core.http.models.HttpRequest;
 import io.clientcore.core.http.models.Response;
 import io.clientcore.core.implementation.instrumentation.AttributeKeys;
+import io.clientcore.core.instrumentation.InstrumentationContext;
 import io.clientcore.core.instrumentation.logging.ClientLogger;
-import io.clientcore.core.util.Context;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -78,7 +78,10 @@ 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<>(), httpRequest.getRequestOptions().getContext());
+        InstrumentationContext instrumentationContext = httpRequest.getRequestOptions() == null
+            ? null
+            : httpRequest.getRequestOptions().getInstrumentationContext();
+        return attemptRedirect(next, 1, new LinkedHashSet<>(), instrumentationContext);
     }
 
     /**
@@ -86,7 +89,7 @@ public Response process(HttpRequest httpRequest, HttpPipelineNextPolicy next)
      * new redirect URI.
      */
     private Response attemptRedirect(final HttpPipelineNextPolicy next, final int redirectAttempt,
-        LinkedHashSet attemptedRedirectUris, Context context) {
+        LinkedHashSet attemptedRedirectUris, InstrumentationContext instrumentationContext) {
 
         // Make sure the context is not modified during redirect, except for the URI
         Response response = next.clone().process();
@@ -94,26 +97,27 @@ private Response attemptRedirect(final HttpPipelineNextPolicy next, final int
         HttpRequestRedirectCondition requestRedirectCondition
             = new HttpRequestRedirectCondition(response, redirectAttempt, attemptedRedirectUris);
         if ((shouldRedirectCondition != null && shouldRedirectCondition.test(requestRedirectCondition))
-            || (shouldRedirectCondition == null && defaultShouldAttemptRedirect(requestRedirectCondition, context))) {
+            || (shouldRedirectCondition == null
+                && defaultShouldAttemptRedirect(requestRedirectCondition, instrumentationContext))) {
             createRedirectRequest(response);
-            return attemptRedirect(next, redirectAttempt + 1, attemptedRedirectUris, context);
+            return attemptRedirect(next, redirectAttempt + 1, attemptedRedirectUris, instrumentationContext);
         }
 
         return response;
     }
 
     private boolean defaultShouldAttemptRedirect(HttpRequestRedirectCondition requestRedirectCondition,
-        Context context) {
+        InstrumentationContext instrumentationContext) {
         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, context)
-            && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), context)
+            && isValidRedirectCount(tryCount, instrumentationContext)
+            && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), instrumentationContext)
             && redirectUri != null
-            && !alreadyAttemptedRedirectUri(redirectUri, attemptedRedirectUris, context)) {
+            && !alreadyAttemptedRedirectUri(redirectUri, attemptedRedirectUris, instrumentationContext)) {
 
             LOGGER.atVerbose()
                 .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount - 1)
@@ -121,7 +125,7 @@ && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), context)
                 .addKeyValue("url.full.to", redirectUri)
                 .addKeyValue("url.full.all", attemptedRedirectUris::toString)
                 .setEventName("http.request.redirect")
-                //.setContext(context)
+                .setContext(instrumentationContext)
                 .log();
 
             attemptedRedirectUris.add(redirectUri);
@@ -139,11 +143,11 @@ && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), context)
      *
      * @return {@code true} if the {@code tryCount} is greater than the {@code maxAttempts}, {@code false} otherwise.
      */
-    private boolean isValidRedirectCount(int tryCount, Context context) {
+    private boolean isValidRedirectCount(int tryCount, InstrumentationContext instrumentationContext) {
         if (tryCount >= this.maxAttempts) {
             LOGGER.atError()
                 .addKeyValue("maxAttempts", this.maxAttempts)
-                //.setContext(context)
+                .setContext(instrumentationContext)
                 .log("Redirect attempts have been exhausted.");
 
             return false;
@@ -162,11 +166,11 @@ private boolean isValidRedirectCount(int tryCount, Context context) {
      * {@code false} otherwise.
      */
     private boolean alreadyAttemptedRedirectUri(String redirectUri, Set attemptedRedirectUris,
-        Context context) {
+        InstrumentationContext instrumentationContext) {
         if (attemptedRedirectUris.contains(redirectUri)) {
             LOGGER.atError()
                 .addKeyValue(URL_FULL_KEY, redirectUri)
-                //.setContext(context)
+                .setContext(instrumentationContext)
                 .log("Request was redirected more than once to the same URI.");
 
             return true;
@@ -182,12 +186,12 @@ private boolean alreadyAttemptedRedirectUri(String redirectUri, Set atte
      *
      * @return {@code true} if the request {@code httpMethod} is a valid http redirect method, {@code false} otherwise.
      */
-    private boolean isAllowedRedirectMethod(HttpMethod httpMethod, Context context) {
+    private boolean isAllowedRedirectMethod(HttpMethod httpMethod, InstrumentationContext instrumentationContext) {
         if (allowedRedirectHttpMethods.contains(httpMethod)) {
             return true;
         } else {
             LOGGER.atError()
-                //.setContext(context)
+                .setContext(instrumentationContext)
                 .addKeyValue(AttributeKeys.HTTP_REQUEST_METHOD_KEY, httpMethod)
                 .log("Request redirection is not enabled for this HTTP method.");
 
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 3ac4c51d580ff..ad99af27de65d 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,6 +9,7 @@
 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.instrumentation.InstrumentationContext;
 import io.clientcore.core.instrumentation.logging.ClientLogger;
 import io.clientcore.core.util.configuration.Configuration;
 
@@ -142,13 +143,17 @@ 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;
 
         try {
             response = next.clone().process();
         } catch (RuntimeException err) {
             if (shouldRetryException(err, tryCount, suppressed)) {
-                logRetryWithError(LOGGER.atVerbose(), tryCount, "Error resume.", err);
+                logRetryWithError(LOGGER.atVerbose(), tryCount, "Error resume.", err, instrumentationContext);
 
                 boolean interrupted = false;
                 long millis = calculateRetryDelay(tryCount).toMillis();
@@ -171,7 +176,8 @@ 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);
+                logRetryWithError(LOGGER.atError(), tryCount, "Retry attempts have been exhausted.", err,
+                    instrumentationContext);
 
                 if (suppressed != null) {
                     suppressed.forEach(err::addSuppressed);
@@ -184,7 +190,7 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex
         if (shouldRetryResponse(response, tryCount, suppressed)) {
             final Duration delayDuration = determineDelayDuration(response, tryCount, delayFromHeaders);
 
-            logRetry(tryCount, delayDuration);
+            logRetry(tryCount, delayDuration, instrumentationContext);
 
             try {
                 response.close();
@@ -204,7 +210,7 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex
             return attempt(httpRequest, next, tryCount + 1, suppressed);
         } else {
             if (tryCount >= maxRetries) {
-                logRetryExhausted(tryCount);
+                logRetryExhausted(tryCount, instrumentationContext);
             }
 
             return response;
@@ -270,20 +276,26 @@ private boolean shouldRetryException(Exception exception, int tryCount, List instrumentationOptions;
-    private final LibraryInstrumentationOptions libraryOptions;
-
-    /**
-     * Creates a new instance of {@link DefaultInstrumentation}.
-     * @param instrumentationOptions the application instrumentation options
-     * @param libraryOptions the library instrumentation options
-     */
-    public DefaultInstrumentation(InstrumentationOptions instrumentationOptions,
-        LibraryInstrumentationOptions libraryOptions) {
-        this.instrumentationOptions = instrumentationOptions;
-        this.libraryOptions = libraryOptions;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Tracer getTracer() {
-        return new DefaultTracer(instrumentationOptions, libraryOptions);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public TraceContextPropagator getW3CTraceContextPropagator() {
-        return DefaultContextPropagator.W3C_TRACE_CONTEXT_PROPAGATOR;
-    }
-
-    public  InstrumentationContext createInstrumentationContext(T context) {
-        if (context instanceof DefaultSpanContext) {
-            return (DefaultSpanContext) context;
-        } else if (context instanceof DefaultSpan) {
-            return ((DefaultSpan) context).spanContext;
-        } else {
-            return DefaultSpanContext.INVALID;
-        }
-    }
-
-    static final class DefaultTracer implements Tracer {
-        private final boolean isEnabled;
-        private final ClientLogger logger;
-
-        DefaultTracer(InstrumentationOptions instrumentationOptions, LibraryInstrumentationOptions libraryOptions) {
-            this.isEnabled = instrumentationOptions == null || instrumentationOptions.isTracingEnabled(); // TODO: probably need additional config for log-based tracing
-
-            Object providedLogger = instrumentationOptions == null ? null : instrumentationOptions.getProvider();
-            if (providedLogger instanceof ClientLogger) {
-                this.logger = (ClientLogger) providedLogger;
-            } else {
-                Map libraryContext = new HashMap<>(2);
-                libraryContext.put("library.version", libraryOptions.getLibraryVersion());
-                libraryContext.put("library.instrumentation.schema_url", libraryOptions.getSchemaUrl());
-
-                this.logger = new ClientLogger(libraryOptions.getLibraryName() + ".tracing", libraryContext);
-            }
-        }
-
-        @Override
-        public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, InstrumentationContext instrumentationContext) {
-            return new DefaultSpanBuilder(this.logger, spanName, spanKind, instrumentationContext);
-        }
-
-        @Override
-        public boolean isEnabled() {
-            return isEnabled;
-        }
-    }
-
-    private static final class DefaultSpanBuilder implements SpanBuilder {
-        private final ClientLogger.LoggingEvent log;
-        private final boolean isRecording;
-        private final DefaultSpanContext parentSpanContext;
-
-        DefaultSpanBuilder(ClientLogger logger, String spanName, SpanKind spanKind, InstrumentationContext instrumentationContext) {
-            isRecording = logger.canLogAtLevel(ClientLogger.LogLevel.INFORMATIONAL);
-            DefaultSpanContext parentSpanContext = instrumentationContext instanceof DefaultSpanContext
-                ? (DefaultSpanContext) instrumentationContext : DefaultSpanContext.INVALID;
-            this.parentSpanContext = parentSpanContext;
-            this.log = logger.atInfo()
-                .addKeyValue("span.parent.id", parentSpanContext.getSpanId())
-                .addKeyValue("span.name", spanName)
-                .addKeyValue("span.kind", spanKind.name());
-        }
-
-        @Override
-        public SpanBuilder setAttribute(String key, Object value) {
-            this.log.addKeyValue(key, value);
-            return this;
-        }
-
-        @Override
-        public Span startSpan() {
-            return new DefaultSpan(log, parentSpanContext, isRecording);
-        }
-    }
-
-    private static final class DefaultSpan implements Span {
-        private final ClientLogger.LoggingEvent log;
-        private final long startTime;
-        private final boolean isRecording;
-        private final DefaultSpanContext spanContext;
-        private String errorType;
-
-        DefaultSpan(ClientLogger.LoggingEvent log, DefaultSpanContext parentSpanContext, boolean isRecording) {
-            this.log = log;
-            this.startTime = System.nanoTime();
-            this.isRecording = isRecording;
-            this.spanContext = DefaultSpanContext.create(parentSpanContext, isRecording, this);
-            if (log != null) {
-                this.log
-                    .addKeyValue("trace.id", spanContext.getTraceId())
-                    .addKeyValue("span.id", spanContext.getSpanId());
-            }
-        }
-
-        DefaultSpan(DefaultSpanContext parentSpanContext) {
-            this(null, parentSpanContext, false);
-        }
-
-        @Override
-        public Span setAttribute(String key, Object value) {
-            if (log != null) {
-                log.addKeyValue(key, value);
-            }
-            return this;
-        }
-
-        @Override
-        public Span setError(String errorType) {
-            this.errorType = errorType;
-            return this;
-        }
-
-        @Override
-        public void end() {
-            end(null);
-        }
-
-        @Override
-        public void end(Throwable error) {
-            if (log == null) {
-                return;
-            }
-
-            if (isRecording) {
-                double durationMs = (System.nanoTime() - startTime) / 1_000_000.0;
-                log.addKeyValue("span.duration.ms", durationMs);
-                if (error != null || errorType != null) {
-                    setAttribute("error.type", errorType != null ? errorType : error.getClass().getCanonicalName());
-                }
-            }
-
-            if (error != null) {
-                log.log("span ended", error);
-            } else {
-                log.log("span ended");
-            }
-        }
-
-        @Override
-        public boolean isRecording() {
-            return isRecording;
-        }
-
-        @Override
-        public TracingScope makeCurrent() {
-            return new DefaultScope(this);
-        }
-
-        @Override
-        public InstrumentationContext getInstrumentationContext() {
-            return spanContext;
-        }
-    }
-
-    private static final class DefaultScope implements TracingScope {
-        private static final ThreadLocal CURRENT_SPAN = new ThreadLocal<>();
-        private final DefaultSpan originalSpan;
-
-        DefaultScope(DefaultSpan span) {
-            this.originalSpan = CURRENT_SPAN.get();
-            CURRENT_SPAN.set(span);
-        }
-
-        @Override
-        public void close() {
-            CURRENT_SPAN.set(originalSpan);
-        }
-    }
-
-    private static final class DefaultContextPropagator implements TraceContextPropagator {
-        static final TraceContextPropagator W3C_TRACE_CONTEXT_PROPAGATOR = new DefaultContextPropagator();
-
-        private DefaultContextPropagator() {
-        }
-
-        @Override
-        public  void inject(InstrumentationContext spanContext, C carrier, TraceContextSetter setter) {
-            if (spanContext.isValid()) {
-                setter.set(carrier, "traceparent", "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId()
-                    + "-" + spanContext.getTraceFlags());
-            }
-        }
-
-        @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 DefaultSpanContext(traceId, spanId, traceFlags, Span.noop());
-                } else {
-                    // TODO log
-                }
-            }
-            return context;
-        }
-
-        private static boolean isValidTraceparent(String traceparent) {
-            // TODO: add more validation
-            return traceparent.startsWith("00-") && traceparent.length() == 55;
-        }
-    }
-
-    private static final class DefaultSpanContext implements InstrumentationContext {
-        static final DefaultSpanContext INVALID = new DefaultSpanContext();
-        private final String traceId;
-        private final String spanId;
-        private final String traceFlags;
-        private final boolean isValid;
-        private final Span span;
-
-        @Override
-        public String getTraceId() {
-            return traceId;
-        }
-
-        @Override
-        public String getSpanId() {
-            return spanId;
-        }
-
-        @Override
-        public boolean isValid() {
-            return isValid;
-        }
-
-        @Override
-        public Span getSpan() {
-            return this.span;
-        }
-
-        @Override
-        public String getTraceFlags() {
-            return traceFlags;
-        }
-
-        DefaultSpanContext() {
-            this.traceId = INVALID_TRACE_ID;
-            this.spanId = INVALID_SPAN_ID;
-            this.traceFlags = "00";
-            this.isValid = false;
-            this.span = Span.noop();
-        }
-
-        DefaultSpanContext(String traceId, String spanId, String traceFlags, Span span) {
-            this.traceId = traceId;
-            this.spanId = spanId;
-            this.traceFlags = traceFlags;
-            this.isValid = true;
-            this.span = span;
-        }
-
-        static DefaultSpanContext create(DefaultSpanContext parent, boolean isSampled, DefaultSpan span) {
-            return parent.isValid()
-                ? new DefaultSpanContext(parent.traceId, getRandomId(16), isSampled ? "01" : "00", span)
-                : new DefaultSpanContext(getRandomId(32), getRandomId(16), isSampled ? "01" : "00", span);
-        }
-
-        /**
-         * Generates random id with given length up to 32 chars.
-         */
-        private static String getRandomId(int length) {
-            // TODO: copy impl from OTel
-            UUID uuid = UUID.randomUUID();
-            return uuid.toString().replace("-", "").substring(32 - length);
-        }
-    }
-}
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 0000000000000..6eb9e3de3a4d1
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackContextPropagator.java
@@ -0,0 +1,115 @@
+// 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;
+
+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("traceparent", traceparent).log("Invalid traceparent header");
+            }
+        }
+        return context == null ? FallbackSpanContext.INVALID : context;
+    }
+
+    private static boolean isValidTraceparent(String traceparent) {
+        if (traceparent == null || traceparent.length() != 55) {
+            return false;
+        }
+
+        // version
+        for (int i = 0; i < 2; i++) {
+            if (traceparent.charAt(i) != '0') {
+                return false;
+            }
+        }
+
+        if (traceparent.charAt(2) != '-') {
+            return false;
+        }
+
+        // trace-id
+        boolean isAllZero = true;
+        for (int i = 3; i < 35; i++) {
+            char c = traceparent.charAt(i);
+            if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
+                return false;
+            }
+            if (c != '0') {
+                isAllZero = false;
+            }
+        }
+        if (isAllZero) {
+            return false;
+        }
+
+        if (traceparent.charAt(35) != '-') {
+            return false;
+        }
+
+        // span-id
+        isAllZero = true;
+        for (int i = 36; i < 52; i++) {
+            char c = traceparent.charAt(i);
+            if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
+                return false;
+            }
+            if (c != '0') {
+                isAllZero = false;
+            }
+        }
+
+        if (isAllZero) {
+            return false;
+        }
+
+        if (traceparent.charAt(52) != '-') {
+            return false;
+        }
+
+        // trace-flags
+        for (int i = 53; i < 55; i++) {
+            char c = traceparent.charAt(i);
+            if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
+                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 0000000000000..109398c9efbd0
--- /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 0000000000000..008b79ce6599c
--- /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 0000000000000..b3d0a5c1fbd0e
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpan.java
@@ -0,0 +1,102 @@
+// 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;
+
+final class FallbackSpan implements Span {
+    private static final String SPAN_END_EVENT = "span.ended";
+
+    private final ClientLogger.LoggingEventBuilder log;
+    private final long startTime;
+    private final FallbackSpanContext spanContext;
+    private String errorType;
+
+    FallbackSpan(ClientLogger.LoggingEventBuilder 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", spanContext.getTraceId()).addKeyValue("span.id", 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.ms", durationMs);
+        if (error != null || errorType != null) {
+            setAttribute("error.type", errorType != null ? errorType : error.getClass().getCanonicalName());
+        }
+
+        log.setEventName(SPAN_END_EVENT);
+        if (error != null) {
+            log.log(null, error);
+        } else {
+            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 0000000000000..e48d6899ca220
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanBuilder.java
@@ -0,0 +1,56 @@
+// 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;
+
+final class FallbackSpanBuilder implements SpanBuilder {
+    static final FallbackSpanBuilder NOOP = new FallbackSpanBuilder();
+    private final ClientLogger.LoggingEventBuilder 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.atInfo();
+        if (log.isEnabled()) {
+            log.addKeyValue("span.name", spanName).addKeyValue("span.kind", spanKind.name());
+            if (parentSpanContext.isValid()) {
+                log.addKeyValue("span.parent.id", 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 0000000000000..50a2999c573ce
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanContext.java
@@ -0,0 +1,102 @@
+// 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();
+    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;
+    }
+
+    private FallbackSpanContext() {
+        this.traceId = INVALID_TRACE_ID;
+        this.spanId = INVALID_SPAN_ID;
+        this.traceFlags = "00";
+        this.isValid = false;
+        this.span = Span.noop();
+    }
+
+    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 0000000000000..28b9341d720b5
--- /dev/null
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracer.java
@@ -0,0 +1,58 @@
+// 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.version", libraryOptions.getLibraryVersion());
+        libraryContext.put("library.instrumentation.schema_url", libraryOptions.getSchemaUrl());
+
+        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 0000000000000..c5390e2f60fad
--- /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();
+    private static final String ALPHABET = "0123456789abcdef";
+
+    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() {
+        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 0000000000000..5dd2110ab7d73
--- /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/OTelInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInstrumentation.java
index 28922d87f9ca1..38783b3bf6bc4 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
@@ -29,7 +29,6 @@
  * A {@link Instrumentation} implementation that uses OpenTelemetry.
  */
 public class OTelInstrumentation implements Instrumentation {
-    public static final OTelInstrumentation DEFAULT_INSTANCE = new OTelInstrumentation(null, null);
     private static final FallbackInvoker GET_PROVIDER_INVOKER;
     private static final FallbackInvoker GET_GLOBAL_OTEL_INVOKER;
 
@@ -67,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;
@@ -118,9 +118,20 @@ 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)) {
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 ead18dbf6d525..d3c6f6ccd74e2 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 a648c065f8344..bfe298ea4f134 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
@@ -16,7 +16,6 @@
 
 class OTelContext {
     private static final ClientLogger LOGGER = new ClientLogger(OTelContext.class);
-    private static final Object ROOT_CONTEXT;
     private static final TracingScope NOOP_SCOPE = () -> {
     };
     private static final FallbackInvoker CURRENT_INVOKER;
@@ -24,9 +23,7 @@ class OTelContext {
     private static final FallbackInvoker WITH_INVOKER;
     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
@@ -39,7 +36,6 @@ class OTelContext {
         ReflectiveInvoker withInvoker = null;
         ReflectiveInvoker getInvoker = null;
 
-        Object hasClientSpanContextKey = null;
         Object clientCoreSpanContextKey = null;
         Object rootContext = null;
 
@@ -54,13 +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);
             }
@@ -70,9 +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;
-        ROOT_CONTEXT = rootContext;
     }
 
     static Object getCurrent() {
@@ -81,10 +73,6 @@ static Object getCurrent() {
         return currentContext;
     }
 
-    static Object getRoot() {
-        return ROOT_CONTEXT;
-    }
-
     static AutoCloseable makeCurrent(Object context) {
         assert CONTEXT_CLASS.isInstance(context);
         Object scope = MAKE_CURRENT_INVOKER.invoke(context);
@@ -112,8 +100,11 @@ static OTelSpan getClientCoreSpan(Object context) {
      * @return the OpenTelemetry context
      */
     static Object fromInstrumentationContext(InstrumentationContext context) {
-        if (context instanceof OTelSpanContext && ((OTelSpanContext) context).getOtelContext() != null) {
-            return ((OTelSpanContext) context).getOtelContext();
+        if (context instanceof OTelSpanContext) {
+            Object otelContext = ((OTelSpanContext) context).getOtelContext();
+            if (otelContext != null) {
+                return otelContext;
+            }
         }
 
         Object currentContext = CURRENT_INVOKER.invoke();
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 97c83a475be88..77e27e86cce95 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
@@ -23,7 +23,6 @@
 import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.STATUS_CODE_CLASS;
 import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelContext.markCoreSpan;
 import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext.INVALID_OTEL_SPAN_CONTEXT;
-import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext.fromOTelSpan;
 
 /**
  * OpenTelemetry implementation of {@link Span}.
@@ -31,6 +30,7 @@
 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;
@@ -41,7 +41,6 @@ public class OTelSpan implements Span {
     private static final FallbackInvoker STORE_IN_CONTEXT_INVOKER;
     private static final FallbackInvoker FROM_CONTEXT_INVOKER;
     private static final FallbackInvoker WRAP_INVOKER;
-    private static final FallbackInvoker CURRENT_INVOKER;
     private static final Object ERROR_STATUS_CODE;
     private final Object otelSpan;
     private final Object otelContext;
@@ -59,10 +58,10 @@ public class OTelSpan implements Span {
         ReflectiveInvoker storeInContextInvoker = null;
         ReflectiveInvoker fromContextInvoker = null;
         ReflectiveInvoker wrapInvoker = null;
-        ReflectiveInvoker currentInvoker = null;
 
         Object errorStatusCode = null;
         OTelSpan noopSpan = null;
+        Object errorTypeAttributeKey = null;
 
         if (OTelInitializer.isInitialized()) {
             try {
@@ -84,13 +83,13 @@ public class OTelSpan implements Span {
                 wrapInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("wrap", SPAN_CONTEXT_CLASS));
                 errorStatusCode = STATUS_CODE_CLASS.getField("ERROR").get(null);
 
-                currentInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("current"));
                 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", "");
             } catch (Throwable t) {
                 OTelInitializer.initError(LOGGER, t);
             }
@@ -104,10 +103,10 @@ 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);
-        CURRENT_INVOKER = new FallbackInvoker(currentInvoker, LOGGER);
         NOOP_SPAN = noopSpan;
 
         ERROR_STATUS_CODE = errorStatusCode;
+        ERROR_TYPE_ATTRIBUTE_KEY = errorTypeAttributeKey;
     }
 
     OTelSpan(Object otelSpan, Object otelParentContext, SpanKind spanKind) {
@@ -181,6 +180,34 @@ 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);
 
@@ -192,12 +219,7 @@ static Object createPropagatingSpan(Object otelContext) {
         return wrapSpanContext(spanContext);
     }
 
-    static OTelSpan createPropagatingSpan(OTelSpanContext spanContext) {
-        Object span = wrapSpanContext(spanContext.getOtelContext());
-        return new OTelSpan(span, spanContext.getOtelContext(), SpanKind.INTERNAL);
-    }
-
-    public static Object fromOTelContext(Object otelContext) {
+    static Object fromOTelContext(Object otelContext) {
         assert CONTEXT_CLASS.isInstance(otelContext);
 
         Object span = FROM_CONTEXT_INVOKER.invoke(otelContext);
@@ -224,32 +246,19 @@ static Object getSpanContext(Object otelSpan) {
         return spanContext;
     }
 
-    Object getOtelSpan() {
-        return otelSpan;
-    }
-
-    SpanKind getSpanKind() {
-        return spanKind;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    public InstrumentationContext getInstrumentationContext() {
-        if (spanContext != null) {
-            return spanContext;
-        }
-
-        spanContext = isInitialized() ? new OTelSpanContext(GET_SPAN_CONTEXT_INVOKER.invoke(otelSpan), otelContext)
-            : OTelSpanContext.getInvalid();
+    static Object storeInContext(Object otelSpan, Object otelContext) {
+        Object updatedContext = STORE_IN_CONTEXT_INVOKER.invoke(otelSpan, otelContext);
 
-        return spanContext;
+        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());
             }
@@ -268,11 +277,6 @@ private static TracingScope wrapOTelScope(AutoCloseable otelScope) {
         };
     }
 
-    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 c74a9745b24a6..0dcf0e53e8a5e 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
@@ -134,8 +134,7 @@ public Span startSpan() {
     }
 
     private boolean shouldSuppress(Object parentContext) {
-        if (suppressNestedSpans
-            && (this.spanKind == SpanKind.CLIENT || this.spanKind == SpanKind.INTERNAL)) {
+        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);
         }
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 388c9ca2ff71e..83370ede4105f 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
@@ -18,7 +18,7 @@
 /**
  * Wrapper around OpenTelemetry SpanContext.
  */
-public class OTelSpanContext implements InstrumentationContext  {
+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";
@@ -58,7 +58,8 @@ public class OTelSpanContext implements InstrumentationContext  {
 
                 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));
+                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);
             }
@@ -73,36 +74,49 @@ public class OTelSpanContext implements InstrumentationContext  {
         CREATE_INVOKER = new FallbackInvoker(createInvoker, INVALID_OTEL_SPAN_CONTEXT, LOGGER);
     }
 
+    /**
+     * 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) {
-        assert otelSpanContext == null || SPAN_CONTEXT_CLASS.isInstance(otelSpanContext);
-        assert otelContext == null || OTelInitializer.CONTEXT_CLASS.isInstance(otelContext);
-
         this.otelSpanContext = otelSpanContext;
         this.otelContext = otelContext;
     }
 
-    static Object toOTelSpanContext(InstrumentationContext context) {
-        if (context instanceof OTelSpanContext) {
-            return ((OTelSpanContext) context).otelSpanContext;
-        }
-
-        return CREATE_INVOKER.invoke(context.getTraceId(), context.getTraceFlags(), null);
-    }
-
+    /**
+     * 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) {
-            otelContext = OTelContext.getCurrent();
+            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);
-        return new OTelSpanContext(otelSpanContext, null); // TODO -this is  not correct
+        Object otelContext = OTelSpan.storeInContext(otelSpan, OTelContext.getCurrent());
+
+        return new OTelSpanContext(otelSpanContext, otelContext);
     }
 
+    /**
+     * Returns an invalid instance of {@link OTelSpanContext}.
+     * @return the instance of {@link OTelSpanContext}
+     */
     public static OTelSpanContext getInvalid() {
         return INVALID;
     }
@@ -165,15 +179,40 @@ public boolean isValid() {
         return isValid;
     }
 
+    /**
+     * {@inheritDoc}
+     */
     @Override
     public Span getSpan() {
-        return isInitialized() && otelContext != null ? OTelContext.getClientCoreSpan(otelContext) : OTelSpan.createPropagatingSpan(this);
+        if (isInitialized()) {
+            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 CREATE_INVOKER.invoke(context.getTraceId(), context.getTraceFlags(), null);
+    }
+
     private boolean isInitialized() {
         return otelSpanContext != null && OTelInitializer.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 505ddbc8c481f..c4d328545879d 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
@@ -11,7 +11,6 @@
 import io.clientcore.core.instrumentation.tracing.TraceContextPropagator;
 import io.clientcore.core.instrumentation.tracing.TraceContextSetter;
 import io.clientcore.core.instrumentation.logging.ClientLogger;
-import io.clientcore.core.util.Context;
 
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
@@ -70,7 +69,8 @@ public OTelTraceContextPropagator(Object otelPropagator) {
     @Override
     public  void inject(InstrumentationContext context, C carrier, TraceContextSetter setter) {
         if (isInitialized()) {
-            INJECT_INVOKER.invoke(otelPropagator, OTelContext.fromInstrumentationContext(context), carrier, Setter.toOTelSetter(setter));
+            INJECT_INVOKER.invoke(otelPropagator, OTelContext.fromInstrumentationContext(context), carrier,
+                Setter.toOTelSetter(setter));
         }
     }
 
@@ -80,8 +80,8 @@ public  void inject(InstrumentationContext context, C carrier, TraceContextSe
     @Override
     public  InstrumentationContext extract(InstrumentationContext context, C carrier, TraceContextGetter getter) {
         if (isInitialized()) {
-            Object updatedContext
-                = EXTRACT_INVOKER.invoke(otelPropagator, OTelContext.fromInstrumentationContext(context), carrier, Getter.toOTelGetter(getter));
+            Object updatedContext = EXTRACT_INVOKER.invoke(otelPropagator,
+                OTelContext.fromInstrumentationContext(context), carrier, Getter.toOTelGetter(getter));
             if (updatedContext != null) {
                 return OTelSpanContext.fromOTelContext(updatedContext);
             }
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 a3d9a88ae5dc7..cecaba6521758 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,7 +3,6 @@
 
 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;
@@ -13,7 +12,6 @@
 import io.clientcore.core.instrumentation.tracing.SpanKind;
 import io.clientcore.core.instrumentation.tracing.Tracer;
 import io.clientcore.core.instrumentation.logging.ClientLogger;
-import io.clientcore.core.util.Context;
 
 import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker;
 import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACER_BUILDER_CLASS;
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 45556807bd425..2e3c0505a2799 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,21 +3,16 @@
 
 package io.clientcore.core.instrumentation;
 
-import io.clientcore.core.implementation.instrumentation.DefaultInstrumentation;
+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.Span;
 import io.clientcore.core.instrumentation.tracing.TraceContextPropagator;
 import io.clientcore.core.instrumentation.tracing.Tracer;
-import io.clientcore.core.util.Context;
 
 import java.util.Objects;
 
 /**
  * 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 { /** @@ -26,12 +21,6 @@ public interface Instrumentation { */ 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. *

@@ -61,6 +50,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(); @@ -68,6 +60,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. @@ -78,15 +73,39 @@ static Instrumentation create(InstrumentationOptions applicationOptions, if (OTelInitializer.isInitialized()) { return new OTelInstrumentation(applicationOptions, libraryOptions); } else { - return new DefaultInstrumentation(applicationOptions, libraryOptions); + return new FallbackInstrumentation(applicationOptions, libraryOptions); } } + /** + * Retrieves the instrumentation context from the given context. The type of the context is determined by the + * instrumentation implementation. + *

+ * For 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);
+     *
+     * 
+ * + * + * @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 DefaultInstrumentation.DEFAULT_INSTANCE.createInstrumentationContext(context); + 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 index 56b425e5c9e61..173b3d7110e5b 100644 --- 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 @@ -1,11 +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/logging/ClientLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java index c27405b3e3836..cba07c4046ebc 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java @@ -522,6 +522,7 @@ private String getMessageWithContext(String message) { message = ""; } + // TODO (limolkova) set context from implicit current span if (this.context != null && this.context.isValid()) { addKeyValue("trace.id", context.getTraceId()); addKeyValue("span.id", context.getSpanId()); 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 index 3c3ff6f7d0fbd..98938a193094f 100644 --- 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 @@ -1,8 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package io.clientcore.core.instrumentation.tracing; import io.clientcore.core.instrumentation.InstrumentationContext; -public class NoopInstrumentationContext implements InstrumentationContext { +final class NoopInstrumentationContext implements InstrumentationContext { public static final NoopInstrumentationContext INSTANCE = new NoopInstrumentationContext(); private NoopInstrumentationContext() { 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 index 9c07770f56a70..8ec976b911b72 100644 --- 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 @@ -1,11 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package io.clientcore.core.instrumentation.tracing; import io.clientcore.core.instrumentation.InstrumentationContext; -class NoopSpan implements Span { +final class NoopSpan implements Span { static final NoopSpan INSTANCE = new NoopSpan(); + private NoopSpan() { } + private static final TracingScope NOOP_SCOPE = () -> { }; 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 c3ef8607f7122..29c587b8ed107 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 @@ -79,6 +79,11 @@ public interface Span { */ TracingScope makeCurrent(); + /** + * Gets the instrumentation context that is used to correlate telemetry data. + * + * @return The instrumentation context. + */ InstrumentationContext getInstrumentationContext(); /** 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 7189b8b9a5a84..6eb49d4fab2f8 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 @@ -4,7 +4,6 @@ package io.clientcore.core.instrumentation.tracing; import io.clientcore.core.instrumentation.InstrumentationContext; -import io.clientcore.core.util.Context; /** * A {@link TraceContextPropagator} injects and extracts tracing context from a carrier, diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Tracer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Tracer.java index 5c98c0df5b03f..577dd3dc3aac3 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Tracer.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Tracer.java @@ -3,7 +3,6 @@ package io.clientcore.core.instrumentation.tracing; -import io.clientcore.core.http.models.RequestOptions; import io.clientcore.core.instrumentation.InstrumentationContext; /** @@ -21,13 +20,16 @@ public interface Tracer { * *

      *
-     * 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()) {
@@ -48,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")
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 0000000000000..11779e0bbf1ba
--- /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 5471609b93fbc..5d294a02fa726 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 @@ -16,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. *

@@ -63,7 +61,10 @@ public void traceCall() { // 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()) { @@ -156,7 +157,9 @@ public void enrichInstrumentationPolicySpans() { // BEGIN: io.clientcore.core.telemetry.tracing.enrichhttpspans HttpPipelinePolicy enrichingPolicy = (request, next) -> { - Span span = Span.noop();//Instrumentation.fromContext(request.getRequestOptions().getContext()); + 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)); } diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyDefaultTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyFallbackTests.java similarity index 98% rename from sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyDefaultTests.java rename to sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyFallbackTests.java index 754b60b9fc463..fe124fecee6a6 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyDefaultTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyFallbackTests.java @@ -24,7 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -public class HttpInstrumentationPolicyDefaultTests { +public class HttpInstrumentationPolicyFallbackTests { private static final InstrumentationOptions OPTIONS = new InstrumentationOptions<>(); private static final InstrumentationOptions DISABLED_TRACING_OPTIONS = new InstrumentationOptions<>().setTracingEnabled(false); diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java new file mode 100644 index 0000000000000..c5f88367a163e --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java @@ -0,0 +1,664 @@ +// 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.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.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.createInstrumentationContext; +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 HttpInstrumentationPolicyLoggingTests { + 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 HttpInstrumentationPolicyLoggingTests() { + 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> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + assertRequestLog(logMessages.get(0), expectedUri, request); + assertEquals(8, logMessages.get(0).size()); + + assertResponseLog(logMessages.get(1), expectedUri, response); + assertEquals(12, logMessages.get(1).size()); + } + + @Test + public void testHttpLoggingTracingDisabled() throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + InstrumentationOptions instrumentationOptions = new InstrumentationOptions<>().setTracingEnabled(false); + HttpLogOptions logOptions = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC); + + HttpPipeline pipeline = createPipeline(instrumentationOptions, logOptions); + + HttpRequest request = createRequest(HttpMethod.GET, URI, logger); + Response response = pipeline.send(request); + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + assertRequestLog(logMessages.get(0), REDACTED_URI, request); + assertEquals(6, logMessages.get(0).size()); + + assertResponseLog(logMessages.get(1), REDACTED_URI, response); + assertEquals(10, logMessages.get(1).size()); + } + + @Test + public void testHttpLoggingTracingDisabledCustomContext() throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + InstrumentationOptions instrumentationOptions = new InstrumentationOptions<>().setTracingEnabled(false); + HttpLogOptions logOptions = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC); + + HttpPipeline pipeline = createPipeline(instrumentationOptions, logOptions); + + HttpRequest request = createRequest(HttpMethod.GET, URI, logger); + request.setRequestOptions(new RequestOptions().setLogger(logger) + .setInstrumentationContext( + createInstrumentationContext("1234567890abcdef1234567890abcdef", "1234567890abcdef"))); + + Response response = pipeline.send(request); + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + assertRequestLog(logMessages.get(0), REDACTED_URI, request); + assertEquals(8, logMessages.get(0).size()); + + assertResponseLog(logMessages.get(1), REDACTED_URI, response); + assertEquals(12, logMessages.get(1).size()); + } + + @Test + public void testTryCount() throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC); + + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options); + + HttpRequest request = createRequest(HttpMethod.GET, URI, logger); + HttpRequestAccessHelper.setRetryCount(request, 42); + Response response = pipeline.send(request); + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + assertEquals(42, logMessages.get(0).get("http.request.resend_count")); + assertEquals(42, logMessages.get(1).get("http.request.resend_count")); + } + + @ParameterizedTest + @MethodSource("testExceptionSeverity") + public void testConnectionException(ClientLogger.LogLevel level, boolean expectExceptionLog) { + ClientLogger logger = setupLogLevelAndGetLogger(level, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS); + + RuntimeException expectedException = new RuntimeException("socket error"); + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options, request -> { + throw expectedException; + }); + + HttpRequest request = createRequest(HttpMethod.GET, URI, logger); + + assertThrows(RuntimeException.class, () -> pipeline.send(request)); + + List> logMessages = parseLogMessages(logCaptureStream); + if (!expectExceptionLog) { + assertEquals(0, logMessages.size()); + } else { + assertExceptionLog(logMessages.get(0), REDACTED_URI, request, expectedException); + } + } + + @ParameterizedTest + @MethodSource("testExceptionSeverity") + public void testRequestBodyException(ClientLogger.LogLevel level, boolean expectExceptionLog) { + ClientLogger logger = setupLogLevelAndGetLogger(level, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); + + TestStream requestStream = new TestStream(1024, new IOException("socket error")); + BinaryData requestBody = BinaryData.fromStream(requestStream, 1024L); + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options); + + HttpRequest request = createRequest(HttpMethod.POST, URI, logger); + request.setBody(requestBody); + + Exception actualException = assertThrows(RuntimeException.class, () -> pipeline.send(request)); + + List> logMessages = parseLogMessages(logCaptureStream); + if (!expectExceptionLog) { + assertEquals(0, logMessages.size()); + } else { + assertExceptionLog(logMessages.get(0), REDACTED_URI, request, actualException); + } + } + + @ParameterizedTest + @MethodSource("testExceptionSeverity") + public void testResponseBodyException(ClientLogger.LogLevel level, boolean expectExceptionLog) { + ClientLogger logger = setupLogLevelAndGetLogger(level, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); + + TestStream responseStream = new TestStream(1024, new IOException("socket error")); + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options, + request -> new MockHttpResponse(request, 200, BinaryData.fromStream(responseStream, 1024L))); + + HttpRequest request = createRequest(HttpMethod.GET, URI, logger); + + Response response = pipeline.send(request); + Exception actualException = assertThrows(RuntimeException.class, () -> response.getBody().toString()); + + List> logMessages = parseLogMessages(logCaptureStream); + if (!expectExceptionLog) { + assertEquals(0, logMessages.size()); + } else { + assertResponseAndExceptionLog(logMessages.get(0), REDACTED_URI, response, actualException); + } + } + + @Test + public void testResponseBodyLoggingOnClose() throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); + + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options, + request -> new MockHttpResponse(request, 200, BinaryData.fromString("Response body"))); + + HttpRequest request = createRequest(HttpMethod.GET, URI, logger); + + Response response = pipeline.send(request); + assertEquals(0, parseLogMessages(logCaptureStream).size()); + + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertResponseLog(logMessages.get(0), REDACTED_URI, response); + } + + @Test + public void testResponseBodyRequestedMultipleTimes() { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); + + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options, + request -> new MockHttpResponse(request, 200, BinaryData.fromString("Response body"))); + + HttpRequest request = createRequest(HttpMethod.GET, URI, logger); + + Response response = pipeline.send(request); + + for (int i = 0; i < 3; i++) { + BinaryData data = response.getBody(); + assertEquals(1, parseLogMessages(logCaptureStream).size()); + assertEquals("Response body", data.toString()); + } + } + + @ParameterizedTest + @MethodSource("allowQueryParamSource") + public void testBasicHttpLoggingRequestOff(Set allowedParams, String expectedUri) throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC) + .setAllowedQueryParamNames(allowedParams); + + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options); + + HttpRequest request = createRequest(HttpMethod.POST, URI, logger); + Response response = pipeline.send(request); + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(1, logMessages.size()); + + assertResponseLog(logMessages.get(0), expectedUri, response); + assertEquals(12, logMessages.get(0).size()); + } + + @ParameterizedTest + @MethodSource("allowedHeaders") + public void testHeadersHttpLogging(Set allowedHeaders) throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS) + .setAllowedHeaderNames(allowedHeaders); + + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options); + + HttpRequest request = createRequest(HttpMethod.GET, URI, logger); + request.getHeaders().set(CUSTOM_REQUEST_ID, "12345"); + Response response = pipeline.send(request); + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + Map requestLog = logMessages.get(0); + assertRequestLog(requestLog, REDACTED_URI, request); + for (HttpHeader header : request.getHeaders()) { + if (allowedHeaders.contains(header.getName())) { + assertEquals(header.getValue(), requestLog.get(header.getName().toString())); + } else { + assertEquals("REDACTED", requestLog.get(header.getName().toString())); + } + } + + Map responseLog = logMessages.get(1); + assertResponseLog(responseLog, REDACTED_URI, response); + for (HttpHeader header : response.getHeaders()) { + if (allowedHeaders.contains(header.getName())) { + assertEquals(header.getValue(), responseLog.get(header.getName().toString())); + } else { + assertEquals("REDACTED", responseLog.get(header.getName().toString())); + } + } + } + + @Test + public void testStringBodyLogging() throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); + + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options, + request -> new MockHttpResponse(request, 200, BinaryData.fromString("Response body"))); + + HttpRequest request = createRequest(HttpMethod.PUT, URI, logger); + request.setBody(BinaryData.fromString("Request body")); + + Response response = pipeline.send(request); + response.close(); + + assertEquals("Response body", response.getBody().toString()); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + Map requestLog = logMessages.get(0); + assertRequestLog(requestLog, REDACTED_URI, request); + assertEquals("Request body", requestLog.get("http.request.body")); + + Map responseLog = logMessages.get(1); + assertResponseLog(responseLog, REDACTED_URI, response); + assertEquals("Response body", responseLog.get("http.request.body")); + } + + @Test + public void testStreamBodyLogging() { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); + + BinaryData responseBody = BinaryData.fromString("Response body"); + TestStream responseStream = new TestStream(responseBody); + + HttpPipeline pipeline + = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options, request -> new MockHttpResponse(request, 200, + BinaryData.fromStream(responseStream, responseBody.getLength()))); + + BinaryData requestBody = BinaryData.fromString("Request body"); + TestStream requestStream = new TestStream(requestBody); + HttpRequest request = createRequest(HttpMethod.PUT, URI, logger); + request.setBody(BinaryData.fromStream(requestStream, requestBody.getLength())); + assertFalse(request.getBody().isReplayable()); + + Response response = pipeline.send(request); + assertTrue(request.getBody().isReplayable()); + assertTrue(response.getBody().isReplayable()); + + assertEquals("Response body", response.getBody().toString()); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + Map requestLog = logMessages.get(0); + assertRequestLog(requestLog, REDACTED_URI, request); + assertEquals("Request body", requestLog.get("http.request.body")); + + Map responseLog = logMessages.get(1); + assertResponseLog(responseLog, REDACTED_URI, response); + assertEquals("Response body", responseLog.get("http.request.body")); + + assertEquals(requestBody.getLength(), requestStream.getPosition()); + assertEquals(responseBody.getLength(), responseStream.getPosition()); + } + + @Test + public void testHugeBodyNotLogged() throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); + + TestStream requestStream = new TestStream(1024 * 1024); + TestStream responseStream = new TestStream(1024 * 1024); + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options, + request -> new MockHttpResponse(request, 200, BinaryData.fromStream(responseStream, (long) 1024 * 1024))); + + HttpRequest request = createRequest(HttpMethod.PUT, URI, logger); + + request.setBody(BinaryData.fromStream(requestStream, 1024 * 1024L)); + + Response response = pipeline.send(request); + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + Map requestLog = logMessages.get(0); + assertRequestLog(requestLog, REDACTED_URI, request); + assertNull(requestLog.get("http.request.body")); + assertEquals(0, requestStream.getPosition()); + + Map responseLog = logMessages.get(1); + assertResponseLog(responseLog, REDACTED_URI, response); + assertNull(responseLog.get("http.request.body")); + assertEquals(0, responseStream.getPosition()); + } + + @Test + public void testBodyWithUnknownLengthNotLogged() throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); + + TestStream requestStream = new TestStream(1024); + TestStream responseStream = new TestStream(1024); + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options, + request -> new MockHttpResponse(request, 200, BinaryData.fromStream(responseStream))); + + HttpRequest request = createRequest(HttpMethod.PUT, URI, logger); + request.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, "1024"); + + request.setBody(BinaryData.fromStream(requestStream)); + + Response response = pipeline.send(request); + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + Map requestLog = logMessages.get(0); + assertRequestLog(requestLog, REDACTED_URI, request); + assertNull(requestLog.get("http.request.body")); + assertEquals(0, requestStream.getPosition()); + + Map responseLog = logMessages.get(1); + assertResponseLog(responseLog, REDACTED_URI, response); + assertNull(responseLog.get("http.request.body")); + assertEquals(0, responseStream.getPosition()); + } + + public static Stream allowQueryParamSource() { + Set twoParams = new HashSet<>(); + twoParams.add("param"); + twoParams.add("api-version"); + + return Stream.of(Arguments.of(twoParams, "https://example.com?param=value&api-version=42"), + Arguments.of(DEFAULT_ALLOWED_QUERY_PARAMS, REDACTED_URI), + Arguments.of(Collections.emptySet(), "https://example.com?param=REDACTED&api-version=REDACTED")); + } + + public static Stream> allowedHeaders() { + Set reducedSet = new HashSet<>(); + reducedSet.add(CUSTOM_REQUEST_ID); + + Set expandedSet = new HashSet<>(DEFAULT_ALLOWED_HEADERS); + expandedSet.add(CUSTOM_REQUEST_ID); + + return Stream.of(reducedSet, DEFAULT_ALLOWED_HEADERS, expandedSet); + } + + public static Stream testExceptionSeverity() { + return Stream.of(Arguments.of(ClientLogger.LogLevel.INFORMATIONAL, true), + Arguments.of(ClientLogger.LogLevel.WARNING, true), Arguments.of(ClientLogger.LogLevel.ERROR, false)); + } + + private static class TestStream extends InputStream { + private final byte[] content; + private int length; + private final IOException throwOnRead; + private int position = 0; + + TestStream(int length) { + this.length = length; + this.throwOnRead = null; + this.content = new byte[length]; + } + + TestStream(BinaryData content) { + this.length = content.getLength().intValue(); + this.throwOnRead = null; + this.content = content.toBytes(); + } + + TestStream(int length, IOException throwOnRead) { + this.length = length; + this.throwOnRead = throwOnRead; + this.content = new byte[length]; + } + + @Override + public int read() throws IOException { + if (throwOnRead != null) { + throw throwOnRead; + } + + if (position >= length) { + return -1; + } + + position++; + return content[position - 1]; + } + + public long getPosition() { + return position; + } + } + + private void assertRequestLog(Map log, String expectedUri, HttpRequest request) { + assertEquals("http.request", log.get("event.name")); + assertEquals(expectedUri, log.get("url.full")); + assertEquals(0, (int) log.get("http.request.resend_count")); + + assertEquals(getLength(request.getBody(), request.getHeaders()), (int) log.get("http.request.body.size")); + assertEquals(request.getHttpMethod().toString(), log.get("http.request.method")); + assertEquals("", log.get("message")); + + assertTraceContext(log, request); + } + + private void assertTraceContext(Map log, HttpRequest request) { + InstrumentationContext context = request.getRequestOptions().getInstrumentationContext(); + if (context != null) { + assertTrue(log.get("trace.id").toString().matches("[0-9a-f]{32}")); + assertTrue(log.get("span.id").toString().matches("[0-9a-f]{16}")); + + assertEquals(context.getTraceId(), log.get("trace.id")); + assertEquals(context.getSpanId(), log.get("span.id")); + } else { + assertNull(log.get("trace.id")); + assertNull(log.get("span.id")); + } + } + + private long getLength(BinaryData body, HttpHeaders headers) { + if (body != null && body.getLength() != null) { + return body.getLength(); + } + + String contentLength = headers.getValue(HttpHeaderName.CONTENT_LENGTH); + if (contentLength != null) { + return Long.parseLong(contentLength); + } + + return 0; + } + + private void assertResponseLog(Map log, String expectedUri, Response response) { + assertEquals("http.response", log.get("event.name")); + assertEquals(expectedUri, log.get("url.full")); + assertEquals(0, (int) log.get("http.request.resend_count")); + + Long expectedRequestLength = getLength(response.getRequest().getBody(), response.getRequest().getHeaders()); + + assertEquals(expectedRequestLength, (int) log.get("http.request.body.size")); + assertEquals(response.getRequest().getHttpMethod().toString(), log.get("http.request.method")); + + assertEquals(response.getStatusCode(), log.get("http.response.status_code")); + + Long expectedResponseLength = getLength(response.getBody(), response.getHeaders()); + assertEquals(expectedResponseLength, (int) log.get("http.response.body.size")); + assertInstanceOf(Double.class, log.get("http.request.time_to_response")); + assertInstanceOf(Double.class, log.get("http.request.duration")); + assertEquals("", log.get("message")); + assertTraceContext(log, response.getRequest()); + } + + private void assertResponseAndExceptionLog(Map log, String expectedUri, Response response, + Throwable error) { + assertEquals("http.response", log.get("event.name")); + assertEquals(expectedUri, log.get("url.full")); + assertEquals(0, (int) log.get("http.request.resend_count")); + + Long expectedRequestLength = getLength(response.getRequest().getBody(), response.getRequest().getHeaders()); + + assertEquals(expectedRequestLength, (int) log.get("http.request.body.size")); + assertEquals(response.getRequest().getHttpMethod().toString(), log.get("http.request.method")); + + assertEquals(response.getStatusCode(), log.get("http.response.status_code")); + + assertInstanceOf(Double.class, log.get("http.request.time_to_response")); + assertInstanceOf(Double.class, log.get("http.request.duration")); + assertEquals(error.getMessage(), log.get("exception.message")); + assertEquals(error.getClass().getCanonicalName(), log.get("exception.type")); + assertEquals("", log.get("message")); + assertTraceContext(log, response.getRequest()); + } + + private void assertExceptionLog(Map log, String expectedUri, HttpRequest request, Throwable error) { + assertEquals("http.response", log.get("event.name")); + assertEquals(expectedUri, log.get("url.full")); + assertEquals(0, (int) log.get("http.request.resend_count")); + + Long expectedRequestLength = getLength(request.getBody(), request.getHeaders()); + assertEquals(expectedRequestLength, (int) log.get("http.request.body.size")); + assertEquals(request.getHttpMethod().toString(), log.get("http.request.method")); + + assertNull(log.get("http.response.status_code")); + assertNull(log.get("http.response.body.size")); + assertNull(log.get("http.request.time_to_response")); + assertInstanceOf(Double.class, log.get("http.request.duration")); + assertEquals(error.getMessage(), log.get("exception.message")); + assertEquals(error.getClass().getCanonicalName(), log.get("exception.type")); + + assertEquals("", log.get("message")); + assertTraceContext(log, request); + } + + private HttpPipeline createPipeline(InstrumentationOptions instrumentationOptions, HttpLogOptions options) { + return createPipeline(instrumentationOptions, options, request -> { + if (request.getBody() != null) { + request.getBody().toString(); + } + return new MockHttpResponse(request, 200, BinaryData.fromString("Hello, world!")); + }); + } + + private HttpPipeline createPipeline(InstrumentationOptions instrumentationOptions, HttpLogOptions options, + Function> httpClient) { + return new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(instrumentationOptions, options)) + .httpClient(httpClient::apply) + .build(); + } + + private HttpRequest createRequest(HttpMethod method, String url, ClientLogger logger) { + HttpRequest request = new HttpRequest(method, url); + request.getHeaders().set(HttpHeaderName.CONTENT_TYPE, "application/json"); + request.getHeaders().set(HttpHeaderName.AUTHORIZATION, "Bearer {token}"); + request.setRequestOptions(new RequestOptions().setLogger(logger)); + + return request; + } +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java new file mode 100644 index 0000000000000..3ea6b2f8a4d86 --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java @@ -0,0 +1,568 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.fallback; + +import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; +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.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.Tracer; +import io.clientcore.core.instrumentation.tracing.TracingScope; +import io.clientcore.core.util.Context; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.assertValidSpanId; +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.assertValidTraceId; +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 io.clientcore.core.instrumentation.tracing.SpanKind.CLIENT; +import static io.clientcore.core.instrumentation.tracing.SpanKind.CONSUMER; +import static io.clientcore.core.instrumentation.tracing.SpanKind.INTERNAL; +import static io.clientcore.core.instrumentation.tracing.SpanKind.PRODUCER; +import static io.clientcore.core.instrumentation.tracing.SpanKind.SERVER; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FallbackInstrumentationTests { + private static final LibraryInstrumentationOptions DEFAULT_LIB_OPTIONS + = new LibraryInstrumentationOptions("test-library"); + private static final Instrumentation DEFAULT_INSTRUMENTATION = Instrumentation.create(null, DEFAULT_LIB_OPTIONS); + private final AccessibleByteArrayOutputStream logCaptureStream; + + private static final TraceContextGetter> GETTER = new TraceContextGetter<>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + public FallbackInstrumentationTests() { + logCaptureStream = new AccessibleByteArrayOutputStream(); + } + + @Test + public void basicTracing() { + Tracer tracer = DEFAULT_INSTRUMENTATION.getTracer(); + assertTrue(tracer.isEnabled()); + + Span span = tracer.spanBuilder("test-span", INTERNAL, null).startSpan(); + + assertValidSpan(span, false); + + testContextInjection(span.getInstrumentationContext(), DEFAULT_INSTRUMENTATION.getW3CTraceContextPropagator()); + + span.end(); + assertEquals(0, parseLogMessages(logCaptureStream).size()); + } + + @Test + public void basicTracingExplicitParentSpan() { + Tracer tracer = DEFAULT_INSTRUMENTATION.getTracer(); + + Span parent = tracer.spanBuilder("parent", INTERNAL, null).startSpan(); + Span child = tracer.spanBuilder("child", INTERNAL, parent.getInstrumentationContext()).startSpan(); + + assertValidSpan(child, false); + assertEquals(parent.getInstrumentationContext().getTraceId(), child.getInstrumentationContext().getTraceId()); + + testContextInjection(child.getInstrumentationContext(), DEFAULT_INSTRUMENTATION.getW3CTraceContextPropagator()); + + child.end(); + parent.end(); + assertEquals(0, parseLogMessages(logCaptureStream).size()); + } + + @Test + @SuppressWarnings("try") + public void basicTracingImplicitParentSpan() { + Tracer tracer = DEFAULT_INSTRUMENTATION.getTracer(); + + assertSame(Span.noop(), FallbackScope.getCurrentSpan()); + Span parent = tracer.spanBuilder("parent", INTERNAL, null).startSpan(); + try (TracingScope scope = parent.makeCurrent()) { + Span child = tracer.spanBuilder("child", CLIENT, null).startSpan(); + + assertValidSpan(child, false); + assertEquals(parent.getInstrumentationContext().getTraceId(), + child.getInstrumentationContext().getTraceId()); + assertSame(parent, FallbackScope.getCurrentSpan()); + assertNotSame(Span.noop(), FallbackScope.getCurrentSpan()); + child.end(); + } + parent.end(); + assertSame(Span.noop(), FallbackScope.getCurrentSpan()); + assertEquals(0, parseLogMessages(logCaptureStream).size()); + } + + @Test + @SuppressWarnings("try") + public void basicTracingExplicitAndImplicitParentSpan() { + Tracer tracer = DEFAULT_INSTRUMENTATION.getTracer(); + + Span span = tracer.spanBuilder("span", INTERNAL, null).startSpan(); + try (TracingScope scope = span.makeCurrent()) { + InstrumentationContext parentContext = createRandomInstrumentationContext(); + + Span child = tracer.spanBuilder("child", CLIENT, parentContext).startSpan(); + assertValidSpan(child, false); + try (TracingScope childScope = child.makeCurrent()) { + assertSame(child, FallbackScope.getCurrentSpan()); + } + assertSame(span, FallbackScope.getCurrentSpan()); + + assertEquals(parentContext.getTraceId(), child.getInstrumentationContext().getTraceId()); + assertNotEquals(span.getInstrumentationContext().getTraceId(), + child.getInstrumentationContext().getTraceId()); + + child.end(); + } + span.end(); + + assertSame(Span.noop(), FallbackScope.getCurrentSpan()); + assertEquals(0, parseLogMessages(logCaptureStream).size()); + } + + @Test + @SuppressWarnings("try") + public void tracingImplicitParentSpan() { + Tracer tracer = DEFAULT_INSTRUMENTATION.getTracer(); + + Span parent = tracer.spanBuilder("parent", INTERNAL, null).startSpan(); + try (TracingScope scope = parent.makeCurrent()) { + Span child1 = tracer.spanBuilder("child1", CLIENT, null).startSpan(); + try (TracingScope childScope1 = child1.makeCurrent()) { + assertSame(child1, FallbackScope.getCurrentSpan()); + } + + assertSame(parent, FallbackScope.getCurrentSpan()); + try (TracingScope childScope1 = child1.makeCurrent()) { + assertSame(child1, FallbackScope.getCurrentSpan()); + } + + Span child2 = tracer.spanBuilder("child2", CLIENT, null).startSpan(); + try (TracingScope childScope2 = child2.makeCurrent()) { + assertSame(child2, FallbackScope.getCurrentSpan()); + Span grandChild = tracer.spanBuilder("grandChild", CLIENT, null).startSpan(); + try (TracingScope grandChildScope = grandChild.makeCurrent()) { + assertSame(grandChild, FallbackScope.getCurrentSpan()); + } + assertSame(child2, FallbackScope.getCurrentSpan()); + } + assertSame(parent, FallbackScope.getCurrentSpan()); + } + parent.end(); + assertEquals(0, parseLogMessages(logCaptureStream).size()); + } + + @Test + public void testWrongScopeClosure() { + Tracer tracer = DEFAULT_INSTRUMENTATION.getTracer(); + + Span span1 = tracer.spanBuilder("span1", INTERNAL, null).startSpan(); + TracingScope scope1 = span1.makeCurrent(); + + Span span2 = tracer.spanBuilder("span2", INTERNAL, null).startSpan(); + TracingScope scope2 = span2.makeCurrent(); + + assertSame(span2, FallbackScope.getCurrentSpan()); + + // should be noop - this span is not current on this thread + scope1.close(); + assertSame(span2, FallbackScope.getCurrentSpan()); + + scope2.close(); + assertSame(span1, FallbackScope.getCurrentSpan()); + + scope1.close(); + assertSame(Span.noop(), FallbackScope.getCurrentSpan()); + } + + @Test + public void basicTracingExplicitParentContext() { + Tracer tracer = DEFAULT_INSTRUMENTATION.getTracer(); + + InstrumentationContext parentContext = createRandomInstrumentationContext(); + Span child = tracer.spanBuilder("parent", INTERNAL, parentContext).startSpan(); + + assertValidSpan(child, false); + assertEquals(parentContext.getTraceId(), child.getInstrumentationContext().getTraceId()); + + testContextInjection(child.getInstrumentationContext(), DEFAULT_INSTRUMENTATION.getW3CTraceContextPropagator()); + + child.end(); + assertEquals(0, parseLogMessages(logCaptureStream).size()); + } + + @Test + public void testEmptyContextExtraction() { + TraceContextPropagator propagator = DEFAULT_INSTRUMENTATION.getW3CTraceContextPropagator(); + + Map carrier = new HashMap<>(); + carrier.put("random-key", "random-value"); + + InstrumentationContext context = propagator.extract(null, carrier, GETTER); + + assertNotNull(context); + assertFalse(context.isValid()); + assertEquals("00", context.getTraceFlags()); + assertEquals(RandomIdUtils.INVALID_SPAN_ID, context.getSpanId()); + assertEquals(RandomIdUtils.INVALID_TRACE_ID, context.getTraceId()); + + assertArrayEquals(new String[] { "random-key" }, carrier.keySet().toArray()); + } + + @Test + public void testValidContextExtraction() { + TraceContextPropagator propagator = DEFAULT_INSTRUMENTATION.getW3CTraceContextPropagator(); + + Map carrier = new HashMap<>(); + carrier.put("random-key", "random-value"); + carrier.put("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"); + + InstrumentationContext context = propagator.extract(null, carrier, GETTER); + + assertNotNull(context); + assertTrue(context.isValid()); + assertEquals("01", context.getTraceFlags()); + assertEquals("00f067aa0ba902b7", context.getSpanId()); + assertEquals("4bf92f3577b34da6a3ce929d0e0e4736", context.getTraceId()); + + assertArrayEquals(new String[] { "random-key", "traceparent" }, carrier.keySet().toArray()); + } + + @ParameterizedTest + @ValueSource( + strings = { + "", + "a random string", + "4bf92f3577b34da6a3ce929d0e0e4736", + "4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7", + "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "0z-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "00--00f067aa0ba902b7-01", + "00-29d0e0e4736-00f067aa0ba902b7-01", + "00-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-00f067aa0ba902b7-01", + "00-00000000000000000000000000000000-00f067aa0ba902b7-01", + "00-000004bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "00-4bf92f3577b34da6a3ce929d0e0e4736--01", + "00-4bf92f3577b34da6a3ce929d0e0e4736-902b7-01", + "00-4bf92f3577b34da6a3ce929d0e0e4736-zzzzzzzzzzzzzzzz-01", + "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01", + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7--", + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0y", + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0000", }) + public void testInvalidContextExtraction(String invalidTraceparent) { + TraceContextPropagator propagator = DEFAULT_INSTRUMENTATION.getW3CTraceContextPropagator(); + + Map carrier = new HashMap<>(); + carrier.put("traceparent", invalidTraceparent); + + InstrumentationContext context = propagator.extract(null, carrier, GETTER); + + assertNotNull(context); + assertFalse(context.isValid()); + assertEquals("00", context.getTraceFlags()); + assertEquals(RandomIdUtils.INVALID_SPAN_ID, context.getSpanId()); + assertEquals(RandomIdUtils.INVALID_TRACE_ID, context.getTraceId()); + } + + @ParameterizedTest + @MethodSource("instrumentationContextSource") + public void testIncomingContextIsIgnored(InstrumentationContext source) { + TraceContextPropagator propagator = DEFAULT_INSTRUMENTATION.getW3CTraceContextPropagator(); + + Map carrier = new HashMap<>(); + carrier.put("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"); + + InstrumentationContext context = propagator.extract(source, carrier, GETTER); + + assertNotNull(context); + assertTrue(context.isValid()); + assertEquals("01", context.getTraceFlags()); + assertEquals("00f067aa0ba902b7", context.getSpanId()); + assertEquals("4bf92f3577b34da6a3ce929d0e0e4736", context.getTraceId()); + } + + public static Stream instrumentationContextSource() { + return Stream.of(createRandomInstrumentationContext(), FallbackSpanContext.INVALID, + new FallbackSpanContext("4000000577b34da6a3ce9000000e4736", "00f0611111a902b7", "00", true, Span.noop()), + new FallbackSpanContext("", "", "42", true, Span.noop())); + } + + @Test + @SuppressWarnings("try") + public void basicTracingDisabledTests() { + InstrumentationOptions options = new InstrumentationOptions<>().setTracingEnabled(false); + Instrumentation instrumentation = Instrumentation.create(options, DEFAULT_LIB_OPTIONS); + + Tracer tracer = instrumentation.getTracer(); + assertFalse(tracer.isEnabled()); + + // should not throw + Span span = tracer.spanBuilder("test-span", INTERNAL, null).setAttribute("test-key", "test-value").startSpan(); + + span.setAttribute("test-key2", "test-value2"); + span.setError("test-error"); + + try (TracingScope scope = span.makeCurrent()) { + assertSame(Span.noop(), FallbackScope.getCurrentSpan()); + } + + assertNotNull(span); + assertNotNull(span.getInstrumentationContext()); + assertFalse(span.getInstrumentationContext().isValid()); + + assertSame(Span.noop(), span); + assertFalse(span.isRecording()); + testContextInjection(span.getInstrumentationContext(), instrumentation.getW3CTraceContextPropagator()); + + span.end(); + } + + @Test + public void createTracerUnknownProvider() { + // should not throw + InstrumentationOptions options = new InstrumentationOptions<>().setProvider("this is not a valid provider"); + Tracer tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); + assertTrue(tracer.isEnabled()); + } + + @Test + public void createInstrumentationBadOptions() { + assertThrows(NullPointerException.class, + () -> Instrumentation.create(new InstrumentationOptions<>(), null).getTracer()); + } + + @ParameterizedTest + @MethodSource("logLevels") + public void basicTracingLogsLevel(ClientLogger.LogLevel logLevel, boolean expectLogs) { + ClientLogger logger = setupLogLevelAndGetLogger(logLevel, logCaptureStream); + InstrumentationOptions options = new InstrumentationOptions<>().setProvider(logger); + Instrumentation instrumentation = Instrumentation.create(options, DEFAULT_LIB_OPTIONS); + Tracer tracer = instrumentation.getTracer(); + + Span span = tracer.spanBuilder("test-span", INTERNAL, null).startSpan(); + assertEquals(expectLogs, span.isRecording()); + span.end(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(expectLogs ? 1 : 0, logMessages.size()); + if (expectLogs) { + assertSpanLog(logMessages.get(0), "test-span", "INTERNAL", span.getInstrumentationContext(), null); + } + } + + public static Stream logLevels() { + return Stream.of(Arguments.of(ClientLogger.LogLevel.ERROR, false), + Arguments.of(ClientLogger.LogLevel.WARNING, false), Arguments.of(ClientLogger.LogLevel.INFORMATIONAL, true), + Arguments.of(ClientLogger.LogLevel.VERBOSE, true)); + } + + @Test + public void basicTracingLogsEnabled() { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + InstrumentationOptions options = new InstrumentationOptions<>().setProvider(logger); + Instrumentation instrumentation = Instrumentation.create(options, DEFAULT_LIB_OPTIONS); + Tracer tracer = instrumentation.getTracer(); + + long startTime = System.nanoTime(); + Span span = tracer.spanBuilder("test-span", INTERNAL, null).startSpan(); + + assertValidSpan(span, true); + testContextInjection(span.getInstrumentationContext(), instrumentation.getW3CTraceContextPropagator()); + + span.end(); + Duration duration = Duration.ofNanos(System.nanoTime() - startTime); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(1, logMessages.size()); + Map loggedSpan = logMessages.get(0); + assertSpanLog(loggedSpan, "test-span", "INTERNAL", span.getInstrumentationContext(), null); + assertTrue((Double) loggedSpan.get("span.duration.ms") <= duration.toNanos() / 1_000_000.0); + } + + @Test + public void tracingWithAttributesLogsEnabled() { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + InstrumentationOptions options = new InstrumentationOptions<>().setProvider(logger); + Tracer tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); + + Span span = tracer.spanBuilder("test-span", PRODUCER, null) + .setAttribute("builder-string-key", "builder-value") + .setAttribute("builder-int-key", 42) + .setAttribute("builder-long-key", 420L) + .setAttribute("builder-double-key", 4.2) + .setAttribute("builder-boolean-key", true) + .startSpan(); + span.setAttribute("span-string-key", "span-value") + .setAttribute("span-int-key", 42) + .setAttribute("span-long-key", 420L) + .setAttribute("span-double-key", 4.2) + .setAttribute("span-boolean-key", false) + .setError("test-error"); + + span.end(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(1, logMessages.size()); + Map loggedSpan = logMessages.get(0); + assertSpanLog(loggedSpan, "test-span", "PRODUCER", span.getInstrumentationContext(), "test-error"); + assertEquals("builder-value", loggedSpan.get("builder-string-key")); + assertEquals(42, loggedSpan.get("builder-int-key")); + assertEquals(420, loggedSpan.get("builder-long-key")); + assertEquals(4.2, loggedSpan.get("builder-double-key")); + assertEquals(true, loggedSpan.get("builder-boolean-key")); + assertEquals("span-value", loggedSpan.get("span-string-key")); + assertEquals(42, loggedSpan.get("span-int-key")); + assertEquals(420, loggedSpan.get("span-long-key")); + assertEquals(4.2, loggedSpan.get("span-double-key")); + assertEquals(false, loggedSpan.get("span-boolean-key")); + } + + @Test + public void tracingWithExceptionLogsEnabled() { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + InstrumentationOptions options = new InstrumentationOptions<>().setProvider(logger); + Tracer tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); + + Span span = tracer.spanBuilder("test-span", SERVER, null).startSpan(); + + IOException exception = new IOException("test-exception"); + span.end(exception); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(1, logMessages.size()); + Map loggedSpan = logMessages.get(0); + assertSpanLog(loggedSpan, "test-span", "SERVER", span.getInstrumentationContext(), + exception.getClass().getCanonicalName()); + assertEquals(exception.getMessage(), loggedSpan.get("exception.message")); + assertEquals("java.io.IOException", loggedSpan.get("exception.type")); + } + + @Test + public void tracingLogsEnabledParent() { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + InstrumentationOptions options = new InstrumentationOptions<>().setProvider(logger); + Tracer tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); + + Span parent = tracer.spanBuilder("parent", CONSUMER, null).startSpan(); + Span child = tracer.spanBuilder("child", CLIENT, parent.getInstrumentationContext()).startSpan(); + parent.end(); + child.end(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + Map parentLog = logMessages.get(0); + Map childLog = logMessages.get(1); + assertSpanLog(parentLog, "parent", "CONSUMER", parent.getInstrumentationContext(), null); + assertSpanLog(childLog, "child", "CLIENT", child.getInstrumentationContext(), null); + assertEquals(childLog.get("span.parent.id"), parentLog.get("span.id")); + assertEquals(childLog.get("trace.id"), parentLog.get("trace.id")); + } + + @Test + public void testCreateInstrumentationContextFromSpan() { + Tracer tracer = DEFAULT_INSTRUMENTATION.getTracer(); + + Span span = tracer.spanBuilder("span", CONSUMER, null).startSpan(); + InstrumentationContext fromSpan = Instrumentation.createInstrumentationContext(span); + assertEquals(span.getInstrumentationContext().getTraceId(), fromSpan.getTraceId()); + assertEquals(span.getInstrumentationContext().getSpanId(), fromSpan.getSpanId()); + assertEquals(span.getInstrumentationContext().getTraceFlags(), fromSpan.getTraceFlags()); + assertEquals(span.getInstrumentationContext().isValid(), fromSpan.isValid()); + assertSame(span, fromSpan.getSpan()); + } + + @Test + public void testCreateInstrumentationContextFromAnotherContext() { + InstrumentationContext testContext = createRandomInstrumentationContext(); + InstrumentationContext fromTestContext = Instrumentation.createInstrumentationContext(testContext); + assertEquals(testContext.getTraceId(), fromTestContext.getTraceId()); + assertEquals(testContext.getSpanId(), fromTestContext.getSpanId()); + assertEquals(testContext.getTraceFlags(), fromTestContext.getTraceFlags()); + assertEquals(testContext.isValid(), fromTestContext.isValid()); + assertSame(Span.noop(), fromTestContext.getSpan()); + } + + @ParameterizedTest + @MethodSource("notSupportedContexts") + public void testCreateInstrumentationContextNotSupported(Object context) { + InstrumentationContext fromNull = Instrumentation.createInstrumentationContext(context); + assertEquals(RandomIdUtils.INVALID_TRACE_ID, fromNull.getTraceId()); + assertEquals(RandomIdUtils.INVALID_SPAN_ID, fromNull.getSpanId()); + assertEquals("00", fromNull.getTraceFlags()); + assertFalse(fromNull.isValid()); + assertSame(Span.noop(), fromNull.getSpan()); + } + + public static Stream notSupportedContexts() { + return Stream.of(null, new Object(), "this is not a valid context", Context.none(), Context.of("key", "value")); + } + + private static void assertValidSpan(Span span, boolean isRecording) { + assertNotNull(span.getInstrumentationContext()); + assertTrue(span.getInstrumentationContext().isValid()); + assertValidSpanId(span.getInstrumentationContext().getSpanId()); + assertValidTraceId(span.getInstrumentationContext().getTraceId()); + assertEquals(isRecording ? "01" : "00", span.getInstrumentationContext().getTraceFlags()); + assertEquals(isRecording, span.isRecording()); + } + + private static void assertSpanLog(Map loggedSpan, String spanName, String spanKind, + InstrumentationContext context, String errorType) { + assertEquals("span.ended", loggedSpan.get("event.name")); + assertEquals(spanName, loggedSpan.get("span.name")); + assertEquals(spanKind, loggedSpan.get("span.kind")); + assertEquals(context.getTraceId(), loggedSpan.get("trace.id")); + assertEquals(context.getSpanId(), loggedSpan.get("span.id")); + + assertInstanceOf(Double.class, loggedSpan.get("span.duration.ms")); + double durationMs = (Double) loggedSpan.get("span.duration.ms"); + assertTrue(durationMs > 0); + assertEquals(errorType, loggedSpan.get("error.type")); + } + + private void testContextInjection(InstrumentationContext context, TraceContextPropagator propagator) { + Map carrier = new HashMap<>(); + propagator.inject(context, carrier, Map::put); + + if (context.isValid()) { + assertFalse(carrier.isEmpty()); + assertEquals("00-" + context.getTraceId() + "-" + context.getSpanId() + "-" + context.getTraceFlags(), + carrier.get("traceparent")); + } else { + assertTrue(carrier.isEmpty()); + } + } +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracingBenchmarks.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracingBenchmarks.java new file mode 100644 index 0000000000000..821c2bda69fc2 --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracingBenchmarks.java @@ -0,0 +1,113 @@ +// 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.InstrumentationOptions; +import io.clientcore.core.instrumentation.LibraryInstrumentationOptions; +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.instrumentation.logging.InstrumentationTestUtils; +import io.clientcore.core.instrumentation.tracing.Span; +import io.clientcore.core.instrumentation.tracing.SpanKind; +import io.clientcore.core.instrumentation.tracing.Tracer; +import io.clientcore.core.instrumentation.tracing.TracingScope; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; + +@Fork(3) +@Warmup(iterations = 5, time = 2) +@Measurement(iterations = 5, time = 10) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Thread) +public class FallbackTracingBenchmarks { + + private Tracer fallbackTracerEnabledWithLogs; + private Tracer fallbackTracerEnabledNoLogs; + private Tracer fallbackTracerDisabled; + + @Setup + public void setupOtel() { + LibraryInstrumentationOptions libraryOptions = new LibraryInstrumentationOptions("test"); + fallbackTracerDisabled + = Instrumentation.create(new InstrumentationOptions<>().setTracingEnabled(false), libraryOptions) + .getTracer(); + + ClientLogger loggerDisabled + = InstrumentationTestUtils.setupLogLevelAndGetLogger(ClientLogger.LogLevel.WARNING, new NoopStream()); + fallbackTracerEnabledNoLogs + = Instrumentation.create(new InstrumentationOptions<>().setProvider(loggerDisabled), libraryOptions) + .getTracer(); + + ClientLogger loggerEnabled + = InstrumentationTestUtils.setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, new NoopStream()); + fallbackTracerEnabledWithLogs + = Instrumentation.create(new InstrumentationOptions<>().setProvider(loggerEnabled), libraryOptions) + .getTracer(); + } + + @Benchmark + public void fallbackTracerDisabled(Blackhole blackhole) { + blackhole.consume(testFallbackSpan(fallbackTracerDisabled)); + } + + @Benchmark + public void fallbackTracerEnabledNoLogs(Blackhole blackhole) { + blackhole.consume(testFallbackSpan(fallbackTracerEnabledNoLogs)); + } + + @Benchmark + public void fallbackTracerEnabledWithLogs(Blackhole blackhole) { + blackhole.consume(testFallbackSpan(fallbackTracerEnabledWithLogs)); + } + + @SuppressWarnings("try") + private Span testFallbackSpan(Tracer tracer) { + Span span = tracer.spanBuilder("test", SpanKind.CLIENT, null).setAttribute("string1", "test").startSpan(); + + if (span.isRecording()) { + span.setAttribute("string2", "test"); + span.setAttribute("int", 42); + span.setAttribute("long", 42L); + span.setAttribute("double", 42.0); + span.setAttribute("boolean", true); + } + + try (TracingScope scope = span.makeCurrent()) { + span.setError("canceled"); + } + span.end(); + + return span; + } + + static class NoopStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + } + + @Override + public void write(byte[] b) throws IOException { + } + } +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java deleted file mode 100644 index 0dbe364bb6ae2..0000000000000 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/tracing/DefaultInstrumentationTests.java +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package io.clientcore.core.implementation.instrumentation.tracing; - -import io.clientcore.core.http.models.RequestOptions; -import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; -import io.clientcore.core.implementation.instrumentation.DefaultLogger; -import io.clientcore.core.instrumentation.Instrumentation; -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.Span; -import io.clientcore.core.instrumentation.tracing.SpanKind; -import io.clientcore.core.instrumentation.tracing.Tracer; -import org.junit.jupiter.api.Test; - -import java.io.PrintStream; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class DefaultInstrumentationTests { - private static final LibraryInstrumentationOptions DEFAULT_LIB_OPTIONS - = new LibraryInstrumentationOptions("test-library"); - private final AccessibleByteArrayOutputStream logCaptureStream; - - public DefaultInstrumentationTests() { - logCaptureStream = new AccessibleByteArrayOutputStream(); - } - - @Test - public void createTracer() { - Tracer tracer = Instrumentation.create(null, DEFAULT_LIB_OPTIONS).getTracer(); - assertTrue(tracer.isEnabled()); - - Span span = tracer.spanBuilder("test-span", SpanKind.INTERNAL, null).startSpan(); - - assertFalse(span.isRecording()); - } - - @Test - public void createTracerTracingDisabled() { - InstrumentationOptions options = new InstrumentationOptions<>().setTracingEnabled(false); - - Tracer tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); - assertFalse(tracer.isEnabled()); - - // should not throw - Span span = tracer.spanBuilder("test-span", SpanKind.INTERNAL, null).startSpan(); - - assertNotNull(span); - assertFalse(span.isRecording()); - } - - @Test - public void createTracerBadArguments() { - InstrumentationOptions options = new InstrumentationOptions<>().setProvider("this is not a valid provider"); - - assertThrows(NullPointerException.class, () -> Instrumentation.create(options, null).getTracer()); - } - - private ClientLogger setupLogLevelAndGetLogger(ClientLogger.LogLevel logLevelToSet, - Map globalContext) { - DefaultLogger logger - = new DefaultLogger(ClientLogger.class.getName(), new PrintStream(logCaptureStream), logLevelToSet); - - return new ClientLogger(ClientLogger.class); - } - -} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java index 1a4d389e38788..971897db13d55 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java @@ -5,6 +5,7 @@ import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; import io.clientcore.core.implementation.instrumentation.DefaultLogger; +import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.serialization.json.JsonOptions; import io.clientcore.core.serialization.json.JsonProviders; import io.clientcore.core.serialization.json.JsonReader; @@ -28,6 +29,8 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.createInvalidInstrumentationContext; +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.createRandomInstrumentationContext; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -752,6 +755,38 @@ public void logAtLevel(LogLevel level) { assertMessage(expectedMessage, byteArraySteamToString(logCaptureStream), LogLevel.INFORMATIONAL, level); } + @Test + public void logWithContext() { + ClientLogger logger = setupLogLevelAndGetLogger(LogLevel.INFORMATIONAL); + InstrumentationContext context = createRandomInstrumentationContext(); + logger.atInfo().setContext(context).addKeyValue("connectionId", "foo").log("message"); + + Map expectedMessage = new HashMap<>(); + expectedMessage.put("message", "message"); + expectedMessage.put("connectionId", "foo"); + expectedMessage.put("trace.id", context.getTraceId()); + expectedMessage.put("span.id", context.getSpanId()); + + assertMessage(expectedMessage, byteArraySteamToString(logCaptureStream), LogLevel.INFORMATIONAL, + LogLevel.INFORMATIONAL); + } + + @Test + public void logWithInvalidContext() { + ClientLogger logger = setupLogLevelAndGetLogger(LogLevel.INFORMATIONAL); + logger.atInfo() + .setContext(createInvalidInstrumentationContext()) + .addKeyValue("connectionId", "foo") + .log("message"); + + Map expectedMessage = new HashMap<>(); + expectedMessage.put("message", "message"); + expectedMessage.put("connectionId", "foo"); + + assertMessage(expectedMessage, byteArraySteamToString(logCaptureStream), LogLevel.INFORMATIONAL, + LogLevel.INFORMATIONAL); + } + private String stackTraceToString(Throwable exception) { StringWriter stringWriter = new StringWriter(); exception.printStackTrace(new PrintWriter(stringWriter)); diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/InstrumentationTestUtils.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/InstrumentationTestUtils.java new file mode 100644 index 0000000000000..7ec1c2edbad5f --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/InstrumentationTestUtils.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.logging; + +import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; +import io.clientcore.core.implementation.instrumentation.DefaultLogger; +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.tracing.Span; +import io.clientcore.core.serialization.json.JsonOptions; +import io.clientcore.core.serialization.json.JsonProviders; +import io.clientcore.core.serialization.json.JsonReader; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class InstrumentationTestUtils { + public static void assertValidSpanId(String spanId) { + assertNotNull(spanId); + assertTrue(spanId.matches("[0-9a-f]{16}")); + } + + public static void assertValidTraceId(String traceId) { + assertNotNull(traceId); + assertTrue(traceId.matches("[0-9a-f]{32}")); + } + + public static ClientLogger setupLogLevelAndGetLogger(ClientLogger.LogLevel logLevelToSet, + OutputStream logCaptureStream) { + DefaultLogger logger + = new DefaultLogger(ClientLogger.class.getName(), new PrintStream(logCaptureStream), logLevelToSet); + + return new ClientLogger(logger, null); + } + + public static List> parseLogMessages(AccessibleByteArrayOutputStream logCaptureStream) { + String fullLog = logCaptureStream.toString(StandardCharsets.UTF_8); + return fullLog.lines().map(InstrumentationTestUtils::parseLogLine).toList(); + } + + private static Map parseLogLine(String logLine) { + String messageJson = logLine.substring(logLine.indexOf(" - ") + 3); + System.out.println(messageJson); + try (JsonReader reader = JsonProviders.createReader(messageJson, new JsonOptions())) { + return reader.readMap(JsonReader::readUntyped); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static TestInstrumentationContext createRandomInstrumentationContext() { + String randomTraceId = UUID.randomUUID().toString().toLowerCase(Locale.ROOT).replace("-", ""); + String randomSpanId = UUID.randomUUID().toString().toLowerCase(Locale.ROOT).replace("-", "").substring(0, 16); + return new TestInstrumentationContext(randomTraceId, randomSpanId, "01", true); + } + + public static TestInstrumentationContext createInstrumentationContext(String traceId, String spanId) { + return new TestInstrumentationContext(traceId, spanId, "00", true); + } + + public static TestInstrumentationContext createInvalidInstrumentationContext() { + return new TestInstrumentationContext("00000000000000000000000000000000", "0000000000000000", "00", false); + } + + public static class TestInstrumentationContext implements InstrumentationContext { + private final String traceId; + private final String spanId; + private final String traceFlags; + private final boolean isValid; + + public TestInstrumentationContext(String traceId, String spanId, String traceFlags, boolean isValid) { + this.traceId = traceId; + this.spanId = spanId; + this.traceFlags = traceFlags; + this.isValid = isValid; + } + + @Override + public String getTraceId() { + return traceId; + } + + @Override + public String getSpanId() { + return spanId; + } + + @Override + public String getTraceFlags() { + return traceFlags; + } + + @Override + public boolean isValid() { + return isValid; + } + + @Override + public Span getSpan() { + return Span.noop(); + } + } + + private InstrumentationTestUtils() { + } +} diff --git a/sdk/clientcore/optional-dependency-tests/pom.xml b/sdk/clientcore/optional-dependency-tests/pom.xml index 1bbe325dffe4a..d6eece36d947b 100644 --- a/sdk/clientcore/optional-dependency-tests/pom.xml +++ b/sdk/clientcore/optional-dependency-tests/pom.xml @@ -109,6 +109,12 @@ 1.37 test + + io.clientcore + core + 1.0.0-beta.2 + test + diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java index 41b8b11f4848d..631ebebb1eed9 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java @@ -52,7 +52,6 @@ import static io.clientcore.core.http.models.HttpHeaderName.TRACEPARENT; import static io.clientcore.core.instrumentation.Instrumentation.DISABLE_TRACING_KEY; -import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY; import static io.clientcore.core.instrumentation.tracing.SpanKind.INTERNAL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -360,7 +359,8 @@ public void enrichSpans() throws IOException { HttpInstrumentationPolicy httpInstrumentationPolicy = new HttpInstrumentationPolicy(otelOptions, logOptions); HttpPipelinePolicy enrichingPolicy = (request, next) -> { - io.clientcore.core.instrumentation.tracing.Span span = request.getRequestOptions().getInstrumentationContext().getSpan(); + io.clientcore.core.instrumentation.tracing.Span span + = request.getRequestOptions().getInstrumentationContext().getSpan(); if (span.isRecording()) { span.setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID)); } diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java index 846563a04176d..5a53c0d982cdf 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java @@ -28,23 +28,25 @@ public class Slf4jLoggerShimIT { @BeforeEach public void setupLogLevels() { - slf4jLoggerShimITLogLevel = System - .setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShimIT", "debug"); - slf4jLoggerShimLogLevel - = System.setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShim", "info"); + slf4jLoggerShimITLogLevel = System.setProperty( + LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShimIT", "debug"); + slf4jLoggerShimLogLevel = System.setProperty( + LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShim", "info"); } @AfterEach public void resetLogLevels() { if (slf4jLoggerShimITLogLevel == null) { - System.clearProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShimIT"); + System.clearProperty( + LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShimIT"); } else { System.setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShimIT", slf4jLoggerShimITLogLevel); } if (slf4jLoggerShimLogLevel == null) { - System.clearProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShim"); + System + .clearProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShim"); } else { System.setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.instrumentation.Slf4jLoggerShim", slf4jLoggerShimLogLevel); diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java index 5188f76479b49..e88fa1dd980bc 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java @@ -154,14 +154,14 @@ public void testExtractInvalid() { public void testExtractPreservesContext() { /*Map carrier = new HashMap<>(); carrier.put("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); - + Context original = Context.of("key", "value"); Context updated = contextPropagator.extract(original, carrier, GETTER); - + io.opentelemetry.context.Context otelContext = (io.opentelemetry.context.Context) updated.get(TRACE_CONTEXT_KEY); assertTrue(io.opentelemetry.api.trace.Span.fromContext(otelContext).getSpanContext().isValid()); - + assertEquals("value", updated.get("key"));*/ } @@ -171,6 +171,7 @@ private String getTraceparent(Span span) { } private String getTraceparent(SpanContext spanContext) { - return "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-" + spanContext.getTraceFlags().asHex(); + return "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-" + + spanContext.getTraceFlags().asHex(); } } diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/InstrumentationTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/InstrumentationTests.java index 308f05dac42da..aa766c1a51e55 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/InstrumentationTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/InstrumentationTests.java @@ -3,12 +3,19 @@ package io.clientcore.core.instrumentation; +import io.clientcore.core.instrumentation.tracing.Span; import io.clientcore.core.instrumentation.tracing.Tracer; +import io.clientcore.core.instrumentation.tracing.TracingScope; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.Context; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; @@ -19,9 +26,12 @@ import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.api.parallel.Isolated; +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.createInvalidInstrumentationContext; +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.createRandomInstrumentationContext; import static io.clientcore.core.instrumentation.tracing.SpanKind.INTERNAL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -142,4 +152,108 @@ public void createTracerWithLibInfo() throws Exception { assertEquals("https://opentelemetry.io/schemas/1.29.0", span.getInstrumentationScopeInfo().getSchemaUrl()); } } + + @Test + public void createInstrumentationContextNull() { + assertNotNull(Instrumentation.createInstrumentationContext(null)); + assertFalse(Instrumentation.createInstrumentationContext(null).isValid()); + } + + @Test + @SuppressWarnings("try") + public void createInstrumentationContextFromOTelSpan() { + OpenTelemetry otel = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); + io.opentelemetry.api.trace.Tracer otelTracer = otel.getTracer("test"); + io.opentelemetry.api.trace.Span otelSpan = otelTracer.spanBuilder("test").startSpan(); + + InstrumentationContext context = Instrumentation.createInstrumentationContext(otelSpan); + assertNotNull(context); + assertTrue(context.isValid()); + assertNotNull(context.getSpan()); + assertEquals(otelSpan.getSpanContext().getSpanId(), context.getSpanId()); + assertEquals(otelSpan.getSpanContext().getTraceId(), context.getTraceId()); + assertEquals(otelSpan.getSpanContext().getTraceFlags().asHex(), context.getTraceFlags()); + + Tracer tracer + = Instrumentation.create(new InstrumentationOptions().setProvider(otel), DEFAULT_LIB_OPTIONS) + .getTracer(); + Span span = tracer.spanBuilder("test", INTERNAL, context).startSpan(); + assertEquals(otelSpan.getSpanContext().getTraceId(), span.getInstrumentationContext().getTraceId()); + + try (TracingScope scope = span.makeCurrent()) { + assertEquals(otelSpan.getSpanContext().getSpanId(), + ((ReadableSpan) io.opentelemetry.api.trace.Span.current()).getParentSpanContext().getSpanId()); + } + } + + @Test + public void createInstrumentationContextFromOTelContext() { + OpenTelemetry otel = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); + io.opentelemetry.api.trace.Tracer otelTracer = otel.getTracer("test"); + io.opentelemetry.api.trace.Span otelSpan = otelTracer.spanBuilder("test").startSpan(); + Context otelContext = Context.current().with(otelSpan); + + InstrumentationContext context = Instrumentation.createInstrumentationContext(otelContext); + assertNotNull(context); + assertTrue(context.isValid()); + assertNotNull(context.getSpan()); + assertEquals(otelSpan.getSpanContext().getSpanId(), context.getSpanId()); + assertEquals(otelSpan.getSpanContext().getTraceId(), context.getTraceId()); + assertEquals(otelSpan.getSpanContext().getTraceFlags().asHex(), context.getTraceFlags()); + } + + @Test + @SuppressWarnings("try") + public void createInstrumentationContextFromOTelSpanContext() { + OpenTelemetry otel = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); + + SpanContext otelSpanContext = SpanContext.create("0123456789abcdef0123456789abcdef", "0123456789abcdef", + TraceFlags.getSampled(), TraceState.builder().put("key", "value").build()); + + InstrumentationContext context = Instrumentation.createInstrumentationContext(otelSpanContext); + assertNotNull(context); + assertTrue(context.isValid()); + assertNotNull(context.getSpan()); + assertEquals(otelSpanContext.getSpanId(), context.getSpanId()); + assertEquals(otelSpanContext.getTraceId(), context.getTraceId()); + assertEquals(otelSpanContext.getTraceFlags().asHex(), context.getTraceFlags()); + + Tracer tracer + = Instrumentation.create(new InstrumentationOptions().setProvider(otel), DEFAULT_LIB_OPTIONS) + .getTracer(); + Span span = tracer.spanBuilder("test", INTERNAL, context).startSpan(); + assertEquals(otelSpanContext.getTraceId(), span.getInstrumentationContext().getTraceId()); + + try (TracingScope scope = span.makeCurrent()) { + ReadableSpan readableSpan = (ReadableSpan) io.opentelemetry.api.trace.Span.current(); + assertEquals(otelSpanContext.getSpanId(), readableSpan.getParentSpanContext().getSpanId()); + + TraceState traceState = readableSpan.getSpanContext().getTraceState(); + assertEquals("value", traceState.get("key")); + assertEquals(1, traceState.size()); + } + } + + @Test + public void createInstrumentationContextFromCustomContext() { + InstrumentationContext customContext = createRandomInstrumentationContext(); + + InstrumentationContext context = Instrumentation.createInstrumentationContext(customContext); + assertNotNull(context); + assertTrue(context.isValid()); + assertNotNull(context.getSpan()); + assertEquals(customContext.getSpanId(), context.getSpanId()); + assertEquals(customContext.getTraceId(), context.getTraceId()); + assertEquals(customContext.getTraceFlags(), context.getTraceFlags()); + } + + @Test + public void createInstrumentationContextFromInvalidContext() { + InstrumentationContext customContext = createInvalidInstrumentationContext(); + + InstrumentationContext context = Instrumentation.createInstrumentationContext(customContext); + assertNotNull(context); + assertFalse(context.isValid()); + assertNotNull(context.getSpan()); + } } diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java index 62dbef3cb8330..0787d9bc74358 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java @@ -185,13 +185,15 @@ public void multipleLayers() { @SuppressWarnings("try") public void testSuppressionExplicitContext(SpanKind outerKind, SpanKind innerKind, int expectedSpanCount) { RequestOptions options = new RequestOptions(); - Span outerSpan - = tracer.spanBuilder("outerSpan", outerKind, options.getInstrumentationContext()).setAttribute("key", "valueOuter").startSpan(); + Span outerSpan = tracer.spanBuilder("outerSpan", outerKind, options.getInstrumentationContext()) + .setAttribute("key", "valueOuter") + .startSpan(); options.setInstrumentationContext(outerSpan.getInstrumentationContext()); - Span innerSpan - = tracer.spanBuilder("innerSpan", innerKind, options.getInstrumentationContext()).setAttribute("key", "valueInner").startSpan(); + Span innerSpan = tracer.spanBuilder("innerSpan", innerKind, options.getInstrumentationContext()) + .setAttribute("key", "valueInner") + .startSpan(); // sanity check - this should not throw innerSpan.setAttribute("anotherKey", "anotherValue"); @@ -330,7 +332,8 @@ public void protocolMethod(RequestOptions options) { @SuppressWarnings("try") public void convenienceMethod(RequestOptions options) { - Span span = tracer.spanBuilder("convenienceMethod", INTERNAL, options.getInstrumentationContext()).startSpan(); + Span span + = tracer.spanBuilder("convenienceMethod", INTERNAL, options.getInstrumentationContext()).startSpan(); options.setInstrumentationContext(span.getInstrumentationContext()); diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java index f8b00eb60aec9..acf1439f91733 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java @@ -195,10 +195,10 @@ public void explicitParentWrongType() { = new RequestOptions().putContext(TRACE_CONTEXT_KEY, "This is not a valid trace context"); Span child = tracer.spanBuilder("child", INTERNAL, requestOptions).startSpan(); child.end(); - + assertEquals(1, exporter.getFinishedSpanItems().size()); SpanData childData = exporter.getFinishedSpanItems().get(0); - + assertEquals("child", childData.getName()); assertFalse(childData.getParentSpanContext().isValid());*/ } From a7478decb0de26f795bc2b31c5f5a3ecb8fe4f24 Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Sun, 5 Jan 2025 11:20:46 -0800 Subject: [PATCH 05/11] cleanup attribute and event names --- .../core/http/models/ProxyOptions.java | 3 +- .../pipeline/HttpInstrumentationPolicy.java | 44 +++--- .../http/pipeline/HttpRedirectPolicy.java | 32 +++- .../core/http/pipeline/HttpRetryPolicy.java | 17 +++ .../instrumentation/AttributeKeys.java | 138 ++++++++++++++++-- .../instrumentation/LoggingEventNames.java | 44 ++++++ .../fallback/FallbackContextPropagator.java | 6 +- .../fallback/FallbackSpan.java | 17 ++- .../fallback/FallbackSpanBuilder.java | 8 +- .../otel/tracing/OTelSpan.java | 3 +- .../instrumentation/logging/ClientLogger.java | 16 +- ...HttpInstrumentationPolicyLoggingTests.java | 16 +- .../FallbackInstrumentationTests.java | 6 +- 13 files changed, 285 insertions(+), 65 deletions(-) create mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LoggingEventNames.java diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/ProxyOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/ProxyOptions.java index 0678ff4c3ea0d..40f4ebbfcb83c 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/ProxyOptions.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/ProxyOptions.java @@ -22,6 +22,7 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.URL_FULL_KEY; import static io.clientcore.core.implementation.util.ImplUtils.isNullOrEmpty; /** @@ -342,7 +343,7 @@ private static ProxyOptions attemptToLoadSystemProxy(Configuration configuration return proxyOptions; } catch (URISyntaxException ex) { - LOGGER.atWarning().addKeyValue("uri", proxyProperty).log(INVALID_PROXY_URI, ex); + LOGGER.atWarning().addKeyValue(URL_FULL_KEY, proxyProperty).log(INVALID_PROXY_URI, ex); return null; } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java index 29a3b3f6404ec..2d81c1aa13b1f 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java @@ -39,16 +39,23 @@ import java.net.URI; import static io.clientcore.core.implementation.UrlRedactionUtil.getRedactedUri; -import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_CONTENT_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_BODY_SIZE_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_DURATION_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_HEADER_CONTENT_LENGTH_KEY; 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_REQUEST_TIME_TO_RESPONSE_KEY; -import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_BODY_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_BODY_CONTENT_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_BODY_SIZE_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_HEADER_CONTENT_LENGTH_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_STATUS_CODE_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SERVER_ADDRESS_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SERVER_PORT_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.URL_FULL_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.USER_AGENT_ORIGINAL_KEY; +import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.HTTP_REQUEST_EVENT_NAME; +import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.HTTP_RESPONSE_EVENT_NAME; import static io.clientcore.core.implementation.util.ImplUtils.isNullOrEmpty; import static io.clientcore.core.instrumentation.Instrumentation.DISABLE_TRACING_KEY; import static io.clientcore.core.instrumentation.tracing.SpanKind.CLIENT; @@ -154,11 +161,6 @@ public final class HttpInstrumentationPolicy implements HttpPipelinePolicy { LIBRARY_OPTIONS = libOptions; } - private static final String SERVER_ADDRESS = "server.address"; - private static final String SERVER_PORT = "server.port"; - private static final String USER_AGENT_ORIGINAL = "user_agent.original"; - private static final String HTTP_REQUEST_EVENT_NAME = "http.request"; - private static final String HTTP_RESPONSE_EVENT_NAME = "http.response"; private static final int MAX_BODY_LOG_SIZE = 1024 * 16; private static final String REDACTED_PLACEHOLDER = "REDACTED"; @@ -207,7 +209,7 @@ public Response process(HttpRequest request, HttpPipelineNextPolicy next) { 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()); + final long requestContentLength = getContentLength(logger, request.getBody(), request.getHeaders(), true); InstrumentationContext context = request.getRequestOptions() == null ? null : request.getRequestOptions().getInstrumentationContext(); @@ -255,7 +257,7 @@ private Span startHttpSpan(HttpRequest request, String sanitizedUrl, Instrumenta 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, request.getUri().getHost()); + .setAttribute(SERVER_ADDRESS_KEY, request.getUri().getHost()); maybeSetServerPort(spanBuilder, request.getUri()); return spanBuilder.startSpan(); } @@ -271,15 +273,15 @@ private Span startHttpSpan(HttpRequest request, String sanitizedUrl, Instrumenta 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: @@ -301,7 +303,7 @@ private void addDetails(HttpRequest request, Response response, int 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) { @@ -384,7 +386,7 @@ private void logRequest(ClientLogger logger, HttpRequest request, long startNano try { BinaryData bufferedBody = request.getBody().toReplayableBinaryData(); request.setBody(bufferedBody); - logBuilder.addKeyValue(HTTP_REQUEST_BODY_KEY, bufferedBody.toString()); + 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, @@ -415,7 +417,7 @@ private Response logResponse(ClientLogger logger, Response response, long .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())); + getContentLength(logger, response.getBody(), response.getHeaders(), false)); addHeadersToLogMessage(response.getHeaders(), logBuilder); } @@ -423,7 +425,7 @@ private Response logResponse(ClientLogger logger, Response response, long if (httpLogDetailLevel.shouldLogBody() && canLogBody(response.getBody())) { return new LoggingHttpResponse<>(response, content -> { if (logBuilder.isEnabled()) { - logBuilder.addKeyValue(HTTP_RESPONSE_BODY_KEY, content.toString()) + logBuilder.addKeyValue(HTTP_RESPONSE_BODY_CONTENT_KEY, content.toString()) .addKeyValue(HTTP_REQUEST_DURATION_KEY, getDurationMs(startNanoTime, System.nanoTime())) .log(); } @@ -459,7 +461,7 @@ private T logException(ClientLogger logger, HttpRequest re addHeadersToLogMessage(response.getHeaders(), log); log .addKeyValue(HTTP_RESPONSE_BODY_SIZE_KEY, - getContentLength(logger, response.getBody(), response.getHeaders())) + getContentLength(logger, response.getBody(), response.getHeaders(), false)) .addKeyValue(HTTP_RESPONSE_STATUS_CODE_KEY, response.getStatusCode()); if (responseStartNanoTime != null) { @@ -485,7 +487,7 @@ private double getDurationMs(long startNs, long endNs) { * @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 + // 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; } @@ -517,7 +519,7 @@ private void addHeadersToLogMessage(HttpHeaders headers, ClientLogger.LoggingEve * @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) { + private static long getContentLength(ClientLogger logger, BinaryData body, HttpHeaders headers, boolean isRequest) { if (body == null) { return 0; } @@ -538,7 +540,9 @@ private static long getContentLength(ClientLogger logger, BinaryData body, HttpH contentLength = Long.parseLong(contentLengthString); } catch (NumberFormatException e) { logger.atVerbose() - .addKeyValue("contentLength", contentLengthString) + .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); } 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 c729cdf14c34e..953971c2fe18c 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 @@ -22,7 +22,10 @@ import java.util.function.Predicate; 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.REQUEST_MAX_ATTEMPT_COUNT_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.URL_FULL_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 @@ -33,8 +36,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); @@ -96,6 +97,19 @@ private Response attemptRedirect(final HttpPipelineNextPolicy next, final int HttpRequestRedirectCondition requestRedirectCondition = new HttpRequestRedirectCondition(response, redirectAttempt, attemptedRedirectUris); + + // TODO (limolkova): we should log one event here: + // - if we are redirecting, log at verbose level + // - if we are not redirecting, log at warning level + // The content: + // - original request URI, method + // - location + // - all prev redirect urls + // - redirect count + // - reason why we are not redirecting + // - maybe human readable message + // This way, someone can query all occurrences of this event and get the context of the redirect. + if ((shouldRedirectCondition != null && shouldRedirectCondition.test(requestRedirectCondition)) || (shouldRedirectCondition == null && defaultShouldAttemptRedirect(requestRedirectCondition, instrumentationContext))) { @@ -120,11 +134,12 @@ && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), instrumentatio && !alreadyAttemptedRedirectUri(redirectUri, attemptedRedirectUris, instrumentationContext)) { LOGGER.atVerbose() + // TODO (lmolkova): -1 is probably not right, test and fix it .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount - 1) - .addKeyValue("url.full.from", response.getRequest().getUri()) - .addKeyValue("url.full.to", redirectUri) - .addKeyValue("url.full.all", attemptedRedirectUris::toString) - .setEventName("http.request.redirect") + .addKeyValue(URL_FULL_KEY, response.getRequest().getUri()) + .addKeyValue(HTTP_RESPONSE_HEADER_LOCATION_KEY, redirectUri) + .addKeyValue("redirect.url.full.all", attemptedRedirectUris::toString) + .setEventName(HTTP_REDIRECT_EVENT_NAME) .setContext(instrumentationContext) .log(); @@ -146,7 +161,8 @@ && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), instrumentatio private boolean isValidRedirectCount(int tryCount, InstrumentationContext instrumentationContext) { if (tryCount >= this.maxAttempts) { LOGGER.atError() - .addKeyValue("maxAttempts", this.maxAttempts) + .addKeyValue(REQUEST_MAX_ATTEMPT_COUNT_KEY, this.maxAttempts) + .setEventName(HTTP_REDIRECT_EVENT_NAME) .setContext(instrumentationContext) .log("Redirect attempts have been exhausted."); @@ -171,6 +187,7 @@ private boolean alreadyAttemptedRedirectUri(String redirectUri, Set atte LOGGER.atError() .addKeyValue(URL_FULL_KEY, redirectUri) .setContext(instrumentationContext) + .setEventName(HTTP_REDIRECT_EVENT_NAME) .log("Request was redirected more than once to the same URI."); return true; @@ -193,6 +210,7 @@ private boolean isAllowedRedirectMethod(HttpMethod httpMethod, InstrumentationCo LOGGER.atError() .setContext(instrumentationContext) .addKeyValue(AttributeKeys.HTTP_REQUEST_METHOD_KEY, httpMethod) + .setEventName(HTTP_REDIRECT_EVENT_NAME) .log("Request redirection is not enabled for this HTTP method."); return false; 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 ad99af27de65d..5e57a63135f56 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 @@ -28,6 +28,8 @@ import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_DURATION_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_RESEND_COUNT_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.REQUEST_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; @@ -149,6 +151,17 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex Response response; + // TODO (limolkova): we should log one event here: + // - if we are retrying, log at verbose level + // - if we are not retrying, log at warning level + // The content: + // - request URI, method + // - try count + // - reason why we are not retrying (if applicable) + // - backoff + // - ... + // This way, someone can query all occurrences of this event and get the context. + try { response = next.clone().process(); } catch (RuntimeException err) { @@ -280,6 +293,7 @@ private static void logRetry(int tryCount, Duration delayDuration, Instrumentati LOGGER.atVerbose() .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount) .addKeyValue(HTTP_REQUEST_DURATION_KEY, delayDuration.toMillis()) + .setEventName(HTTP_RETRY_EVENT_NAME) .setContext(context) .log("Retrying."); } @@ -287,6 +301,8 @@ private static void logRetry(int tryCount, Duration delayDuration, Instrumentati private static void logRetryExhausted(int tryCount, InstrumentationContext context) { LOGGER.atInfo() .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount) + .setEventName(HTTP_RETRY_EVENT_NAME) + .addKeyValue(REQUEST_MAX_ATTEMPT_COUNT_KEY, tryCount) .setContext(context) .log("Retry attempts have been exhausted."); } @@ -295,6 +311,7 @@ private static void logRetryWithError(ClientLogger.LoggingEvent loggingEvent, in String message, Throwable throwable, InstrumentationContext context) { loggingEvent.addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount) .setContext(context) + .setEventName(HTTP_RETRY_EVENT_NAME) .log(message, throwable); } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java index 31419c659a193..08b93bc99e5f3 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java @@ -4,7 +4,7 @@ package io.clientcore.core.implementation.instrumentation; /** - * Constants used as keys in semantic logging, tracing, metrics following + * Constants used as keys in semantic logging, tracing, metrics (mostly) following * OpenTelemetry semantic conventions. *

* These keys unify how core logs HTTP requests, responses or anything @@ -14,37 +14,117 @@ */ 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"; + /** - * Key representing HTTP method. + * The HTTP request method. + * http.request.method attribute */ public static final String HTTP_REQUEST_METHOD_KEY = "http.request.method"; /** - * Key representing try count, the value starts with {@code 0} on the first try + * 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"; /** - * Key representing request URI. + * 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 URL_FULL_KEY = "url.full"; + public static final String HTTP_REQUEST_BODY_SIZE_KEY = "http.request.body.size"; /** - * Key representing request body content length. + * The value of request content length header. + * http.request.header.content-length attribute */ - public static final String HTTP_REQUEST_BODY_SIZE_KEY = "http.request.body.size"; + 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"; /** - * Key representing request body content length. + * 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"; /** - * Key representing response status code. The value should be a number. + * 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. @@ -60,13 +140,49 @@ public final class AttributeKeys { * Key representing request body. The value should be populated conditionally * if populated at all. */ - public static final String HTTP_REQUEST_BODY_KEY = "http.request.body"; + 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_KEY = "http.request.body"; + 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 REQUEST_MAX_ATTEMPT_COUNT_KEY = "request.max_attempt_count"; + + /** + * 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/instrumentation/LoggingEventNames.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LoggingEventNames.java new file mode 100644 index 0000000000000..a01cb8091dd99 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LoggingEventNames.java @@ -0,0 +1,44 @@ +// 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 when an HTTP request has failed and is being retried. + * The event describes the reason for retrying the request and 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/instrumentation/fallback/FallbackContextPropagator.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackContextPropagator.java index 6eb9e3de3a4d1..d168deea15c26 100644 --- 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 @@ -10,6 +10,8 @@ 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(); @@ -41,7 +43,9 @@ public InstrumentationContext extract(InstrumentationContext context, C carr String traceFlags = traceparent.substring(53, 55); return new FallbackSpanContext(traceId, spanId, traceFlags, true, Span.noop()); } else { - LOGGER.atVerbose().addKeyValue("traceparent", traceparent).log("Invalid traceparent header"); + LOGGER.atVerbose() + .addKeyValue(HTTP_REQUEST_HEADER_TRACEPARENT_KEY, traceparent) + .log("Invalid traceparent header"); } } return context == null ? FallbackSpanContext.INVALID : context; 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 index b3d0a5c1fbd0e..c6ae45aea0fce 100644 --- 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 @@ -8,9 +8,13 @@ import io.clientcore.core.instrumentation.tracing.Span; import io.clientcore.core.instrumentation.tracing.TracingScope; -final class FallbackSpan implements Span { - private static final String SPAN_END_EVENT = "span.ended"; +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.LoggingEventBuilder log; private final long startTime; private final FallbackSpanContext spanContext; @@ -21,7 +25,8 @@ final class FallbackSpan implements Span { this.startTime = isRecording ? System.nanoTime() : 0; this.spanContext = FallbackSpanContext.fromParent(parentSpanContext, isRecording, this); if (log != null && log.isEnabled()) { - this.log.addKeyValue("trace.id", spanContext.getTraceId()).addKeyValue("span.id", spanContext.getSpanId()); + this.log.addKeyValue(TRACE_ID_KEY, spanContext.getTraceId()) + .addKeyValue(SPAN_ID_KEY, spanContext.getSpanId()); } } @@ -63,12 +68,12 @@ public void end(Throwable error) { } double durationMs = (System.nanoTime() - startTime) / 1_000_000.0; - log.addKeyValue("span.duration.ms", durationMs); + log.addKeyValue(SPAN_DURATION_KEY, durationMs); if (error != null || errorType != null) { - setAttribute("error.type", errorType != null ? errorType : error.getClass().getCanonicalName()); + setAttribute(ERROR_TYPE_KEY, errorType != null ? errorType : error.getClass().getCanonicalName()); } - log.setEventName(SPAN_END_EVENT); + log.setEventName(SPAN_ENDED_EVENT_NAME); if (error != null) { log.log(null, error); } else { 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 index e48d6899ca220..761d510e581ed 100644 --- 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 @@ -9,6 +9,10 @@ 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.LoggingEventBuilder log; @@ -24,9 +28,9 @@ private FallbackSpanBuilder() { this.parentSpanContext = FallbackSpanContext.fromInstrumentationContext(instrumentationContext); this.log = logger.atInfo(); if (log.isEnabled()) { - log.addKeyValue("span.name", spanName).addKeyValue("span.kind", spanKind.name()); + log.addKeyValue(SPAN_NAME_KEY, spanName).addKeyValue(SPAN_KIND_KEY, spanKind.name()); if (parentSpanContext.isValid()) { - log.addKeyValue("span.parent.id", parentSpanContext.getSpanId()); + log.addKeyValue(SPAN_PARENT_ID_KEY, parentSpanContext.getSpanId()); } } } 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 77e27e86cce95..48ca484fee586 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 @@ -16,6 +16,7 @@ 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; @@ -89,7 +90,7 @@ public class OTelSpan implements Span { Object rootContext = OTelContext.getCurrent(); noopSpan = new OTelSpan(invalidSpan, rootContext); - errorTypeAttributeKey = OTelAttributeKey.getKey("error.type", ""); + errorTypeAttributeKey = OTelAttributeKey.getKey(ERROR_TYPE_KEY, ""); } catch (Throwable t) { OTelInitializer.initError(LOGGER, t); } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java index cba07c4046ebc..e5ee2b2d4887c 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java @@ -24,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 @@ -507,12 +513,12 @@ public T log(String message, T throwable) { private void setThrowableInternal(Throwable throwable, boolean isDebugEnabled) { 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()); } } } @@ -524,8 +530,8 @@ private String getMessageWithContext(String message) { // TODO (limolkova) set context from implicit current span if (this.context != null && this.context.isValid()) { - addKeyValue("trace.id", context.getTraceId()); - addKeyValue("span.id", context.getSpanId()); + addKeyValue(TRACE_ID_KEY, context.getTraceId()); + addKeyValue(SPAN_ID_KEY, context.getSpanId()); } int pairsCount diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java index c5f88367a163e..2b424727acd01 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java @@ -360,11 +360,11 @@ public void testStringBodyLogging() throws IOException { Map requestLog = logMessages.get(0); assertRequestLog(requestLog, REDACTED_URI, request); - assertEquals("Request body", requestLog.get("http.request.body")); + assertEquals("Request body", requestLog.get("http.request.body.content")); Map responseLog = logMessages.get(1); assertResponseLog(responseLog, REDACTED_URI, response); - assertEquals("Response body", responseLog.get("http.request.body")); + assertEquals("Response body", responseLog.get("http.request.body.content")); } @Test @@ -396,11 +396,11 @@ public void testStreamBodyLogging() { Map requestLog = logMessages.get(0); assertRequestLog(requestLog, REDACTED_URI, request); - assertEquals("Request body", requestLog.get("http.request.body")); + assertEquals("Request body", requestLog.get("http.request.body.content")); Map responseLog = logMessages.get(1); assertResponseLog(responseLog, REDACTED_URI, response); - assertEquals("Response body", responseLog.get("http.request.body")); + assertEquals("Response body", responseLog.get("http.request.body.content")); assertEquals(requestBody.getLength(), requestStream.getPosition()); assertEquals(responseBody.getLength(), responseStream.getPosition()); @@ -428,12 +428,12 @@ public void testHugeBodyNotLogged() throws IOException { Map requestLog = logMessages.get(0); assertRequestLog(requestLog, REDACTED_URI, request); - assertNull(requestLog.get("http.request.body")); + assertNull(requestLog.get("http.request.body.content")); assertEquals(0, requestStream.getPosition()); Map responseLog = logMessages.get(1); assertResponseLog(responseLog, REDACTED_URI, response); - assertNull(responseLog.get("http.request.body")); + assertNull(responseLog.get("http.request.body.content")); assertEquals(0, responseStream.getPosition()); } @@ -460,12 +460,12 @@ public void testBodyWithUnknownLengthNotLogged() throws IOException { Map requestLog = logMessages.get(0); assertRequestLog(requestLog, REDACTED_URI, request); - assertNull(requestLog.get("http.request.body")); + assertNull(requestLog.get("http.request.body.content")); assertEquals(0, requestStream.getPosition()); Map responseLog = logMessages.get(1); assertResponseLog(responseLog, REDACTED_URI, response); - assertNull(responseLog.get("http.request.body")); + assertNull(responseLog.get("http.request.body.content")); assertEquals(0, responseStream.getPosition()); } diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java index 3ea6b2f8a4d86..651a4f0956867 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java @@ -409,7 +409,7 @@ public void basicTracingLogsEnabled() { assertEquals(1, logMessages.size()); Map loggedSpan = logMessages.get(0); assertSpanLog(loggedSpan, "test-span", "INTERNAL", span.getInstrumentationContext(), null); - assertTrue((Double) loggedSpan.get("span.duration.ms") <= duration.toNanos() / 1_000_000.0); + assertTrue((Double) loggedSpan.get("span.duration") <= duration.toNanos() / 1_000_000.0); } @Test @@ -547,8 +547,8 @@ private static void assertSpanLog(Map loggedSpan, String spanNam assertEquals(context.getTraceId(), loggedSpan.get("trace.id")); assertEquals(context.getSpanId(), loggedSpan.get("span.id")); - assertInstanceOf(Double.class, loggedSpan.get("span.duration.ms")); - double durationMs = (Double) loggedSpan.get("span.duration.ms"); + assertInstanceOf(Double.class, loggedSpan.get("span.duration")); + double durationMs = (Double) loggedSpan.get("span.duration"); assertTrue(durationMs > 0); assertEquals(errorType, loggedSpan.get("error.type")); } From 7ffc5931167516c6eee7a3961795b1e7f6534cc0 Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Sun, 5 Jan 2025 13:39:07 -0800 Subject: [PATCH 06/11] Retry policy logging clean up --- .../pipeline/HttpInstrumentationPolicy.java | 34 +-- .../http/pipeline/HttpRedirectPolicy.java | 56 ++-- .../core/http/pipeline/HttpRetryPolicy.java | 91 +++--- .../instrumentation/AttributeKeys.java | 12 +- .../instrumentation/LoggingEventNames.java | 5 +- .../core/instrumentation/Instrumentation.java | 6 - ...ttpInstrumentationPolicyFallbackTests.java | 17 -- ...HttpInstrumentationPolicyLoggingTests.java | 277 +++++++++++++++--- .../HttpInstrumentationPolicyTests.java | 22 -- 9 files changed, 333 insertions(+), 187 deletions(-) diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java index 2d81c1aa13b1f..117f54d58fd1d 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java @@ -24,7 +24,6 @@ import io.clientcore.core.instrumentation.tracing.TraceContextSetter; import io.clientcore.core.instrumentation.tracing.Tracer; import io.clientcore.core.instrumentation.logging.ClientLogger; -import io.clientcore.core.util.Context; import io.clientcore.core.util.binarydata.BinaryData; import java.io.IOException; @@ -57,7 +56,6 @@ import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.HTTP_REQUEST_EVENT_NAME; import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.HTTP_RESPONSE_EVENT_NAME; import static io.clientcore.core.implementation.util.ImplUtils.isNullOrEmpty; -import static io.clientcore.core.instrumentation.Instrumentation.DISABLE_TRACING_KEY; import static io.clientcore.core.instrumentation.tracing.SpanKind.CLIENT; /** @@ -200,7 +198,7 @@ public HttpInstrumentationPolicy(InstrumentationOptions instrumentationOption @SuppressWarnings("try") @Override public Response process(HttpRequest request, HttpPipelineNextPolicy next) { - boolean isTracingEnabled = isTracingEnabled(request); + boolean isTracingEnabled = tracer.isEnabled(); if (!isTracingEnabled && httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { return next.process(); } @@ -214,11 +212,16 @@ public Response process(HttpRequest request, HttpPipelineNextPolicy next) { InstrumentationContext context = request.getRequestOptions() == null ? null : request.getRequestOptions().getInstrumentationContext(); Span span = Span.noop(); - if (isTracingEnabled(request)) { + if (isTracingEnabled) { span = startHttpSpan(request, redactedUrl, context); context = span.getInstrumentationContext(); request.getRequestOptions().setInstrumentationContext(context); - traceContextPropagator.inject(span.getInstrumentationContext(), request.getHeaders(), SETTER); + } + + // even if tracing is disabled, we could have valid context to propagate + // explicitly provided by the application. + if (context != null && context.isValid()) { + traceContextPropagator.inject(context, request.getHeaders(), SETTER); } logRequest(logger, request, startNs, requestContentLength, redactedUrl, tryCount, context); @@ -312,24 +315,6 @@ private void addDetails(HttpRequest request, Response response, int tryCount, // TODO (lmolkova) url.template and experimental features } - private boolean isTracingEnabled(HttpRequest httpRequest) { - if (!tracer.isEnabled()) { - return false; - } - - if (httpRequest.getRequestOptions() == null) { - return true; - } - - Context context = httpRequest.getRequestOptions().getContext(); - Object disableTracing = context.get(DISABLE_TRACING_KEY); - if (disableTracing instanceof Boolean) { - return !((Boolean) disableTracing); - } - - return true; - } - private static Throwable unwrap(Throwable t) { while (t.getCause() != null) { t = t.getCause(); @@ -470,7 +455,8 @@ private T logException(ClientLogger logger, HttpRequest re } } - return log.log(null, throwable); + log.log(null, unwrap(throwable)); + return throwable; } private double getDurationMs(long startNs, long endNs) { 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 953971c2fe18c..81d761a156e28 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 @@ -23,7 +23,7 @@ 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.REQUEST_MAX_ATTEMPT_COUNT_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_MAX_ATTEMPT_COUNT_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.URL_FULL_KEY; import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.HTTP_REDIRECT_EVENT_NAME; @@ -82,15 +82,18 @@ public Response process(HttpRequest httpRequest, HttpPipelineNextPolicy next) InstrumentationContext instrumentationContext = httpRequest.getRequestOptions() == null ? null : httpRequest.getRequestOptions().getInstrumentationContext(); - return attemptRedirect(next, 1, new LinkedHashSet<>(), instrumentationContext); + + ClientLogger logger = getLogger(httpRequest); + return attemptRedirect(logger, next, 1, 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, InstrumentationContext instrumentationContext) { + 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(); @@ -109,31 +112,30 @@ private Response attemptRedirect(final HttpPipelineNextPolicy next, final int // - reason why we are not redirecting // - maybe human readable message // This way, someone can query all occurrences of this event and get the context of the redirect. - if ((shouldRedirectCondition != null && shouldRedirectCondition.test(requestRedirectCondition)) || (shouldRedirectCondition == null - && defaultShouldAttemptRedirect(requestRedirectCondition, instrumentationContext))) { + && defaultShouldAttemptRedirect(logger, requestRedirectCondition, instrumentationContext))) { createRedirectRequest(response); - return attemptRedirect(next, redirectAttempt + 1, attemptedRedirectUris, instrumentationContext); + return attemptRedirect(logger, next, redirectAttempt + 1, attemptedRedirectUris, instrumentationContext); } return response; } - private boolean defaultShouldAttemptRedirect(HttpRequestRedirectCondition requestRedirectCondition, - InstrumentationContext instrumentationContext) { + private boolean defaultShouldAttemptRedirect(ClientLogger logger, + HttpRequestRedirectCondition requestRedirectCondition, InstrumentationContext instrumentationContext) { 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, instrumentationContext) - && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), instrumentationContext) + && isValidRedirectCount(logger, tryCount, instrumentationContext) + && isAllowedRedirectMethod(logger, response.getRequest().getHttpMethod(), instrumentationContext) && redirectUri != null - && !alreadyAttemptedRedirectUri(redirectUri, attemptedRedirectUris, instrumentationContext)) { + && !alreadyAttemptedRedirectUri(logger, redirectUri, attemptedRedirectUris, instrumentationContext)) { - LOGGER.atVerbose() + logger.atVerbose() // TODO (lmolkova): -1 is probably not right, test and fix it .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount - 1) .addKeyValue(URL_FULL_KEY, response.getRequest().getUri()) @@ -158,10 +160,11 @@ && isAllowedRedirectMethod(response.getRequest().getHttpMethod(), instrumentatio * * @return {@code true} if the {@code tryCount} is greater than the {@code maxAttempts}, {@code false} otherwise. */ - private boolean isValidRedirectCount(int tryCount, InstrumentationContext instrumentationContext) { + private boolean isValidRedirectCount(ClientLogger logger, int tryCount, + InstrumentationContext instrumentationContext) { if (tryCount >= this.maxAttempts) { - LOGGER.atError() - .addKeyValue(REQUEST_MAX_ATTEMPT_COUNT_KEY, this.maxAttempts) + logger.atError() + .addKeyValue(RETRY_MAX_ATTEMPT_COUNT_KEY, this.maxAttempts) .setEventName(HTTP_REDIRECT_EVENT_NAME) .setContext(instrumentationContext) .log("Redirect attempts have been exhausted."); @@ -181,10 +184,10 @@ private boolean isValidRedirectCount(int tryCount, InstrumentationContext instru * @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, - InstrumentationContext instrumentationContext) { + private boolean alreadyAttemptedRedirectUri(ClientLogger logger, String redirectUri, + Set attemptedRedirectUris, InstrumentationContext instrumentationContext) { if (attemptedRedirectUris.contains(redirectUri)) { - LOGGER.atError() + logger.atError() .addKeyValue(URL_FULL_KEY, redirectUri) .setContext(instrumentationContext) .setEventName(HTTP_REDIRECT_EVENT_NAME) @@ -203,11 +206,12 @@ private boolean alreadyAttemptedRedirectUri(String redirectUri, Set atte * * @return {@code true} if the request {@code httpMethod} is a valid http redirect method, {@code false} otherwise. */ - private boolean isAllowedRedirectMethod(HttpMethod httpMethod, InstrumentationContext instrumentationContext) { + private boolean isAllowedRedirectMethod(ClientLogger logger, HttpMethod httpMethod, + InstrumentationContext instrumentationContext) { if (allowedRedirectHttpMethods.contains(httpMethod)) { return true; } else { - LOGGER.atError() + logger.atError() .setContext(instrumentationContext) .addKeyValue(AttributeKeys.HTTP_REQUEST_METHOD_KEY, httpMethod) .setEventName(HTTP_REDIRECT_EVENT_NAME) @@ -243,4 +247,14 @@ private void createRedirectRequest(Response redirectResponse) { throw LOGGER.logThrowableAsError(new UncheckedIOException(e)); } } + + 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 5e57a63135f56..dea3e33191713 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 @@ -26,9 +26,10 @@ import java.util.function.Predicate; import java.util.function.Supplier; -import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_DURATION_KEY; import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_RESEND_COUNT_KEY; -import static io.clientcore.core.implementation.instrumentation.AttributeKeys.REQUEST_MAX_ATTEMPT_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; @@ -150,26 +151,19 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex : httpRequest.getRequestOptions().getInstrumentationContext(); Response response; - - // TODO (limolkova): we should log one event here: - // - if we are retrying, log at verbose level - // - if we are not retrying, log at warning level - // The content: - // - request URI, method - // - try count - // - reason why we are not retrying (if applicable) - // - backoff - // - ... - // This way, someone can query all occurrences of this event and get the context. + ClientLogger logger = getLogger(httpRequest); try { response = next.clone().process(); } catch (RuntimeException err) { if (shouldRetryException(err, tryCount, suppressed)) { - logRetryWithError(LOGGER.atVerbose(), tryCount, "Error resume.", err, instrumentationContext); + + 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); @@ -180,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; @@ -189,21 +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, - instrumentationContext); + logRetry(logger.atError(), 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, instrumentationContext); + logRetry(logger.atVerbose(), tryCount, delayDuration, null, false, instrumentationContext); try { response.close(); @@ -211,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); @@ -223,9 +216,10 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex return attempt(httpRequest, next, tryCount + 1, suppressed); } else { if (tryCount >= maxRetries) { - logRetryExhausted(tryCount, instrumentationContext); + // TODO (limolkova): do we have better heuristic to determine if we're not retrying because of error + // or because we got successful response? + logRetry(logger.atWarning(), tryCount, null, null, true, instrumentationContext); } - return response; } } @@ -289,30 +283,25 @@ private boolean shouldRetryException(Exception exception, int tryCount, List diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyFallbackTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyFallbackTests.java index fe124fecee6a6..2417835db52f1 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyFallbackTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyFallbackTests.java @@ -15,14 +15,11 @@ import org.junit.jupiter.params.provider.ValueSource; import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.SocketException; import static io.clientcore.core.http.models.HttpHeaderName.TRACEPARENT; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; public class HttpInstrumentationPolicyFallbackTests { private static final InstrumentationOptions OPTIONS = new InstrumentationOptions<>(); @@ -62,18 +59,4 @@ public void simpleRequestTracingEnabled(int statusCode) throws IOException { assertNotNull(response.getRequest().getHeaders().get(TRACEPARENT)); } } - - @Test - public void exceptionTracingDisabled() { - SocketException exception = new SocketException("test exception"); - HttpPipeline pipeline - = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(OPTIONS, ENABLED_HTTP_LOG_OPTIONS)) - .httpClient(request -> { - throw exception; - }) - .build(); - - assertThrows(UncheckedIOException.class, - () -> pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost/")).close()); - } } diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java index 2b424727acd01..c69c4f7547a9c 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java @@ -10,6 +10,7 @@ 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; @@ -27,15 +28,21 @@ 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; @@ -100,10 +107,10 @@ public void testBasicHttpLogging(Set allowedParams, String expectedUri) List> logMessages = parseLogMessages(logCaptureStream); assertEquals(2, logMessages.size()); - assertRequestLog(logMessages.get(0), expectedUri, request); + assertRequestLog(logMessages.get(0), expectedUri, request, null, 0); assertEquals(8, logMessages.get(0).size()); - assertResponseLog(logMessages.get(1), expectedUri, response); + assertResponseLog(logMessages.get(1), expectedUri, response, 0); assertEquals(12, logMessages.get(1).size()); } @@ -122,10 +129,10 @@ public void testHttpLoggingTracingDisabled() throws IOException { List> logMessages = parseLogMessages(logCaptureStream); assertEquals(2, logMessages.size()); - assertRequestLog(logMessages.get(0), REDACTED_URI, request); + assertRequestLog(logMessages.get(0), request); assertEquals(6, logMessages.get(0).size()); - assertResponseLog(logMessages.get(1), REDACTED_URI, response); + assertResponseLog(logMessages.get(1), response); assertEquals(10, logMessages.get(1).size()); } @@ -137,10 +144,9 @@ public void testHttpLoggingTracingDisabledCustomContext() throws IOException { HttpPipeline pipeline = createPipeline(instrumentationOptions, logOptions); - HttpRequest request = createRequest(HttpMethod.GET, URI, logger); - request.setRequestOptions(new RequestOptions().setLogger(logger) - .setInstrumentationContext( - createInstrumentationContext("1234567890abcdef1234567890abcdef", "1234567890abcdef"))); + InstrumentationContext instrumentationContext + = createInstrumentationContext("1234567890abcdef1234567890abcdef", "1234567890abcdef"); + HttpRequest request = createRequest(HttpMethod.GET, URI, logger, instrumentationContext); Response response = pipeline.send(request); response.close(); @@ -148,10 +154,10 @@ public void testHttpLoggingTracingDisabledCustomContext() throws IOException { List> logMessages = parseLogMessages(logCaptureStream); assertEquals(2, logMessages.size()); - assertRequestLog(logMessages.get(0), REDACTED_URI, request); + assertRequestLog(logMessages.get(0), request); assertEquals(8, logMessages.get(0).size()); - assertResponseLog(logMessages.get(1), REDACTED_URI, response); + assertResponseLog(logMessages.get(1), response); assertEquals(12, logMessages.get(1).size()); } @@ -193,7 +199,7 @@ public void testConnectionException(ClientLogger.LogLevel level, boolean expectE if (!expectExceptionLog) { assertEquals(0, logMessages.size()); } else { - assertExceptionLog(logMessages.get(0), REDACTED_URI, request, expectedException); + assertExceptionLog(logMessages.get(0), request, expectedException); } } @@ -203,20 +209,21 @@ public void testRequestBodyException(ClientLogger.LogLevel level, boolean expect ClientLogger logger = setupLogLevelAndGetLogger(level, logCaptureStream); HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); - TestStream requestStream = new TestStream(1024, new IOException("socket error")); + IOException expectedException = new IOException("socket error"); + TestStream requestStream = new TestStream(1024, expectedException); BinaryData requestBody = BinaryData.fromStream(requestStream, 1024L); HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options); HttpRequest request = createRequest(HttpMethod.POST, URI, logger); request.setBody(requestBody); - Exception actualException = assertThrows(RuntimeException.class, () -> pipeline.send(request)); + assertThrows(RuntimeException.class, () -> pipeline.send(request)); List> logMessages = parseLogMessages(logCaptureStream); if (!expectExceptionLog) { assertEquals(0, logMessages.size()); } else { - assertExceptionLog(logMessages.get(0), REDACTED_URI, request, actualException); + assertExceptionLog(logMessages.get(0), request, expectedException); } } @@ -226,20 +233,21 @@ public void testResponseBodyException(ClientLogger.LogLevel level, boolean expec ClientLogger logger = setupLogLevelAndGetLogger(level, logCaptureStream); HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); - TestStream responseStream = new TestStream(1024, new IOException("socket error")); + IOException expectedException = new IOException("socket error"); + TestStream responseStream = new TestStream(1024, expectedException); HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options, request -> new MockHttpResponse(request, 200, BinaryData.fromStream(responseStream, 1024L))); HttpRequest request = createRequest(HttpMethod.GET, URI, logger); Response response = pipeline.send(request); - Exception actualException = assertThrows(RuntimeException.class, () -> response.getBody().toString()); + assertThrows(RuntimeException.class, () -> response.getBody().toString()); List> logMessages = parseLogMessages(logCaptureStream); if (!expectExceptionLog) { assertEquals(0, logMessages.size()); } else { - assertResponseAndExceptionLog(logMessages.get(0), REDACTED_URI, response, actualException); + assertResponseAndExceptionLog(logMessages.get(0), REDACTED_URI, response, expectedException); } } @@ -259,7 +267,7 @@ public void testResponseBodyLoggingOnClose() throws IOException { response.close(); List> logMessages = parseLogMessages(logCaptureStream); - assertResponseLog(logMessages.get(0), REDACTED_URI, response); + assertResponseLog(logMessages.get(0), response); } @Test @@ -297,7 +305,7 @@ public void testBasicHttpLoggingRequestOff(Set allowedParams, String exp List> logMessages = parseLogMessages(logCaptureStream); assertEquals(1, logMessages.size()); - assertResponseLog(logMessages.get(0), expectedUri, response); + assertResponseLog(logMessages.get(0), expectedUri, response, 0); assertEquals(12, logMessages.get(0).size()); } @@ -319,7 +327,7 @@ public void testHeadersHttpLogging(Set allowedHeaders) throws IO assertEquals(2, logMessages.size()); Map requestLog = logMessages.get(0); - assertRequestLog(requestLog, REDACTED_URI, request); + assertRequestLog(requestLog, request); for (HttpHeader header : request.getHeaders()) { if (allowedHeaders.contains(header.getName())) { assertEquals(header.getValue(), requestLog.get(header.getName().toString())); @@ -329,7 +337,7 @@ public void testHeadersHttpLogging(Set allowedHeaders) throws IO } Map responseLog = logMessages.get(1); - assertResponseLog(responseLog, REDACTED_URI, response); + assertResponseLog(responseLog, response); for (HttpHeader header : response.getHeaders()) { if (allowedHeaders.contains(header.getName())) { assertEquals(header.getValue(), responseLog.get(header.getName().toString())); @@ -359,11 +367,11 @@ public void testStringBodyLogging() throws IOException { assertEquals(2, logMessages.size()); Map requestLog = logMessages.get(0); - assertRequestLog(requestLog, REDACTED_URI, request); + assertRequestLog(requestLog, request); assertEquals("Request body", requestLog.get("http.request.body.content")); Map responseLog = logMessages.get(1); - assertResponseLog(responseLog, REDACTED_URI, response); + assertResponseLog(responseLog, response); assertEquals("Response body", responseLog.get("http.request.body.content")); } @@ -395,11 +403,11 @@ public void testStreamBodyLogging() { assertEquals(2, logMessages.size()); Map requestLog = logMessages.get(0); - assertRequestLog(requestLog, REDACTED_URI, request); + assertRequestLog(requestLog, request); assertEquals("Request body", requestLog.get("http.request.body.content")); Map responseLog = logMessages.get(1); - assertResponseLog(responseLog, REDACTED_URI, response); + assertResponseLog(responseLog, response); assertEquals("Response body", responseLog.get("http.request.body.content")); assertEquals(requestBody.getLength(), requestStream.getPosition()); @@ -427,12 +435,12 @@ public void testHugeBodyNotLogged() throws IOException { assertEquals(2, logMessages.size()); Map requestLog = logMessages.get(0); - assertRequestLog(requestLog, REDACTED_URI, request); + assertRequestLog(requestLog, request); assertNull(requestLog.get("http.request.body.content")); assertEquals(0, requestStream.getPosition()); Map responseLog = logMessages.get(1); - assertResponseLog(responseLog, REDACTED_URI, response); + assertResponseLog(responseLog, response); assertNull(responseLog.get("http.request.body.content")); assertEquals(0, responseStream.getPosition()); } @@ -459,16 +467,136 @@ public void testBodyWithUnknownLengthNotLogged() throws IOException { assertEquals(2, logMessages.size()); Map requestLog = logMessages.get(0); - assertRequestLog(requestLog, REDACTED_URI, request); + assertRequestLog(requestLog, request); assertNull(requestLog.get("http.request.body.content")); assertEquals(0, requestStream.getPosition()); Map responseLog = logMessages.get(1); - assertResponseLog(responseLog, REDACTED_URI, response); + assertResponseLog(responseLog, response); assertNull(responseLog.get("http.request.body.content")); assertEquals(0, responseStream.getPosition()); } + @SuppressWarnings("try") + @Test + public void tracingWithRetriesException() throws IOException { + AtomicInteger count = new AtomicInteger(0); + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC); + + AtomicReference firstTryContext = new AtomicReference<>(); + UnknownHostException expectedException = new UnknownHostException("test exception"); + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(new HttpRetryPolicy(), new HttpInstrumentationPolicy(DEFAULT_INSTRUMENTATION_OPTIONS, options)) + .httpClient(request -> { + assertEquals(traceparent(request.getRequestOptions().getInstrumentationContext()), + request.getHeaders().get(TRACEPARENT).getValue()); + if (count.getAndIncrement() == 0) { + firstTryContext.set(request.getRequestOptions().getInstrumentationContext()); + throw expectedException; + } else { + return new MockHttpResponse(request, 200); + } + }) + .build(); + + InstrumentationContext parentContext + = createInstrumentationContext("1234567890abcdef1234567890abcdef", "1234567890abcdef"); + HttpRequest request = createRequest(HttpMethod.PUT, URI, logger, parentContext); + Response response = pipeline.send(request); + response.close(); + + assertEquals(2, count.get()); + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(5, logMessages.size()); + assertRequestLog(logMessages.get(0), REDACTED_URI, request, firstTryContext.get(), 0); + assertExceptionLog(logMessages.get(1), REDACTED_URI, request, expectedException, firstTryContext.get(), 0); + + assertRetryLog(logMessages.get(2), 0, 3, parentContext, true); + + assertRequestLog(logMessages.get(3), REDACTED_URI, request, null, 1); + assertResponseLog(logMessages.get(4), REDACTED_URI, response, 1); + } + + @Test + public void tracingWithRetriesStatusCode() throws IOException { + AtomicInteger count = new AtomicInteger(0); + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC); + + AtomicReference firstTryContext = new AtomicReference<>(); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(new HttpRetryPolicy(), new HttpInstrumentationPolicy(DEFAULT_INSTRUMENTATION_OPTIONS, options)) + .httpClient(request -> { + if (count.getAndIncrement() == 0) { + firstTryContext.set(request.getRequestOptions().getInstrumentationContext()); + return new MockHttpResponse(request, 500); + } else { + return new MockHttpResponse(request, 200); + } + }) + .build(); + + InstrumentationContext parentContext = createRandomInstrumentationContext(); + HttpRequest request = createRequest(HttpMethod.PUT, URI, logger, parentContext); + Response response = pipeline.send(request); + response.close(); + + assertEquals(2, count.get()); + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(5, logMessages.size()); + assertResponseLog(logMessages.get(1), REDACTED_URI, 0, 500, firstTryContext.get()); + assertRetryLog(logMessages.get(2), 0, 3, parentContext, true); + assertResponseLog(logMessages.get(4), REDACTED_URI, response, 1); + } + + @ParameterizedTest + @MethodSource("logLevels") + public void retryPolicyLoggingRetriesExhausted(ClientLogger.LogLevel logLevel, boolean expectRetryingLogs, + boolean expectExhaustedLog) throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(logLevel, logCaptureStream); + + int maxRetries = 3; + HttpRetryOptions retryOptions = new HttpRetryOptions(maxRetries, Duration.ofMillis(5)); + + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpRetryPolicy(retryOptions)) + .httpClient(request -> new MockHttpResponse(request, 500)) + .build(); + + InstrumentationContext parentContext = createRandomInstrumentationContext(); + HttpRequest request = createRequest(HttpMethod.PUT, URI, logger, parentContext); + Response response = pipeline.send(request); + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + + int expectedLogCount = expectRetryingLogs ? maxRetries : 0; + if (expectExhaustedLog) { + expectedLogCount++; + } + + assertEquals(expectedLogCount, logMessages.size()); + + if (expectRetryingLogs) { + for (int i = 0; i < maxRetries; i++) { + assertRetryLog(logMessages.get(i), i, 3, parentContext, true); + } + } + + if (expectExhaustedLog) { + Map lastLog = logMessages.get(logMessages.size() - 1); + assertRetryLog(lastLog, 3, 3, parentContext, false); + } + } + + public static Stream logLevels() { + return Stream.of(Arguments.of(ClientLogger.LogLevel.ERROR, false, false), + Arguments.of(ClientLogger.LogLevel.WARNING, false, true), + Arguments.of(ClientLogger.LogLevel.INFORMATIONAL, false, true), + Arguments.of(ClientLogger.LogLevel.VERBOSE, true, true)); + } + public static Stream allowQueryParamSource() { Set twoParams = new HashSet<>(); twoParams.add("param"); @@ -537,20 +665,44 @@ public long getPosition() { } } - private void assertRequestLog(Map log, String expectedUri, HttpRequest request) { + private void assertRequestLog(Map log, HttpRequest request) { + assertRequestLog(log, REDACTED_URI, request, null, 0); + } + + private void assertRequestLog(Map log, String expectedUri, HttpRequest request, + InstrumentationContext context, int tryCount) { assertEquals("http.request", log.get("event.name")); assertEquals(expectedUri, log.get("url.full")); - assertEquals(0, (int) log.get("http.request.resend_count")); + assertEquals(tryCount, (int) log.get("http.request.resend_count")); assertEquals(getLength(request.getBody(), request.getHeaders()), (int) log.get("http.request.body.size")); assertEquals(request.getHttpMethod().toString(), log.get("http.request.method")); assertEquals("", log.get("message")); - assertTraceContext(log, request); + if (context == null) { + context = request.getRequestOptions().getInstrumentationContext(); + } + + assertTraceContext(log, context); + } + + private void assertRetryLog(Map log, int tryCount, int maxAttempts, InstrumentationContext context, + boolean isRetrying) { + assertEquals("http.retry", log.get("event.name")); + assertEquals(tryCount, (int) log.get("http.request.resend_count")); + if (isRetrying) { + assertInstanceOf(Integer.class, log.get("retry.delay")); + assertFalse((boolean) log.get("retry.was_last_attempt")); + } else { + assertNull(log.get("retry.delay")); + assertTrue((boolean) log.get("retry.was_last_attempt")); + } + assertEquals(maxAttempts, log.get("retry.max_attempt_count")); + assertEquals("", log.get("message")); + assertTraceContext(log, context); } - private void assertTraceContext(Map log, HttpRequest request) { - InstrumentationContext context = request.getRequestOptions().getInstrumentationContext(); + private void assertTraceContext(Map log, InstrumentationContext context) { if (context != null) { assertTrue(log.get("trace.id").toString().matches("[0-9a-f]{32}")); assertTrue(log.get("span.id").toString().matches("[0-9a-f]{16}")); @@ -576,24 +728,35 @@ private long getLength(BinaryData body, HttpHeaders headers) { return 0; } - private void assertResponseLog(Map log, String expectedUri, Response response) { - assertEquals("http.response", log.get("event.name")); - assertEquals(expectedUri, log.get("url.full")); - assertEquals(0, (int) log.get("http.request.resend_count")); + private void assertResponseLog(Map log, Response response) { + assertResponseLog(log, REDACTED_URI, response, 0); + } + + private void assertResponseLog(Map log, String expectedUri, Response response, int tryCount) { + assertResponseLog(log, expectedUri, tryCount, response.getStatusCode(), + response.getRequest().getRequestOptions().getInstrumentationContext()); Long expectedRequestLength = getLength(response.getRequest().getBody(), response.getRequest().getHeaders()); assertEquals(expectedRequestLength, (int) log.get("http.request.body.size")); assertEquals(response.getRequest().getHttpMethod().toString(), log.get("http.request.method")); - assertEquals(response.getStatusCode(), log.get("http.response.status_code")); + assertInstanceOf(Double.class, log.get("http.request.time_to_response")); + assertInstanceOf(Double.class, log.get("http.request.duration")); + } + + private void assertResponseLog(Map log, String expectedUri, int tryCount, int statusCode, + InstrumentationContext context) { + assertEquals("http.response", log.get("event.name")); + assertEquals(expectedUri, log.get("url.full")); + assertEquals(tryCount, (int) log.get("http.request.resend_count")); + + assertEquals(statusCode, log.get("http.response.status_code")); - Long expectedResponseLength = getLength(response.getBody(), response.getHeaders()); - assertEquals(expectedResponseLength, (int) log.get("http.response.body.size")); assertInstanceOf(Double.class, log.get("http.request.time_to_response")); assertInstanceOf(Double.class, log.get("http.request.duration")); assertEquals("", log.get("message")); - assertTraceContext(log, response.getRequest()); + assertTraceContext(log, context); } private void assertResponseAndExceptionLog(Map log, String expectedUri, Response response, @@ -614,13 +777,18 @@ private void assertResponseAndExceptionLog(Map log, String expec assertEquals(error.getMessage(), log.get("exception.message")); assertEquals(error.getClass().getCanonicalName(), log.get("exception.type")); assertEquals("", log.get("message")); - assertTraceContext(log, response.getRequest()); + assertTraceContext(log, response.getRequest().getRequestOptions().getInstrumentationContext()); } - private void assertExceptionLog(Map log, String expectedUri, HttpRequest request, Throwable error) { + private void assertExceptionLog(Map log, HttpRequest request, Throwable error) { + assertExceptionLog(log, REDACTED_URI, request, error, null, 0); + } + + private void assertExceptionLog(Map log, String expectedUri, HttpRequest request, Throwable error, + InstrumentationContext context, int tryCount) { assertEquals("http.response", log.get("event.name")); assertEquals(expectedUri, log.get("url.full")); - assertEquals(0, (int) log.get("http.request.resend_count")); + assertEquals(tryCount, (int) log.get("http.request.resend_count")); Long expectedRequestLength = getLength(request.getBody(), request.getHeaders()); assertEquals(expectedRequestLength, (int) log.get("http.request.body.size")); @@ -634,7 +802,11 @@ private void assertExceptionLog(Map log, String expectedUri, Htt assertEquals(error.getClass().getCanonicalName(), log.get("exception.type")); assertEquals("", log.get("message")); - assertTraceContext(log, request); + + if (context == null) { + context = request.getRequestOptions().getInstrumentationContext(); + } + assertTraceContext(log, context); } private HttpPipeline createPipeline(InstrumentationOptions instrumentationOptions, HttpLogOptions options) { @@ -654,11 +826,22 @@ private HttpPipeline createPipeline(InstrumentationOptions instrumentationOpt } private HttpRequest createRequest(HttpMethod method, String url, ClientLogger logger) { + return createRequest(method, url, logger, null); + } + + private HttpRequest createRequest(HttpMethod method, String url, ClientLogger logger, + InstrumentationContext context) { HttpRequest request = new HttpRequest(method, url); request.getHeaders().set(HttpHeaderName.CONTENT_TYPE, "application/json"); request.getHeaders().set(HttpHeaderName.AUTHORIZATION, "Bearer {token}"); - request.setRequestOptions(new RequestOptions().setLogger(logger)); + request.setRequestOptions(new RequestOptions().setLogger(logger).setInstrumentationContext(context)); return request; } + + private String traceparent(InstrumentationContext instrumentationContext) { + return String.format("00-%s-%s-%s", instrumentationContext.getTraceId(), instrumentationContext.getSpanId(), + instrumentationContext.getTraceFlags()); + } + } diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java index 631ebebb1eed9..f6b4c6d122360 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java @@ -51,7 +51,6 @@ import java.util.concurrent.atomic.AtomicReference; import static io.clientcore.core.http.models.HttpHeaderName.TRACEPARENT; -import static io.clientcore.core.instrumentation.Instrumentation.DISABLE_TRACING_KEY; import static io.clientcore.core.instrumentation.tracing.SpanKind.INTERNAL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -312,27 +311,6 @@ public void tracingIsDisabledOnInstance() throws IOException { assertEquals(0, exporter.getFinishedSpanItems().size()); } - @Test - public void tracingIsDisabledOnRequest() throws IOException { - InstrumentationOptions options - = new InstrumentationOptions().setProvider(openTelemetry); - HttpPipeline pipeline - = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(options, null)).httpClient(request -> { - assertFalse(Span.current().getSpanContext().isValid()); - assertFalse(Span.current().isRecording()); - assertNull(request.getHeaders().get(TRACEPARENT)); - return new MockHttpResponse(request, 200); - }).build(); - - URI url = URI.create("http://localhost/"); - - RequestOptions requestOptions = new RequestOptions().putContext(DISABLE_TRACING_KEY, true); - - pipeline.send(new HttpRequest(HttpMethod.GET, url).setRequestOptions(requestOptions)).close(); - assertNotNull(exporter.getFinishedSpanItems()); - assertEquals(0, exporter.getFinishedSpanItems().size()); - } - @Test public void userAgentIsRecorded() throws IOException { HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) From 5886f4c6a4bdafd678e72e2ef887187cccdb3023 Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Sun, 5 Jan 2025 18:21:04 -0800 Subject: [PATCH 07/11] redirect policy logging cleanup --- sdk/clientcore/core/spotbugs-exclude.xml | 2 +- .../http/pipeline/HttpRedirectPolicy.java | 148 ++++++------------ .../instrumentation/AttributeKeys.java | 2 +- ...a => HttpInstrumentationLoggingTests.java} | 143 ++++++++++++++++- 4 files changed, 189 insertions(+), 106 deletions(-) rename sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/{HttpInstrumentationPolicyLoggingTests.java => HttpInstrumentationLoggingTests.java} (84%) diff --git a/sdk/clientcore/core/spotbugs-exclude.xml b/sdk/clientcore/core/spotbugs-exclude.xml index 2661e212a909c..a94ed75f922db 100644 --- a/sdk/clientcore/core/spotbugs-exclude.xml +++ b/sdk/clientcore/core/spotbugs-exclude.xml @@ -245,7 +245,7 @@ - + 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 81d761a156e28..d18d820328e02 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,23 +8,26 @@ 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.instrumentation.AttributeKeys; 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.URL_FULL_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; /** @@ -84,7 +87,7 @@ public Response process(HttpRequest httpRequest, HttpPipelineNextPolicy next) : httpRequest.getRequestOptions().getInstrumentationContext(); ClientLogger logger = getLogger(httpRequest); - return attemptRedirect(logger, next, 1, new LinkedHashSet<>(), instrumentationContext); + return attemptRedirect(logger, next, 0, new LinkedHashSet<>(), instrumentationContext); } /** @@ -101,17 +104,6 @@ private Response attemptRedirect(ClientLogger logger, final HttpPipelineNextP HttpRequestRedirectCondition requestRedirectCondition = new HttpRequestRedirectCondition(response, redirectAttempt, attemptedRedirectUris); - // TODO (limolkova): we should log one event here: - // - if we are redirecting, log at verbose level - // - if we are not redirecting, log at warning level - // The content: - // - original request URI, method - // - location - // - all prev redirect urls - // - redirect count - // - reason why we are not redirecting - // - maybe human readable message - // This way, someone can query all occurrences of this event and get the context of the redirect. if ((shouldRedirectCondition != null && shouldRedirectCondition.test(requestRedirectCondition)) || (shouldRedirectCondition == null && defaultShouldAttemptRedirect(logger, requestRedirectCondition, instrumentationContext))) { @@ -123,75 +115,35 @@ && defaultShouldAttemptRedirect(logger, requestRedirectCondition, instrumentatio } private boolean defaultShouldAttemptRedirect(ClientLogger logger, - HttpRequestRedirectCondition requestRedirectCondition, InstrumentationContext instrumentationContext) { + 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(logger, tryCount, instrumentationContext) - && isAllowedRedirectMethod(logger, response.getRequest().getHttpMethod(), instrumentationContext) - && redirectUri != null - && !alreadyAttemptedRedirectUri(logger, redirectUri, attemptedRedirectUris, instrumentationContext)) { + 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; + } - logger.atVerbose() - // TODO (lmolkova): -1 is probably not right, test and fix it - .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount - 1) - .addKeyValue(URL_FULL_KEY, response.getRequest().getUri()) - .addKeyValue(HTTP_RESPONSE_HEADER_LOCATION_KEY, redirectUri) - .addKeyValue("redirect.url.full.all", attemptedRedirectUris::toString) - .setEventName(HTTP_REDIRECT_EVENT_NAME) - .setContext(instrumentationContext) - .log(); - - attemptedRedirectUris.add(redirectUri); - - return true; - } - - 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(ClientLogger logger, int tryCount, - InstrumentationContext instrumentationContext) { - if (tryCount >= this.maxAttempts) { - logger.atError() - .addKeyValue(RETRY_MAX_ATTEMPT_COUNT_KEY, this.maxAttempts) - .setEventName(HTTP_REDIRECT_EVENT_NAME) - .setContext(instrumentationContext) - .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(ClientLogger logger, String redirectUri, - Set attemptedRedirectUris, InstrumentationContext instrumentationContext) { - if (attemptedRedirectUris.contains(redirectUri)) { - logger.atError() - .addKeyValue(URL_FULL_KEY, redirectUri) - .setContext(instrumentationContext) - .setEventName(HTTP_REDIRECT_EVENT_NAME) - .log("Request was redirected more than once to the same URI."); + attemptedRedirectUris.add(redirectUri); return true; } @@ -199,28 +151,6 @@ private boolean alreadyAttemptedRedirectUri(ClientLogger logger, String redirect 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(ClientLogger logger, HttpMethod httpMethod, - InstrumentationContext instrumentationContext) { - if (allowedRedirectHttpMethods.contains(httpMethod)) { - return true; - } else { - logger.atError() - .setContext(instrumentationContext) - .addKeyValue(AttributeKeys.HTTP_REQUEST_METHOD_KEY, httpMethod) - .setEventName(HTTP_REDIRECT_EVENT_NAME) - .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. * @@ -248,6 +178,32 @@ private void createRedirectRequest(Response redirectResponse) { } } + private void logRedirect(ClientLogger logger, boolean lastAttempt, String redirectUri, int tryCount, + HttpMethod method, String message, InstrumentationContext context) { + ClientLogger.LoggingEventBuilder 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) + .setContext(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; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java index bd3343934cbfe..e04b0f6d01f18 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/AttributeKeys.java @@ -160,7 +160,7 @@ public final class AttributeKeys { public static final String RETRY_DELAY_KEY = "retry.delay"; /** - * Key representing whether the retry just performed was the last attempt. + * 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"; diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationLoggingTests.java similarity index 84% rename from sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java rename to sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationLoggingTests.java index c69c4f7547a9c..394316b605a73 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyLoggingTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationLoggingTests.java @@ -53,7 +53,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @Execution(ExecutionMode.SAME_THREAD) -public class HttpInstrumentationPolicyLoggingTests { +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(); @@ -63,7 +63,7 @@ public class HttpInstrumentationPolicyLoggingTests { private final AccessibleByteArrayOutputStream logCaptureStream; - public HttpInstrumentationPolicyLoggingTests() { + public HttpInstrumentationLoggingTests() { this.logCaptureStream = new AccessibleByteArrayOutputStream(); } @@ -512,7 +512,7 @@ public void tracingWithRetriesException() throws IOException { assertRequestLog(logMessages.get(0), REDACTED_URI, request, firstTryContext.get(), 0); assertExceptionLog(logMessages.get(1), REDACTED_URI, request, expectedException, firstTryContext.get(), 0); - assertRetryLog(logMessages.get(2), 0, 3, parentContext, true); + assertRetryLog(logMessages.get(2), 0, 3, true, parentContext); assertRequestLog(logMessages.get(3), REDACTED_URI, request, null, 1); assertResponseLog(logMessages.get(4), REDACTED_URI, response, 1); @@ -547,7 +547,7 @@ public void tracingWithRetriesStatusCode() throws IOException { List> logMessages = parseLogMessages(logCaptureStream); assertEquals(5, logMessages.size()); assertResponseLog(logMessages.get(1), REDACTED_URI, 0, 500, firstTryContext.get()); - assertRetryLog(logMessages.get(2), 0, 3, parentContext, true); + assertRetryLog(logMessages.get(2), 0, 3, true, parentContext); assertResponseLog(logMessages.get(4), REDACTED_URI, response, 1); } @@ -580,16 +580,127 @@ public void retryPolicyLoggingRetriesExhausted(ClientLogger.LogLevel logLevel, b if (expectRetryingLogs) { for (int i = 0; i < maxRetries; i++) { - assertRetryLog(logMessages.get(i), i, 3, parentContext, true); + assertRetryLog(logMessages.get(i), i, 3, true, parentContext); } } if (expectExhaustedLog) { Map lastLog = logMessages.get(logMessages.size() - 1); - assertRetryLog(lastLog, 3, 3, parentContext, false); + assertRetryLog(lastLog, 3, 3, false, parentContext); } } + @Test + public void tracingWithRedirects() throws IOException { + AtomicInteger count = new AtomicInteger(0); + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC); + + AtomicReference firstRedirectContext = new AtomicReference<>(); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(new HttpRedirectPolicy(), new HttpInstrumentationPolicy(DEFAULT_INSTRUMENTATION_OPTIONS, options)) + .httpClient(request -> { + if (count.getAndIncrement() == 0) { + firstRedirectContext.set(request.getRequestOptions().getInstrumentationContext()); + HttpHeaders httpHeaders = new HttpHeaders().set(HttpHeaderName.LOCATION, + "http://redirecthost/" + count.get() + "?param=value&api-version=42"); + return new MockHttpResponse(request, 302, httpHeaders); + } else { + return new MockHttpResponse(request, 200); + } + }) + .build(); + + InstrumentationContext parentContext = createRandomInstrumentationContext(); + HttpRequest request = createRequest(HttpMethod.GET, URI, logger, parentContext); + Response response = pipeline.send(request); + response.close(); + assertEquals(2, count.get()); + + List> logMessages = parseLogMessages(logCaptureStream); + + assertEquals(5, logMessages.size()); + assertResponseLog(logMessages.get(1), REDACTED_URI, 0, 302, firstRedirectContext.get()); + + assertRedirectLog(logMessages.get(2), 0, 3, true, "http://redirecthost/1?param=REDACTED&api-version=REDACTED", + HttpMethod.GET, "", parentContext); + assertResponseLog(logMessages.get(4), "http://redirecthost/1?param=REDACTED&api-version=42", response, 0); + } + + @Test + public void redirectLoggingMethodNotSupported() throws IOException { + AtomicInteger count = new AtomicInteger(0); + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpRedirectPolicy()).httpClient(request -> { + count.getAndIncrement(); + HttpHeaders httpHeaders = new HttpHeaders().set(HttpHeaderName.LOCATION, "http://redirecthost/"); + return new MockHttpResponse(request, 302, httpHeaders); + }).build(); + + InstrumentationContext parentContext = createRandomInstrumentationContext(); + HttpRequest request = createRequest(HttpMethod.PUT, URI, logger, parentContext); + Response response = pipeline.send(request); + response.close(); + assertEquals(1, count.get()); + + List> logMessages = parseLogMessages(logCaptureStream); + + assertEquals(1, logMessages.size()); + assertRedirectLog(logMessages.get(0), 0, 3, false, "http://redirecthost/", HttpMethod.PUT, + "Request redirection is not enabled for this HTTP method.", parentContext); + } + + @Test + public void redirectToTheSameUri() throws IOException { + AtomicInteger count = new AtomicInteger(0); + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpRedirectPolicy()).httpClient(request -> { + count.getAndIncrement(); + HttpHeaders httpHeaders = new HttpHeaders().set(HttpHeaderName.LOCATION, "http://redirecthost/"); + return new MockHttpResponse(request, 302, httpHeaders); + }).build(); + + InstrumentationContext parentContext = createRandomInstrumentationContext(); + HttpRequest request = createRequest(HttpMethod.GET, URI, logger, parentContext); + Response response = pipeline.send(request); + response.close(); + assertEquals(2, count.get()); + + List> logMessages = parseLogMessages(logCaptureStream); + + assertEquals(2, logMessages.size()); + assertRedirectLog(logMessages.get(0), 0, 3, true, "http://redirecthost/", HttpMethod.GET, "", parentContext); + assertRedirectLog(logMessages.get(1), 1, 3, false, "http://redirecthost/", HttpMethod.GET, + "Request was redirected more than once to the same URI.", parentContext); + } + + @Test + public void redirectAttemptsExhausted() throws IOException { + AtomicInteger count = new AtomicInteger(0); + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpRedirectPolicy()).httpClient(request -> { + count.getAndIncrement(); + HttpHeaders httpHeaders + = new HttpHeaders().set(HttpHeaderName.LOCATION, "http://redirecthost/" + count.get()); + return new MockHttpResponse(request, 302, httpHeaders); + }).build(); + + InstrumentationContext parentContext = createRandomInstrumentationContext(); + HttpRequest request = createRequest(HttpMethod.GET, URI, logger, parentContext); + Response response = pipeline.send(request); + response.close(); + assertEquals(3, count.get()); + + List> logMessages = parseLogMessages(logCaptureStream); + + assertEquals(3, logMessages.size()); + assertRedirectLog(logMessages.get(0), 0, 3, true, "http://redirecthost/1", HttpMethod.GET, "", parentContext); + assertRedirectLog(logMessages.get(1), 1, 3, true, "http://redirecthost/2", HttpMethod.GET, "", parentContext); + assertRedirectLog(logMessages.get(2), 2, 3, false, "http://redirecthost/3", HttpMethod.GET, + "Redirect attempts have been exhausted.", parentContext); + } + public static Stream logLevels() { return Stream.of(Arguments.of(ClientLogger.LogLevel.ERROR, false, false), Arguments.of(ClientLogger.LogLevel.WARNING, false, true), @@ -686,8 +797,8 @@ private void assertRequestLog(Map log, String expectedUri, HttpR assertTraceContext(log, context); } - private void assertRetryLog(Map log, int tryCount, int maxAttempts, InstrumentationContext context, - boolean isRetrying) { + private void assertRetryLog(Map log, int tryCount, int maxAttempts, boolean isRetrying, + InstrumentationContext context) { assertEquals("http.retry", log.get("event.name")); assertEquals(tryCount, (int) log.get("http.request.resend_count")); if (isRetrying) { @@ -702,6 +813,22 @@ private void assertRetryLog(Map log, int tryCount, int maxAttemp assertTraceContext(log, context); } + private void assertRedirectLog(Map log, int tryCount, int maxAttempts, boolean shouldRedirect, + String redirectUri, HttpMethod method, String message, InstrumentationContext context) { + assertEquals("http.redirect", log.get("event.name")); + assertEquals(tryCount, (int) log.get("http.request.resend_count")); + assertEquals(method.toString(), log.get("http.request.method")); + assertEquals(redirectUri, log.get("http.response.header.location")); + if (shouldRedirect) { + assertFalse((boolean) log.get("retry.was_last_attempt")); + } else { + assertTrue((boolean) log.get("retry.was_last_attempt")); + } + assertEquals(maxAttempts, log.get("retry.max_attempt_count")); + assertEquals(message, log.get("message")); + assertTraceContext(log, context); + } + private void assertTraceContext(Map log, InstrumentationContext context) { if (context != null) { assertTrue(log.get("trace.id").toString().matches("[0-9a-f]{32}")); From 057608c88a6100678b1daa215ae6d6683eb13c2f Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Mon, 6 Jan 2025 18:36:39 -0800 Subject: [PATCH 08/11] clean up --- .../core/http/models/HttpLogOptions.java | 6 +- .../pipeline/HttpInstrumentationPolicy.java | 77 ++- .../core/http/pipeline/HttpLoggingPolicy.java | 337 ---------- .../http/pipeline/HttpRedirectPolicy.java | 4 +- .../core/http/pipeline/HttpRequestLogger.java | 37 -- .../http/pipeline/HttpResponseLogger.java | 57 -- .../core/http/pipeline/HttpRetryPolicy.java | 8 +- ...aryInstrumentationOptionsAccessHelper.java | 1 - .../fallback/FallbackSpan.java | 4 +- .../fallback/FallbackSpanBuilder.java | 2 +- .../fallback/FallbackTracer.java | 9 +- .../core/implementation/util/LoggingKeys.java | 78 --- .../instrumentation/logging/ClientLogger.java | 45 +- .../core/src/main/java/module-info.java | 2 +- .../HttpInstrumentationLoggingTests.java | 2 +- .../logging/ClientLoggerTests.java | 4 +- .../logging/HttpLoggingPolicyTests.java | 618 ------------------ .../src/main/java/module-info.java | 1 - .../optional-dependency-tests/pom.xml | 6 - .../core/instrumentation/TracerTests.java | 14 - 20 files changed, 71 insertions(+), 1241 deletions(-) delete mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java delete mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRequestLogger.java delete mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpResponseLogger.java delete mode 100644 sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/LoggingKeys.java delete mode 100644 sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/HttpLoggingPolicyTests.java diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpLogOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpLogOptions.java index 54b83990e8aae..1814586f3779d 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpLogOptions.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/HttpLogOptions.java @@ -22,9 +22,9 @@ public final class HttpLogOptions { private Set allowedHeaderNames; private Set allowedQueryParamNames; private static final List DEFAULT_HEADERS_ALLOWLIST - = Arrays.asList(HttpHeaderName.ACCEPT, HttpHeaderName.CACHE_CONTROL, HttpHeaderName.CONNECTION, - HttpHeaderName.CONTENT_LENGTH, HttpHeaderName.CONTENT_TYPE, HttpHeaderName.DATE, HttpHeaderName.ETAG, - HttpHeaderName.EXPIRES, HttpHeaderName.IF_MATCH, HttpHeaderName.IF_MODIFIED_SINCE, + = Arrays.asList(HttpHeaderName.TRACEPARENT, HttpHeaderName.ACCEPT, HttpHeaderName.CACHE_CONTROL, + HttpHeaderName.CONNECTION, HttpHeaderName.CONTENT_LENGTH, HttpHeaderName.CONTENT_TYPE, HttpHeaderName.DATE, + HttpHeaderName.ETAG, HttpHeaderName.EXPIRES, HttpHeaderName.IF_MATCH, HttpHeaderName.IF_MODIFIED_SINCE, HttpHeaderName.IF_NONE_MATCH, HttpHeaderName.IF_UNMODIFIED_SINCE, HttpHeaderName.LAST_MODIFIED, HttpHeaderName.PRAGMA, HttpHeaderName.RETRY_AFTER, HttpHeaderName.SERVER, HttpHeaderName.TRANSFER_ENCODING, HttpHeaderName.USER_AGENT, HttpHeaderName.WWW_AUTHENTICATE); diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java index 117f54d58fd1d..679d30246ee7a 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java @@ -66,7 +66,7 @@ * 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 after - * {@link HttpRetryPolicy} and {@link HttpRedirectPolicy} so that it's executed on each try (redirect), and logging happens + * {@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}. @@ -136,7 +136,7 @@ */ public final class HttpInstrumentationPolicy implements HttpPipelinePolicy { private static final ClientLogger LOGGER = new ClientLogger(HttpInstrumentationPolicy.class); - private static final HttpLogOptions DEFAULT_HTTP_LOG_OPTIONS = new HttpLogOptions(); + private static final HttpLogOptions DEFAULT_LOG_OPTIONS = new HttpLogOptions(); private static final String LIBRARY_NAME; private static final String LIBRARY_VERSION; private static final LibraryInstrumentationOptions LIBRARY_OPTIONS; @@ -183,7 +183,7 @@ public HttpInstrumentationPolicy(InstrumentationOptions instrumentationOption this.tracer = instrumentation.getTracer(); this.traceContextPropagator = instrumentation.getW3CTraceContextPropagator(); - HttpLogOptions logOptionsToUse = logOptions == null ? DEFAULT_HTTP_LOG_OPTIONS : logOptions; + HttpLogOptions logOptionsToUse = logOptions == null ? DEFAULT_LOG_OPTIONS : logOptions; this.httpLogDetailLevel = logOptionsToUse.getLogLevel(); this.allowedHeaderNames = logOptionsToUse.getAllowedHeaderNames(); this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames() @@ -209,29 +209,33 @@ public Response process(HttpRequest request, HttpPipelineNextPolicy next) { int tryCount = HttpRequestAccessHelper.getTryCount(request); final long requestContentLength = getContentLength(logger, request.getBody(), request.getHeaders(), true); - InstrumentationContext context + InstrumentationContext instrumentationContext = request.getRequestOptions() == null ? null : request.getRequestOptions().getInstrumentationContext(); Span span = Span.noop(); if (isTracingEnabled) { - span = startHttpSpan(request, redactedUrl, context); - context = span.getInstrumentationContext(); - request.getRequestOptions().setInstrumentationContext(context); + if (request.getRequestOptions() == null || request.getRequestOptions() == RequestOptions.none()) { + request.setRequestOptions(new RequestOptions()); + } + + span = startHttpSpan(request, redactedUrl, instrumentationContext); + instrumentationContext = span.getInstrumentationContext(); + request.getRequestOptions().setInstrumentationContext(instrumentationContext); } - // even if tracing is disabled, we could have valid context to propagate - // explicitly provided by the application. - if (context != null && context.isValid()) { - traceContextPropagator.inject(context, request.getHeaders(), SETTER); + // 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); } - logRequest(logger, request, startNs, requestContentLength, redactedUrl, tryCount, context); + logRequest(logger, request, startNs, requestContentLength, redactedUrl, tryCount, instrumentationContext); try (TracingScope scope = span.makeCurrent()) { Response response = next.process(); if (response == null) { LOGGER.atError() - .setContext(span.getInstrumentationContext()) + .setInstrumentationContext(span.getInstrumentationContext()) .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod()) .addKeyValue(URL_FULL_KEY, redactedUrl) .log( @@ -241,22 +245,19 @@ public Response process(HttpRequest request, HttpPipelineNextPolicy next) { } addDetails(request, response, tryCount, span); - response = logResponse(logger, response, startNs, requestContentLength, redactedUrl, tryCount, context); + response = logResponse(logger, response, startNs, requestContentLength, redactedUrl, tryCount, + instrumentationContext); span.end(); return response; } catch (RuntimeException t) { - var ex = logException(logger, request, null, t, startNs, null, requestContentLength, redactedUrl, tryCount, - context); span.end(unwrap(t)); - throw ex; + // 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, InstrumentationContext context) { - if (request.getRequestOptions() == null || request.getRequestOptions() == RequestOptions.none()) { - request.setRequestOptions(new RequestOptions()); - } - SpanBuilder spanBuilder = tracer.spanBuilder(request.getHttpMethod().toString(), CLIENT, context) .setAttribute(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod().toString()) .setAttribute(URL_FULL_KEY, sanitizedUrl) @@ -352,14 +353,14 @@ private static Map getProperties(String propertiesFileName) { } private void logRequest(ClientLogger logger, HttpRequest request, long startNanoTime, long requestContentLength, - String redactedUrl, int tryCount, InstrumentationContext context) { + 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) - .setContext(context) + .setInstrumentationContext(context) .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod()) .addKeyValue(URL_FULL_KEY, redactedUrl) .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount) @@ -383,7 +384,7 @@ private void logRequest(ClientLogger logger, HttpRequest request, long startNano } private Response logResponse(ClientLogger logger, Response response, long startNanoTime, - long requestContentLength, String redactedUrl, int tryCount, InstrumentationContext context) { + long requestContentLength, String redactedUrl, int tryCount, InstrumentationContext context) { ClientLogger.LoggingEvent logBuilder = logger.atLevel(HTTP_RESPONSE_LOG_LEVEL); if (httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { return response; @@ -394,7 +395,7 @@ private Response logResponse(ClientLogger logger, Response response, long // 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) - .setContext(context) + .setInstrumentationContext(context) .addKeyValue(HTTP_REQUEST_METHOD_KEY, response.getRequest().getHttpMethod()) .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount) .addKeyValue(URL_FULL_KEY, redactedUrl) @@ -426,8 +427,8 @@ private Response logResponse(ClientLogger logger, Response response, long } private T logException(ClientLogger logger, HttpRequest request, Response response, - T throwable, long startNanoTime, Long responseStartNanoTime, long requestContentLength, String redactedUrl, - int tryCount, InstrumentationContext context) { + 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) { @@ -435,23 +436,21 @@ private T logException(ClientLogger logger, HttpRequest re } log.setEventName(HTTP_RESPONSE_EVENT_NAME) - .setContext(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())); + .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()); + 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.addKeyValue(HTTP_REQUEST_TIME_TO_RESPONSE_KEY, getDurationMs(startNanoTime, responseStartNanoTime)); } } 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 9a891f67073aa..0000000000000 --- 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.instrumentation.logging.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/HttpRedirectPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRedirectPolicy.java index d18d820328e02..fbd3e84b3171a 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 @@ -180,7 +180,7 @@ private void createRedirectRequest(Response redirectResponse) { private void logRedirect(ClientLogger logger, boolean lastAttempt, String redirectUri, int tryCount, HttpMethod method, String message, InstrumentationContext context) { - ClientLogger.LoggingEventBuilder log = lastAttempt ? logger.atWarning() : logger.atVerbose(); + 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) @@ -188,7 +188,7 @@ private void logRedirect(ClientLogger logger, boolean lastAttempt, String redire .addKeyValue(HTTP_RESPONSE_HEADER_LOCATION_KEY, redactUri(redirectUri)) .addKeyValue(RETRY_WAS_LAST_ATTEMPT_KEY, lastAttempt) .setEventName(HTTP_REDIRECT_EVENT_NAME) - .setContext(context) + .setInstrumentationContext(context) .log(message); } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRequestLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRequestLogger.java deleted file mode 100644 index 2c678e9550a73..0000000000000 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRequestLogger.java +++ /dev/null @@ -1,37 +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.HttpRequest; -import io.clientcore.core.instrumentation.logging.ClientLogger; - -/** - * Manages logging HTTP requests in {@link HttpInstrumentationPolicy}. - */ -@FunctionalInterface -public interface HttpRequestLogger { - /** - * Gets the {@link ClientLogger.LogLevel} used to log the HTTP request. - *

- * By default, this will return {@link ClientLogger.LogLevel#INFORMATIONAL}. - * - * @param request The request being logged. - * @return The {@link ClientLogger.LogLevel} used to log the HTTP request. - */ - default ClientLogger.LogLevel getLogLevel(HttpRequest request) { - return ClientLogger.LogLevel.VERBOSE; - } - - /** - * Logs the HTTP request. - *

- * To get the {@link ClientLogger.LogLevel} used to log the HTTP request use {@link #getLogLevel(HttpRequest)}. - * - * @param logger The {@link ClientLogger} used to log the HTTP request. - * @param request The request being logged. - * @param redactedUrl The redacted URL of the request. - * @param tryCount The number of times the request has been attempted. - */ - void logRequest(ClientLogger logger, HttpRequest request, String redactedUrl, int tryCount); -} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpResponseLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpResponseLogger.java deleted file mode 100644 index 7ebf8e4a3fb17..0000000000000 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpResponseLogger.java +++ /dev/null @@ -1,57 +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.HttpRequest; -import io.clientcore.core.http.models.Response; -import io.clientcore.core.instrumentation.logging.ClientLogger; - -/** - * Manages logging HTTP responses in {@link HttpInstrumentationPolicy}. - */ -public interface HttpResponseLogger { - /** - * Gets the {@link ClientLogger.LogLevel} used to log the HTTP response. - *

- * By default, this will return {@link ClientLogger.LogLevel#INFORMATIONAL}. - * - * @param response The response being logged. - * @return The {@link ClientLogger.LogLevel} used to log the HTTP response. - */ - default ClientLogger.LogLevel getLogLevel(Response response) { - return ClientLogger.LogLevel.INFORMATIONAL; - } - - /** - * Logs the HTTP response. - *

- * To get the {@link ClientLogger.LogLevel} used to log the HTTP response use {@link #getLogLevel(Response)} . - * - * @param logger The {@link ClientLogger} used to log the response. - * @param response The response being logged. - * @param startNanoTime The start time of the HTTP call captured via {@link System#nanoTime()}. - * @param headersNanoTime The time when headers were received captured via {@link System#nanoTime()}. {@code null} if headers were not received. - * @param redactedUrl The sanitized URL of the HTTP call. - * @param tryCount The try count of the HTTP call starting from 0. - */ - void logResponse(ClientLogger logger, Response response, long startNanoTime, long headersNanoTime, - String redactedUrl, int tryCount); - - /** - * Logs the HTTP request exception. If logging request body is enabled, it captures - *

- * To get the {@link ClientLogger.LogLevel} used to log the HTTP response use {@link #getLogLevel(Response)} . - * - * @param logger The {@link ClientLogger} used to log the response. - * @param request The request instance. - * @param response The response being logged or {@code null} if exception was thrown before response was received. - * @param throwable The exception that was thrown. - * @param startNanoTime The start time of the HTTP call captured via {@link System#nanoTime()}. - * @param headersNanoTime The time when headers were received captured via {@link System#nanoTime()}. {@code null} if headers were not received. - * @param sanitizedUrl The sanitized URL of the HTTP call. - * @param tryCount The try count of the HTTP call starting from 0. - */ - void logException(ClientLogger logger, HttpRequest request, Response response, Throwable throwable, - long startNanoTime, Long headersNanoTime, String sanitizedUrl, int tryCount); -} 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 dea3e33191713..62eee5b36d269 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 @@ -216,7 +216,7 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex return attempt(httpRequest, next, tryCount + 1, suppressed); } else { if (tryCount >= maxRetries) { - // TODO (limolkova): do we have better heuristic to determine if we're not retrying because of error + // 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); } @@ -283,14 +283,14 @@ private boolean shouldRetryException(Exception exception, int tryCount, List instrumentationO if (providedLogger instanceof ClientLogger) { return (ClientLogger) providedLogger; } - Map libraryContext = new HashMap<>(2); - libraryContext.put("library.version", libraryOptions.getLibraryVersion()); - libraryContext.put("library.instrumentation.schema_url", libraryOptions.getSchemaUrl()); - - return new ClientLogger(libraryOptions.getLibraryName() + ".tracing", libraryContext); + return new ClientLogger(libraryOptions.getLibraryName() + ".tracing"); } /** 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 5687999792964..0000000000000 --- 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/instrumentation/logging/ClientLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java index e5ee2b2d4887c..7e8f0ca257cbb 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java @@ -67,19 +67,8 @@ 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. - * @throws RuntimeException when logging configuration is invalid depending on SLF4J implementation. - */ - public ClientLogger(String className, Map context) { logger = new Slf4jLoggerShim(getClassPathFromClassName(className)); - globalContext = context == null ? null : Collections.unmodifiableMap(context); + globalContext = null; } /** @@ -456,7 +445,7 @@ public LoggingEvent addKeyValue(String key, Supplier valueSupplier) { * @param context operation context. * @return The updated {@code LoggingEventBuilder} object. */ - public LoggingEvent setContext(InstrumentationContext context) { + public LoggingEvent setInstrumentationContext(InstrumentationContext context) { this.context = context; return this; } @@ -504,32 +493,30 @@ public void log(String message) { public T log(String message, T throwable) { if (this.isEnabled) { boolean isDebugEnabled = logger.canLogAtLevel(LogLevel.VERBOSE); - setThrowableInternal(throwable, isDebugEnabled); - // TODO: we should not trace-id/span-id (by default) when otel is enabled? + if (throwable != null) { + 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_KEY, stackTrace.toString()); + } + } logger.performLogging(level, getMessageWithContext(message), isDebugEnabled ? throwable : null); } return throwable; } - private void setThrowableInternal(Throwable throwable, boolean isDebugEnabled) { - if (throwable != null) { - 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_KEY, stackTrace.toString()); - } - } - } - private String getMessageWithContext(String message) { if (message == null) { message = ""; } - // TODO (limolkova) set context from implicit current span 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()); } @@ -554,7 +541,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/module-info.java b/sdk/clientcore/core/src/main/java/module-info.java index 025ef8b177a02..dee608a9a5b6e 100644 --- a/sdk/clientcore/core/src/main/java/module-info.java +++ b/sdk/clientcore/core/src/main/java/module-info.java @@ -5,9 +5,9 @@ * This module provides core functionality for the Java SDK. */ module io.clientcore.core { + requires transitive java.xml; requires java.net.http; - requires java.desktop; // public API surface area exports io.clientcore.core.annotation; 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 index 394316b605a73..99fa479e25119 100644 --- 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 @@ -169,7 +169,7 @@ public void testTryCount() throws IOException { HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options); HttpRequest request = createRequest(HttpMethod.GET, URI, logger); - HttpRequestAccessHelper.setRetryCount(request, 42); + HttpRequestAccessHelper.setTryCount(request, 42); Response response = pipeline.send(request); response.close(); diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java index 971897db13d55..8500e08922e42 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java @@ -759,7 +759,7 @@ public void logAtLevel(LogLevel level) { public void logWithContext() { ClientLogger logger = setupLogLevelAndGetLogger(LogLevel.INFORMATIONAL); InstrumentationContext context = createRandomInstrumentationContext(); - logger.atInfo().setContext(context).addKeyValue("connectionId", "foo").log("message"); + logger.atInfo().setInstrumentationContext(context).addKeyValue("connectionId", "foo").log("message"); Map expectedMessage = new HashMap<>(); expectedMessage.put("message", "message"); @@ -775,7 +775,7 @@ public void logWithContext() { public void logWithInvalidContext() { ClientLogger logger = setupLogLevelAndGetLogger(LogLevel.INFORMATIONAL); logger.atInfo() - .setContext(createInvalidInstrumentationContext()) + .setInstrumentationContext(createInvalidInstrumentationContext()) .addKeyValue("connectionId", "foo") .log("message"); diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/HttpLoggingPolicyTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/HttpLoggingPolicyTests.java deleted file mode 100644 index bbc8faf201a2e..0000000000000 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/HttpLoggingPolicyTests.java +++ /dev/null @@ -1,618 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// we want to access package-private ClientLogger constructor -package io.clientcore.core.instrumentation.logging; - -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.RequestOptions; -import io.clientcore.core.http.models.Response; -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.implementation.AccessibleByteArrayOutputStream; -import io.clientcore.core.implementation.http.HttpRequestAccessHelper; -import io.clientcore.core.implementation.instrumentation.DefaultLogger; -import io.clientcore.core.serialization.json.JsonOptions; -import io.clientcore.core.serialization.json.JsonProviders; -import io.clientcore.core.serialization.json.JsonReader; -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.io.PrintStream; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Stream; - -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 HttpLoggingPolicyTests { - 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 final AccessibleByteArrayOutputStream logCaptureStream; - - public HttpLoggingPolicyTests() { - this.logCaptureStream = new AccessibleByteArrayOutputStream(); - } - - @ParameterizedTest - @MethodSource("disabledHttpLoggingSource") - public void testDisabledHttpLogging(ClientLogger.LogLevel logLevel, HttpLogOptions.HttpLogDetailLevel httpLogLevel) - throws IOException { - ClientLogger logger = setupLogLevelAndGetLogger(logLevel); - - HttpPipeline pipeline = createPipeline(new HttpLogOptions().setLogLevel(httpLogLevel)); - HttpRequest request = new HttpRequest(HttpMethod.GET, URI); - request.setRequestOptions(new RequestOptions().setLogger(logger)); - - pipeline.send(request).close(); - - assertEquals(0, parseLogMessages().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); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC) - .setAllowedQueryParamNames(allowedParams); - - HttpPipeline pipeline = createPipeline(options); - - HttpRequest request = createRequest(HttpMethod.GET, URI, logger); - Response response = pipeline.send(request); - response.close(); - - List> logMessages = parseLogMessages(); - assertEquals(2, logMessages.size()); - - assertRequestLog(logMessages.get(0), expectedUri, request); - assertEquals(6, logMessages.get(0).size()); - - assertResponseLog(logMessages.get(1), expectedUri, response); - assertEquals(10, logMessages.get(1).size()); - } - - @Test - public void testTryCount() throws IOException { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC); - - HttpPipeline pipeline = createPipeline(options); - - HttpRequest request = createRequest(HttpMethod.GET, URI, logger); - HttpRequestAccessHelper.setTryCount(request, 42); - Response response = pipeline.send(request); - response.close(); - - List> logMessages = parseLogMessages(); - assertEquals(2, logMessages.size()); - - assertEquals(42, logMessages.get(0).get("tryCount")); - assertEquals(42, logMessages.get(1).get("tryCount")); - } - - @ParameterizedTest - @MethodSource("testExceptionSeverity") - public void testConnectionException(ClientLogger.LogLevel level, boolean expectExceptionLog) { - ClientLogger logger = setupLogLevelAndGetLogger(level); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS); - - RuntimeException expectedException = new RuntimeException("socket error"); - HttpPipeline pipeline = createPipeline(options, request -> { - throw expectedException; - }); - - HttpRequest request = createRequest(HttpMethod.GET, URI, logger); - - assertThrows(RuntimeException.class, () -> pipeline.send(request)); - - List> logMessages = parseLogMessages(); - if (!expectExceptionLog) { - assertEquals(0, logMessages.size()); - } else { - assertExceptionLog(logMessages.get(0), REDACTED_URI, request, expectedException); - } - } - - @ParameterizedTest - @MethodSource("testExceptionSeverity") - public void testRequestBodyException(ClientLogger.LogLevel level, boolean expectExceptionLog) { - ClientLogger logger = setupLogLevelAndGetLogger(level); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); - - TestStream requestStream = new TestStream(1024, new IOException("socket error")); - BinaryData requestBody = BinaryData.fromStream(requestStream, 1024L); - HttpPipeline pipeline = createPipeline(options); - - HttpRequest request = createRequest(HttpMethod.POST, URI, logger); - request.setBody(requestBody); - - Exception actualException = assertThrows(RuntimeException.class, () -> pipeline.send(request)); - - List> logMessages = parseLogMessages(); - if (!expectExceptionLog) { - assertEquals(0, logMessages.size()); - } else { - assertExceptionLog(logMessages.get(0), REDACTED_URI, request, actualException); - } - } - - @ParameterizedTest - @MethodSource("testExceptionSeverity") - public void testResponseBodyException(ClientLogger.LogLevel level, boolean expectExceptionLog) { - ClientLogger logger = setupLogLevelAndGetLogger(level); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); - - TestStream responseStream = new TestStream(1024, new IOException("socket error")); - HttpPipeline pipeline = createPipeline(options, - request -> new MockHttpResponse(request, 200, BinaryData.fromStream(responseStream, 1024L))); - - HttpRequest request = createRequest(HttpMethod.GET, URI, logger); - - Response response = pipeline.send(request); - Exception actualException = assertThrows(RuntimeException.class, () -> response.getBody().toString()); - - List> logMessages = parseLogMessages(); - if (!expectExceptionLog) { - assertEquals(0, logMessages.size()); - } else { - assertResponseAndExceptionLog(logMessages.get(0), REDACTED_URI, response, actualException); - } - } - - @Test - public void testResponseBodyLoggingOnClose() throws IOException { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); - - HttpPipeline pipeline = createPipeline(options, - request -> new MockHttpResponse(request, 200, BinaryData.fromString("Response body"))); - - HttpRequest request = createRequest(HttpMethod.GET, URI, logger); - - Response response = pipeline.send(request); - assertEquals(0, parseLogMessages().size()); - - response.close(); - - List> logMessages = parseLogMessages(); - assertResponseLog(logMessages.get(0), REDACTED_URI, response); - } - - @Test - public void testResponseBodyRequestedMultipleTimes() { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); - - HttpPipeline pipeline = createPipeline(options, - request -> new MockHttpResponse(request, 200, BinaryData.fromString("Response body"))); - - HttpRequest request = createRequest(HttpMethod.GET, URI, logger); - - Response response = pipeline.send(request); - - for (int i = 0; i < 3; i++) { - BinaryData data = response.getBody(); - assertEquals(1, parseLogMessages().size()); - assertEquals("Response body", data.toString()); - } - } - - @ParameterizedTest - @MethodSource("allowQueryParamSource") - public void testBasicHttpLoggingRequestOff(Set allowedParams, String expectedUri) throws IOException { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC) - .setAllowedQueryParamNames(allowedParams); - - HttpPipeline pipeline = createPipeline(options); - - HttpRequest request = createRequest(HttpMethod.POST, URI, logger); - Response response = pipeline.send(request); - response.close(); - - List> logMessages = parseLogMessages(); - assertEquals(1, logMessages.size()); - - assertResponseLog(logMessages.get(0), expectedUri, response); - assertEquals(10, logMessages.get(0).size()); - } - - @ParameterizedTest - @MethodSource("allowedHeaders") - public void testHeadersHttpLogging(Set allowedHeaders) throws IOException { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS) - .setAllowedHeaderNames(allowedHeaders); - - HttpPipeline pipeline = createPipeline(options); - - HttpRequest request = createRequest(HttpMethod.GET, URI, logger); - request.getHeaders().set(CUSTOM_REQUEST_ID, "12345"); - Response response = pipeline.send(request); - response.close(); - - List> logMessages = parseLogMessages(); - assertEquals(2, logMessages.size()); - - Map requestLog = logMessages.get(0); - assertRequestLog(requestLog, REDACTED_URI, request); - for (HttpHeader header : request.getHeaders()) { - if (allowedHeaders.contains(header.getName())) { - assertEquals(header.getValue(), requestLog.get(header.getName().toString())); - } else { - assertEquals("REDACTED", requestLog.get(header.getName().toString())); - } - } - - Map responseLog = logMessages.get(1); - assertResponseLog(responseLog, REDACTED_URI, response); - for (HttpHeader header : response.getHeaders()) { - if (allowedHeaders.contains(header.getName())) { - assertEquals(header.getValue(), responseLog.get(header.getName().toString())); - } else { - assertEquals("REDACTED", responseLog.get(header.getName().toString())); - } - } - } - - @Test - public void testStringBodyLogging() throws IOException { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); - - HttpPipeline pipeline = createPipeline(options, - request -> new MockHttpResponse(request, 200, BinaryData.fromString("Response body"))); - - HttpRequest request = createRequest(HttpMethod.PUT, URI, logger); - request.setBody(BinaryData.fromString("Request body")); - - Response response = pipeline.send(request); - response.close(); - - assertEquals("Response body", response.getBody().toString()); - - List> logMessages = parseLogMessages(); - assertEquals(2, logMessages.size()); - - Map requestLog = logMessages.get(0); - assertRequestLog(requestLog, REDACTED_URI, request); - assertEquals("Request body", requestLog.get("body")); - - Map responseLog = logMessages.get(1); - assertResponseLog(responseLog, REDACTED_URI, response); - assertEquals("Response body", responseLog.get("body")); - } - - @Test - public void testStreamBodyLogging() { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); - - BinaryData responseBody = BinaryData.fromString("Response body"); - TestStream responseStream = new TestStream(responseBody); - - HttpPipeline pipeline = createPipeline(options, request -> new MockHttpResponse(request, 200, - BinaryData.fromStream(responseStream, responseBody.getLength()))); - - BinaryData requestBody = BinaryData.fromString("Request body"); - TestStream requestStream = new TestStream(requestBody); - HttpRequest request = createRequest(HttpMethod.PUT, URI, logger); - request.setBody(BinaryData.fromStream(requestStream, requestBody.getLength())); - assertFalse(request.getBody().isReplayable()); - - Response response = pipeline.send(request); - assertTrue(request.getBody().isReplayable()); - assertTrue(response.getBody().isReplayable()); - - assertEquals("Response body", response.getBody().toString()); - - List> logMessages = parseLogMessages(); - assertEquals(2, logMessages.size()); - - Map requestLog = logMessages.get(0); - assertRequestLog(requestLog, REDACTED_URI, request); - assertEquals("Request body", requestLog.get("body")); - - Map responseLog = logMessages.get(1); - assertResponseLog(responseLog, REDACTED_URI, response); - assertEquals("Response body", responseLog.get("body")); - - assertEquals(requestBody.getLength(), requestStream.getPosition()); - assertEquals(responseBody.getLength(), responseStream.getPosition()); - } - - @Test - public void testHugeBodyNotLogged() throws IOException { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); - - TestStream requestStream = new TestStream(1024 * 1024); - TestStream responseStream = new TestStream(1024 * 1024); - HttpPipeline pipeline = createPipeline(options, - request -> new MockHttpResponse(request, 200, BinaryData.fromStream(responseStream, (long) 1024 * 1024))); - - HttpRequest request = createRequest(HttpMethod.PUT, URI, logger); - - request.setBody(BinaryData.fromStream(requestStream, 1024 * 1024L)); - - Response response = pipeline.send(request); - response.close(); - - List> logMessages = parseLogMessages(); - assertEquals(2, logMessages.size()); - - Map requestLog = logMessages.get(0); - assertRequestLog(requestLog, REDACTED_URI, request); - assertNull(requestLog.get("body")); - assertEquals(0, requestStream.getPosition()); - - Map responseLog = logMessages.get(1); - assertResponseLog(responseLog, REDACTED_URI, response); - assertNull(responseLog.get("body")); - assertEquals(0, responseStream.getPosition()); - } - - @Test - public void testBodyWithUnknownLengthNotLogged() throws IOException { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE); - HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); - - TestStream requestStream = new TestStream(1024); - TestStream responseStream = new TestStream(1024); - HttpPipeline pipeline = createPipeline(options, - request -> new MockHttpResponse(request, 200, BinaryData.fromStream(responseStream))); - - HttpRequest request = createRequest(HttpMethod.PUT, URI, logger); - request.getHeaders().set(HttpHeaderName.CONTENT_LENGTH, "1024"); - - request.setBody(BinaryData.fromStream(requestStream)); - - Response response = pipeline.send(request); - response.close(); - - List> logMessages = parseLogMessages(); - assertEquals(2, logMessages.size()); - - Map requestLog = logMessages.get(0); - assertRequestLog(requestLog, REDACTED_URI, request); - assertNull(requestLog.get("body")); - assertEquals(0, requestStream.getPosition()); - - Map responseLog = logMessages.get(1); - assertResponseLog(responseLog, REDACTED_URI, response); - assertNull(responseLog.get("body")); - assertEquals(0, responseStream.getPosition()); - } - - public static Stream allowQueryParamSource() { - Set twoParams = new HashSet<>(); - twoParams.add("param"); - twoParams.add("api-version"); - - return Stream.of(Arguments.of(twoParams, "https://example.com?param=value&api-version=42"), - Arguments.of(DEFAULT_ALLOWED_QUERY_PARAMS, REDACTED_URI), - Arguments.of(Collections.emptySet(), "https://example.com?param=REDACTED&api-version=REDACTED")); - } - - public static Stream> allowedHeaders() { - Set reducedSet = new HashSet<>(); - reducedSet.add(CUSTOM_REQUEST_ID); - - Set expandedSet = new HashSet<>(DEFAULT_ALLOWED_HEADERS); - expandedSet.add(CUSTOM_REQUEST_ID); - - return Stream.of(reducedSet, DEFAULT_ALLOWED_HEADERS, expandedSet); - } - - public static Stream testExceptionSeverity() { - return Stream.of(Arguments.of(ClientLogger.LogLevel.INFORMATIONAL, true), - Arguments.of(ClientLogger.LogLevel.WARNING, true), Arguments.of(ClientLogger.LogLevel.ERROR, false)); - } - - private static class TestStream extends InputStream { - private final byte[] content; - private int length; - private final IOException throwOnRead; - private int position = 0; - - TestStream(int length) { - this.length = length; - this.throwOnRead = null; - this.content = new byte[length]; - } - - TestStream(BinaryData content) { - this.length = content.getLength().intValue(); - this.throwOnRead = null; - this.content = content.toBytes(); - } - - TestStream(int length, IOException throwOnRead) { - this.length = length; - this.throwOnRead = throwOnRead; - this.content = new byte[length]; - } - - @Override - public int read() throws IOException { - if (throwOnRead != null) { - throw throwOnRead; - } - - if (position >= length) { - return -1; - } - - position++; - return content[position - 1]; - } - - public long getPosition() { - return position; - } - } - - private void assertRequestLog(Map log, String expectedUri, HttpRequest request) { - assertEquals("http.request", log.get("event.name")); - assertEquals(expectedUri, log.get("uri")); - assertEquals(0, (int) log.get("tryCount")); - - assertEquals(getLength(request.getBody(), request.getHeaders()), (int) log.get("requestContentLength")); - assertEquals(request.getHttpMethod().toString(), log.get("method")); - assertEquals("", log.get("message")); - } - - private long getLength(BinaryData body, HttpHeaders headers) { - if (body != null && body.getLength() != null) { - return body.getLength(); - } - - String contentLength = headers.getValue(HttpHeaderName.CONTENT_LENGTH); - if (contentLength != null) { - return Long.parseLong(contentLength); - } - - return 0; - } - - private void assertResponseLog(Map log, String expectedUri, Response response) { - assertEquals("http.response", log.get("event.name")); - assertEquals(expectedUri, log.get("uri")); - assertEquals(0, (int) log.get("tryCount")); - - Long expectedRequestLength = getLength(response.getRequest().getBody(), response.getRequest().getHeaders()); - - assertEquals(expectedRequestLength, (int) log.get("requestContentLength")); - assertEquals(response.getRequest().getHttpMethod().toString(), log.get("method")); - - assertEquals(response.getStatusCode(), log.get("statusCode")); - - Long expectedResponseLength = getLength(response.getBody(), response.getHeaders()); - assertEquals(expectedResponseLength, (int) log.get("responseContentLength")); - assertInstanceOf(Double.class, log.get("timeToResponseMs")); - assertInstanceOf(Double.class, log.get("durationMs")); - assertEquals("", log.get("message")); - } - - private void assertResponseAndExceptionLog(Map log, String expectedUri, Response response, - Throwable error) { - assertEquals("http.response", log.get("event.name")); - assertEquals(expectedUri, log.get("uri")); - assertEquals(0, (int) log.get("tryCount")); - - Long expectedRequestLength = getLength(response.getRequest().getBody(), response.getRequest().getHeaders()); - - assertEquals(expectedRequestLength, (int) log.get("requestContentLength")); - assertEquals(response.getRequest().getHttpMethod().toString(), log.get("method")); - - assertEquals(response.getStatusCode(), log.get("statusCode")); - - assertInstanceOf(Double.class, log.get("timeToResponseMs")); - assertInstanceOf(Double.class, log.get("durationMs")); - assertEquals(error.getMessage(), log.get("exception.message")); - assertEquals(error.getClass().getCanonicalName(), log.get("exception.type")); - assertEquals("", log.get("message")); - } - - private void assertExceptionLog(Map log, String expectedUri, HttpRequest request, Throwable error) { - assertEquals("http.response", log.get("event.name")); - assertEquals(expectedUri, log.get("uri")); - assertEquals(0, (int) log.get("tryCount")); - - Long expectedRequestLength = getLength(request.getBody(), request.getHeaders()); - assertEquals(expectedRequestLength, (int) log.get("requestContentLength")); - assertEquals(request.getHttpMethod().toString(), log.get("method")); - - assertNull(log.get("statusCode")); - assertNull(log.get("responseContentLength")); - assertNull(log.get("timeToResponseMs")); - assertInstanceOf(Double.class, log.get("durationMs")); - assertEquals(error.getMessage(), log.get("exception.message")); - assertEquals(error.getClass().getCanonicalName(), log.get("exception.type")); - - assertEquals("", log.get("message")); - } - - private ClientLogger setupLogLevelAndGetLogger(ClientLogger.LogLevel logLevelToSet) { - DefaultLogger logger - = new DefaultLogger(ClientLogger.class.getName(), new PrintStream(logCaptureStream), logLevelToSet); - - return new ClientLogger(logger, null); - } - - private HttpPipeline createPipeline(HttpLogOptions options) { - return createPipeline(options, request -> { - if (request.getBody() != null) { - request.getBody().toString(); - } - return new MockHttpResponse(request, 200, BinaryData.fromString("Hello, world!")); - }); - } - - private HttpPipeline createPipeline(HttpLogOptions options, Function> httpClient) { - return new HttpPipelineBuilder().policies(new HttpLoggingPolicy(options)).httpClient(httpClient::apply).build(); - } - - private List> parseLogMessages() { - String fullLog = logCaptureStream.toString(StandardCharsets.UTF_8); - return fullLog.lines().map(this::parseLogLine).toList(); - } - - private Map parseLogLine(String logLine) { - String messageJson = logLine.substring(logLine.indexOf(" - ") + 3); - System.out.println(messageJson); - try (JsonReader reader = JsonProviders.createReader(messageJson, new JsonOptions())) { - return reader.readMap(JsonReader::readUntyped); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - private HttpRequest createRequest(HttpMethod method, String url, ClientLogger logger) { - HttpRequest request = new HttpRequest(method, url); - request.getHeaders().set(HttpHeaderName.CONTENT_TYPE, "application/json"); - request.getHeaders().set(HttpHeaderName.AUTHORIZATION, "Bearer {token}"); - request.setRequestOptions(new RequestOptions().setLogger(logger)); - - return request; - } -} diff --git a/sdk/clientcore/http-stress/src/main/java/module-info.java b/sdk/clientcore/http-stress/src/main/java/module-info.java index aad37b1fa3e6a..c7ad143213cec 100644 --- a/sdk/clientcore/http-stress/src/main/java/module-info.java +++ b/sdk/clientcore/http-stress/src/main/java/module-info.java @@ -20,7 +20,6 @@ requires io.opentelemetry.sdk.autoconfigure; requires io.opentelemetry.sdk.autoconfigure.spi; requires io.opentelemetry.sdk.trace; - requires java.instrument; exports io.clientcore.http.stress; exports io.clientcore.http.stress.util; diff --git a/sdk/clientcore/optional-dependency-tests/pom.xml b/sdk/clientcore/optional-dependency-tests/pom.xml index d6eece36d947b..1bbe325dffe4a 100644 --- a/sdk/clientcore/optional-dependency-tests/pom.xml +++ b/sdk/clientcore/optional-dependency-tests/pom.xml @@ -109,12 +109,6 @@ 1.37 test - - io.clientcore - core - 1.0.0-beta.2 - test - diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java index acf1439f91733..6b9cd3c528e04 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java @@ -189,20 +189,6 @@ public void explicitParent() throws Exception { assertEquals(parentData.getSpanId(), childData.getParentSpanId()); } - @Test - public void explicitParentWrongType() { - /*RequestOptions requestOptions - = new RequestOptions().putContext(TRACE_CONTEXT_KEY, "This is not a valid trace context"); - Span child = tracer.spanBuilder("child", INTERNAL, requestOptions).startSpan(); - child.end(); - - assertEquals(1, exporter.getFinishedSpanItems().size()); - SpanData childData = exporter.getFinishedSpanItems().get(0); - - assertEquals("child", childData.getName()); - assertFalse(childData.getParentSpanContext().isValid());*/ - } - public static Stream kindSource() { return Stream.of(Arguments.of(SpanKind.INTERNAL, io.opentelemetry.api.trace.SpanKind.INTERNAL), Arguments.of(SpanKind.CLIENT, io.opentelemetry.api.trace.SpanKind.CLIENT), From 1c52a867e70c202a65f2ad8834ad3b4ed36d9f0d Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Mon, 6 Jan 2025 19:55:17 -0800 Subject: [PATCH 09/11] clean up --- .../core/http/pipeline/HttpRetryPolicy.java | 2 +- .../fallback/FallbackSpan.java | 6 +--- .../fallback/FallbackSpanBuilder.java | 2 +- .../fallback/FallbackTracer.java | 10 +++++- .../instrumentation/logging/ClientLogger.java | 28 ++++++++++++---- .../HttpInstrumentationLoggingTests.java | 33 +++++++++---------- .../FallbackInstrumentationTests.java | 19 +++++++---- .../logging/ClientLoggerTests.java | 6 +--- 8 files changed, 62 insertions(+), 44 deletions(-) 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 62eee5b36d269..398ae3857a395 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 @@ -183,7 +183,7 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex return attempt(httpRequest, next, tryCount + 1, suppressedLocal); } else { - logRetry(logger.atError(), tryCount, null, err, true, instrumentationContext); + logRetry(logger.atWarning(), tryCount, null, err, true, instrumentationContext); if (suppressed != null) { suppressed.forEach(err::addSuppressed); 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 index 80a6fdaae6388..33c420b318763 100644 --- 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 @@ -74,11 +74,7 @@ public void end(Throwable error) { } log.setEventName(SPAN_ENDED_EVENT_NAME); - if (error != null) { - log.log(null, error); - } else { - log.log(null); - } + log.log(null); } /** 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 index 85a9c8f119952..b2e67a3a5abcb 100644 --- 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 @@ -26,7 +26,7 @@ private FallbackSpanBuilder() { FallbackSpanBuilder(ClientLogger logger, String spanName, SpanKind spanKind, InstrumentationContext instrumentationContext) { this.parentSpanContext = FallbackSpanContext.fromInstrumentationContext(instrumentationContext); - this.log = logger.atInfo(); + this.log = logger.atVerbose(); if (log.isEnabled()) { log.addKeyValue(SPAN_NAME_KEY, spanName).addKeyValue(SPAN_KIND_KEY, spanKind.name()); if (parentSpanContext.isValid()) { 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 index f5dea1cc838e4..006d86ebeacf2 100644 --- 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 @@ -11,6 +11,9 @@ 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; @@ -28,7 +31,12 @@ private static ClientLogger getLogger(InstrumentationOptions instrumentationO if (providedLogger instanceof ClientLogger) { return (ClientLogger) providedLogger; } - return new ClientLogger(libraryOptions.getLibraryName() + ".tracing"); + + Map libraryContext = new HashMap<>(2); + libraryContext.put("library.name", libraryOptions.getLibraryName()); + libraryContext.put("library.version", libraryOptions.getLibraryVersion()); + + return new ClientLogger(libraryOptions.getLibraryName() + ".tracing", libraryContext); } /** diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java index 7e8f0ca257cbb..5974d6708f06a 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java @@ -67,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); } /** @@ -508,10 +520,6 @@ 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 @@ -523,10 +531,16 @@ private String getMessageWithContext(String message) { 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()) { 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 index 99fa479e25119..f66fbf73974c0 100644 --- 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 @@ -108,10 +108,10 @@ public void testBasicHttpLogging(Set allowedParams, String expectedUri) assertEquals(2, logMessages.size()); assertRequestLog(logMessages.get(0), expectedUri, request, null, 0); - assertEquals(8, logMessages.get(0).size()); + assertEquals(7, logMessages.get(0).size()); assertResponseLog(logMessages.get(1), expectedUri, response, 0); - assertEquals(12, logMessages.get(1).size()); + assertEquals(11, logMessages.get(1).size()); } @Test @@ -130,10 +130,10 @@ public void testHttpLoggingTracingDisabled() throws IOException { assertEquals(2, logMessages.size()); assertRequestLog(logMessages.get(0), request); - assertEquals(6, logMessages.get(0).size()); + assertEquals(5, logMessages.get(0).size()); assertResponseLog(logMessages.get(1), response); - assertEquals(10, logMessages.get(1).size()); + assertEquals(9, logMessages.get(1).size()); } @Test @@ -155,10 +155,10 @@ public void testHttpLoggingTracingDisabledCustomContext() throws IOException { assertEquals(2, logMessages.size()); assertRequestLog(logMessages.get(0), request); - assertEquals(8, logMessages.get(0).size()); + assertEquals(7, logMessages.get(0).size()); assertResponseLog(logMessages.get(1), response); - assertEquals(12, logMessages.get(1).size()); + assertEquals(11, logMessages.get(1).size()); } @Test @@ -306,7 +306,7 @@ public void testBasicHttpLoggingRequestOff(Set allowedParams, String exp assertEquals(1, logMessages.size()); assertResponseLog(logMessages.get(0), expectedUri, response, 0); - assertEquals(12, logMessages.get(0).size()); + assertEquals(11, logMessages.get(0).size()); } @ParameterizedTest @@ -624,7 +624,7 @@ public void tracingWithRedirects() throws IOException { assertResponseLog(logMessages.get(1), REDACTED_URI, 0, 302, firstRedirectContext.get()); assertRedirectLog(logMessages.get(2), 0, 3, true, "http://redirecthost/1?param=REDACTED&api-version=REDACTED", - HttpMethod.GET, "", parentContext); + HttpMethod.GET, null, parentContext); assertResponseLog(logMessages.get(4), "http://redirecthost/1?param=REDACTED&api-version=42", response, 0); } @@ -670,7 +670,7 @@ public void redirectToTheSameUri() throws IOException { List> logMessages = parseLogMessages(logCaptureStream); assertEquals(2, logMessages.size()); - assertRedirectLog(logMessages.get(0), 0, 3, true, "http://redirecthost/", HttpMethod.GET, "", parentContext); + assertRedirectLog(logMessages.get(0), 0, 3, true, "http://redirecthost/", HttpMethod.GET, null, parentContext); assertRedirectLog(logMessages.get(1), 1, 3, false, "http://redirecthost/", HttpMethod.GET, "Request was redirected more than once to the same URI.", parentContext); } @@ -695,8 +695,8 @@ public void redirectAttemptsExhausted() throws IOException { List> logMessages = parseLogMessages(logCaptureStream); assertEquals(3, logMessages.size()); - assertRedirectLog(logMessages.get(0), 0, 3, true, "http://redirecthost/1", HttpMethod.GET, "", parentContext); - assertRedirectLog(logMessages.get(1), 1, 3, true, "http://redirecthost/2", HttpMethod.GET, "", parentContext); + assertRedirectLog(logMessages.get(0), 0, 3, true, "http://redirecthost/1", HttpMethod.GET, null, parentContext); + assertRedirectLog(logMessages.get(1), 1, 3, true, "http://redirecthost/2", HttpMethod.GET, null, parentContext); assertRedirectLog(logMessages.get(2), 2, 3, false, "http://redirecthost/3", HttpMethod.GET, "Redirect attempts have been exhausted.", parentContext); } @@ -788,7 +788,7 @@ private void assertRequestLog(Map log, String expectedUri, HttpR assertEquals(getLength(request.getBody(), request.getHeaders()), (int) log.get("http.request.body.size")); assertEquals(request.getHttpMethod().toString(), log.get("http.request.method")); - assertEquals("", log.get("message")); + assertNull(log.get("message")); if (context == null) { context = request.getRequestOptions().getInstrumentationContext(); @@ -809,7 +809,7 @@ private void assertRetryLog(Map log, int tryCount, int maxAttemp assertTrue((boolean) log.get("retry.was_last_attempt")); } assertEquals(maxAttempts, log.get("retry.max_attempt_count")); - assertEquals("", log.get("message")); + assertNull(log.get("message")); assertTraceContext(log, context); } @@ -882,7 +882,7 @@ private void assertResponseLog(Map log, String expectedUri, int assertInstanceOf(Double.class, log.get("http.request.time_to_response")); assertInstanceOf(Double.class, log.get("http.request.duration")); - assertEquals("", log.get("message")); + assertNull(log.get("message")); assertTraceContext(log, context); } @@ -903,7 +903,7 @@ private void assertResponseAndExceptionLog(Map log, String expec assertInstanceOf(Double.class, log.get("http.request.duration")); assertEquals(error.getMessage(), log.get("exception.message")); assertEquals(error.getClass().getCanonicalName(), log.get("exception.type")); - assertEquals("", log.get("message")); + assertNull(log.get("message")); assertTraceContext(log, response.getRequest().getRequestOptions().getInstrumentationContext()); } @@ -927,8 +927,7 @@ private void assertExceptionLog(Map log, String expectedUri, Htt assertInstanceOf(Double.class, log.get("http.request.duration")); assertEquals(error.getMessage(), log.get("exception.message")); assertEquals(error.getClass().getCanonicalName(), log.get("exception.type")); - - assertEquals("", log.get("message")); + assertNull(log.get("message")); if (context == null) { context = request.getRequestOptions().getInstrumentationContext(); diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java index 651a4f0956867..84a301a12564c 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java @@ -45,6 +45,7 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -385,13 +386,14 @@ public void basicTracingLogsLevel(ClientLogger.LogLevel logLevel, boolean expect public static Stream logLevels() { return Stream.of(Arguments.of(ClientLogger.LogLevel.ERROR, false), - Arguments.of(ClientLogger.LogLevel.WARNING, false), Arguments.of(ClientLogger.LogLevel.INFORMATIONAL, true), + Arguments.of(ClientLogger.LogLevel.WARNING, false), + Arguments.of(ClientLogger.LogLevel.INFORMATIONAL, false), Arguments.of(ClientLogger.LogLevel.VERBOSE, true)); } @Test public void basicTracingLogsEnabled() { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); InstrumentationOptions options = new InstrumentationOptions<>().setProvider(logger); Instrumentation instrumentation = Instrumentation.create(options, DEFAULT_LIB_OPTIONS); Tracer tracer = instrumentation.getTracer(); @@ -410,11 +412,16 @@ public void basicTracingLogsEnabled() { Map loggedSpan = logMessages.get(0); assertSpanLog(loggedSpan, "test-span", "INTERNAL", span.getInstrumentationContext(), null); assertTrue((Double) loggedSpan.get("span.duration") <= duration.toNanos() / 1_000_000.0); + + // lib info is null since custom logger is provided, we can't add global context. + // we'll add it in user app in common case + assertNull(loggedSpan.get("library.name")); + assertNull(loggedSpan.get("library.version")); } @Test public void tracingWithAttributesLogsEnabled() { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); InstrumentationOptions options = new InstrumentationOptions<>().setProvider(logger); Tracer tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); @@ -452,7 +459,7 @@ public void tracingWithAttributesLogsEnabled() { @Test public void tracingWithExceptionLogsEnabled() { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); InstrumentationOptions options = new InstrumentationOptions<>().setProvider(logger); Tracer tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); @@ -466,13 +473,11 @@ public void tracingWithExceptionLogsEnabled() { Map loggedSpan = logMessages.get(0); assertSpanLog(loggedSpan, "test-span", "SERVER", span.getInstrumentationContext(), exception.getClass().getCanonicalName()); - assertEquals(exception.getMessage(), loggedSpan.get("exception.message")); - assertEquals("java.io.IOException", loggedSpan.get("exception.type")); } @Test public void tracingLogsEnabledParent() { - ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.INFORMATIONAL, logCaptureStream); + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); InstrumentationOptions options = new InstrumentationOptions<>().setProvider(logger); Tracer tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java index 8500e08922e42..e5c1162ac44e8 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java @@ -262,7 +262,7 @@ public void logWithNullSupplier(LogLevel logLevel) { } String logValues = byteArraySteamToString(logCaptureStream); - assertMessage(Collections.singletonMap("message", ""), logValues, logLevel, logLevel); + assertMessage(Collections.emptyMap(), logValues, logLevel, logLevel); } @ParameterizedTest @@ -443,7 +443,6 @@ public void logWithContextNullMessage() { logger.atVerbose().addKeyValue("connectionId", "foo").addKeyValue("linkName", true).log(null); Map expectedMessage = new HashMap<>(); - expectedMessage.put("message", ""); expectedMessage.put("connectionId", "foo"); expectedMessage.put("linkName", true); @@ -507,7 +506,6 @@ public void logWithContextNullSupplier() { logger.atError().addKeyValue("connectionId", "foo").addKeyValue("linkName", (String) null).log(null); Map expectedMessage = new HashMap<>(); - expectedMessage.put("message", ""); expectedMessage.put("connectionId", "foo"); expectedMessage.put("linkName", null); @@ -679,7 +677,6 @@ public void logWithContextRuntimeException(LogLevel logLevelToConfigure) { .log(null, runtimeException)); Map expectedMessage = new HashMap<>(); - expectedMessage.put("message", ""); expectedMessage.put("connectionId", "foo"); expectedMessage.put("linkName", "bar"); expectedMessage.put("exception.type", runtimeException.getClass().getCanonicalName()); @@ -710,7 +707,6 @@ public void logWithContextThrowable(LogLevel logLevelToConfigure) { .log(null, ioException)); Map expectedMessage = new HashMap<>(); - expectedMessage.put("message", ""); expectedMessage.put("connectionId", "foo"); expectedMessage.put("linkName", "bar"); expectedMessage.put("exception.type", ioException.getClass().getCanonicalName()); From d00c417a319cb7752e19579bb29423c0e6693336 Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Mon, 6 Jan 2025 20:22:02 -0800 Subject: [PATCH 10/11] update snippets --- .../core/instrumentation/Instrumentation.java | 23 ++++++++++++++++++- .../core/instrumentation/package-info.java | 4 ++-- .../models/ContextJavaDocCodeSnippets.java | 17 +++++--------- .../TelemetryJavaDocCodeSnippets.java | 17 ++++++-------- .../ContextPropagationTests.java | 15 ------------ 5 files changed, 37 insertions(+), 39 deletions(-) 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 0618f07eac424..c1de07959f73d 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 @@ -75,7 +75,7 @@ static Instrumentation create(InstrumentationOptions applicationOptions, * Retrieves the instrumentation context from the given context. The type of the context is determined by the * instrumentation implementation. *

- * For OpenTelemetry, the context can be a {@code io.opentelemetry.api.trace.Span}, {@code io.opentelemetry.api.trace.SpanContext}, + * 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}. * *

@@ -91,6 +91,27 @@ static Instrumentation create(InstrumentationOptions applicationOptions,
      * 
* * + * + *
+     *
+     * 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. 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 61f58befec5aa..63fa95b9eba61 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/samples/java/io/clientcore/core/models/ContextJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/ContextJavaDocCodeSnippets.java index 5d38ddaa7ed6a..6b48c5e194def 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/optional-dependency-tests/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java b/sdk/clientcore/optional-dependency-tests/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java index d62907e43843a..157d476671c2f 100644 --- a/sdk/clientcore/optional-dependency-tests/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java +++ b/sdk/clientcore/optional-dependency-tests/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java @@ -7,24 +7,21 @@ 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.http.pipeline.InstrumentationPolicy; import io.clientcore.core.instrumentation.tracing.SpanKind; import io.clientcore.core.instrumentation.tracing.TracingScope; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import java.io.IOException; import java.io.UncheckedIOException; -import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY; - /** * Application developers are expected to configure OpenTelemetry * to leverage instrumentation code in client libraries. @@ -142,9 +139,9 @@ public void correlationWithExplicitContext() { // 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); // END: io.clientcore.core.telemetry.correlationwithexplicitcontext @@ -152,7 +149,7 @@ public void correlationWithExplicitContext() { static class SampleClientBuilder { private InstrumentationOptions instrumentationOptions; - // TODO (limolkova): do we need InstrumnetationTrait? + // TODO (limolkova): do we need InstrumentationTrait? public SampleClientBuilder instrumentationOptions(InstrumentationOptions instrumentationOptions) { this.instrumentationOptions = instrumentationOptions; return this; @@ -160,7 +157,7 @@ public SampleClientBuilder instrumentationOptions(InstrumentationOptions inst public SampleClient build() { return new SampleClient(instrumentationOptions, new HttpPipelineBuilder() - .policies(new InstrumentationPolicy(instrumentationOptions, null)) + .policies(new HttpInstrumentationPolicy(instrumentationOptions, null)) .build()); } } @@ -181,14 +178,14 @@ public void clientCall() { @SuppressWarnings("try") public void clientCall(RequestOptions options) { - io.clientcore.core.instrumentation.tracing.Span span = tracer.spanBuilder("clientCall", SpanKind.CLIENT, options) + io.clientcore.core.instrumentation.tracing.Span span = tracer.spanBuilder("clientCall", SpanKind.CLIENT, null) .startSpan(); if (options == null) { options = new RequestOptions(); } - options.setContext(options.getContext().put(TRACE_CONTEXT_KEY, span)); + options.setInstrumentationContext(span.getInstrumentationContext()); try (TracingScope scope = span.makeCurrent()) { Response response = httpPipeline.send(new HttpRequest(HttpMethod.GET, "https://example.com")); diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java index e88fa1dd980bc..d603fdf3c0ea4 100644 --- a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java @@ -150,21 +150,6 @@ public void testExtractInvalid() { assertFalse(extracted.isValid()); } - @Test - public void testExtractPreservesContext() { - /*Map carrier = new HashMap<>(); - carrier.put("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); - - Context original = Context.of("key", "value"); - Context updated = contextPropagator.extract(original, carrier, GETTER); - - io.opentelemetry.context.Context otelContext - = (io.opentelemetry.context.Context) updated.get(TRACE_CONTEXT_KEY); - assertTrue(io.opentelemetry.api.trace.Span.fromContext(otelContext).getSpanContext().isValid()); - - assertEquals("value", updated.get("key"));*/ - } - private String getTraceparent(Span span) { InstrumentationContext spanContext = span.getInstrumentationContext(); return "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-" + spanContext.getTraceFlags(); From 4db1075be34b35bbe03d98bb5cff43a8139d0e1d Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Fri, 10 Jan 2025 18:37:24 -0800 Subject: [PATCH 11/11] feedback --- .../core/http/models/PagedIterable.java | 2 +- .../fallback/FallbackContextPropagator.java | 35 +++++++++---------- .../fallback/FallbackSpanContext.java | 11 ++---- .../fallback/RandomIdUtils.java | 6 ++-- .../instrumentation/otel/OTelInitializer.java | 2 +- 5 files changed, 23 insertions(+), 33 deletions(-) diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedIterable.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedIterable.java index 8e0b8d29f4eb6..3425ad51289de 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedIterable.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedIterable.java @@ -3,7 +3,7 @@ package io.clientcore.core.http.models; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.util.Iterator; import java.util.NoSuchElementException; 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 index d168deea15c26..c61837b0138a3 100644 --- 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 @@ -51,23 +51,28 @@ public InstrumentationContext extract(InstrumentationContext context, C carr 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; } - // version - for (int i = 0; i < 2; i++) { - if (traceparent.charAt(i) != '0') { - return false; - } - } - - if (traceparent.charAt(2) != '-') { + // 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 + // 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); @@ -82,11 +87,7 @@ private static boolean isValidTraceparent(String traceparent) { return false; } - if (traceparent.charAt(35) != '-') { - return false; - } - - // span-id + // 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); @@ -102,11 +103,7 @@ private static boolean isValidTraceparent(String traceparent) { return false; } - if (traceparent.charAt(52) != '-') { - return false; - } - - // trace-flags + // trace-flags - 2 lower case hex characters for (int i = 53; i < 55; i++) { char c = traceparent.charAt(i); if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { 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 index 50a2999c573ce..f5a588451d05d 100644 --- 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 @@ -12,7 +12,8 @@ import static io.clientcore.core.implementation.instrumentation.fallback.RandomIdUtils.generateTraceId; final class FallbackSpanContext implements InstrumentationContext { - static final FallbackSpanContext INVALID = new FallbackSpanContext(); + 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; @@ -59,14 +60,6 @@ public String getTraceFlags() { return traceFlags; } - private FallbackSpanContext() { - this.traceId = INVALID_TRACE_ID; - this.spanId = INVALID_SPAN_ID; - this.traceFlags = "00"; - this.isValid = false; - this.span = Span.noop(); - } - FallbackSpanContext(String traceId, String spanId, String traceFlags, boolean isValid, Span span) { this.traceId = traceId; this.spanId = spanId; 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 index c5390e2f60fad..205f9b77be48f 100644 --- 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 @@ -23,7 +23,6 @@ class RandomIdUtils { private static final int TRACE_ID_HEX_LENGTH = 32; private static final int SPAN_ID_HEX_LENGTH = 16; private static final char[] ENCODING = buildEncodingArray(); - private static final String ALPHABET = "0123456789abcdef"; public static String generateSpanId() { long id; @@ -80,10 +79,11 @@ private static void byteToBase16(byte value, char[] dest, int destOffset) { } 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); + 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/otel/OTelInitializer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInitializer.java index f6af8ae1ad380..3e9b02871bd68 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 @@ -184,7 +184,7 @@ public static void runtimeError(ClientLogger logger, Throwable t) { * @return true if OTel is initialized successfully, false otherwise */ public static boolean isInitialized() { - return INSTANCE == null || INSTANCE.initialized; + return INSTANCE.initialized; } }