diff --git a/sdk/clientcore/core/checkstyle-suppressions.xml b/sdk/clientcore/core/checkstyle-suppressions.xml index e81feb377364..88e7a72d28fe 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/pom.xml b/sdk/clientcore/core/pom.xml index 25d191c6d96a..a145023a8871 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 2235ca95cf86..a94ed75f922d 100644 --- a/sdk/clientcore/core/spotbugs-exclude.xml +++ b/sdk/clientcore/core/spotbugs-exclude.xml @@ -71,7 +71,8 @@ - + + @@ -121,8 +122,8 @@ - - + + @@ -165,9 +166,9 @@ - + - + @@ -237,13 +238,14 @@ + - + @@ -259,6 +261,7 @@ + @@ -399,4 +402,16 @@ + + + + + + + + + + + + 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 7fd0aed22ec2..3f1ccc8a8e84 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 2ae30c0bcd2e..d408eb0dab41 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 3a4263ac2c33..2a0a2dc99b1a 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 8158c66e9f9d..9e0916d4aa9a 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 7fb033ea3501..8f5769b39920 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 154bac4f63a2..3ad9cb165f13 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 b1bb668f78dc..9769a05ec5fd 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 68c471617fab..c94e3f4e8d71 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 7362d6b17809..5f1d30cc4e53 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/PagedIterable.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedIterable.java index 8e0b8d29f4eb..3425ad51289d 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/http/models/ProxyOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/ProxyOptions.java index 8c676bfb7223..40f4ebbfcb83 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; @@ -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/models/RequestOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/RequestOptions.java index 16095265d6a8..19f9bd7e1ca5 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,8 @@ 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.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 bcc9370c62ca..679d30246ee7 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,15 +3,18 @@ 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.instrumentation.LibraryInstrumentationOptionsAccessHelper; 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; @@ -20,21 +23,39 @@ 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.util.Context; +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.util.binarydata.BinaryData; import java.io.IOException; import java.io.InputStream; import java.util.Collections; +import java.util.Locale; 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.instrumentation.Instrumentation.DISABLE_TRACING_KEY; -import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_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_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.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 or redirect and logging happens * in the scope of the span. *

* The policy supports basic customizations using {@link InstrumentationOptions} and {@link HttpLogOptions}. @@ -62,8 +83,7 @@ * HttpPipeline pipeline = new HttpPipelineBuilder() * .policies( * new HttpRetryPolicy(), - * new HttpInstrumentationPolicy(instrumentationOptions, logOptions), - * new HttpLoggingPolicy(logOptions)) + * new HttpInstrumentationPolicy(instrumentationOptions, logOptions)) * .build(); * * @@ -81,8 +101,7 @@ * HttpPipeline pipeline = new HttpPipelineBuilder() * .policies( * new HttpRetryPolicy(), - * new HttpInstrumentationPolicy(instrumentationOptions, logOptions), - * new HttpLoggingPolicy(logOptions)) + * new HttpInstrumentationPolicy(instrumentationOptions, logOptions)) * .build(); * * @@ -93,9 +112,11 @@ *

  *
  * HttpPipelinePolicy enrichingPolicy = (request, next) -> {
- *     Object span = request.getRequestOptions().getContext().get(TRACE_CONTEXT_KEY);
- *     if (span instanceof Span) {
- *         ((Span)span).setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID));
+ *     Span span = request.getRequestOptions() == null
+ *         ? Span.noop()
+ *         : request.getRequestOptions().getInstrumentationContext().getSpan();
+ *     if (span.isRecording()) {
+ *         span.setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID));
  *     }
  *
  *     return next.process();
@@ -105,8 +126,7 @@
  *     .policies(
  *         new HttpRetryPolicy(),
  *         new HttpInstrumentationPolicy(instrumentationOptions, logOptions),
- *         enrichingPolicy,
- *         new HttpLoggingPolicy(logOptions))
+ *         enrichingPolicy)
  *     .build();
  *
  *
@@ -139,17 +159,19 @@ public final class HttpInstrumentationPolicy implements HttpPipelinePolicy {
         LIBRARY_OPTIONS = libOptions;
     }
 
-    private static final String HTTP_REQUEST_METHOD = "http.request.method";
-    private static final String HTTP_RESPONSE_STATUS_CODE = "http.response.status_code";
-    private static final String SERVER_ADDRESS = "server.address";
-    private static final String SERVER_PORT = "server.port";
-    private static final String URL_FULL = "url.full";
-    private static final String HTTP_REQUEST_RESEND_COUNT = "http.request.resend_count";
-    private static final String USER_AGENT_ORIGINAL = "user_agent.original";
+    private static final int MAX_BODY_LOG_SIZE = 1024 * 16;
+    private static final String REDACTED_PLACEHOLDER = "REDACTED";
+
+    // request log level is low (verbose) since almost all request details are also
+    // captured on the response log.
+    private static final ClientLogger.LogLevel HTTP_REQUEST_LOG_LEVEL = ClientLogger.LogLevel.VERBOSE;
+    private static final ClientLogger.LogLevel HTTP_RESPONSE_LOG_LEVEL = ClientLogger.LogLevel.INFORMATIONAL;
 
     private final Tracer tracer;
     private final TraceContextPropagator traceContextPropagator;
     private final Set allowedQueryParameterNames;
+    private final HttpLogOptions.HttpLogDetailLevel httpLogDetailLevel;
+    private final Set allowedHeaderNames;
 
     /**
      * Creates a new instrumentation policy.
@@ -162,7 +184,12 @@ public HttpInstrumentationPolicy(InstrumentationOptions instrumentationOption
         this.traceContextPropagator = instrumentation.getW3CTraceContextPropagator();
 
         HttpLogOptions logOptionsToUse = logOptions == null ? DEFAULT_LOG_OPTIONS : logOptions;
-        this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames();
+        this.httpLogDetailLevel = logOptionsToUse.getLogLevel();
+        this.allowedHeaderNames = logOptionsToUse.getAllowedHeaderNames();
+        this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames()
+            .stream()
+            .map(queryParamName -> queryParamName.toLowerCase(Locale.ROOT))
+            .collect(Collectors.toSet());
     }
 
     /**
@@ -171,40 +198,70 @@ public HttpInstrumentationPolicy(InstrumentationOptions instrumentationOption
     @SuppressWarnings("try")
     @Override
     public Response process(HttpRequest request, HttpPipelineNextPolicy next) {
-        if (!isTracingEnabled(request)) {
+        boolean isTracingEnabled = tracer.isEnabled();
+        if (!isTracingEnabled && httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
             return next.process();
         }
 
-        String sanitizedUrl = getRedactedUri(request.getUri(), allowedQueryParameterNames);
-        Span span = startHttpSpan(request, sanitizedUrl);
+        ClientLogger logger = getLogger(request);
+        final long startNs = System.nanoTime();
+        String redactedUrl = getRedactedUri(request.getUri(), allowedQueryParameterNames);
+        int tryCount = HttpRequestAccessHelper.getTryCount(request);
+        final long requestContentLength = getContentLength(logger, request.getBody(), request.getHeaders(), true);
+
+        InstrumentationContext instrumentationContext
+            = request.getRequestOptions() == null ? null : request.getRequestOptions().getInstrumentationContext();
+        Span span = Span.noop();
+        if (isTracingEnabled) {
+            if (request.getRequestOptions() == null || request.getRequestOptions() == RequestOptions.none()) {
+                request.setRequestOptions(new RequestOptions());
+            }
+
+            span = startHttpSpan(request, redactedUrl, instrumentationContext);
+            instrumentationContext = span.getInstrumentationContext();
+            request.getRequestOptions().setInstrumentationContext(instrumentationContext);
+        }
 
-        if (request.getRequestOptions() == RequestOptions.none()) {
-            request = request.setRequestOptions(new RequestOptions());
+        // even if tracing is disabled, we could have a valid context to propagate
+        // if it was provided by the application explicitly.
+        if (instrumentationContext != null && instrumentationContext.isValid()) {
+            traceContextPropagator.inject(instrumentationContext, request.getHeaders(), SETTER);
         }
 
-        Context context = request.getRequestOptions().getContext().put(TRACE_CONTEXT_KEY, span);
-        request.getRequestOptions().setContext(context);
-        propagateContext(context, request.getHeaders());
+        logRequest(logger, request, startNs, requestContentLength, redactedUrl, tryCount, instrumentationContext);
 
         try (TracingScope scope = span.makeCurrent()) {
             Response response = next.process();
 
-            addDetails(request, response, span);
+            if (response == null) {
+                LOGGER.atError()
+                    .setInstrumentationContext(span.getInstrumentationContext())
+                    .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod())
+                    .addKeyValue(URL_FULL_KEY, redactedUrl)
+                    .log(
+                        "HTTP response is null and no exception is thrown. Please report it to the client library maintainers.");
+
+                return null;
+            }
 
+            addDetails(request, response, tryCount, span);
+            response = logResponse(logger, response, startNs, requestContentLength, redactedUrl, tryCount,
+                instrumentationContext);
             span.end();
             return response;
-        } catch (Throwable t) {
+        } catch (RuntimeException t) {
             span.end(unwrap(t));
-            throw t;
+            // TODO (limolkova) test otel scope still covers this
+            throw logException(logger, request, null, t, startNs, null, requestContentLength, redactedUrl, tryCount,
+                instrumentationContext);
         }
     }
 
-    private Span startHttpSpan(HttpRequest request, String sanitizedUrl) {
-        SpanBuilder spanBuilder
-            = tracer.spanBuilder(request.getHttpMethod().toString(), CLIENT, request.getRequestOptions())
-                .setAttribute(HTTP_REQUEST_METHOD, request.getHttpMethod().toString())
-                .setAttribute(URL_FULL, sanitizedUrl)
-                .setAttribute(SERVER_ADDRESS, request.getUri().getHost());
+    private Span startHttpSpan(HttpRequest request, String sanitizedUrl, InstrumentationContext context) {
+        SpanBuilder spanBuilder = tracer.spanBuilder(request.getHttpMethod().toString(), CLIENT, context)
+            .setAttribute(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod().toString())
+            .setAttribute(URL_FULL_KEY, sanitizedUrl)
+            .setAttribute(SERVER_ADDRESS_KEY, request.getUri().getHost());
         maybeSetServerPort(spanBuilder, request.getUri());
         return spanBuilder.startSpan();
     }
@@ -220,15 +277,15 @@ private Span startHttpSpan(HttpRequest request, String sanitizedUrl) {
     private static void maybeSetServerPort(SpanBuilder spanBuilder, URI uri) {
         int port = uri.getPort();
         if (port != -1) {
-            spanBuilder.setAttribute(SERVER_PORT, port);
+            spanBuilder.setAttribute(SERVER_PORT_KEY, port);
         } else {
             switch (uri.getScheme()) {
                 case "http":
-                    spanBuilder.setAttribute(SERVER_PORT, 80);
+                    spanBuilder.setAttribute(SERVER_PORT_KEY, 80);
                     break;
 
                 case "https":
-                    spanBuilder.setAttribute(SERVER_PORT, 443);
+                    spanBuilder.setAttribute(SERVER_PORT_KEY, 443);
                     break;
 
                 default:
@@ -237,21 +294,20 @@ private static void maybeSetServerPort(SpanBuilder spanBuilder, URI uri) {
         }
     }
 
-    private void addDetails(HttpRequest request, Response response, Span span) {
+    private void addDetails(HttpRequest request, Response response, int tryCount, Span span) {
         if (!span.isRecording()) {
             return;
         }
 
-        span.setAttribute(HTTP_RESPONSE_STATUS_CODE, (long) response.getStatusCode());
+        span.setAttribute(HTTP_RESPONSE_STATUS_CODE_KEY, (long) response.getStatusCode());
 
-        int tryCount = HttpRequestAccessHelper.getTryCount(request);
         if (tryCount > 0) {
-            span.setAttribute(HTTP_REQUEST_RESEND_COUNT, (long) tryCount);
+            span.setAttribute(HTTP_REQUEST_RESEND_COUNT_KEY, (long) tryCount);
         }
 
         String userAgent = request.getHeaders().getValue(HttpHeaderName.USER_AGENT);
         if (userAgent != null) {
-            span.setAttribute(USER_AGENT_ORIGINAL, userAgent);
+            span.setAttribute(USER_AGENT_ORIGINAL_KEY, userAgent);
         }
 
         if (response.getStatusCode() >= 400) {
@@ -260,29 +316,21 @@ private void addDetails(HttpRequest request, Response response, Span span) {
         // TODO (lmolkova) url.template and experimental features
     }
 
-    private boolean isTracingEnabled(HttpRequest httpRequest) {
-        if (!tracer.isEnabled()) {
-            return false;
-        }
-
-        Context context = httpRequest.getRequestOptions().getContext();
-        Object disableTracing = context.get(DISABLE_TRACING_KEY);
-        if (disableTracing instanceof Boolean) {
-            return !((Boolean) disableTracing);
-        }
-
-        return true;
-    }
-
-    private Throwable unwrap(Throwable t) {
+    private static Throwable unwrap(Throwable t) {
         while (t.getCause() != null) {
             t = t.getCause();
         }
         return t;
     }
 
-    private void propagateContext(Context context, HttpHeaders headers) {
-        traceContextPropagator.inject(context, headers, SETTER);
+    private ClientLogger getLogger(HttpRequest httpRequest) {
+        ClientLogger logger = null;
+
+        if (httpRequest.getRequestOptions() != null && httpRequest.getRequestOptions().getLogger() != null) {
+            logger = httpRequest.getRequestOptions().getLogger();
+        }
+
+        return logger == null ? LOGGER : logger;
     }
 
     private static Map getProperties(String propertiesFileName) {
@@ -303,4 +351,231 @@ private static Map getProperties(String propertiesFileName) {
 
         return Collections.emptyMap();
     }
+
+    private void logRequest(ClientLogger logger, HttpRequest request, long startNanoTime, long requestContentLength,
+        String redactedUrl, int tryCount, InstrumentationContext context) {
+        ClientLogger.LoggingEvent logBuilder = logger.atLevel(HTTP_REQUEST_LOG_LEVEL);
+        if (!logBuilder.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
+            return;
+        }
+
+        logBuilder.setEventName(HTTP_REQUEST_EVENT_NAME)
+            .setInstrumentationContext(context)
+            .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod())
+            .addKeyValue(URL_FULL_KEY, redactedUrl)
+            .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount)
+            .addKeyValue(HTTP_REQUEST_BODY_SIZE_KEY, requestContentLength);
+
+        addHeadersToLogMessage(request.getHeaders(), logBuilder);
+
+        if (httpLogDetailLevel.shouldLogBody() && canLogBody(request.getBody())) {
+            try {
+                BinaryData bufferedBody = request.getBody().toReplayableBinaryData();
+                request.setBody(bufferedBody);
+                logBuilder.addKeyValue(HTTP_REQUEST_BODY_CONTENT_KEY, bufferedBody.toString());
+            } catch (RuntimeException e) {
+                // we'll log exception at the appropriate level.
+                throw logException(logger, request, null, e, startNanoTime, null, requestContentLength, redactedUrl,
+                    tryCount, context);
+            }
+        }
+
+        logBuilder.log();
+    }
+
+    private Response logResponse(ClientLogger logger, Response response, long startNanoTime,
+        long requestContentLength, String redactedUrl, int tryCount, InstrumentationContext context) {
+        ClientLogger.LoggingEvent logBuilder = logger.atLevel(HTTP_RESPONSE_LOG_LEVEL);
+        if (httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
+            return response;
+        }
+
+        long responseStartNanoTime = System.nanoTime();
+
+        // response may be disabled, but we still need to log the exception if an exception occurs during stream reading.
+        if (logBuilder.isEnabled()) {
+            logBuilder.setEventName(HTTP_RESPONSE_EVENT_NAME)
+                .setInstrumentationContext(context)
+                .addKeyValue(HTTP_REQUEST_METHOD_KEY, response.getRequest().getHttpMethod())
+                .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount)
+                .addKeyValue(URL_FULL_KEY, redactedUrl)
+                .addKeyValue(HTTP_REQUEST_TIME_TO_RESPONSE_KEY, getDurationMs(startNanoTime, responseStartNanoTime))
+                .addKeyValue(HTTP_RESPONSE_STATUS_CODE_KEY, response.getStatusCode())
+                .addKeyValue(HTTP_REQUEST_BODY_SIZE_KEY, requestContentLength)
+                .addKeyValue(HTTP_RESPONSE_BODY_SIZE_KEY,
+                    getContentLength(logger, response.getBody(), response.getHeaders(), false));
+
+            addHeadersToLogMessage(response.getHeaders(), logBuilder);
+        }
+
+        if (httpLogDetailLevel.shouldLogBody() && canLogBody(response.getBody())) {
+            return new LoggingHttpResponse<>(response, content -> {
+                if (logBuilder.isEnabled()) {
+                    logBuilder.addKeyValue(HTTP_RESPONSE_BODY_CONTENT_KEY, content.toString())
+                        .addKeyValue(HTTP_REQUEST_DURATION_KEY, getDurationMs(startNanoTime, System.nanoTime()))
+                        .log();
+                }
+            }, throwable -> logException(logger, response.getRequest(), response, throwable, startNanoTime,
+                responseStartNanoTime, requestContentLength, redactedUrl, tryCount, context));
+        }
+
+        if (logBuilder.isEnabled()) {
+            logBuilder.addKeyValue(HTTP_REQUEST_DURATION_KEY, getDurationMs(startNanoTime, System.nanoTime())).log();
+        }
+
+        return response;
+    }
+
+    private  T logException(ClientLogger logger, HttpRequest request, Response response,
+        T throwable, long startNanoTime, Long responseStartNanoTime, long requestContentLength, String redactedUrl,
+        int tryCount, InstrumentationContext context) {
+
+        ClientLogger.LoggingEvent log = logger.atLevel(ClientLogger.LogLevel.WARNING);
+        if (!log.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) {
+            return throwable;
+        }
+
+        log.setEventName(HTTP_RESPONSE_EVENT_NAME)
+            .setInstrumentationContext(context)
+            .addKeyValue(HTTP_REQUEST_METHOD_KEY, request.getHttpMethod())
+            .addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount)
+            .addKeyValue(URL_FULL_KEY, redactedUrl)
+            .addKeyValue(HTTP_REQUEST_BODY_SIZE_KEY, requestContentLength)
+            .addKeyValue(HTTP_REQUEST_DURATION_KEY, getDurationMs(startNanoTime, System.nanoTime()));
+
+        if (response != null) {
+            addHeadersToLogMessage(response.getHeaders(), log);
+            log.addKeyValue(HTTP_RESPONSE_BODY_SIZE_KEY,
+                getContentLength(logger, response.getBody(), response.getHeaders(), false))
+                .addKeyValue(HTTP_RESPONSE_STATUS_CODE_KEY, response.getStatusCode());
+
+            if (responseStartNanoTime != null) {
+                log.addKeyValue(HTTP_REQUEST_TIME_TO_RESPONSE_KEY, getDurationMs(startNanoTime, responseStartNanoTime));
+            }
+        }
+
+        log.log(null, unwrap(throwable));
+        return throwable;
+    }
+
+    private double getDurationMs(long startNs, long endNs) {
+        return (endNs - startNs) / 1_000_000.0;
+    }
+
+    /**
+     * Determines if the request or response body should be logged.
+     *
+     * 

The request or response body is logged if the body is replayable, content length is known, + * isn't empty, and is less than 16KB in size.

+ * + * @param data The request or response body. + * @return A flag indicating if the request or response body should be logged. + */ + private static boolean canLogBody(BinaryData data) { + // TODO (limolkova) we might want to filter out binary data, but + // if somebody enabled logging it - why not log it? + return data != null && data.getLength() != null && data.getLength() > 0 && data.getLength() < MAX_BODY_LOG_SIZE; + } + + /** + * Adds HTTP headers into the StringBuilder that is generating the log message. + * + * @param headers HTTP headers on the request or response. + * @param logBuilder Log message builder. + */ + private void addHeadersToLogMessage(HttpHeaders headers, ClientLogger.LoggingEvent logBuilder) { + if (httpLogDetailLevel.shouldLogHeaders()) { + for (HttpHeader header : headers) { + HttpHeaderName headerName = header.getName(); + String headerValue = allowedHeaderNames.contains(headerName) ? header.getValue() : REDACTED_PLACEHOLDER; + logBuilder.addKeyValue(headerName.toString(), headerValue); + } + } + } + + /** + * Attempts to get request or response body content length. + *

+ * If the body length is known, it will be returned. + * Otherwise, the method parses Content-Length header. + * + * @param logger Logger used to log a warning if the Content-Length header is an invalid number. + * @param body The request or response body object. + * @param headers HTTP headers that are checked for containing Content-Length. + * @return The numeric value of the Content-Length header or 0 if the header is not present or invalid. + */ + private static long getContentLength(ClientLogger logger, BinaryData body, HttpHeaders headers, boolean isRequest) { + if (body == null) { + return 0; + } + + if (body.getLength() != null) { + return body.getLength(); + } + + long contentLength = 0; + + String contentLengthString = headers.getValue(HttpHeaderName.CONTENT_LENGTH); + + if (isNullOrEmpty(contentLengthString)) { + return contentLength; + } + + try { + contentLength = Long.parseLong(contentLengthString); + } catch (NumberFormatException e) { + logger.atVerbose() + .addKeyValue( + isRequest ? HTTP_REQUEST_HEADER_CONTENT_LENGTH_KEY : HTTP_RESPONSE_HEADER_CONTENT_LENGTH_KEY, + contentLengthString) + .log("Could not parse the HTTP header content-length", e); + } + + return contentLength; + } + + private static final class LoggingHttpResponse extends HttpResponse { + private final Consumer onContent; + private final Consumer onException; + private final BinaryData originalBody; + private BinaryData bufferedBody; + + private LoggingHttpResponse(Response actualResponse, Consumer onContent, + Consumer onException) { + super(actualResponse.getRequest(), actualResponse.getStatusCode(), actualResponse.getHeaders(), + actualResponse.getValue()); + + this.onContent = onContent; + this.onException = onException; + this.originalBody = actualResponse.getBody(); + } + + @Override + public BinaryData getBody() { + if (bufferedBody != null) { + return bufferedBody; + } + + try { + bufferedBody = originalBody.toReplayableBinaryData(); + onContent.accept(bufferedBody); + return bufferedBody; + } catch (RuntimeException e) { + // we'll log exception at the appropriate level. + onException.accept(e); + throw e; + } + } + + @Override + public void close() throws IOException { + if (bufferedBody == null) { + getBody(); + } + if (bufferedBody != null) { + bufferedBody.close(); + } + originalBody.close(); + } + } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java deleted file mode 100644 index c43ebca9a3c3..000000000000 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package io.clientcore.core.http.pipeline; - -import io.clientcore.core.http.models.HttpHeader; -import io.clientcore.core.http.models.HttpHeaderName; -import io.clientcore.core.http.models.HttpHeaders; -import io.clientcore.core.http.models.HttpLogOptions; -import io.clientcore.core.http.models.HttpRequest; -import io.clientcore.core.http.models.HttpResponse; -import io.clientcore.core.http.models.Response; -import io.clientcore.core.implementation.http.HttpRequestAccessHelper; -import io.clientcore.core.implementation.util.LoggingKeys; -import io.clientcore.core.util.ClientLogger; -import io.clientcore.core.util.binarydata.BinaryData; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import static io.clientcore.core.http.models.HttpHeaderName.TRACEPARENT; -import static io.clientcore.core.implementation.UrlRedactionUtil.getRedactedUri; -import static io.clientcore.core.implementation.util.ImplUtils.isNullOrEmpty; - -/** - * The pipeline policy that handles logging of HTTP requests and responses. - */ -public class HttpLoggingPolicy implements HttpPipelinePolicy { - private static final HttpLogOptions DEFAULT_HTTP_LOG_OPTIONS = new HttpLogOptions(); - private static final List ALWAYS_ALLOWED_HEADERS = Collections.singletonList(TRACEPARENT); - private static final int MAX_BODY_LOG_SIZE = 1024 * 16; - private static final String REDACTED_PLACEHOLDER = "REDACTED"; - private static final ClientLogger LOGGER = new ClientLogger(HttpLoggingPolicy.class); - private final HttpLogOptions.HttpLogDetailLevel httpLogDetailLevel; - private final Set allowedHeaderNames; - - private final Set allowedQueryParameterNames; - - private static final String HTTP_REQUEST_EVENT_NAME = "http.request"; - private static final String HTTP_RESPONSE_EVENT_NAME = "http.response"; - - // request log level is low (verbose) since almost all request details are also - // captured on the response log. - private static final ClientLogger.LogLevel HTTP_REQUEST_LOG_LEVEL = ClientLogger.LogLevel.VERBOSE; - private static final ClientLogger.LogLevel HTTP_RESPONSE_LOG_LEVEL = ClientLogger.LogLevel.INFORMATIONAL; - - /** - * Creates an HttpLoggingPolicy with the given log configurations. - * - * @param httpLogOptions The HTTP logging configuration options. - */ - public HttpLoggingPolicy(HttpLogOptions httpLogOptions) { - HttpLogOptions logOptionsToUse = httpLogOptions == null ? DEFAULT_HTTP_LOG_OPTIONS : httpLogOptions; - this.httpLogDetailLevel = logOptionsToUse.getLogLevel(); - this.allowedHeaderNames = logOptionsToUse.getAllowedHeaderNames(); - this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames() - .stream() - .map(queryParamName -> queryParamName.toLowerCase(Locale.ROOT)) - .collect(Collectors.toSet()); - } - - @Override - public Response process(HttpRequest httpRequest, HttpPipelineNextPolicy next) { - // No logging will be performed, trigger a no-op. - if (httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { - return next.process(); - } - - ClientLogger logger = getLogger(httpRequest); - - final long startNs = System.nanoTime(); - final String redactedUrl = getRedactedUri(httpRequest.getUri(), allowedQueryParameterNames); - final int tryCount = HttpRequestAccessHelper.getTryCount(httpRequest); - final long requestContentLength = httpRequest.getBody() == null - ? 0 - : getContentLength(logger, httpRequest.getBody(), httpRequest.getHeaders()); - - logRequest(logger, httpRequest, startNs, requestContentLength, redactedUrl, tryCount); - - try { - Response response = next.process(); - - if (response == null) { - LOGGER.atError() - .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, httpRequest.getHttpMethod()) - .addKeyValue(LoggingKeys.URI_KEY, redactedUrl) - .log( - "HTTP response is null and no exception is thrown. Please report it to the client library maintainers."); - - return null; - } - - return logResponse(logger, response, startNs, requestContentLength, redactedUrl, tryCount); - } catch (RuntimeException e) { - throw logException(logger, httpRequest, null, e, startNs, null, requestContentLength, redactedUrl, - tryCount); - } - } - - private ClientLogger getLogger(HttpRequest request) { - if (request.getRequestOptions() != null && request.getRequestOptions().getLogger() != null) { - return request.getRequestOptions().getLogger(); - } - - return LOGGER; - } - - private void logRequest(ClientLogger logger, HttpRequest request, long startNanoTime, long requestContentLength, - String redactedUrl, int tryCount) { - ClientLogger.LoggingEvent logBuilder = logger.atLevel(HTTP_REQUEST_LOG_LEVEL); - if (!logBuilder.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { - return; - } - - logBuilder.setEventName(HTTP_REQUEST_EVENT_NAME) - .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, request.getHttpMethod()) - .addKeyValue(LoggingKeys.URI_KEY, redactedUrl) - .addKeyValue(LoggingKeys.TRY_COUNT_KEY, tryCount) - .addKeyValue(LoggingKeys.REQUEST_CONTENT_LENGTH_KEY, requestContentLength); - - addHeadersToLogMessage(request.getHeaders(), logBuilder); - - if (httpLogDetailLevel.shouldLogBody() && canLogBody(request.getBody())) { - try { - BinaryData bufferedBody = request.getBody().toReplayableBinaryData(); - request.setBody(bufferedBody); - logBuilder.addKeyValue(LoggingKeys.BODY_KEY, bufferedBody.toString()); - } catch (RuntimeException e) { - // we'll log exception at the appropriate level. - throw logException(logger, request, null, e, startNanoTime, null, requestContentLength, redactedUrl, - tryCount); - } - } - - logBuilder.log(); - } - - private Response logResponse(ClientLogger logger, Response response, long startNanoTime, - long requestContentLength, String redactedUrl, int tryCount) { - ClientLogger.LoggingEvent logBuilder = logger.atLevel(HTTP_RESPONSE_LOG_LEVEL); - if (httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { - return response; - } - - long responseStartNanoTime = System.nanoTime(); - - // response may be disabled, but we still need to log the exception if an exception occurs during stream reading. - if (logBuilder.isEnabled()) { - logBuilder.setEventName(HTTP_RESPONSE_EVENT_NAME) - .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, response.getRequest().getHttpMethod()) - .addKeyValue(LoggingKeys.TRY_COUNT_KEY, tryCount) - .addKeyValue(LoggingKeys.URI_KEY, redactedUrl) - .addKeyValue(LoggingKeys.TIME_TO_RESPONSE_MS_KEY, getDurationMs(startNanoTime, responseStartNanoTime)) - .addKeyValue(LoggingKeys.STATUS_CODE_KEY, response.getStatusCode()) - .addKeyValue(LoggingKeys.REQUEST_CONTENT_LENGTH_KEY, requestContentLength) - .addKeyValue(LoggingKeys.RESPONSE_CONTENT_LENGTH_KEY, - getContentLength(logger, response.getBody(), response.getHeaders())); - - addHeadersToLogMessage(response.getHeaders(), logBuilder); - } - - if (httpLogDetailLevel.shouldLogBody() && canLogBody(response.getBody())) { - return new LoggingHttpResponse<>(response, content -> { - if (logBuilder.isEnabled()) { - logBuilder.addKeyValue(LoggingKeys.BODY_KEY, content.toString()) - .addKeyValue(LoggingKeys.DURATION_MS_KEY, getDurationMs(startNanoTime, System.nanoTime())) - .log(); - } - }, throwable -> logException(logger, response.getRequest(), response, throwable, startNanoTime, - responseStartNanoTime, requestContentLength, redactedUrl, tryCount)); - } - - if (logBuilder.isEnabled()) { - logBuilder.addKeyValue(LoggingKeys.DURATION_MS_KEY, getDurationMs(startNanoTime, System.nanoTime())).log(); - } - - return response; - } - - private T logException(ClientLogger logger, HttpRequest request, Response response, - T throwable, long startNanoTime, Long responseStartNanoTime, long requestContentLength, String redactedUrl, - int tryCount) { - ClientLogger.LoggingEvent logBuilder = logger.atLevel(ClientLogger.LogLevel.WARNING); - if (!logBuilder.isEnabled() || httpLogDetailLevel == HttpLogOptions.HttpLogDetailLevel.NONE) { - return throwable; - } - - logBuilder.setEventName(HTTP_RESPONSE_EVENT_NAME) - .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, request.getHttpMethod()) - .addKeyValue(LoggingKeys.TRY_COUNT_KEY, tryCount) - .addKeyValue(LoggingKeys.URI_KEY, redactedUrl) - .addKeyValue(LoggingKeys.REQUEST_CONTENT_LENGTH_KEY, requestContentLength) - .addKeyValue(LoggingKeys.DURATION_MS_KEY, getDurationMs(startNanoTime, System.nanoTime())); - - if (response != null) { - addHeadersToLogMessage(response.getHeaders(), logBuilder); - logBuilder - .addKeyValue(LoggingKeys.RESPONSE_CONTENT_LENGTH_KEY, - getContentLength(logger, response.getBody(), response.getHeaders())) - .addKeyValue(LoggingKeys.STATUS_CODE_KEY, response.getStatusCode()); - - if (responseStartNanoTime != null) { - logBuilder.addKeyValue(LoggingKeys.TIME_TO_RESPONSE_MS_KEY, - getDurationMs(startNanoTime, responseStartNanoTime)); - } - } - - return logBuilder.log(null, throwable); - } - - private double getDurationMs(long startNs, long endNs) { - return (endNs - startNs) / 1_000_000.0; - } - - /** - * Determines if the request or response body should be logged. - * - *

The request or response body is logged if the body is replayable, content length is known, - * isn't empty, and is less than 16KB in size.

- * - * @param data The request or response body. - * @return A flag indicating if the request or response body should be logged. - */ - private static boolean canLogBody(BinaryData data) { - // TODO: limolkova - we might want to filter out binary data, but - // if somebody enabled logging it - why not log it? - return data != null && data.getLength() != null && data.getLength() > 0 && data.getLength() < MAX_BODY_LOG_SIZE; - } - - /** - * Adds HTTP headers into the StringBuilder that is generating the log message. - * - * @param headers HTTP headers on the request or response. - * @param logBuilder Log message builder. - */ - private void addHeadersToLogMessage(HttpHeaders headers, ClientLogger.LoggingEvent logBuilder) { - if (httpLogDetailLevel.shouldLogHeaders()) { - for (HttpHeader header : headers) { - HttpHeaderName headerName = header.getName(); - String headerValue = allowedHeaderNames.contains(headerName) ? header.getValue() : REDACTED_PLACEHOLDER; - logBuilder.addKeyValue(headerName.toString(), headerValue); - } - } else { - for (HttpHeaderName headerName : ALWAYS_ALLOWED_HEADERS) { - String headerValue = headers.getValue(headerName); - if (headerValue != null) { - logBuilder.addKeyValue(headerName.toString(), headerValue); - } - } - } - } - - /** - * Attempts to get request or response body content length. - *

- * If the body length is known, it will be returned. - * Otherwise, the method parses Content-Length header. - * - * @param logger Logger used to log a warning if the Content-Length header is an invalid number. - * @param body The request or response body object. - * @param headers HTTP headers that are checked for containing Content-Length. - * @return The numeric value of the Content-Length header or 0 if the header is not present or invalid. - */ - private static long getContentLength(ClientLogger logger, BinaryData body, HttpHeaders headers) { - if (body != null && body.getLength() != null) { - return body.getLength(); - } - - long contentLength = 0; - - String contentLengthString = headers.getValue(HttpHeaderName.CONTENT_LENGTH); - - if (isNullOrEmpty(contentLengthString)) { - return contentLength; - } - - try { - contentLength = Long.parseLong(contentLengthString); - } catch (NumberFormatException | NullPointerException e) { - logger.atVerbose() - .addKeyValue("contentLength", contentLengthString) - .log("Could not parse the HTTP header content-length", e); - } - - return contentLength; - } - - private static final class LoggingHttpResponse extends HttpResponse { - private final Consumer onContent; - private final Consumer onException; - private final BinaryData originalBody; - private BinaryData bufferedBody; - - private LoggingHttpResponse(Response actualResponse, Consumer onContent, - Consumer onException) { - super(actualResponse.getRequest(), actualResponse.getStatusCode(), actualResponse.getHeaders(), - actualResponse.getValue()); - - this.onContent = onContent; - this.onException = onException; - this.originalBody = actualResponse.getBody(); - } - - @Override - public BinaryData getBody() { - if (bufferedBody != null) { - return bufferedBody; - } - - try { - bufferedBody = originalBody.toReplayableBinaryData(); - onContent.accept(bufferedBody); - return bufferedBody; - } catch (RuntimeException e) { - // we'll log exception at the appropriate level. - onException.accept(e); - throw e; - } - } - - @Override - public void close() throws IOException { - if (bufferedBody == null) { - getBody(); - } - if (bufferedBody != null) { - bufferedBody.close(); - } - originalBody.close(); - } - } -} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpPipelineNextPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpPipelineNextPolicy.java index 8b6856e1a5f1..ed070bd50991 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpPipelineNextPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpPipelineNextPolicy.java @@ -5,7 +5,7 @@ import io.clientcore.core.http.models.Response; import io.clientcore.core.implementation.http.HttpPipelineCallState; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRedirectPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRedirectPolicy.java index 6bdddc826cd9..fbd3e84b3171 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRedirectPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRedirectPolicy.java @@ -8,18 +8,28 @@ import io.clientcore.core.http.models.HttpRedirectOptions; import io.clientcore.core.http.models.HttpRequest; import io.clientcore.core.http.models.Response; -import io.clientcore.core.implementation.util.LoggingKeys; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.io.IOException; import java.io.UncheckedIOException; import java.net.HttpURLConnection; +import java.net.URI; +import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; import java.util.function.Predicate; +import static io.clientcore.core.implementation.UrlRedactionUtil.getRedactedUri; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_METHOD_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_RESEND_COUNT_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_RESPONSE_HEADER_LOCATION_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_MAX_ATTEMPT_COUNT_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_WAS_LAST_ATTEMPT_KEY; +import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.HTTP_REDIRECT_EVENT_NAME; + /** * A {@link HttpPipelinePolicy} that redirects a {@link HttpRequest} when an HTTP Redirect is received as a * {@link Response response}. @@ -29,8 +39,6 @@ public final class HttpRedirectPolicy implements HttpPipelinePolicy { private final int maxAttempts; private final Predicate shouldRedirectCondition; private static final int DEFAULT_MAX_REDIRECT_ATTEMPTS = 3; - private static final String REDIRECT_URIS_KEY = "redirectUris"; - private static final String ORIGINATING_REQUEST_URI_KEY = "originatingRequestUri"; private static final EnumSet DEFAULT_REDIRECT_ALLOWED_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD); @@ -74,86 +82,68 @@ public HttpRedirectPolicy(HttpRedirectOptions redirectOptions) { @Override public Response process(HttpRequest httpRequest, HttpPipelineNextPolicy next) { // Reset the attemptedRedirectUris for each individual request. - return attemptRedirect(next, 1, new LinkedHashSet<>()); + InstrumentationContext instrumentationContext = httpRequest.getRequestOptions() == null + ? null + : httpRequest.getRequestOptions().getInstrumentationContext(); + + ClientLogger logger = getLogger(httpRequest); + return attemptRedirect(logger, next, 0, new LinkedHashSet<>(), instrumentationContext); } /** * Function to process through the HTTP Response received in the pipeline and redirect sending the request with a * new redirect URI. */ - private Response attemptRedirect(final HttpPipelineNextPolicy next, final int redirectAttempt, - LinkedHashSet attemptedRedirectUris) { + private Response attemptRedirect(ClientLogger logger, final HttpPipelineNextPolicy next, + final int redirectAttempt, LinkedHashSet attemptedRedirectUris, + InstrumentationContext instrumentationContext) { + // Make sure the context is not modified during redirect, except for the URI Response response = next.clone().process(); HttpRequestRedirectCondition requestRedirectCondition = new HttpRequestRedirectCondition(response, redirectAttempt, attemptedRedirectUris); + if ((shouldRedirectCondition != null && shouldRedirectCondition.test(requestRedirectCondition)) - || (shouldRedirectCondition == null && defaultShouldAttemptRedirect(requestRedirectCondition))) { + || (shouldRedirectCondition == null + && defaultShouldAttemptRedirect(logger, requestRedirectCondition, instrumentationContext))) { createRedirectRequest(response); - return attemptRedirect(next, redirectAttempt + 1, attemptedRedirectUris); + return attemptRedirect(logger, next, redirectAttempt + 1, attemptedRedirectUris, instrumentationContext); } return response; } - private boolean defaultShouldAttemptRedirect(HttpRequestRedirectCondition requestRedirectCondition) { + private boolean defaultShouldAttemptRedirect(ClientLogger logger, + HttpRequestRedirectCondition requestRedirectCondition, InstrumentationContext context) { Response response = requestRedirectCondition.getResponse(); int tryCount = requestRedirectCondition.getTryCount(); Set attemptedRedirectUris = requestRedirectCondition.getRedirectedUris(); String redirectUri = response.getHeaders().getValue(this.locationHeader); - if (isValidRedirectStatusCode(response.getStatusCode()) - && isValidRedirectCount(tryCount) - && isAllowedRedirectMethod(response.getRequest().getHttpMethod()) - && redirectUri != null - && !alreadyAttemptedRedirectUri(redirectUri, attemptedRedirectUris)) { - - LOGGER.atVerbose() - .addKeyValue(LoggingKeys.TRY_COUNT_KEY, tryCount) - .addKeyValue(REDIRECT_URIS_KEY, attemptedRedirectUris::toString) - .addKeyValue(ORIGINATING_REQUEST_URI_KEY, response.getRequest().getUri()) - .log("Redirecting."); - - attemptedRedirectUris.add(redirectUri); - - return true; - } - - return false; - } + if (isValidRedirectStatusCode(response.getStatusCode()) && redirectUri != null) { + HttpMethod method = response.getRequest().getHttpMethod(); + if (tryCount >= this.maxAttempts - 1) { + logRedirect(logger, true, redirectUri, tryCount, method, "Redirect attempts have been exhausted.", + context); + return false; + } - /** - * Check if the attempt count of the redirect is less than the {@code maxAttempts} - * - * @param tryCount the try count for the HTTP request associated to the HTTP response. - * - * @return {@code true} if the {@code tryCount} is greater than the {@code maxAttempts}, {@code false} otherwise. - */ - private boolean isValidRedirectCount(int tryCount) { - if (tryCount >= this.maxAttempts) { - LOGGER.atError().addKeyValue("maxAttempts", this.maxAttempts).log("Redirect attempts have been exhausted."); + if (!allowedRedirectHttpMethods.contains(response.getRequest().getHttpMethod())) { + logRedirect(logger, true, redirectUri, tryCount, method, + "Request redirection is not enabled for this HTTP method.", context); + return false; + } - return false; - } + if (attemptedRedirectUris.contains(redirectUri)) { + logRedirect(logger, true, redirectUri, tryCount, method, + "Request was redirected more than once to the same URI.", context); + return false; + } - return true; - } + logRedirect(logger, false, redirectUri, tryCount, method, null, context); - /** - * Check if the redirect uri provided in the response headers is already attempted. - * - * @param redirectUri the redirect uri provided in the response header. - * @param attemptedRedirectUris the set containing a list of attempted redirect locations. - * - * @return {@code true} if the redirectUri provided in the response header is already being attempted for redirect, - * {@code false} otherwise. - */ - private boolean alreadyAttemptedRedirectUri(String redirectUri, Set attemptedRedirectUris) { - if (attemptedRedirectUris.contains(redirectUri)) { - LOGGER.atError() - .addKeyValue(LoggingKeys.REDIRECT_URI_KEY, redirectUri) - .log("Request was redirected more than once to the same URI."); + attemptedRedirectUris.add(redirectUri); return true; } @@ -161,25 +151,6 @@ private boolean alreadyAttemptedRedirectUri(String redirectUri, Set atte return false; } - /** - * Check if the request http method is a valid redirect method. - * - * @param httpMethod the http method of the request. - * - * @return {@code true} if the request {@code httpMethod} is a valid http redirect method, {@code false} otherwise. - */ - private boolean isAllowedRedirectMethod(HttpMethod httpMethod) { - if (allowedRedirectHttpMethods.contains(httpMethod)) { - return true; - } else { - LOGGER.atError() - .addKeyValue(LoggingKeys.HTTP_METHOD_KEY, httpMethod) - .log("Request redirection is not enabled for this HTTP method."); - - return false; - } - } - /** * Checks if the incoming request status code is a valid redirect status code. * @@ -205,6 +176,41 @@ private void createRedirectRequest(Response redirectResponse) { } catch (IOException e) { throw LOGGER.logThrowableAsError(new UncheckedIOException(e)); } + } + + private void logRedirect(ClientLogger logger, boolean lastAttempt, String redirectUri, int tryCount, + HttpMethod method, String message, InstrumentationContext context) { + ClientLogger.LoggingEvent log = lastAttempt ? logger.atWarning() : logger.atVerbose(); + if (log.isEnabled()) { + log.addKeyValue(HTTP_REQUEST_RESEND_COUNT_KEY, tryCount) + .addKeyValue(RETRY_MAX_ATTEMPT_COUNT_KEY, maxAttempts) + .addKeyValue(HTTP_REQUEST_METHOD_KEY, method) + .addKeyValue(HTTP_RESPONSE_HEADER_LOCATION_KEY, redactUri(redirectUri)) + .addKeyValue(RETRY_WAS_LAST_ATTEMPT_KEY, lastAttempt) + .setEventName(HTTP_REDIRECT_EVENT_NAME) + .setInstrumentationContext(context) + .log(message); + } + } + + private String redactUri(String location) { + URI uri; + try { + uri = URI.create(location); + } catch (IllegalArgumentException e) { + return null; + } + // TODO: make it configurable? Or don't log URL? + return getRedactedUri(uri, Collections.emptySet()); + } + + private ClientLogger getLogger(HttpRequest httpRequest) { + ClientLogger logger = null; + + if (httpRequest.getRequestOptions() != null && httpRequest.getRequestOptions().getLogger() != null) { + logger = httpRequest.getRequestOptions().getLogger(); + } + return logger == null ? LOGGER : logger; } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRetryPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRetryPolicy.java index 477e2119a0eb..398ae3857a39 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRetryPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpRetryPolicy.java @@ -9,8 +9,8 @@ import io.clientcore.core.http.models.Response; import io.clientcore.core.implementation.http.HttpRequestAccessHelper; import io.clientcore.core.implementation.util.ImplUtils; -import io.clientcore.core.implementation.util.LoggingKeys; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.configuration.Configuration; import java.io.IOException; @@ -26,6 +26,11 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_RESEND_COUNT_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_DELAY_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_WAS_LAST_ATTEMPT_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.RETRY_MAX_ATTEMPT_COUNT_KEY; +import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.HTTP_RETRY_EVENT_NAME; import static io.clientcore.core.implementation.util.ImplUtils.isNullOrEmpty; import static io.clientcore.core.util.configuration.Configuration.PROPERTY_REQUEST_RETRY_COUNT; @@ -141,16 +146,24 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex // It can be used by the policies during the process call. HttpRequestAccessHelper.setTryCount(httpRequest, tryCount); + final InstrumentationContext instrumentationContext = httpRequest.getRequestOptions() == null + ? null + : httpRequest.getRequestOptions().getInstrumentationContext(); + Response response; + ClientLogger logger = getLogger(httpRequest); try { response = next.clone().process(); } catch (RuntimeException err) { if (shouldRetryException(err, tryCount, suppressed)) { - logRetryWithError(LOGGER.atVerbose(), tryCount, "Error resume.", err); + + Duration delayDuration = calculateRetryDelay(tryCount); + logRetry(logger.atVerbose(), tryCount, delayDuration, err, false, instrumentationContext); boolean interrupted = false; - long millis = calculateRetryDelay(tryCount).toMillis(); + long millis = delayDuration.toMillis(); + if (millis > 0) { try { Thread.sleep(millis); @@ -161,7 +174,7 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex } if (interrupted) { - throw LOGGER.logThrowableAsError(err); + throw logger.logThrowableAsError(err); } List suppressedLocal = suppressed == null ? new LinkedList<>() : suppressed; @@ -170,20 +183,20 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex return attempt(httpRequest, next, tryCount + 1, suppressedLocal); } else { - logRetryWithError(LOGGER.atError(), tryCount, "Retry attempts have been exhausted.", err); + logRetry(logger.atWarning(), tryCount, null, err, true, instrumentationContext); if (suppressed != null) { suppressed.forEach(err::addSuppressed); } - throw LOGGER.logThrowableAsError(err); + throw logger.logThrowableAsError(err); } } if (shouldRetryResponse(response, tryCount, suppressed)) { final Duration delayDuration = determineDelayDuration(response, tryCount, delayFromHeaders); - logRetry(tryCount, delayDuration); + logRetry(logger.atVerbose(), tryCount, delayDuration, null, false, instrumentationContext); try { response.close(); @@ -191,7 +204,7 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex throw LOGGER.logThrowableAsError(new UncheckedIOException(e)); } - long millis = calculateRetryDelay(tryCount).toMillis(); + long millis = delayDuration.toMillis(); if (millis > 0) { try { Thread.sleep(millis); @@ -203,9 +216,10 @@ private Response attempt(final HttpRequest httpRequest, final HttpPipelineNex return attempt(httpRequest, next, tryCount + 1, suppressed); } else { if (tryCount >= maxRetries) { - logRetryExhausted(tryCount); + // TODO (limolkova): do we have better heuristic to determine if we're retrying because of error + // or because we got successful response? + logRetry(logger.atWarning(), tryCount, null, null, true, instrumentationContext); } - return response; } } @@ -269,20 +283,25 @@ private boolean shouldRetryException(Exception exception, int tryCount, ListOpenTelemetry semantic conventions. + *

+ * These keys unify how core logs HTTP requests, responses or anything + * else and simplify telemetry analysis. + *

+ * When reporting in client libraries, please do the best effort to stay consistent with these keys, but copy the value. + */ +public final class AttributeKeys { + // Standard attribute names (defined in OpenTelemetry semantic conventions) + + /** + * A class of error the operation ended with such as a fully-qualified exception type or a domain-specific error code. + * error.type attribute + */ + public static final String ERROR_TYPE_KEY = "error.type"; + + /** + * Exception message. + * exception.message attribute + */ + public static final String EXCEPTION_MESSAGE_KEY = "exception.message"; + + /** + * Exception stacktrace. + * exception.stacktrace attribute + */ + public static final String EXCEPTION_STACKTRACE_KEY = "exception.stacktrace"; + + /** + * Exception type. + * exception.type attribute + */ + public static final String EXCEPTION_TYPE_KEY = "exception.type"; + + /** + * The name of the logging event. + * event.name attribute + */ + public static final String EVENT_NAME_KEY = "event.name"; + + /** + * The HTTP request method. + * http.request.method attribute + */ + public static final String HTTP_REQUEST_METHOD_KEY = "http.request.method"; + + /** + * The ordinal number of request resending attempt (for any reason, including redirects) + * The value starts with {@code 0} on the first try + * and should be an {@code int} number. + * http.request.resend_count attribute + */ + public static final String HTTP_REQUEST_RESEND_COUNT_KEY = "http.request.resend_count"; + + /** + * The size of the request payload body in bytes. It usually matches the value of the Content-Length header. + * http.request.body.size attribute + */ + public static final String HTTP_REQUEST_BODY_SIZE_KEY = "http.request.body.size"; + + /** + * The value of request content length header. + * http.request.header.content-length attribute + */ + public static final String HTTP_REQUEST_HEADER_CONTENT_LENGTH_KEY = "http.request.header.content-length"; + + /** + * The value of request traceparent header. + * http.request.header.content-length attribute + */ + public static final String HTTP_REQUEST_HEADER_TRACEPARENT_KEY = "http.request.header.traceparent"; + + /** + * The value of response content length header. + * http.response.header.content-length attribute + */ + public static final String HTTP_RESPONSE_HEADER_CONTENT_LENGTH_KEY = "http.response.header.content-length"; + + /** + * The value of response location header indicating the URL to redirect to. + * http.response.header.location attribute + */ + public static final String HTTP_RESPONSE_HEADER_LOCATION_KEY = "http.response.header.location"; + + /** + * The size of the response payload body in bytes. It usually matches the value of the Content-Length header. + * http.response.body.size attribute + */ + public static final String HTTP_RESPONSE_BODY_SIZE_KEY = "http.response.body.size"; + + /** + * The HTTP response status code. The value should be a number. + * http.response.status_code attribute + */ + public static final String HTTP_RESPONSE_STATUS_CODE_KEY = "http.response.status_code"; + + /** + * Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + * server.address attribute + */ + public static final String SERVER_ADDRESS_KEY = "server.address"; + + /** + * Server port number. + * server.port attribute + */ + public static final String SERVER_PORT_KEY = "server.port"; + + /** + * The request user agent. + * user_agent.original attribute + */ + public static final String USER_AGENT_ORIGINAL_KEY = "user_agent.original"; + + /** + * Absolute URL describing a network resource. + * url.full attribute + */ + public static final String URL_FULL_KEY = "url.full"; + + // Custom attribute names, use with caution + /** + * Key representing duration of call in milliseconds, the value should be a number. + */ + public static final String HTTP_REQUEST_TIME_TO_RESPONSE_KEY = "http.request.time_to_response"; + + /** + * Key representing duration of call in milliseconds, the value should be a number. + */ + public static final String HTTP_REQUEST_DURATION_KEY = "http.request.duration"; + + /** + * Key representing request body. The value should be populated conditionally + * if populated at all. + */ + public static final String HTTP_REQUEST_BODY_CONTENT_KEY = "http.request.body.content"; + + /** + * Key representing response body. The value should be populated conditionally + * if populated at all. + */ + public static final String HTTP_RESPONSE_BODY_CONTENT_KEY = "http.request.body.content"; + + /** + * Key representing maximum number of redirects or retries. It's reported when the number of redirects or retries + * was exhausted. + */ + public static final String RETRY_MAX_ATTEMPT_COUNT_KEY = "retry.max_attempt_count"; + + /** + * Key representing delay before next retry attempt in milliseconds. The value should be a number. + */ + public static final String RETRY_DELAY_KEY = "retry.delay"; + + /** + * Key representing whether the retry jor redirect ust performed was the last attempt. + */ + public static final String RETRY_WAS_LAST_ATTEMPT_KEY = "retry.was_last_attempt"; + + /** + * Key representing span id on logs. + */ + public static final String SPAN_ID_KEY = "span.id"; + + /** + * Key representing parent span id on logs. + */ + public static final String SPAN_PARENT_ID_KEY = "span.parent.id"; + + /** + * Key representing span name on logs. + */ + public static final String SPAN_NAME_KEY = "span.name"; + + /** + * Key representing span kind on logs. + */ + public static final String SPAN_KIND_KEY = "span.kind"; + + /** + * Key representing span duration (in milliseconds) on logs. The value should be a number. + */ + public static final String SPAN_DURATION_KEY = "span.duration"; + + /** + * Key representing trace id on logs. + */ + public static final String TRACE_ID_KEY = "trace.id"; + + private AttributeKeys() { + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DefaultLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultLogger.java similarity index 96% rename from sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DefaultLogger.java rename to sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultLogger.java index 18c3e58042d4..df717ff5a92b 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DefaultLogger.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/DefaultLogger.java @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package io.clientcore.core.implementation.util; +package io.clientcore.core.implementation.instrumentation; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.implementation.util.EnvironmentConfiguration; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.configuration.Configuration; import java.io.PrintStream; @@ -13,7 +14,7 @@ import java.time.LocalDateTime; import java.time.temporal.ChronoField; -import static io.clientcore.core.util.ClientLogger.LogLevel; +import static io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel; /** * This class is an internal implementation of slf4j logger. diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LoggingEventNames.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LoggingEventNames.java new file mode 100644 index 000000000000..a3a3213e2186 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LoggingEventNames.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation; + +/** + * This class contains the names of the logging events that are emitted by the client core. + */ +public class LoggingEventNames { + // HTTP logging event names. None of them are defined in otel semantic conventions. + /** + * Identifies event that is logged when an HTTP request is sent. + * Depending on configuration and implementation, this event may be logged when request headers are sent or when + * the request body is fully written. + */ + public static final String HTTP_REQUEST_EVENT_NAME = "http.request"; + + /** + * Identifies event that is logged when an HTTP response is received. + * Depending on configuration and implementation, this event may be logged when response headers and status code + * are received or when the response body is fully read. + */ + public static final String HTTP_RESPONSE_EVENT_NAME = "http.response"; + + /** + * Identifies event that is logged when an HTTP request is being redirected to another URL. + * The event describes whether the redirect will be followed or not along with redirect context. + */ + public static final String HTTP_REDIRECT_EVENT_NAME = "http.redirect"; + + /** + * Identifies event that is logged after an HTTP request has failed and is considered to be retried. + * The event describes whether the retry will be performed or not. + */ + public static final String HTTP_RETRY_EVENT_NAME = "http.retry"; + + // Other logging event names + + /** + * Identifies event that is logged when a span is ended. + */ + public static final String SPAN_ENDED_EVENT_NAME = "span.ended"; +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Slf4jLoggerShim.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShim.java similarity index 95% rename from sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Slf4jLoggerShim.java rename to sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShim.java index 8ecd69e34f32..7d7074724d37 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Slf4jLoggerShim.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShim.java @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package io.clientcore.core.implementation.util; +package io.clientcore.core.implementation.instrumentation; import io.clientcore.core.implementation.ReflectionUtils; import io.clientcore.core.implementation.ReflectiveInvoker; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; -import static io.clientcore.core.util.ClientLogger.LogLevel.ERROR; -import static io.clientcore.core.util.ClientLogger.LogLevel.INFORMATIONAL; -import static io.clientcore.core.util.ClientLogger.LogLevel.VERBOSE; -import static io.clientcore.core.util.ClientLogger.LogLevel.WARNING; +import static io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel.ERROR; +import static io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel.INFORMATIONAL; +import static io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel.VERBOSE; +import static io.clientcore.core.instrumentation.logging.ClientLogger.LogLevel.WARNING; public class Slf4jLoggerShim { private static final DefaultLogger DEFAULT_LOGGER = new DefaultLogger(Slf4jLoggerShim.class); diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackContextPropagator.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackContextPropagator.java new file mode 100644 index 000000000000..7af20884e802 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackContextPropagator.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.fallback; + +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.instrumentation.tracing.Span; +import io.clientcore.core.instrumentation.tracing.TraceContextGetter; +import io.clientcore.core.instrumentation.tracing.TraceContextPropagator; +import io.clientcore.core.instrumentation.tracing.TraceContextSetter; + +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.HTTP_REQUEST_HEADER_TRACEPARENT_KEY; + +final class FallbackContextPropagator implements TraceContextPropagator { + private static final ClientLogger LOGGER = new ClientLogger(FallbackContextPropagator.class); + static final TraceContextPropagator W3C_TRACE_CONTEXT_PROPAGATOR = new FallbackContextPropagator(); + + private FallbackContextPropagator() { + } + + /** + * {@inheritDoc} + */ + @Override + public void inject(InstrumentationContext spanContext, C carrier, TraceContextSetter setter) { + if (spanContext.isValid()) { + setter.set(carrier, "traceparent", + "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-" + spanContext.getTraceFlags()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public InstrumentationContext extract(InstrumentationContext context, C carrier, TraceContextGetter getter) { + String traceparent = getter.get(carrier, "traceparent"); + if (traceparent != null) { + if (isValidTraceparent(traceparent)) { + String traceId = traceparent.substring(3, 35); + String spanId = traceparent.substring(36, 52); + String traceFlags = traceparent.substring(53, 55); + return new FallbackSpanContext(traceId, spanId, traceFlags, true, Span.noop()); + } else { + LOGGER.atVerbose() + .addKeyValue(HTTP_REQUEST_HEADER_TRACEPARENT_KEY, traceparent) + .log("Invalid traceparent header"); + } + } + return context == null ? FallbackSpanContext.INVALID : context; + } + + /** + * Validates the traceparent header according to W3C Trace Context + * + * @param traceparent the traceparent header value + * @return true if the traceparent header is valid, false otherwise + */ + private static boolean isValidTraceparent(String traceparent) { + if (traceparent == null || traceparent.length() != 55) { + return false; + } + + // valid traceparent format: --- + // version - only 00 is supported + if (traceparent.charAt(0) != '0' + || traceparent.charAt(1) != '0' + || traceparent.charAt(2) != '-' + || traceparent.charAt(35) != '-' + || traceparent.charAt(52) != '-') { + return false; + } + + // trace-id - 32 lower case hex characters, all 0 is invalid + boolean isAllZero = true; + for (int i = 3; i < 35; i++) { + char c = traceparent.charAt(i); + if (c < '0' || c > 'f' || (c > '9' && c < 'a')) { + return false; + } + if (c != '0') { + isAllZero = false; + } + } + if (isAllZero) { + return false; + } + + // span-id - 16 lower case hex characters, all 0 is invalid + isAllZero = true; + for (int i = 36; i < 52; i++) { + char c = traceparent.charAt(i); + if (c < '0' || c > 'f' || (c > '9' && c < 'a')) { + return false; + } + if (c != '0') { + isAllZero = false; + } + } + + if (isAllZero) { + return false; + } + + // trace-flags - 2 lower case hex characters + for (int i = 53; i < 55; i++) { + char c = traceparent.charAt(i); + if (c < '0' || c > 'f' || (c > '9' && c < 'a')) { + return false; + } + } + + return true; + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentation.java new file mode 100644 index 000000000000..109398c9efbd --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentation.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.fallback; + +import io.clientcore.core.instrumentation.Instrumentation; +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.InstrumentationOptions; +import io.clientcore.core.instrumentation.LibraryInstrumentationOptions; +import io.clientcore.core.instrumentation.tracing.TraceContextPropagator; +import io.clientcore.core.instrumentation.tracing.Tracer; + +/** + * Fallback implementation of {@link Instrumentation} which implements basic correlation and context propagation + * and, when enabled, records traces as logs. + */ +public class FallbackInstrumentation implements Instrumentation { + public static final FallbackInstrumentation DEFAULT_INSTANCE = new FallbackInstrumentation(null, null); + + private final InstrumentationOptions instrumentationOptions; + private final LibraryInstrumentationOptions libraryOptions; + + /** + * Creates a new instance of {@link FallbackInstrumentation}. + * @param instrumentationOptions the application instrumentation options + * @param libraryOptions the library instrumentation options + */ + public FallbackInstrumentation(InstrumentationOptions instrumentationOptions, + LibraryInstrumentationOptions libraryOptions) { + this.instrumentationOptions = instrumentationOptions; + this.libraryOptions = libraryOptions; + } + + /** + * {@inheritDoc} + */ + @Override + public Tracer getTracer() { + return new FallbackTracer(instrumentationOptions, libraryOptions); + } + + /** + * {@inheritDoc} + */ + @Override + public TraceContextPropagator getW3CTraceContextPropagator() { + return FallbackContextPropagator.W3C_TRACE_CONTEXT_PROPAGATOR; + } + + /** + * Creates a new instance of {@link InstrumentationContext} from the given object. + * It recognizes {@link FallbackSpanContext}, {@link FallbackSpan}, and generic {@link InstrumentationContext} + * as a source and converts them to {@link FallbackSpanContext}. + * @param context the context object to convert + * @return the instance of {@link InstrumentationContext} which is invalid if the context is not recognized + * @param the type of the context object + */ + public InstrumentationContext createInstrumentationContext(T context) { + if (context instanceof InstrumentationContext) { + return FallbackSpanContext.fromInstrumentationContext((InstrumentationContext) context); + } else if (context instanceof FallbackSpan) { + return ((FallbackSpan) context).getInstrumentationContext(); + } else { + return FallbackSpanContext.INVALID; + } + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackScope.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackScope.java new file mode 100644 index 000000000000..008b79ce6599 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackScope.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.fallback; + +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.instrumentation.tracing.Span; +import io.clientcore.core.instrumentation.tracing.TracingScope; + +final class FallbackScope implements TracingScope { + private static final ClientLogger LOGGER = new ClientLogger(FallbackScope.class); + private static final ThreadLocal CURRENT_SPAN = new ThreadLocal<>(); + private final FallbackSpan originalSpan; + private final FallbackSpan span; + + FallbackScope(FallbackSpan span) { + this.originalSpan = CURRENT_SPAN.get(); + this.span = span; + CURRENT_SPAN.set(span); + } + + /** + * {@inheritDoc} + */ + @Override + public void close() { + if (CURRENT_SPAN.get() == span) { + CURRENT_SPAN.set(originalSpan); + } else { + LOGGER.atVerbose().log("Attempting to close scope that is not the current. Ignoring."); + } + } + + static Span getCurrentSpan() { + FallbackSpan span = CURRENT_SPAN.get(); + return span == null ? Span.noop() : span; + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpan.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpan.java new file mode 100644 index 000000000000..33c420b31876 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpan.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.fallback; + +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.instrumentation.tracing.Span; +import io.clientcore.core.instrumentation.tracing.TracingScope; + +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.ERROR_TYPE_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_DURATION_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_ID_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.TRACE_ID_KEY; +import static io.clientcore.core.implementation.instrumentation.LoggingEventNames.SPAN_ENDED_EVENT_NAME; + +final class FallbackSpan implements Span { + private final ClientLogger.LoggingEvent log; + private final long startTime; + private final FallbackSpanContext spanContext; + private String errorType; + + FallbackSpan(ClientLogger.LoggingEvent log, FallbackSpanContext parentSpanContext, boolean isRecording) { + this.log = log; + this.startTime = isRecording ? System.nanoTime() : 0; + this.spanContext = FallbackSpanContext.fromParent(parentSpanContext, isRecording, this); + if (log != null && log.isEnabled()) { + this.log.addKeyValue(TRACE_ID_KEY, spanContext.getTraceId()) + .addKeyValue(SPAN_ID_KEY, spanContext.getSpanId()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Span setAttribute(String key, Object value) { + if (log != null) { + log.addKeyValue(key, value); + } + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public Span setError(String errorType) { + this.errorType = errorType; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public void end() { + end(null); + } + + /** + * {@inheritDoc} + */ + @Override + public void end(Throwable error) { + if (log == null || !log.isEnabled()) { + return; + } + + double durationMs = (System.nanoTime() - startTime) / 1_000_000.0; + log.addKeyValue(SPAN_DURATION_KEY, durationMs); + if (error != null || errorType != null) { + setAttribute(ERROR_TYPE_KEY, errorType != null ? errorType : error.getClass().getCanonicalName()); + } + + log.setEventName(SPAN_ENDED_EVENT_NAME); + log.log(null); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isRecording() { + return log != null && log.isEnabled(); + } + + /** + * {@inheritDoc} + */ + @Override + public TracingScope makeCurrent() { + return new FallbackScope(this); + } + + /** + * {@inheritDoc} + */ + @Override + public InstrumentationContext getInstrumentationContext() { + return spanContext; + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanBuilder.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanBuilder.java new file mode 100644 index 000000000000..b2e67a3a5abc --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanBuilder.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.fallback; + +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.instrumentation.tracing.Span; +import io.clientcore.core.instrumentation.tracing.SpanBuilder; +import io.clientcore.core.instrumentation.tracing.SpanKind; + +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_KIND_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_NAME_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_PARENT_ID_KEY; + +final class FallbackSpanBuilder implements SpanBuilder { + static final FallbackSpanBuilder NOOP = new FallbackSpanBuilder(); + private final ClientLogger.LoggingEvent log; + private final FallbackSpanContext parentSpanContext; + + private FallbackSpanBuilder() { + this.log = null; + this.parentSpanContext = FallbackSpanContext.INVALID; + } + + FallbackSpanBuilder(ClientLogger logger, String spanName, SpanKind spanKind, + InstrumentationContext instrumentationContext) { + this.parentSpanContext = FallbackSpanContext.fromInstrumentationContext(instrumentationContext); + this.log = logger.atVerbose(); + if (log.isEnabled()) { + log.addKeyValue(SPAN_NAME_KEY, spanName).addKeyValue(SPAN_KIND_KEY, spanKind.name()); + if (parentSpanContext.isValid()) { + log.addKeyValue(SPAN_PARENT_ID_KEY, parentSpanContext.getSpanId()); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public SpanBuilder setAttribute(String key, Object value) { + if (log != null) { + log.addKeyValue(key, value); + } + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public Span startSpan() { + if (log != null) { + return new FallbackSpan(log, parentSpanContext, log.isEnabled()); + } + + return Span.noop(); + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanContext.java new file mode 100644 index 000000000000..f5a588451d05 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackSpanContext.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.fallback; + +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.tracing.Span; + +import static io.clientcore.core.implementation.instrumentation.fallback.RandomIdUtils.INVALID_SPAN_ID; +import static io.clientcore.core.implementation.instrumentation.fallback.RandomIdUtils.INVALID_TRACE_ID; +import static io.clientcore.core.implementation.instrumentation.fallback.RandomIdUtils.generateSpanId; +import static io.clientcore.core.implementation.instrumentation.fallback.RandomIdUtils.generateTraceId; + +final class FallbackSpanContext implements InstrumentationContext { + static final FallbackSpanContext INVALID + = new FallbackSpanContext(INVALID_TRACE_ID, INVALID_SPAN_ID, "00", false, Span.noop()); + private final String traceId; + private final String spanId; + private final String traceFlags; + private final boolean isValid; + private final Span span; + + /** + * {@inheritDoc} + */ + @Override + public String getTraceId() { + return traceId; + } + + /** + * {@inheritDoc} + */ + @Override + public String getSpanId() { + return spanId; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isValid() { + return isValid; + } + + /** + * {@inheritDoc} + */ + @Override + public Span getSpan() { + return this.span; + } + + /** + * {@inheritDoc} + */ + @Override + public String getTraceFlags() { + return traceFlags; + } + + FallbackSpanContext(String traceId, String spanId, String traceFlags, boolean isValid, Span span) { + this.traceId = traceId; + this.spanId = spanId; + this.traceFlags = traceFlags; + this.isValid = isValid; + this.span = span; + } + + static FallbackSpanContext fromParent(InstrumentationContext parent, boolean isSampled, FallbackSpan span) { + return parent.isValid() + ? new FallbackSpanContext(parent.getTraceId(), generateSpanId(), isSampled ? "01" : "00", true, span) + : new FallbackSpanContext(generateTraceId(), generateSpanId(), isSampled ? "01" : "00", true, span); + } + + static FallbackSpanContext fromInstrumentationContext(InstrumentationContext instrumentationContext) { + if (instrumentationContext instanceof FallbackSpanContext) { + return (FallbackSpanContext) instrumentationContext; + } + + if (instrumentationContext != null) { + return new FallbackSpanContext(instrumentationContext.getTraceId(), instrumentationContext.getSpanId(), + instrumentationContext.getTraceFlags(), instrumentationContext.isValid(), + instrumentationContext.getSpan()); + } + + Span currentSpan = FallbackScope.getCurrentSpan(); + if (currentSpan != Span.noop()) { + return (FallbackSpanContext) currentSpan.getInstrumentationContext(); + } + + return INVALID; + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracer.java new file mode 100644 index 000000000000..006d86ebeacf --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackTracer.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.fallback; + +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.InstrumentationOptions; +import io.clientcore.core.instrumentation.LibraryInstrumentationOptions; +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.instrumentation.tracing.SpanBuilder; +import io.clientcore.core.instrumentation.tracing.SpanKind; +import io.clientcore.core.instrumentation.tracing.Tracer; + +import java.util.HashMap; +import java.util.Map; + +final class FallbackTracer implements Tracer { + private static final ClientLogger LOGGER = new ClientLogger(FallbackTracer.class); + private final boolean isEnabled; + private final ClientLogger logger; + + FallbackTracer(InstrumentationOptions instrumentationOptions, LibraryInstrumentationOptions libraryOptions) { + // TODO (limolkova): do we need additional config to enable fallback tracing? Or maybe we enable it only if logs are enabled? + this.isEnabled = instrumentationOptions == null || instrumentationOptions.isTracingEnabled(); + this.logger = isEnabled ? getLogger(instrumentationOptions, libraryOptions) : LOGGER; + } + + private static ClientLogger getLogger(InstrumentationOptions instrumentationOptions, + LibraryInstrumentationOptions libraryOptions) { + Object providedLogger = instrumentationOptions == null ? null : instrumentationOptions.getProvider(); + if (providedLogger instanceof ClientLogger) { + return (ClientLogger) providedLogger; + } + + Map libraryContext = new HashMap<>(2); + libraryContext.put("library.name", libraryOptions.getLibraryName()); + libraryContext.put("library.version", libraryOptions.getLibraryVersion()); + + return new ClientLogger(libraryOptions.getLibraryName() + ".tracing", libraryContext); + } + + /** + * {@inheritDoc} + */ + @Override + public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, InstrumentationContext instrumentationContext) { + return isEnabled + ? new FallbackSpanBuilder(logger, spanName, spanKind, instrumentationContext) + : FallbackSpanBuilder.NOOP; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEnabled() { + return isEnabled; + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/RandomIdUtils.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/RandomIdUtils.java new file mode 100644 index 000000000000..205f9b77be48 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/RandomIdUtils.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This code was copied from the OpenTelemetry Java SDK +// https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RandomIdGenerator.java +// and modified to fit the needs of the ClientCore library. +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.clientcore.core.implementation.instrumentation.fallback; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +class RandomIdUtils { + public static final String INVALID_TRACE_ID = "00000000000000000000000000000000"; + public static final String INVALID_SPAN_ID = "0000000000000000"; + + private static final int BYTE_BASE16 = 2; + private static final long INVALID_ID = 0; + private static final int TRACE_ID_HEX_LENGTH = 32; + private static final int SPAN_ID_HEX_LENGTH = 16; + private static final char[] ENCODING = buildEncodingArray(); + + public static String generateSpanId() { + long id; + do { + id = ThreadLocalRandom.current().nextLong(); + } while (id == INVALID_ID); + return getSpanId(id); + } + + public static String generateTraceId() { + Random random = ThreadLocalRandom.current(); + long idHi = random.nextLong(); + long idLo; + do { + idLo = random.nextLong(); + } while (idLo == INVALID_ID); + return getTraceId(idHi, idLo); + } + + private static String getSpanId(long id) { + if (id == 0) { + return INVALID_SPAN_ID; + } + char[] result = new char[SPAN_ID_HEX_LENGTH]; + longToBase16String(id, result, 0); + return new String(result, 0, SPAN_ID_HEX_LENGTH); + } + + private static String getTraceId(long traceIdLongHighPart, long traceIdLongLowPart) { + if (traceIdLongHighPart == 0 && traceIdLongLowPart == 0) { + return INVALID_TRACE_ID; + } + char[] chars = new char[TRACE_ID_HEX_LENGTH]; + longToBase16String(traceIdLongHighPart, chars, 0); + longToBase16String(traceIdLongLowPart, chars, 16); + return new String(chars, 0, TRACE_ID_HEX_LENGTH); + } + + private static void longToBase16String(long value, char[] dest, int destOffset) { + byteToBase16((byte) (value >> 56 & 0xFFL), dest, destOffset); + byteToBase16((byte) (value >> 48 & 0xFFL), dest, destOffset + BYTE_BASE16); + byteToBase16((byte) (value >> 40 & 0xFFL), dest, destOffset + 2 * BYTE_BASE16); + byteToBase16((byte) (value >> 32 & 0xFFL), dest, destOffset + 3 * BYTE_BASE16); + byteToBase16((byte) (value >> 24 & 0xFFL), dest, destOffset + 4 * BYTE_BASE16); + byteToBase16((byte) (value >> 16 & 0xFFL), dest, destOffset + 5 * BYTE_BASE16); + byteToBase16((byte) (value >> 8 & 0xFFL), dest, destOffset + 6 * BYTE_BASE16); + byteToBase16((byte) (value & 0xFFL), dest, destOffset + 7 * BYTE_BASE16); + } + + private static void byteToBase16(byte value, char[] dest, int destOffset) { + int b = value & 0xFF; + dest[destOffset] = ENCODING[b]; + dest[destOffset + 1] = ENCODING[b | 0x100]; + } + + private static char[] buildEncodingArray() { + String alphabet = "0123456789abcdef"; + char[] encoding = new char[512]; + for (int i = 0; i < 256; ++i) { + encoding[i] = alphabet.charAt(i >>> 4); + encoding[i | 0x100] = alphabet.charAt(i & 0xF); + } + return encoding; + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/package-info.java new file mode 100644 index 000000000000..5dd2110ab7d7 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/fallback/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains fallback implementation of {@link java.lang.instrument.Instrumentation} + * that implements basic distributed tracing. + */ +package io.clientcore.core.implementation.instrumentation.fallback; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/FallbackInvoker.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/FallbackInvoker.java index 72d85b0da7fd..f3920928d277 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/FallbackInvoker.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/FallbackInvoker.java @@ -4,7 +4,7 @@ package io.clientcore.core.implementation.instrumentation.otel; import io.clientcore.core.implementation.ReflectiveInvoker; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; /** * A wrapper around a {@link ReflectiveInvoker} that provides a fallback value if the invocation fails, @@ -39,6 +39,7 @@ public FallbackInvoker(ReflectiveInvoker inner, Object fallback, ClientLogger lo /** * Invokes the inner invoker and returns the fallback value if the invocation fails. + * * @return the result of the invocation or the fallback value */ public Object invoke() { @@ -52,6 +53,7 @@ public Object invoke() { /** * Invokes the inner invoker and returns the fallback value if the invocation fails. + * * @param argOrTarget the argument or target * @return the result of the invocation or the fallback value */ @@ -66,6 +68,7 @@ public Object invoke(Object argOrTarget) { /** * Invokes the inner invoker and returns the fallback value if the invocation fails. + * * @param argOrTarget the argument or target * @param arg1 the first argument * @return the result of the invocation or the fallback value @@ -81,6 +84,7 @@ public Object invoke(Object argOrTarget, Object arg1) { /** * Invokes the inner invoker and returns the fallback value if the invocation fails. + * * @param argOrTarget the argument or target * @param arg1 the first argument * @param arg2 the second argument @@ -97,6 +101,7 @@ public Object invoke(Object argOrTarget, Object arg1, Object arg2) { /** * Invokes the inner invoker and returns the fallback value if the invocation fails. + * * @param argOrTarget the argument or target * @param arg1 the first argument * @param arg2 the second argument diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelAttributeKey.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelAttributeKey.java index 4e1b1932aa1c..d6facac4803a 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelAttributeKey.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelAttributeKey.java @@ -4,7 +4,7 @@ package io.clientcore.core.implementation.instrumentation.otel; import io.clientcore.core.implementation.ReflectiveInvoker; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.ATTRIBUTE_KEY_CLASS; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInitializer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInitializer.java index 7b0e5eb1b2a2..3e9b02871bd6 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInitializer.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInitializer.java @@ -3,7 +3,7 @@ package io.clientcore.core.implementation.instrumentation.otel; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; /** * This class is used to initialize OpenTelemetry. @@ -186,4 +186,5 @@ public static void runtimeError(ClientLogger logger, Throwable t) { public static boolean isInitialized() { return INSTANCE.initialized; } + } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInstrumentation.java index 6edbbb51a4eb..38783b3bf6bc 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInstrumentation.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInstrumentation.java @@ -4,18 +4,24 @@ package io.clientcore.core.implementation.instrumentation.otel; import io.clientcore.core.implementation.ReflectiveInvoker; +import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpan; +import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext; import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelTraceContextPropagator; import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelTracer; +import io.clientcore.core.instrumentation.Instrumentation; +import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.instrumentation.LibraryInstrumentationOptions; import io.clientcore.core.instrumentation.InstrumentationOptions; -import io.clientcore.core.instrumentation.Instrumentation; import io.clientcore.core.instrumentation.tracing.TraceContextPropagator; import io.clientcore.core.instrumentation.tracing.Tracer; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.GLOBAL_OTEL_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.OTEL_CLASS; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CLASS; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CONTEXT_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACER_PROVIDER_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.W3C_PROPAGATOR_CLASS; @@ -60,6 +66,7 @@ public class OTelInstrumentation implements Instrumentation { W3C_PROPAGATOR_INSTANCE = new OTelTraceContextPropagator(w3cPropagatorInstance); } + public static final OTelInstrumentation DEFAULT_INSTANCE = new OTelInstrumentation(null, null); private final Object otelInstance; private final LibraryInstrumentationOptions libraryOptions; @@ -111,6 +118,32 @@ public TraceContextPropagator getW3CTraceContextPropagator() { return OTelInitializer.isInitialized() ? W3C_PROPAGATOR_INSTANCE : OTelTraceContextPropagator.NOOP; } + /** + * Creates a new instance of {@link InstrumentationContext} from the given object. + * It recognizes {@code io.opentelemetry.api.trace.Span}, {@code io.opentelemetry.api.trace.SpanContext}, + * {@code io.opentelemetry.context.Context} and generic {@link InstrumentationContext} + * as a source and converts them to {@link InstrumentationContext}. + * @param context the context object to convert + * @return the instance of {@link InstrumentationContext} which is invalid if the context is not recognized + * @param the type of the context object + */ + public InstrumentationContext createInstrumentationContext(T context) { + if (context instanceof InstrumentationContext) { + return (InstrumentationContext) context; + } else if (context instanceof OTelSpan) { + return ((OTelSpan) context).getInstrumentationContext(); + } else if (SPAN_CLASS.isInstance(context)) { + return OTelSpanContext.fromOTelSpan(context); + } else if (CONTEXT_CLASS.isInstance(context)) { + return OTelSpanContext.fromOTelContext(context); + } else if (SPAN_CONTEXT_CLASS.isInstance(context)) { + return new OTelSpanContext(context, null); + } + + return OTelSpanContext.getInvalid(); + + } + private Object getOtelInstance() { // not caching global to prevent caching instance that was not setup yet at the start time. return otelInstance != null ? otelInstance : GET_GLOBAL_OTEL_INVOKER.invoke(); diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/package-info.java index ead18dbf6d52..d3c6f6ccd74e 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/package-info.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/package-info.java @@ -2,6 +2,6 @@ // Licensed under the MIT License. /** - * This package contains the implementation of the OpenTelemetry telemetry provider. + * This package contains OpenTelemetry-based implementation of {@link java.lang.instrument.Instrumentation} */ package io.clientcore.core.implementation.instrumentation.otel; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelContext.java index 1d7219bc52a8..bfe298ea4f13 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelContext.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelContext.java @@ -6,9 +6,9 @@ import io.clientcore.core.implementation.ReflectiveInvoker; import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker; import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; +import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.instrumentation.tracing.TracingScope; -import io.clientcore.core.instrumentation.tracing.SpanKind; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS; @@ -24,19 +24,19 @@ class OTelContext { private static final FallbackInvoker GET_INVOKER; // this context key will indicate if the span is created by client core - // AND has client or internal kind (logical client operation) // this is used to suppress multiple spans created for the same logical operation // such as convenience API on top of protocol methods when both as instrumented. // We might need to suppress logical server (consumer) spans in the future, but that // was not necessary so far - private static final Object HAS_CLIENT_SPAN_CONTEXT_KEY; + private static final Object CLIENT_CORE_SPAN_CONTEXT_KEY; static { ReflectiveInvoker currentInvoker = null; ReflectiveInvoker makeCurrentInvoker = null; ReflectiveInvoker withInvoker = null; ReflectiveInvoker getInvoker = null; - Object hasClientSpanContextKey = null; + + Object clientCoreSpanContextKey = null; Object rootContext = null; if (OTelInitializer.isInitialized()) { @@ -50,10 +50,11 @@ class OTelContext { ReflectiveInvoker contextKeyNamedInvoker = getMethodInvoker(CONTEXT_KEY_CLASS, CONTEXT_KEY_CLASS.getMethod("named", String.class)); - hasClientSpanContextKey = contextKeyNamedInvoker.invoke("client-core-call"); + clientCoreSpanContextKey = contextKeyNamedInvoker.invoke("client-core-span"); ReflectiveInvoker rootInvoker = getMethodInvoker(CONTEXT_CLASS, CONTEXT_CLASS.getMethod("root")); rootContext = rootInvoker.invoke(); + } catch (Throwable t) { OTelInitializer.initError(LOGGER, t); } @@ -63,7 +64,7 @@ class OTelContext { MAKE_CURRENT_INVOKER = new FallbackInvoker(makeCurrentInvoker, NOOP_SCOPE, LOGGER); WITH_INVOKER = new FallbackInvoker(withInvoker, LOGGER); GET_INVOKER = new FallbackInvoker(getInvoker, LOGGER); - HAS_CLIENT_SPAN_CONTEXT_KEY = hasClientSpanContextKey; + CLIENT_CORE_SPAN_CONTEXT_KEY = clientCoreSpanContextKey; } static Object getCurrent() { @@ -79,21 +80,41 @@ static AutoCloseable makeCurrent(Object context) { return (AutoCloseable) scope; } - static Object markCoreSpan(Object context, SpanKind spanKind) { + static Object markCoreSpan(Object context, OTelSpan span) { assert CONTEXT_CLASS.isInstance(context); - if (spanKind == SpanKind.CLIENT || spanKind == SpanKind.INTERNAL) { - Object updatedContext = WITH_INVOKER.invoke(context, HAS_CLIENT_SPAN_CONTEXT_KEY, Boolean.TRUE); - if (updatedContext != null) { - return updatedContext; - } - } - return context; + Object updatedContext = WITH_INVOKER.invoke(context, CLIENT_CORE_SPAN_CONTEXT_KEY, span); + return updatedContext == null ? context : updatedContext; } - static boolean hasClientCoreSpan(Object context) { + static OTelSpan getClientCoreSpan(Object context) { assert CONTEXT_CLASS.isInstance(context); - Object flag = GET_INVOKER.invoke(context, HAS_CLIENT_SPAN_CONTEXT_KEY); - assert flag == null || flag instanceof Boolean; - return Boolean.TRUE.equals(flag); + Object clientCoreSpan = GET_INVOKER.invoke(context, CLIENT_CORE_SPAN_CONTEXT_KEY); + assert clientCoreSpan == null || clientCoreSpan instanceof OTelSpan; + return (OTelSpan) clientCoreSpan; + } + + /** + * Get the OpenTelemetry context from the given context. + * + * @param context the context + * @return the OpenTelemetry context + */ + static Object fromInstrumentationContext(InstrumentationContext context) { + if (context instanceof OTelSpanContext) { + Object otelContext = ((OTelSpanContext) context).getOtelContext(); + if (otelContext != null) { + return otelContext; + } + } + + Object currentContext = CURRENT_INVOKER.invoke(); + if (context != null) { + Object spanContext = OTelSpanContext.toOTelSpanContext(context); + Object span = OTelSpan.wrapSpanContext(spanContext); + + return OTelSpan.storeInContext(span, currentContext); + } + + return currentContext; } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpan.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpan.java index 52a31d0f42e3..48ca484fee58 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpan.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpan.java @@ -7,14 +7,16 @@ import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker; import io.clientcore.core.implementation.instrumentation.otel.OTelAttributeKey; import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; +import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.instrumentation.tracing.TracingScope; import io.clientcore.core.instrumentation.tracing.Span; import io.clientcore.core.instrumentation.tracing.SpanKind; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.util.Objects; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.ERROR_TYPE_KEY; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.ATTRIBUTE_KEY_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CLASS; @@ -28,6 +30,8 @@ */ public class OTelSpan implements Span { private static final ClientLogger LOGGER = new ClientLogger(OTelSpan.class); + static final OTelSpan NOOP_SPAN; + private static final Object ERROR_TYPE_ATTRIBUTE_KEY; private static final TracingScope NOOP_SCOPE = () -> { }; private static final FallbackInvoker SET_ATTRIBUTE_INVOKER; @@ -42,7 +46,9 @@ public class OTelSpan implements Span { private final Object otelSpan; private final Object otelContext; private final boolean isRecording; + private final SpanKind spanKind; private String errorType; + private OTelSpanContext spanContext; static { ReflectiveInvoker setAttributeInvoker = null; @@ -55,6 +61,8 @@ public class OTelSpan implements Span { ReflectiveInvoker wrapInvoker = null; Object errorStatusCode = null; + OTelSpan noopSpan = null; + Object errorTypeAttributeKey = null; if (OTelInitializer.isInitialized()) { try { @@ -75,6 +83,14 @@ public class OTelSpan implements Span { wrapInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("wrap", SPAN_CONTEXT_CLASS)); errorStatusCode = STATUS_CODE_CLASS.getField("ERROR").get(null); + + ReflectiveInvoker getInvalidInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("getInvalid")); + + Object invalidSpan = getInvalidInvoker.invoke(); + Object rootContext = OTelContext.getCurrent(); + + noopSpan = new OTelSpan(invalidSpan, rootContext); + errorTypeAttributeKey = OTelAttributeKey.getKey(ERROR_TYPE_KEY, ""); } catch (Throwable t) { OTelInitializer.initError(LOGGER, t); } @@ -88,16 +104,26 @@ public class OTelSpan implements Span { STORE_IN_CONTEXT_INVOKER = new FallbackInvoker(storeInContextInvoker, LOGGER); FROM_CONTEXT_INVOKER = new FallbackInvoker(fromContextInvoker, LOGGER); WRAP_INVOKER = new FallbackInvoker(wrapInvoker, LOGGER); + NOOP_SPAN = noopSpan; ERROR_STATUS_CODE = errorStatusCode; + ERROR_TYPE_ATTRIBUTE_KEY = errorTypeAttributeKey; } OTelSpan(Object otelSpan, Object otelParentContext, SpanKind spanKind) { this.otelSpan = otelSpan; this.isRecording = otelSpan != null && (boolean) IS_RECORDING_INVOKER.invoke(otelSpan); + this.spanKind = spanKind; Object contextWithSpan = otelSpan != null ? storeInContext(otelSpan, otelParentContext) : otelParentContext; - this.otelContext = markCoreSpan(contextWithSpan, spanKind); + this.otelContext = markCoreSpan(contextWithSpan, this); + } + + private OTelSpan(Object otelSpan, Object otelContext) { + this.otelSpan = otelSpan; + this.isRecording = false; + this.spanKind = null; + this.otelContext = otelContext; } /** @@ -139,17 +165,6 @@ public void end() { endSpan(null); } - /** - * Gets span context. - * - * @return the span context. - */ - public OTelSpanContext getSpanContext() { - return isInitialized() - ? new OTelSpanContext(GET_SPAN_CONTEXT_INVOKER.invoke(otelSpan)) - : OTelSpanContext.getInvalid(); - } - /** * {@inheritDoc} */ @@ -166,29 +181,85 @@ public TracingScope makeCurrent() { return isInitialized() ? wrapOTelScope(OTelContext.makeCurrent(otelContext)) : NOOP_SCOPE; } + /** + * {@inheritDoc} + */ + public InstrumentationContext getInstrumentationContext() { + if (spanContext != null) { + return spanContext; + } + + spanContext = isInitialized() + ? new OTelSpanContext(GET_SPAN_CONTEXT_INVOKER.invoke(otelSpan), otelContext) + : OTelSpanContext.getInvalid(); + + return spanContext; + } + + SpanKind getSpanKind() { + return spanKind; + } + + Object getOtelSpan() { + return otelSpan; + } + + static OTelSpan createPropagatingSpan(OTelSpanContext spanContext) { + Object span = wrapSpanContext(spanContext.getOtelSpanContext()); + return new OTelSpan(span, spanContext.getOtelContext()); + } + static Object createPropagatingSpan(Object otelContext) { assert CONTEXT_CLASS.isInstance(otelContext); - Object span = FROM_CONTEXT_INVOKER.invoke(otelContext); - assert SPAN_CLASS.isInstance(span); + Object span = fromOTelContext(otelContext); Object spanContext = GET_SPAN_CONTEXT_INVOKER.invoke(span); assert SPAN_CONTEXT_CLASS.isInstance(spanContext); - Object propagatingSpan = WRAP_INVOKER.invoke(spanContext); + return wrapSpanContext(spanContext); + } + + static Object fromOTelContext(Object otelContext) { + assert CONTEXT_CLASS.isInstance(otelContext); + + Object span = FROM_CONTEXT_INVOKER.invoke(otelContext); + assert SPAN_CLASS.isInstance(span); + + return span; + } + + static Object wrapSpanContext(Object otelSpanContext) { + assert SPAN_CONTEXT_CLASS.isInstance(otelSpanContext); + + Object propagatingSpan = WRAP_INVOKER.invoke(otelSpanContext); assert SPAN_CLASS.isInstance(propagatingSpan); return propagatingSpan; } - Object getOtelContext() { - return otelContext; + static Object getSpanContext(Object otelSpan) { + assert SPAN_CLASS.isInstance(otelSpan); + + Object spanContext = GET_SPAN_CONTEXT_INVOKER.invoke(otelSpan); + assert SPAN_CONTEXT_CLASS.isInstance(spanContext); + + return spanContext; + } + + static Object storeInContext(Object otelSpan, Object otelContext) { + Object updatedContext = STORE_IN_CONTEXT_INVOKER.invoke(otelSpan, otelContext); + + return updatedContext != null ? updatedContext : otelContext; } private void endSpan(Throwable throwable) { if (isInitialized()) { if (errorType != null || throwable != null) { - setAttribute("error.type", errorType != null ? errorType : throwable.getClass().getCanonicalName()); + + String errorTypeStr = errorType != null ? errorType : throwable.getClass().getCanonicalName(); + SET_ATTRIBUTE_INVOKER.invoke(otelSpan, ERROR_TYPE_ATTRIBUTE_KEY, errorTypeStr); + SET_STATUS_INVOKER.invoke(otelSpan, ERROR_STATUS_CODE, throwable == null ? null : throwable.getMessage()); } @@ -207,11 +278,6 @@ private static TracingScope wrapOTelScope(AutoCloseable otelScope) { }; } - private static Object storeInContext(Object otelSpan, Object otelContext) { - Object updatedContext = STORE_IN_CONTEXT_INVOKER.invoke(otelSpan, otelContext); - return updatedContext != null ? updatedContext : otelContext; - } - private boolean isInitialized() { return otelSpan != null && OTelInitializer.isInitialized(); } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanBuilder.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanBuilder.java index ccd5e25dfb1a..0dcf0e53e8a5 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanBuilder.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanBuilder.java @@ -7,12 +7,12 @@ import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker; import io.clientcore.core.implementation.instrumentation.LibraryInstrumentationOptionsAccessHelper; import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; +import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.instrumentation.LibraryInstrumentationOptions; import io.clientcore.core.instrumentation.tracing.Span; import io.clientcore.core.instrumentation.tracing.SpanBuilder; import io.clientcore.core.instrumentation.tracing.SpanKind; -import io.clientcore.core.util.ClientLogger; -import io.clientcore.core.util.Context; +import io.clientcore.core.instrumentation.logging.ClientLogger; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; import static io.clientcore.core.implementation.instrumentation.otel.OTelAttributeKey.castAttributeValue; @@ -21,17 +21,15 @@ import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_BUILDER_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_KIND_CLASS; -import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelUtils.getOTelContext; /** * OpenTelemetry implementation of {@link SpanBuilder}. */ public class OTelSpanBuilder implements SpanBuilder { static final OTelSpanBuilder NOOP - = new OTelSpanBuilder(null, SpanKind.INTERNAL, Context.none(), new LibraryInstrumentationOptions("noop")); + = new OTelSpanBuilder(null, SpanKind.INTERNAL, null, new LibraryInstrumentationOptions("noop")); private static final ClientLogger LOGGER = new ClientLogger(OTelSpanBuilder.class); - private static final OTelSpan NOOP_SPAN; private static final FallbackInvoker SET_PARENT_INVOKER; private static final FallbackInvoker SET_ATTRIBUTE_INVOKER; private static final FallbackInvoker SET_SPAN_KIND_INVOKER; @@ -45,7 +43,7 @@ public class OTelSpanBuilder implements SpanBuilder { private final Object otelSpanBuilder; private final boolean suppressNestedSpans; private final SpanKind spanKind; - private final Context context; + private final InstrumentationContext context; static { ReflectiveInvoker setParentInvoker = null; @@ -58,7 +56,6 @@ public class OTelSpanBuilder implements SpanBuilder { Object clientKind = null; Object producerKind = null; Object consumerKind = null; - OTelSpan noopSpan = null; if (OTelInitializer.isInitialized()) { try { @@ -78,18 +75,15 @@ public class OTelSpanBuilder implements SpanBuilder { clientKind = SPAN_KIND_CLASS.getField("CLIENT").get(null); producerKind = SPAN_KIND_CLASS.getField("PRODUCER").get(null); consumerKind = SPAN_KIND_CLASS.getField("CONSUMER").get(null); - - noopSpan = new OTelSpan(null, OTelContext.getCurrent(), SpanKind.INTERNAL); } catch (Throwable t) { OTelInitializer.initError(LOGGER, t); } } - NOOP_SPAN = noopSpan; SET_PARENT_INVOKER = new FallbackInvoker(setParentInvoker, LOGGER); SET_ATTRIBUTE_INVOKER = new FallbackInvoker(setAttributeInvoker, LOGGER); SET_SPAN_KIND_INVOKER = new FallbackInvoker(setSpanKindInvoker, LOGGER); - START_SPAN_INVOKER = new FallbackInvoker(startSpanInvoker, NOOP_SPAN, LOGGER); + START_SPAN_INVOKER = new FallbackInvoker(startSpanInvoker, OTelSpan.NOOP_SPAN.getOtelSpan(), LOGGER); INTERNAL_KIND = internalKind; SERVER_KIND = serverKind; CLIENT_KIND = clientKind; @@ -98,7 +92,7 @@ public class OTelSpanBuilder implements SpanBuilder { } - OTelSpanBuilder(Object otelSpanBuilder, SpanKind kind, Context parent, + OTelSpanBuilder(Object otelSpanBuilder, SpanKind kind, InstrumentationContext parent, LibraryInstrumentationOptions libraryOptions) { this.otelSpanBuilder = otelSpanBuilder; this.suppressNestedSpans = libraryOptions == null @@ -125,7 +119,7 @@ public SpanBuilder setAttribute(String key, Object value) { @Override public Span startSpan() { if (isInitialized()) { - Object otelParentContext = getOTelContext(context); + Object otelParentContext = OTelContext.fromInstrumentationContext(context); SET_PARENT_INVOKER.invoke(otelSpanBuilder, otelParentContext); SET_SPAN_KIND_INVOKER.invoke(otelSpanBuilder, toOtelSpanKind(spanKind)); Object otelSpan = shouldSuppress(otelParentContext) @@ -136,13 +130,16 @@ public Span startSpan() { } } - return NOOP_SPAN; + return OTelSpan.NOOP_SPAN; } private boolean shouldSuppress(Object parentContext) { - return suppressNestedSpans - && (this.spanKind == SpanKind.CLIENT || this.spanKind == SpanKind.INTERNAL) - && OTelContext.hasClientCoreSpan(parentContext); + if (suppressNestedSpans && (this.spanKind == SpanKind.CLIENT || this.spanKind == SpanKind.INTERNAL)) { + OTelSpan span = OTelContext.getClientCoreSpan(parentContext); + return span != null && (span.getSpanKind() == SpanKind.INTERNAL || span.getSpanKind() == SpanKind.CLIENT); + } + + return false; } private Object toOtelSpanKind(SpanKind spanKind) { diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanContext.java index 9433464c2281..83370ede4105 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanContext.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanContext.java @@ -6,15 +6,19 @@ import io.clientcore.core.implementation.ReflectiveInvoker; import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker; import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.instrumentation.tracing.Span; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CONTEXT_CLASS; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACE_FLAGS_CLASS; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACE_STATE_CLASS; /** * Wrapper around OpenTelemetry SpanContext. */ -public class OTelSpanContext { +public class OTelSpanContext implements InstrumentationContext { public static final Object INVALID_OTEL_SPAN_CONTEXT; private static final String INVALID_TRACE_ID = "00000000000000000000000000000000"; private static final String INVALID_SPAN_ID = "0000000000000000"; @@ -24,12 +28,22 @@ public class OTelSpanContext { private static final FallbackInvoker GET_SPAN_ID_INVOKER; private static final FallbackInvoker GET_TRACE_ID_INVOKER; private static final FallbackInvoker GET_TRACE_FLAGS_INVOKER; + private static final FallbackInvoker IS_VALID_INVOKER; + private static final FallbackInvoker CREATE_INVOKER; private final Object otelSpanContext; + private final Object otelContext; + private String traceId; + private String spanId; + private String traceFlags; + private Boolean isValid; + static { ReflectiveInvoker getSpanIdInvoker = null; ReflectiveInvoker getTraceIdInvoker = null; ReflectiveInvoker getTraceFlagsInvoker = null; + ReflectiveInvoker isValidInvoker = null; + ReflectiveInvoker createInvoker = null; Object invalidInstance = null; @@ -43,58 +57,160 @@ public class OTelSpanContext { = getMethodInvoker(SPAN_CONTEXT_CLASS, SPAN_CONTEXT_CLASS.getMethod("getInvalid")); invalidInstance = getInvalidInvoker.invoke(); + isValidInvoker = getMethodInvoker(SPAN_CONTEXT_CLASS, SPAN_CONTEXT_CLASS.getMethod("isValid")); + createInvoker = getMethodInvoker(SPAN_CONTEXT_CLASS, SPAN_CONTEXT_CLASS.getMethod("create", + String.class, String.class, TRACE_FLAGS_CLASS, TRACE_STATE_CLASS)); } catch (Throwable t) { OTelInitializer.initError(LOGGER, t); } } INVALID_OTEL_SPAN_CONTEXT = invalidInstance; - INVALID = new OTelSpanContext(invalidInstance); + INVALID = new OTelSpanContext(invalidInstance, null); + IS_VALID_INVOKER = new FallbackInvoker(isValidInvoker, false, LOGGER); GET_SPAN_ID_INVOKER = new FallbackInvoker(getSpanIdInvoker, INVALID_SPAN_ID, LOGGER); GET_TRACE_ID_INVOKER = new FallbackInvoker(getTraceIdInvoker, INVALID_TRACE_ID, LOGGER); GET_TRACE_FLAGS_INVOKER = new FallbackInvoker(getTraceFlagsInvoker, INVALID_TRACE_FLAGS, LOGGER); + CREATE_INVOKER = new FallbackInvoker(createInvoker, INVALID_OTEL_SPAN_CONTEXT, LOGGER); } - OTelSpanContext(Object otelSpanContext) { + /** + * Creates a new instance of {@link OTelSpanContext} from an OpenTelemetry {@code SpanContext}. + * + * @param otelSpanContext the instance of OpenTelemetry {@code io.opentelemetry.api.trace.SpanContext} + * @param otelContext the instance of OpenTelemetry {@code io.opentelemetry.context.Context}. + * It is used to propagate additional information within the process along with {@link InstrumentationContext}. + */ + public OTelSpanContext(Object otelSpanContext, Object otelContext) { this.otelSpanContext = otelSpanContext; + this.otelContext = otelContext; + } + + /** + * Creates a new instance of {@link OTelSpanContext} from an OpenTelemetry {@code io.opentelemetry.context.Context}. + * @param otelContext the instance of OpenTelemetry {@code io.opentelemetry.context.Context} + * + * @return the instance of {@link OTelSpanContext} + */ + public static OTelSpanContext fromOTelContext(Object otelContext) { + if (otelContext == null) { + return INVALID; + } + Object otelSpan = OTelSpan.fromOTelContext(otelContext); + Object otelSpanContext = OTelSpan.getSpanContext(otelSpan); + return new OTelSpanContext(otelSpanContext, otelContext); + } + + /** + * Creates a new instance of {@link OTelSpanContext} from an OpenTelemetry {@code io.opentelemetry.api.trace.Span}. + * @param otelSpan the instance of OpenTelemetry {@code io.opentelemetry.api.trace.Span} + * @return the instance of {@link OTelSpanContext} + */ + public static OTelSpanContext fromOTelSpan(Object otelSpan) { + Object otelSpanContext = OTelSpan.getSpanContext(otelSpan); + Object otelContext = OTelSpan.storeInContext(otelSpan, OTelContext.getCurrent()); + + return new OTelSpanContext(otelSpanContext, otelContext); } - static OTelSpanContext getInvalid() { + /** + * Returns an invalid instance of {@link OTelSpanContext}. + * @return the instance of {@link OTelSpanContext} + */ + public static OTelSpanContext getInvalid() { return INVALID; } /** - * Gets trace id. - * - * @return the trace id. + * {@inheritDoc} */ public String getTraceId() { - return isInitialized() ? (String) GET_TRACE_ID_INVOKER.invoke(otelSpanContext) : INVALID_TRACE_ID; + if (traceId != null) { + return traceId; + } + + traceId = isInitialized() ? (String) GET_TRACE_ID_INVOKER.invoke(otelSpanContext) : INVALID_TRACE_ID; + return traceId; } /** - * Gets span id. - * - * @return the span id. + * {@inheritDoc} */ public String getSpanId() { - return isInitialized() ? (String) GET_SPAN_ID_INVOKER.invoke(otelSpanContext) : INVALID_SPAN_ID; + if (spanId != null) { + return spanId; + } + + spanId = isInitialized() ? (String) GET_SPAN_ID_INVOKER.invoke(otelSpanContext) : INVALID_SPAN_ID; + return spanId; } /** - * Gets trace flags. - * - * @return the trace flags. + * {@inheritDoc} */ + @Override public String getTraceFlags() { + if (traceFlags != null) { + return traceFlags; + } + + if (isInitialized()) { + Object traceFlagsObj = GET_TRACE_FLAGS_INVOKER.invoke(otelSpanContext); + if (traceFlagsObj != null) { + traceFlags = traceFlagsObj.toString(); + } + } else { + traceFlags = INVALID_TRACE_FLAGS; + } + + return traceFlags; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isValid() { + if (isValid != null) { + return isValid; + } + + isValid = isInitialized() && (Boolean) IS_VALID_INVOKER.invoke(otelSpanContext); + return isValid; + } + + /** + * {@inheritDoc} + */ + @Override + public Span getSpan() { if (isInitialized()) { - Object traceFlags = GET_TRACE_FLAGS_INVOKER.invoke(otelSpanContext); - if (traceFlags != null) { - return traceFlags.toString(); + if (otelContext != null) { + OTelSpan coreSpan = OTelContext.getClientCoreSpan(otelContext); + if (coreSpan != null) { + return coreSpan; + } } + + return OTelSpan.createPropagatingSpan(this); + } + return Span.noop(); + } + + Object getOtelContext() { + return otelContext; + } + + Object getOtelSpanContext() { + return otelSpanContext; + } + + static Object toOTelSpanContext(InstrumentationContext context) { + if (context instanceof OTelSpanContext) { + return ((OTelSpanContext) context).otelSpanContext; } - return INVALID_TRACE_FLAGS; + return CREATE_INVOKER.invoke(context.getTraceId(), context.getTraceFlags(), null); } private boolean isInitialized() { diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java index eb673edc9fa6..c4d328545879 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java @@ -6,11 +6,11 @@ import io.clientcore.core.implementation.ReflectiveInvoker; import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker; import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; +import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.instrumentation.tracing.TraceContextGetter; import io.clientcore.core.instrumentation.tracing.TraceContextPropagator; import io.clientcore.core.instrumentation.tracing.TraceContextSetter; -import io.clientcore.core.util.ClientLogger; -import io.clientcore.core.util.Context; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @@ -22,8 +22,6 @@ import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TEXT_MAP_GETTER_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TEXT_MAP_PROPAGATOR_CLASS; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TEXT_MAP_SETTER_CLASS; -import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelUtils.getOTelContext; -import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY; /** * OpenTelemetry implementation of {@link TraceContextPropagator}. @@ -69,9 +67,10 @@ public OTelTraceContextPropagator(Object otelPropagator) { * {@inheritDoc} */ @Override - public void inject(Context context, C carrier, TraceContextSetter setter) { + public void inject(InstrumentationContext context, C carrier, TraceContextSetter setter) { if (isInitialized()) { - INJECT_INVOKER.invoke(otelPropagator, getOTelContext(context), carrier, Setter.toOTelSetter(setter)); + INJECT_INVOKER.invoke(otelPropagator, OTelContext.fromInstrumentationContext(context), carrier, + Setter.toOTelSetter(setter)); } } @@ -79,12 +78,12 @@ public void inject(Context context, C carrier, TraceContextSetter setter) * {@inheritDoc} */ @Override - public Context extract(Context context, C carrier, TraceContextGetter getter) { + public InstrumentationContext extract(InstrumentationContext context, C carrier, TraceContextGetter getter) { if (isInitialized()) { - Object updatedContext - = EXTRACT_INVOKER.invoke(otelPropagator, getOTelContext(context), carrier, Getter.toOTelGetter(getter)); + Object updatedContext = EXTRACT_INVOKER.invoke(otelPropagator, + OTelContext.fromInstrumentationContext(context), carrier, Getter.toOTelGetter(getter)); if (updatedContext != null) { - return context.put(TRACE_CONTEXT_KEY, updatedContext); + return OTelSpanContext.fromOTelContext(updatedContext); } } return context; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTracer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTracer.java index 15948ab3005b..cecaba652175 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTracer.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTracer.java @@ -3,16 +3,15 @@ package io.clientcore.core.implementation.instrumentation.otel.tracing; -import io.clientcore.core.http.models.RequestOptions; import io.clientcore.core.implementation.ReflectiveInvoker; import io.clientcore.core.implementation.instrumentation.otel.FallbackInvoker; import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; +import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.instrumentation.LibraryInstrumentationOptions; import io.clientcore.core.instrumentation.tracing.SpanBuilder; import io.clientcore.core.instrumentation.tracing.SpanKind; import io.clientcore.core.instrumentation.tracing.Tracer; -import io.clientcore.core.util.ClientLogger; -import io.clientcore.core.util.Context; +import io.clientcore.core.instrumentation.logging.ClientLogger; import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACER_BUILDER_CLASS; @@ -94,12 +93,11 @@ public OTelTracer(Object otelTracerProvider, LibraryInstrumentationOptions libra * {@inheritDoc} */ @Override - public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, RequestOptions options) { + public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, InstrumentationContext parentContext) { if (isEnabled()) { Object otelSpanBuilder = SPAN_BUILDER_INVOKER.invoke(otelTracer, spanName); if (otelSpanBuilder != null) { - Context parent = options == null ? Context.none() : options.getContext(); - return new OTelSpanBuilder(otelSpanBuilder, spanKind, parent, libraryOptions); + return new OTelSpanBuilder(otelSpanBuilder, spanKind, parentContext, libraryOptions); } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelUtils.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelUtils.java deleted file mode 100644 index 1857512b82b4..000000000000 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package io.clientcore.core.implementation.instrumentation.otel.tracing; - -import io.clientcore.core.instrumentation.Instrumentation; -import io.clientcore.core.util.ClientLogger; -import io.clientcore.core.util.Context; - -import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.CONTEXT_CLASS; - -/** - * Utility class for OpenTelemetry. - */ -public final class OTelUtils { - private static final ClientLogger LOGGER = new ClientLogger(OTelUtils.class); - - /** - * Get the OpenTelemetry context from the given context. - * - * @param context the context - * @return the OpenTelemetry context - */ - public static Object getOTelContext(Context context) { - Object parent = context.get(Instrumentation.TRACE_CONTEXT_KEY); - if (CONTEXT_CLASS.isInstance(parent)) { - return parent; - } else if (parent instanceof OTelSpan) { - return ((OTelSpan) parent).getOtelContext(); - } else if (parent != null) { - LOGGER.atVerbose() - .addKeyValue("expectedType", CONTEXT_CLASS.getName()) - .addKeyValue("actualType", parent.getClass().getName()) - .log("Context does not contain an OpenTelemetry context. Ignoring it."); - } - - return OTelContext.getCurrent(); - } - - private OTelUtils() { - } -} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DateTimeRfc1123.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DateTimeRfc1123.java index 6e656d9043ee..247dbf5577a5 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DateTimeRfc1123.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/DateTimeRfc1123.java @@ -3,7 +3,7 @@ package io.clientcore.core.implementation.util; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.nio.charset.StandardCharsets; import java.time.DateTimeException; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/ImplUtils.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/ImplUtils.java index 880fbfd78f79..dd88ef33f2bd 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/ImplUtils.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/ImplUtils.java @@ -5,7 +5,7 @@ import io.clientcore.core.http.models.HttpHeaderName; import io.clientcore.core.http.models.HttpHeaders; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.configuration.Configuration; import java.io.FileOutputStream; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/InternalContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/InternalContext.java index 217a020f04f0..a0f0a10d89e4 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/InternalContext.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/InternalContext.java @@ -2,7 +2,7 @@ // Licensed under the MIT License. package io.clientcore.core.implementation.util; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.Context; import java.util.Map; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/JsonSerializer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/JsonSerializer.java index 7c776106b794..b9db02e6a314 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/JsonSerializer.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/JsonSerializer.java @@ -8,7 +8,7 @@ import io.clientcore.core.serialization.json.JsonReader; import io.clientcore.core.serialization.json.JsonSerializable; import io.clientcore.core.serialization.json.JsonWriter; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.serializer.ObjectSerializer; import io.clientcore.core.util.serializer.SerializationFormat; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/LoggingKeys.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/LoggingKeys.java deleted file mode 100644 index 568799979296..000000000000 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/LoggingKeys.java +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package io.clientcore.core.implementation.util; - -/** - * Constants used as keys in semantic logging. Logging keys unify how core logs HTTP requests, responses or anything - * else and simplify log analysis. - *

- * When logging in client libraries, please do the best effort to stay consistent with these keys, but copy the value. - */ -public final class LoggingKeys { - private LoggingKeys() { - - } - - /** - * Key representing HTTP method. - */ - public static final String HTTP_METHOD_KEY = "method"; - - /** - * Key representing try count, the value starts with {@code 0} on the first try - * and should be an {@code int} number. - */ - public static final String TRY_COUNT_KEY = "tryCount"; - - /** - * Key representing time in milliseconds from request start to the moment response (headers and response code) were received, - * the value should be a number. - *

- * Depending on the implementation and content type, this time may include time to receive the body. - */ - public static final String TIME_TO_RESPONSE_MS_KEY = "timeToResponseMs"; - - /** - * Key representing duration of call in milliseconds, the value should be a number. - *

- * This time represents the most accurate duration that logging policy can record. - *

- * If exception was thrown, this time represents time to exception. - * If response was received and body logging is disabled, it represents time to get the response (headers and status code). - * If response was received and body logging is enabled, it represents time-to-last-byte (or, if response was closed before - * body was fully received, time to closure). - */ - public static final String DURATION_MS_KEY = "durationMs"; - - /** - * Key representing URI request was redirected to. - */ - public static final String REDIRECT_URI_KEY = "redirectUri"; - - /** - * Key representing request URI. - */ - public static final String URI_KEY = "uri"; - - /** - * Key representing request body content length. - */ - public static final String REQUEST_CONTENT_LENGTH_KEY = "requestContentLength"; - - /** - * Key representing response body content length. - */ - public static final String RESPONSE_CONTENT_LENGTH_KEY = "responseContentLength"; - - /** - * Key representing request body. The value should be populated conditionally - * if populated at all. - */ - public static final String BODY_KEY = "body"; - - /** - * Key representing response status code. The value should be a number. - */ - public static final String STATUS_CODE_KEY = "statusCode"; -} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Providers.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Providers.java index 5e8abe009477..51806137e62e 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Providers.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/Providers.java @@ -2,7 +2,7 @@ // Licensed under the MIT License. package io.clientcore.core.implementation.util; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.util.HashMap; import java.util.Iterator; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/SliceInputStream.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/SliceInputStream.java index e602cb8e2d73..d2d94091cd4d 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/SliceInputStream.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/SliceInputStream.java @@ -3,7 +3,7 @@ package io.clientcore.core.implementation.util; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.io.IOException; import java.io.InputStream; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/StreamUtil.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/StreamUtil.java index fed2ba45d15f..bcf0be2856e4 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/StreamUtil.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/StreamUtil.java @@ -3,7 +3,7 @@ package io.clientcore.core.implementation.util; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.io.IOException; import java.io.InputStream; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/XmlSerializer.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/XmlSerializer.java index 4580213e72a7..b50c7d89550f 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/XmlSerializer.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/util/XmlSerializer.java @@ -7,7 +7,7 @@ import io.clientcore.core.serialization.xml.XmlReader; import io.clientcore.core.serialization.xml.XmlSerializable; import io.clientcore.core.serialization.xml.XmlWriter; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.serializer.ObjectSerializer; import io.clientcore.core.util.serializer.SerializationFormat; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/Instrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/Instrumentation.java index a5ca91ac6e09..c1de07959f73 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/Instrumentation.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/Instrumentation.java @@ -3,6 +3,7 @@ package io.clientcore.core.instrumentation; +import io.clientcore.core.implementation.instrumentation.fallback.FallbackInstrumentation; import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; import io.clientcore.core.implementation.instrumentation.otel.OTelInstrumentation; import io.clientcore.core.instrumentation.tracing.TraceContextPropagator; @@ -10,27 +11,10 @@ import java.util.Objects; -import static io.clientcore.core.instrumentation.NoopInstrumentation.NOOP_PROVIDER; - /** * A container that can resolve observability provider and its components. Only OpenTelemetry is supported. - * - *

This interface is intended to be used by client libraries. Application developers - * should use OpenTelemetry API directly

*/ public interface Instrumentation { - /** - * The key used to disable tracing on a per-request basis. - * To disable tracing, set this key to {@code true} on the request context. - */ - String DISABLE_TRACING_KEY = "disable-tracing"; - - /** - * The key used to set the parent trace context explicitly. - * To set the trace context, set this key to a value of {@code io.opentelemetry.context.Context}. - */ - String TRACE_CONTEXT_KEY = "trace-context"; - /** * Gets the tracer. *

@@ -60,6 +44,9 @@ public interface Instrumentation { /** * Gets the implementation of W3C Trace Context propagator. * + *

This method is intended to be used by client libraries. Application developers + * should use OpenTelemetry API directly

+ * * @return The context propagator. */ TraceContextPropagator getW3CTraceContextPropagator(); @@ -67,6 +54,9 @@ public interface Instrumentation { /** * Gets the singleton instance of the resolved telemetry provider. * + *

This method is intended to be used by client libraries. Application developers + * should use OpenTelemetry API directly

+ * * @param applicationOptions Telemetry collection options provided by the application. * @param libraryOptions Library-specific telemetry collection options. * @return The instance of telemetry provider implementation. @@ -77,7 +67,60 @@ static Instrumentation create(InstrumentationOptions applicationOptions, if (OTelInitializer.isInitialized()) { return new OTelInstrumentation(applicationOptions, libraryOptions); } else { - return NOOP_PROVIDER; + return new FallbackInstrumentation(applicationOptions, libraryOptions); + } + } + + /** + * Retrieves the instrumentation context from the given context. The type of the context is determined by the + * instrumentation implementation. + *

+ * When using OpenTelemetry, the context can be a {@code io.opentelemetry.api.trace.Span}, {@code io.opentelemetry.api.trace.SpanContext}, + * {@code io.opentelemetry.context.Context} or any implementation of {@link InstrumentationContext}. + * + *

+     *
+     * SampleClient client = new SampleClientBuilder().build();
+     *
+     * RequestOptions options = new RequestOptions()
+     *     .setInstrumentationContext(new MyInstrumentationContext("e4eaaaf2d48f4bf3b299a8a2a2a77ad7", "5e0c63257de34c56"));
+     *
+     * // run on another thread
+     * client.clientCall(options);
+     *
+     * 
+ * + * + * + *
+     *
+     * Tracer tracer = GlobalOpenTelemetry.getTracer("sample");
+     * Span span = tracer.spanBuilder("my-operation")
+     *     .startSpan();
+     * SampleClient client = new SampleClientBuilder().build();
+     *
+     * // Propagating context implicitly is preferred way in synchronous code.
+     * // However, in asynchronous code, context may need to be propagated explicitly using RequestOptions
+     * // and explicit io.clientcore.core.util.Context.
+     *
+     * RequestOptions options = new RequestOptions()
+     *     .setInstrumentationContext(Instrumentation.createInstrumentationContext(span));
+     *
+     * // run on another thread - all telemetry will be correlated with the span created above
+     * client.clientCall(options);
+     *
+     * 
+ * + * + * @param context the context to retrieve the instrumentation context from. + * @return the instrumentation context. + * @param the type of the context. + */ + static InstrumentationContext createInstrumentationContext(T context) { + if (OTelInitializer.isInitialized()) { + return OTelInstrumentation.DEFAULT_INSTANCE.createInstrumentationContext(context); + } else { + return FallbackInstrumentation.DEFAULT_INSTANCE.createInstrumentationContext(context); } } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationContext.java new file mode 100644 index 000000000000..173b3d7110e5 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationContext.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation; + +import io.clientcore.core.instrumentation.tracing.Span; + +/** + * The instrumentation context that is used to correlate telemetry data. + * It's created along with the {@link Span} by + * the client libraries and is propagated through SDK code. + *

+ * It must provide access to W3C Trace Context + * properties. Implementations may use it to propagate additional information within the process. + * + * @see Instrumentation#createInstrumentationContext(Object) + */ +public interface InstrumentationContext { + /** + * Gets the trace id - 32-char long hex-encoded string that identifies end-to-end operation. + * @return the trace id. + */ + String getTraceId(); + + /** + * Gets the span id - 16-char hex-encoded string that identifies span - an individual + * operation within a trace. + * @return the span id. + */ + String getSpanId(); + + /** + * Gets the trace flags - 2-char hex-encoded string that represents trace flags. + * Flag with value "01" indicates that the span is sampled, "00" indicates that it is + * not sampled. + * @return the trace flags. + */ + String getTraceFlags(); + + /** + * Checks if the context is valid - i.e. if it can be propagated. + * Invalid contexts are ignored by the instrumentation. + * + * @return true if the context is valid, false otherwise. + */ + boolean isValid(); + + /** + * Gets the span that is associated with this context. If there is no span associated with this context, + * returns a no-op span. + * + * @return the span. + */ + Span getSpan(); +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/NoopInstrumentation.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/NoopInstrumentation.java deleted file mode 100644 index 53205494031c..000000000000 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/NoopInstrumentation.java +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package io.clientcore.core.instrumentation; - -import io.clientcore.core.instrumentation.tracing.Span; -import io.clientcore.core.instrumentation.tracing.SpanBuilder; -import io.clientcore.core.instrumentation.tracing.TraceContextGetter; -import io.clientcore.core.instrumentation.tracing.TraceContextPropagator; -import io.clientcore.core.instrumentation.tracing.TraceContextSetter; -import io.clientcore.core.instrumentation.tracing.Tracer; -import io.clientcore.core.instrumentation.tracing.TracingScope; -import io.clientcore.core.util.Context; - -class NoopInstrumentation implements Instrumentation { - static final Instrumentation NOOP_PROVIDER = new NoopInstrumentation(); - - @Override - public Tracer getTracer() { - return NOOP_TRACER; - } - - @Override - public TraceContextPropagator getW3CTraceContextPropagator() { - return NOOP_CONTEXT_PROPAGATOR; - } - - private static final Span NOOP_SPAN = new Span() { - @Override - public Span setAttribute(String key, Object value) { - return this; - } - - @Override - public Span setError(String errorType) { - return this; - } - - @Override - public void end() { - } - - @Override - public void end(Throwable error) { - } - - @Override - public boolean isRecording() { - return false; - } - - @Override - public TracingScope makeCurrent() { - return NOOP_SCOPE; - } - }; - - private static final SpanBuilder NOOP_SPAN_BUILDER = new SpanBuilder() { - @Override - public SpanBuilder setAttribute(String key, Object value) { - return this; - } - - @Override - public Span startSpan() { - return NOOP_SPAN; - } - }; - - private static final TracingScope NOOP_SCOPE = () -> { - }; - private static final Tracer NOOP_TRACER = (name, kind, ctx) -> NOOP_SPAN_BUILDER; - - private static final TraceContextPropagator NOOP_CONTEXT_PROPAGATOR = new TraceContextPropagator() { - - @Override - public void inject(Context context, C carrier, TraceContextSetter setter) { - - } - - @Override - public Context extract(Context context, C carrier, TraceContextGetter getter) { - return context; - } - }; -} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/ClientLogger.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java similarity index 88% rename from sdk/clientcore/core/src/main/java/io/clientcore/core/util/ClientLogger.java rename to sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java index 6fea3c48010d..5974d6708f06 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/ClientLogger.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/ClientLogger.java @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package io.clientcore.core.util; +package io.clientcore.core.instrumentation.logging; import io.clientcore.core.annotation.Metadata; import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; -import io.clientcore.core.implementation.util.DefaultLogger; -import io.clientcore.core.implementation.util.Slf4jLoggerShim; +import io.clientcore.core.implementation.instrumentation.Slf4jLoggerShim; +import io.clientcore.core.implementation.instrumentation.DefaultLogger; +import io.clientcore.core.instrumentation.InstrumentationContext; import io.clientcore.core.serialization.json.JsonWriter; import io.clientcore.core.serialization.json.implementation.DefaultJsonWriter; import io.clientcore.core.util.configuration.Configuration; @@ -23,6 +24,12 @@ import java.util.function.Supplier; import static io.clientcore.core.annotation.TypeConditions.FLUENT; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.EVENT_NAME_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.EXCEPTION_MESSAGE_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.EXCEPTION_STACKTRACE_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.EXCEPTION_TYPE_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.SPAN_ID_KEY; +import static io.clientcore.core.implementation.instrumentation.AttributeKeys.TRACE_ID_KEY; /** * This is a fluent logger helper class that wraps an SLF4J Logger (if available) or a default implementation of the @@ -60,8 +67,20 @@ public ClientLogger(Class clazz) { * @throws RuntimeException when logging configuration is invalid depending on SLF4J implementation. */ public ClientLogger(String className) { + this(className, null); + } + + /** + * Retrieves a logger for the passed class name. + * + * @param className Class name creating the logger. + * @param context Context to be populated on every log record written with this logger. + * Objects are serialized with {@code toString()} method. + * @throws RuntimeException when logging configuration is invalid depending on SLF4J implementation. + */ + public ClientLogger(String className, Map context) { logger = new Slf4jLoggerShim(getClassPathFromClassName(className)); - globalContext = null; + globalContext = context == null ? null : Collections.unmodifiableMap(context); } /** @@ -288,6 +307,7 @@ public static final class LoggingEvent { private final boolean isEnabled; private Map keyValuePairs; private String eventName; + private InstrumentationContext context = null; /** * Creates {@code LoggingEvent} for provided level and {@link ClientLogger}. @@ -430,6 +450,18 @@ public LoggingEvent addKeyValue(String key, Supplier valueSupplier) { return this; } + /** + * Sets operation context on the log event being created. + * It's used to correlate logs between each other and with other telemetry items. + * + * @param context operation context. + * @return The updated {@code LoggingEventBuilder} object. + */ + public LoggingEvent setInstrumentationContext(InstrumentationContext context) { + this.context = context; + return this; + } + /** * Sets the event name for the current log event. The event name is used to query all logs * that describe the same event. It must not contain any dynamic parts. @@ -444,6 +476,7 @@ public LoggingEvent setEventName(String eventName) { /** * Logs event annotated with context. + * Logs event with context. */ public void log() { log(null); @@ -473,12 +506,12 @@ public T log(String message, T throwable) { if (this.isEnabled) { boolean isDebugEnabled = logger.canLogAtLevel(LogLevel.VERBOSE); if (throwable != null) { - addKeyValueInternal("exception.type", throwable.getClass().getCanonicalName()); - addKeyValueInternal("exception.message", throwable.getMessage()); + addKeyValueInternal(EXCEPTION_TYPE_KEY, throwable.getClass().getCanonicalName()); + addKeyValueInternal(EXCEPTION_MESSAGE_KEY, throwable.getMessage()); if (isDebugEnabled) { StringBuilder stackTrace = new StringBuilder(); DefaultLogger.appendThrowable(stackTrace, throwable); - addKeyValue("exception.stacktrace", stackTrace.toString()); + addKeyValue(EXCEPTION_STACKTRACE_KEY, stackTrace.toString()); } } logger.performLogging(level, getMessageWithContext(message), isDebugEnabled ? throwable : null); @@ -487,16 +520,27 @@ public T log(String message, T throwable) { } private String getMessageWithContext(String message) { - if (message == null) { - message = ""; + if (this.context != null && this.context.isValid()) { + // TODO (limolkova) we can set context from implicit current span + // we should also support OTel as a logging provider and avoid adding redundant + // traceId and spanId to the logs + + addKeyValue(TRACE_ID_KEY, context.getTraceId()); + addKeyValue(SPAN_ID_KEY, context.getSpanId()); } int pairsCount = (keyValuePairs == null ? 0 : keyValuePairs.size()) + (globalPairs == null ? 0 : globalPairs.size()); - int speculatedSize = 20 + pairsCount * 20 + message.length(); + + int messageLength = message == null ? 0 : message.length(); + int speculatedSize = 20 + pairsCount * 20 + messageLength; try (AccessibleByteArrayOutputStream outputStream = new AccessibleByteArrayOutputStream(speculatedSize); JsonWriter jsonWriter = DefaultJsonWriter.toStream(outputStream, null)) { - jsonWriter.writeStartObject().writeStringField("message", message); + jsonWriter.writeStartObject(); + + if (message != null) { + jsonWriter.writeStringField("message", message); + } if (globalPairs != null) { for (Map.Entry kvp : globalPairs.entrySet()) { @@ -511,7 +555,7 @@ private String getMessageWithContext(String message) { } if (eventName != null) { - jsonWriter.writeStringField("event.name", eventName); + jsonWriter.writeStringField(EVENT_NAME_KEY, eventName); } jsonWriter.writeEndObject().flush(); diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/package-info.java new file mode 100644 index 000000000000..1887eaa4ca51 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/logging/package-info.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Package containing core logging primitives to be used by client libraries. + *

+ * + * Classes in this package are intended to be used by client libraries only. Application developers + * should use SLF4J or another logging facade directly + */ +package io.clientcore.core.instrumentation.logging; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/package-info.java index 61f58befec5a..63fa95b9eba6 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/package-info.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/package-info.java @@ -97,9 +97,9 @@ * // and explicit io.clientcore.core.util.Context. * * RequestOptions options = new RequestOptions() - * .setContext(io.clientcore.core.util.Context.of(TRACE_CONTEXT_KEY, Context.current().with(span))); + * .setInstrumentationContext(Instrumentation.createInstrumentationContext(span)); * - * // run on another thread + * // run on another thread - all telemetry will be correlated with the span created above * client.clientCall(options); * *

diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopInstrumentationContext.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopInstrumentationContext.java new file mode 100644 index 000000000000..98938a193094 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopInstrumentationContext.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.tracing; + +import io.clientcore.core.instrumentation.InstrumentationContext; + +final class NoopInstrumentationContext implements InstrumentationContext { + public static final NoopInstrumentationContext INSTANCE = new NoopInstrumentationContext(); + + private NoopInstrumentationContext() { + } + + @Override + public String getTraceId() { + return null; + } + + @Override + public String getSpanId() { + return null; + } + + @Override + public String getTraceFlags() { + return null; + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public Span getSpan() { + return Span.noop(); + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopSpan.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopSpan.java new file mode 100644 index 000000000000..8ec976b911b7 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/NoopSpan.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.tracing; + +import io.clientcore.core.instrumentation.InstrumentationContext; + +final class NoopSpan implements Span { + static final NoopSpan INSTANCE = new NoopSpan(); + + private NoopSpan() { + } + + private static final TracingScope NOOP_SCOPE = () -> { + }; + + @Override + public Span setAttribute(String key, Object value) { + return this; + } + + @Override + public Span setError(String errorType) { + return this; + } + + @Override + public void end(Throwable throwable) { + + } + + @Override + public void end() { + + } + + @Override + public boolean isRecording() { + return false; + } + + @Override + public TracingScope makeCurrent() { + return NOOP_SCOPE; + } + + @Override + public InstrumentationContext getInstrumentationContext() { + return NoopInstrumentationContext.INSTANCE; + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Span.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Span.java index 1a4ea4143515..29c587b8ed10 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Span.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Span.java @@ -3,6 +3,8 @@ package io.clientcore.core.instrumentation.tracing; +import io.clientcore.core.instrumentation.InstrumentationContext; + /** * A {@code Span} represents a single operation within a trace. Spans can be nested to form a trace tree. *

This interface is intended to be used by client libraries only. Application developers should use OpenTelemetry API directly

@@ -76,4 +78,19 @@ public interface Span { * @return The {@link TracingScope} object. */ TracingScope makeCurrent(); + + /** + * Gets the instrumentation context that is used to correlate telemetry data. + * + * @return The instrumentation context. + */ + InstrumentationContext getInstrumentationContext(); + + /** + * Returns a no-op span. + * @return A no-op span. + */ + static Span noop() { + return NoopSpan.INSTANCE; + } } diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextPropagator.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextPropagator.java index d5bffecb6039..6eb49d4fab2f 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextPropagator.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextPropagator.java @@ -3,7 +3,7 @@ package io.clientcore.core.instrumentation.tracing; -import io.clientcore.core.util.Context; +import io.clientcore.core.instrumentation.InstrumentationContext; /** * A {@link TraceContextPropagator} injects and extracts tracing context from a carrier, @@ -19,7 +19,7 @@ public interface TraceContextPropagator { * @param setter The setter to use to inject the context into the carrier. * @param 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 +31,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 3223d411e358..577dd3dc3aac 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,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. @@ -20,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()) {
@@ -47,7 +50,7 @@ public interface Tracer {
      * 
      * 
      *
-     * Span sendSpan = tracer.spanBuilder("send {queue-name}", SpanKind.PRODUCER, requestOptions)
+     * Span sendSpan = tracer.spanBuilder("send {queue-name}", SpanKind.PRODUCER, null)
      *     // Some of the attributes should be provided at the start time (as documented in semantic conventions) -
      *     // they can be used by client apps to sample spans.
      *     .setAttribute("messaging.system", "servicebus")
@@ -73,10 +76,10 @@ public interface Tracer {
      *
      * @param spanName The name of the span.
      * @param spanKind The kind of the span.
-     * @param requestOptions The request options.
+     * @param instrumentationContext The parent context.
      * @return The span builder.
      */
-    SpanBuilder spanBuilder(String spanName, SpanKind spanKind, RequestOptions requestOptions);
+    SpanBuilder spanBuilder(String spanName, SpanKind spanKind, InstrumentationContext instrumentationContext);
 
     /**
      * Checks if the tracer is enabled.
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/json/models/JsonNumber.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/json/models/JsonNumber.java
index 0a1c9b266c32..050184c4b42e 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/json/models/JsonNumber.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/json/models/JsonNumber.java
@@ -6,7 +6,7 @@
 import io.clientcore.core.serialization.json.JsonReader;
 import io.clientcore.core.serialization.json.JsonToken;
 import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 
 import java.io.IOException;
 import java.math.BigDecimal;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/xml/XmlReader.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/xml/XmlReader.java
index 613be6a2a80d..ce0023ca023a 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/xml/XmlReader.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/serialization/xml/XmlReader.java
@@ -4,7 +4,7 @@
 package io.clientcore.core.serialization.xml;
 
 import io.clientcore.core.serialization.xml.implementation.aalto.stax.InputFactoryImpl;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 
 import javax.xml.XMLConstants;
 import javax.xml.namespace.QName;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Context.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Context.java
index 1282962f25df..b126a11d0839 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Context.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Context.java
@@ -5,6 +5,7 @@
 
 import io.clientcore.core.annotation.Metadata;
 import io.clientcore.core.implementation.util.InternalContext;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 
 import java.util.Map;
 
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/SharedExecutorService.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/SharedExecutorService.java
index bffb558bc42b..5f9680e2063e 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/SharedExecutorService.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/SharedExecutorService.java
@@ -7,6 +7,7 @@
 import io.clientcore.core.implementation.ReflectiveInvoker;
 import io.clientcore.core.implementation.util.EnvironmentConfiguration;
 import io.clientcore.core.implementation.util.ImplUtils;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 
 import java.time.Duration;
 import java.util.Collection;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/auth/CompositeChallengeHandler.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/auth/CompositeChallengeHandler.java
index 5eca550da569..2dfd99822358 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/auth/CompositeChallengeHandler.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/auth/CompositeChallengeHandler.java
@@ -4,7 +4,7 @@
 
 import io.clientcore.core.http.models.HttpRequest;
 import io.clientcore.core.http.models.Response;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 
 import java.util.List;
 
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/FileBinaryData.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/FileBinaryData.java
index 1b6093c2441a..4b3dfe28af94 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/FileBinaryData.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/FileBinaryData.java
@@ -5,7 +5,7 @@
 
 import io.clientcore.core.implementation.util.SliceInputStream;
 import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 import io.clientcore.core.util.serializer.ObjectSerializer;
 
 import java.io.BufferedInputStream;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/InputStreamBinaryData.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/InputStreamBinaryData.java
index 47db439ae074..ee30aea3f0dd 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/InputStreamBinaryData.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/InputStreamBinaryData.java
@@ -8,7 +8,7 @@
 import io.clientcore.core.implementation.util.IterableOfByteBuffersInputStream;
 import io.clientcore.core.implementation.util.StreamUtil;
 import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 import io.clientcore.core.util.serializer.ObjectSerializer;
 
 import java.io.IOException;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/ListByteBufferBinaryData.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/ListByteBufferBinaryData.java
index 1c227a8f8e91..4938c053e0fd 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/ListByteBufferBinaryData.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/ListByteBufferBinaryData.java
@@ -6,7 +6,7 @@
 import io.clientcore.core.implementation.util.ImplUtils;
 import io.clientcore.core.implementation.util.IterableOfByteBuffersInputStream;
 import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 import io.clientcore.core.util.serializer.ObjectSerializer;
 
 import java.io.IOException;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/SerializableBinaryData.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/SerializableBinaryData.java
index 09174ee28559..3853ceb5da31 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/SerializableBinaryData.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/binarydata/SerializableBinaryData.java
@@ -4,7 +4,7 @@
 package io.clientcore.core.util.binarydata;
 
 import io.clientcore.core.serialization.json.JsonWriter;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 import io.clientcore.core.util.serializer.ObjectSerializer;
 
 import java.io.ByteArrayInputStream;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/Configuration.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/Configuration.java
index 8b2878c73e94..6e749a54c3c9 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/Configuration.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/Configuration.java
@@ -7,7 +7,7 @@
 import io.clientcore.core.http.client.HttpClientProvider;
 import io.clientcore.core.implementation.util.EnvironmentConfiguration;
 import io.clientcore.core.implementation.util.ImplUtils;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 
 import java.util.Collections;
 import java.util.HashMap;
diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/ConfigurationBuilder.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/ConfigurationBuilder.java
index 03e34eba45ae..20f487aed48d 100644
--- a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/ConfigurationBuilder.java
+++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/configuration/ConfigurationBuilder.java
@@ -5,7 +5,7 @@
 
 import io.clientcore.core.annotation.Metadata;
 import io.clientcore.core.implementation.util.EnvironmentConfiguration;
-import io.clientcore.core.util.ClientLogger;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
 
 import java.util.Collections;
 import java.util.HashMap;
diff --git a/sdk/clientcore/core/src/main/java/module-info.java b/sdk/clientcore/core/src/main/java/module-info.java
index 36127c7b6cd6..dee608a9a5b6 100644
--- a/sdk/clientcore/core/src/main/java/module-info.java
+++ b/sdk/clientcore/core/src/main/java/module-info.java
@@ -28,6 +28,7 @@
     exports io.clientcore.core.util.auth;
     exports io.clientcore.core.instrumentation;
     exports io.clientcore.core.instrumentation.tracing;
+    exports io.clientcore.core.instrumentation.logging;
 
     uses io.clientcore.core.http.client.HttpClientProvider;
 
diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java
new file mode 100644
index 000000000000..11779e0bbf1b
--- /dev/null
+++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.core.instrumentation;
+
+import io.clientcore.core.http.models.HttpMethod;
+import io.clientcore.core.http.models.HttpRequest;
+import io.clientcore.core.http.models.RequestOptions;
+import io.clientcore.core.http.models.Response;
+import io.clientcore.core.http.pipeline.HttpInstrumentationPolicy;
+import io.clientcore.core.http.pipeline.HttpPipeline;
+import io.clientcore.core.http.pipeline.HttpPipelineBuilder;
+import io.clientcore.core.instrumentation.logging.ClientLogger;
+import io.clientcore.core.instrumentation.tracing.Span;
+import io.clientcore.core.instrumentation.tracing.SpanKind;
+import io.clientcore.core.instrumentation.tracing.TracingScope;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+/**
+ * Application developers that don't have OpenTelemetry on the classpath
+ * can take advantage of basic distributed tracing providing log correlation.
+ * 

+ * Instrumented client libraries start a span for each client call and + * propagate the span context to the HTTP pipeline that creates + * a new span for each HTTP request. + *

+ * All logs emitted by the client library are automatically correlated + * with the encompassing span contexts. + *

+ * Span context is propagated to the endpoint using W3C Trace Context format. + *

+ * You can also receive logs describing generated spans by enabling {library-name}.tracing + * logger at INFO level. + *

+ * You can pass custom correlation IDs (following W3C Trace Context format) in {@link RequestOptions}. + */ +public class TelemetryJavaDocCodeSnippets { + + /** + * To get basic distributed tracing, just use client libraries as usual. + */ + public void fallbackTracing() { + // BEGIN: io.clientcore.core.telemetry.fallback.tracing + + SampleClient client = new SampleClientBuilder().build(); + client.clientCall(); + + // END: io.clientcore.core.telemetry.fallback.tracing + } + + /** + * You can pass custom logger to the client library to receive spans from + * this library in the logs. + */ + public void useCustomLogger() { + // BEGIN: io.clientcore.core.telemetry.usecustomlogger + + ClientLogger logger = new ClientLogger("sample-client-traces"); + + InstrumentationOptions instrumentationOptions = new InstrumentationOptions() + .setProvider(logger); + + SampleClient client = new SampleClientBuilder().instrumentationOptions(instrumentationOptions).build(); + client.clientCall(); + + // END: io.clientcore.core.telemetry.usecustomlogger + } + + /** + * This code snippet shows how to disable distributed tracing + * for a specific instance of client. + */ + public void disableDistributedTracing() { + // BEGIN: io.clientcore.core.telemetry.fallback.disabledistributedtracing + + InstrumentationOptions instrumentationOptions = new InstrumentationOptions<>() + .setTracingEnabled(false); + + SampleClient client = new SampleClientBuilder().instrumentationOptions(instrumentationOptions).build(); + client.clientCall(); + + // END: io.clientcore.core.telemetry.fallback.disabledistributedtracing + } + + /** + * This code snippet shows how to assign custom traceId and spanId to the client call. + */ + public void correlationWithExplicitContext() { + // BEGIN: io.clientcore.core.telemetry.fallback.correlationwithexplicitcontext + + SampleClient client = new SampleClientBuilder().build(); + + RequestOptions options = new RequestOptions() + .setInstrumentationContext(new MyInstrumentationContext("e4eaaaf2d48f4bf3b299a8a2a2a77ad7", "5e0c63257de34c56")); + + // run on another thread + client.clientCall(options); + + // END: io.clientcore.core.telemetry.fallback.correlationwithexplicitcontext + } + + static class MyInstrumentationContext implements InstrumentationContext { + private final String traceId; + private final String spanId; + + MyInstrumentationContext(String traceId, String spanId) { + this.traceId = traceId; + this.spanId = spanId; + } + + @Override + public String getTraceId() { + return traceId; + } + + @Override + public String getSpanId() { + return spanId; + } + + @Override + public String getTraceFlags() { + return "00"; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public Span getSpan() { + return Span.noop(); + } + } + + static class SampleClientBuilder { + private InstrumentationOptions instrumentationOptions; + // TODO (limolkova): do we need InstrumnetationTrait? + public SampleClientBuilder instrumentationOptions(InstrumentationOptions instrumentationOptions) { + this.instrumentationOptions = instrumentationOptions; + return this; + } + + public SampleClient build() { + return new SampleClient(instrumentationOptions, new HttpPipelineBuilder() + .policies(new HttpInstrumentationPolicy(instrumentationOptions, null)) + .build()); + } + } + + static class SampleClient { + private final static LibraryInstrumentationOptions LIBRARY_OPTIONS = new LibraryInstrumentationOptions("sample"); + private final HttpPipeline httpPipeline; + private final io.clientcore.core.instrumentation.tracing.Tracer tracer; + + SampleClient(InstrumentationOptions instrumentationOptions, HttpPipeline httpPipeline) { + this.httpPipeline = httpPipeline; + this.tracer = Instrumentation.create(instrumentationOptions, LIBRARY_OPTIONS).getTracer(); + } + + public void clientCall() { + this.clientCall(null); + } + + @SuppressWarnings("try") + public void clientCall(RequestOptions options) { + Span span = tracer.spanBuilder("clientCall", SpanKind.CLIENT, options.getInstrumentationContext()) + .startSpan(); + + if (options == null) { + options = new RequestOptions(); + } + + options.setInstrumentationContext(span.getInstrumentationContext()); + + try (TracingScope scope = span.makeCurrent()) { + Response response = httpPipeline.send(new HttpRequest(HttpMethod.GET, "https://example.com")); + response.close(); + span.end(); + } catch (Throwable t) { + span.end(t); + + if (t instanceof IOException) { + throw new UncheckedIOException((IOException) t); + } else if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else { + throw new RuntimeException(t); + } + } + } + } +} diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java index 0fc62912cf21..5d294a02fa72 100644 --- a/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java @@ -7,7 +7,6 @@ import io.clientcore.core.http.models.HttpLogOptions; import io.clientcore.core.http.models.RequestOptions; import io.clientcore.core.http.pipeline.HttpInstrumentationPolicy; -import io.clientcore.core.http.pipeline.HttpLoggingPolicy; import io.clientcore.core.http.pipeline.HttpPipeline; import io.clientcore.core.http.pipeline.HttpPipelineBuilder; import io.clientcore.core.http.pipeline.HttpPipelinePolicy; @@ -17,8 +16,6 @@ import io.clientcore.core.instrumentation.tracing.Tracer; import io.clientcore.core.instrumentation.tracing.TracingScope; -import static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY; - /** * THESE CODE SNIPPETS ARE INTENDED FOR CLIENT LIBRARY DEVELOPERS ONLY. *

@@ -50,6 +47,7 @@ public void createTracer() { /** * This example shows minimal distributed tracing instrumentation. */ + @SuppressWarnings("try") public void traceCall() { Tracer tracer = Instrumentation.create(null, LIBRARY_OPTIONS).getTracer(); @@ -57,13 +55,16 @@ public void traceCall() { // BEGIN: io.clientcore.core.telemetry.tracing.tracecall - Span span = tracer.spanBuilder("{operationName}", SpanKind.CLIENT, requestOptions) + Span span = tracer.spanBuilder("{operationName}", SpanKind.CLIENT, null) .startSpan(); // we'll propagate context implicitly using span.makeCurrent() as shown later. // Libraries that write async code should propagate context explicitly in addition to implicit propagation. if (tracer.isEnabled()) { - requestOptions.putContext(TRACE_CONTEXT_KEY, span); + if (requestOptions == null) { + requestOptions = new RequestOptions(); + } + requestOptions.setInstrumentationContext(span.getInstrumentationContext()); } try (TracingScope scope = span.makeCurrent()) { @@ -83,6 +84,7 @@ public void traceCall() { /** * This example shows full distributed tracing instrumentation that adds attributes. */ + @SuppressWarnings("try") public void traceWithAttributes() { Tracer tracer = Instrumentation.create(null, LIBRARY_OPTIONS).getTracer(); @@ -90,7 +92,7 @@ public void traceWithAttributes() { // BEGIN: io.clientcore.core.telemetry.tracing.tracewithattributes - Span sendSpan = tracer.spanBuilder("send {queue-name}", SpanKind.PRODUCER, requestOptions) + Span sendSpan = tracer.spanBuilder("send {queue-name}", SpanKind.PRODUCER, null) // Some of the attributes should be provided at the start time (as documented in semantic conventions) - // they can be used by client apps to sample spans. .setAttribute("messaging.system", "servicebus") @@ -123,8 +125,7 @@ public void configureInstrumentationPolicy() { HttpPipeline pipeline = new HttpPipelineBuilder() .policies( new HttpRetryPolicy(), - new HttpInstrumentationPolicy(instrumentationOptions, logOptions), - new HttpLoggingPolicy(logOptions)) + new HttpInstrumentationPolicy(instrumentationOptions, logOptions)) .build(); // END: io.clientcore.core.telemetry.tracing.instrumentationpolicy @@ -143,8 +144,7 @@ public void customizeInstrumentationPolicy() { HttpPipeline pipeline = new HttpPipelineBuilder() .policies( new HttpRetryPolicy(), - new HttpInstrumentationPolicy(instrumentationOptions, logOptions), - new HttpLoggingPolicy(logOptions)) + new HttpInstrumentationPolicy(instrumentationOptions, logOptions)) .build(); // END: io.clientcore.core.telemetry.tracing.customizeinstrumentationpolicy @@ -157,9 +157,11 @@ public void enrichInstrumentationPolicySpans() { // BEGIN: io.clientcore.core.telemetry.tracing.enrichhttpspans HttpPipelinePolicy enrichingPolicy = (request, next) -> { - Object span = request.getRequestOptions().getContext().get(TRACE_CONTEXT_KEY); - if (span instanceof Span) { - ((Span)span).setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID)); + Span span = request.getRequestOptions() == null + ? Span.noop() + : request.getRequestOptions().getInstrumentationContext().getSpan(); + if (span.isRecording()) { + span.setAttribute("custom.request.id", request.getHeaders().getValue(CUSTOM_REQUEST_ID)); } return next.process(); @@ -169,8 +171,7 @@ public void enrichInstrumentationPolicySpans() { .policies( new HttpRetryPolicy(), new HttpInstrumentationPolicy(instrumentationOptions, logOptions), - enrichingPolicy, - new HttpLoggingPolicy(logOptions)) + enrichingPolicy) .build(); diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/BinaryDataJavaDocCodeSnippet.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/BinaryDataJavaDocCodeSnippet.java index f727a2f34161..69fb89b4ed97 100644 --- a/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/BinaryDataJavaDocCodeSnippet.java +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/BinaryDataJavaDocCodeSnippet.java @@ -3,7 +3,7 @@ package io.clientcore.core.models; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.binarydata.BinaryData; import io.clientcore.core.implementation.util.JsonSerializer; import io.clientcore.core.util.serializer.ObjectSerializer; diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/ContextJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/ContextJavaDocCodeSnippets.java index 5d38ddaa7ed6..6b48c5e194de 100644 --- a/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/ContextJavaDocCodeSnippets.java +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/models/ContextJavaDocCodeSnippets.java @@ -18,11 +18,8 @@ public void constructContextObject() { // Create an empty context having no data Context emptyContext = Context.none(); - // OpenTelemetry context can be optionally passed using PARENT_TRACE_CONTEXT_KEY - // when OpenTelemetry context is not provided explicitly, ambient - // io.opentelemetry.context.Context.current() is used - - // Context contextWithSpan = new Context(PARENT_TRACE_CONTEXT_KEY, openTelemetryContext); + // Create a context with one key value pair + Context contextWithOnePair = Context.of("key", "value"); // END: io.clientcore.core.util.context#object-object } @@ -31,17 +28,15 @@ public void constructContextObject() { */ public void putToContext() { // BEGIN: io.clientcore.core.util.context.addData#object-object - // Users can pass parent trace context information and additional metadata to attach to spans created by SDKs // using the io.clientcore.core.util.Context object. + + Context originalContext = Context.none(); + final String hostNameValue = "host-name-value"; final String entityPathValue = "entity-path-value"; - // TraceContext represents a tracing solution context type - io.opentelemetry.context.Context for OpenTelemetry. - final TraceContext parentContext = TraceContext.root(); - Context parentSpanContext = Context.of("PARENT_TRACE_CONTEXT_KEY", parentContext); - // Add a new key value pair to the existing context object. - Context updatedContext = parentSpanContext.put("HOST_NAME_KEY", hostNameValue) + Context updatedContext = originalContext.put("HOST_NAME_KEY", hostNameValue) .put("ENTITY_PATH_KEY", entityPathValue); // Both key values found on the same updated context object diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/ClientLoggerJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/ClientLoggerJavaDocCodeSnippets.java index 9c3c9a7d9538..d3d64fa2097a 100644 --- a/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/ClientLoggerJavaDocCodeSnippets.java +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/ClientLoggerJavaDocCodeSnippets.java @@ -4,6 +4,7 @@ package io.clientcore.core.util; import io.clientcore.core.http.models.Response; +import io.clientcore.core.instrumentation.logging.ClientLogger; import java.io.File; import java.io.IOException; diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationLoggingTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationLoggingTests.java new file mode 100644 index 000000000000..f66fbf73974c --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationLoggingTests.java @@ -0,0 +1,973 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.http.pipeline; + +import io.clientcore.core.http.MockHttpResponse; +import io.clientcore.core.http.models.HttpHeader; +import io.clientcore.core.http.models.HttpHeaderName; +import io.clientcore.core.http.models.HttpHeaders; +import io.clientcore.core.http.models.HttpLogOptions; +import io.clientcore.core.http.models.HttpMethod; +import io.clientcore.core.http.models.HttpRequest; +import io.clientcore.core.http.models.HttpRetryOptions; +import io.clientcore.core.http.models.RequestOptions; +import io.clientcore.core.http.models.Response; +import io.clientcore.core.implementation.AccessibleByteArrayOutputStream; +import io.clientcore.core.implementation.http.HttpRequestAccessHelper; +import io.clientcore.core.instrumentation.InstrumentationContext; +import io.clientcore.core.instrumentation.InstrumentationOptions; +import io.clientcore.core.instrumentation.logging.ClientLogger; +import io.clientcore.core.util.binarydata.BinaryData; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.io.InputStream; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Stream; + +import static io.clientcore.core.http.models.HttpHeaderName.TRACEPARENT; +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.createInstrumentationContext; +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.createRandomInstrumentationContext; +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.parseLogMessages; +import static io.clientcore.core.instrumentation.logging.InstrumentationTestUtils.setupLogLevelAndGetLogger; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Execution(ExecutionMode.SAME_THREAD) +public class HttpInstrumentationLoggingTests { + private static final String URI = "https://example.com?param=value&api-version=42"; + private static final String REDACTED_URI = "https://example.com?param=REDACTED&api-version=42"; + private static final Set DEFAULT_ALLOWED_QUERY_PARAMS = new HttpLogOptions().getAllowedQueryParamNames(); + private static final Set DEFAULT_ALLOWED_HEADERS = new HttpLogOptions().getAllowedHeaderNames(); + private static final HttpHeaderName CUSTOM_REQUEST_ID = HttpHeaderName.fromString("custom-request-id"); + private static final InstrumentationOptions DEFAULT_INSTRUMENTATION_OPTIONS = null; + + private final AccessibleByteArrayOutputStream logCaptureStream; + + public HttpInstrumentationLoggingTests() { + this.logCaptureStream = new AccessibleByteArrayOutputStream(); + } + + @ParameterizedTest + @MethodSource("disabledHttpLoggingSource") + public void testDisabledHttpLogging(ClientLogger.LogLevel logLevel, HttpLogOptions.HttpLogDetailLevel httpLogLevel) + throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(logLevel, logCaptureStream); + + HttpPipeline pipeline + = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, new HttpLogOptions().setLogLevel(httpLogLevel)); + HttpRequest request = new HttpRequest(HttpMethod.GET, URI); + request.setRequestOptions(new RequestOptions().setLogger(logger)); + + pipeline.send(request).close(); + + assertEquals(0, parseLogMessages(logCaptureStream).size()); + } + + public static Stream disabledHttpLoggingSource() { + return Stream.of(Arguments.of(ClientLogger.LogLevel.VERBOSE, HttpLogOptions.HttpLogDetailLevel.NONE), + Arguments.of(ClientLogger.LogLevel.WARNING, HttpLogOptions.HttpLogDetailLevel.BASIC), + Arguments.of(ClientLogger.LogLevel.WARNING, HttpLogOptions.HttpLogDetailLevel.HEADERS), + Arguments.of(ClientLogger.LogLevel.WARNING, HttpLogOptions.HttpLogDetailLevel.BODY), + Arguments.of(ClientLogger.LogLevel.WARNING, HttpLogOptions.HttpLogDetailLevel.BODY_AND_HEADERS)); + } + + @ParameterizedTest + @MethodSource("allowQueryParamSource") + public void testBasicHttpLogging(Set allowedParams, String expectedUri) throws IOException { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BASIC) + .setAllowedQueryParamNames(allowedParams); + + HttpPipeline pipeline = createPipeline(DEFAULT_INSTRUMENTATION_OPTIONS, options); + + HttpRequest request = createRequest(HttpMethod.GET, URI, logger); + Response response = pipeline.send(request); + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + assertRequestLog(logMessages.get(0), expectedUri, request, null, 0); + assertEquals(7, logMessages.get(0).size()); + + assertResponseLog(logMessages.get(1), expectedUri, response, 0); + assertEquals(11, 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), request); + assertEquals(5, logMessages.get(0).size()); + + assertResponseLog(logMessages.get(1), response); + assertEquals(9, 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); + + InstrumentationContext instrumentationContext + = createInstrumentationContext("1234567890abcdef1234567890abcdef", "1234567890abcdef"); + HttpRequest request = createRequest(HttpMethod.GET, URI, logger, instrumentationContext); + + Response response = pipeline.send(request); + response.close(); + + List> logMessages = parseLogMessages(logCaptureStream); + assertEquals(2, logMessages.size()); + + assertRequestLog(logMessages.get(0), request); + assertEquals(7, logMessages.get(0).size()); + + assertResponseLog(logMessages.get(1), response); + assertEquals(11, 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.setTryCount(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), 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); + + 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); + + assertThrows(RuntimeException.class, () -> pipeline.send(request)); + + List> logMessages = parseLogMessages(logCaptureStream); + if (!expectExceptionLog) { + assertEquals(0, logMessages.size()); + } else { + assertExceptionLog(logMessages.get(0), request, expectedException); + } + } + + @ParameterizedTest + @MethodSource("testExceptionSeverity") + public void testResponseBodyException(ClientLogger.LogLevel level, boolean expectExceptionLog) { + ClientLogger logger = setupLogLevelAndGetLogger(level, logCaptureStream); + HttpLogOptions options = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.BODY); + + 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); + assertThrows(RuntimeException.class, () -> response.getBody().toString()); + + List> logMessages = parseLogMessages(logCaptureStream); + if (!expectExceptionLog) { + assertEquals(0, logMessages.size()); + } else { + assertResponseAndExceptionLog(logMessages.get(0), REDACTED_URI, response, expectedException); + } + } + + @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), 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, 0); + assertEquals(11, 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, 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, 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, request); + assertEquals("Request body", requestLog.get("http.request.body.content")); + + Map responseLog = logMessages.get(1); + assertResponseLog(responseLog, response); + assertEquals("Response body", responseLog.get("http.request.body.content")); + } + + @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, request); + assertEquals("Request body", requestLog.get("http.request.body.content")); + + Map responseLog = logMessages.get(1); + assertResponseLog(responseLog, response); + assertEquals("Response body", responseLog.get("http.request.body.content")); + + 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, request); + assertNull(requestLog.get("http.request.body.content")); + assertEquals(0, requestStream.getPosition()); + + Map responseLog = logMessages.get(1); + assertResponseLog(responseLog, response); + assertNull(responseLog.get("http.request.body.content")); + 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, request); + assertNull(requestLog.get("http.request.body.content")); + assertEquals(0, requestStream.getPosition()); + + Map responseLog = logMessages.get(1); + 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, true, parentContext); + + 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, true, parentContext); + 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, true, parentContext); + } + } + + if (expectExhaustedLog) { + Map lastLog = logMessages.get(logMessages.size() - 1); + 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, null, 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, null, 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, 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); + } + + 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"); + 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, 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(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")); + assertNull(log.get("message")); + + if (context == null) { + context = request.getRequestOptions().getInstrumentationContext(); + } + + assertTraceContext(log, context); + } + + 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) { + 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")); + assertNull(log.get("message")); + 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}")); + 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, 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")); + + 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")); + + assertInstanceOf(Double.class, log.get("http.request.time_to_response")); + assertInstanceOf(Double.class, log.get("http.request.duration")); + assertNull(log.get("message")); + assertTraceContext(log, context); + } + + 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")); + assertNull(log.get("message")); + assertTraceContext(log, response.getRequest().getRequestOptions().getInstrumentationContext()); + } + + 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(tryCount, (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")); + assertNull(log.get("message")); + + if (context == null) { + context = request.getRequestOptions().getInstrumentationContext(); + } + assertTraceContext(log, context); + } + + 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) { + 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).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/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyNoopTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyFallbackTests.java similarity index 56% 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/HttpInstrumentationPolicyFallbackTests.java index 38f13b79d5a4..2417835db52f 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/HttpInstrumentationPolicyFallbackTests.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; @@ -14,42 +15,48 @@ 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 HttpInstrumentationPolicyNoopTests { +public class HttpInstrumentationPolicyFallbackTests { 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 { - HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(OPTIONS, null)) - .httpClient(request -> new MockHttpResponse(request, statusCode)) + @Test + public void simpleRequestTracingDisabled() throws IOException { + HttpPipeline pipeline = new HttpPipelineBuilder() + .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)); } } - @Test - public void exceptionTracingDisabled() { - SocketException exception = new SocketException("test exception"); + @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, null)).httpClient(request -> { - throw exception; - }).build(); + = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(OPTIONS, ENABLED_HTTP_LOG_OPTIONS)) + .httpClient(request -> new MockHttpResponse(request, statusCode)) + .build(); - assertThrows(UncheckedIOException.class, - () -> pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost/")).close()); + // 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)); + } } } 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 b07be1215b50..66ed63f2613b 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 d0d3cf13b1ca..1a459531da8a 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/fallback/FallbackInstrumentationTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java new file mode 100644 index 000000000000..84a301a12564 --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/instrumentation/fallback/FallbackInstrumentationTests.java @@ -0,0 +1,573 @@ +// 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.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; + +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, false), + Arguments.of(ClientLogger.LogLevel.VERBOSE, true)); + } + + @Test + public void basicTracingLogsEnabled() { + 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(); + + 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") <= 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.VERBOSE, 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.VERBOSE, 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()); + } + + @Test + public void tracingLogsEnabledParent() { + ClientLogger logger = setupLogLevelAndGetLogger(ClientLogger.LogLevel.VERBOSE, 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")); + double durationMs = (Double) loggedSpan.get("span.duration"); + 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 000000000000..821c2bda69fc --- /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/util/ClientLoggerTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/instrumentation/logging/ClientLoggerTests.java similarity index 95% 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 7375c46827ed..e5c1162ac44e 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,15 @@ // 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.instrumentation.InstrumentationContext; 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; @@ -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; @@ -259,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 @@ -440,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); @@ -504,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); @@ -676,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()); @@ -707,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()); @@ -752,6 +751,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().setInstrumentationContext(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() + .setInstrumentationContext(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 000000000000..7ec1c2edbad5 --- /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/core/src/test/java/io/clientcore/core/shared/HttpClientTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/shared/HttpClientTests.java index d1d86484eb1f..5fab3a5472d6 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/core/src/test/java/io/clientcore/core/util/HttpLoggingPolicyTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/util/HttpLoggingPolicyTests.java deleted file mode 100644 index 0a936e35a6f2..000000000000 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/util/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.util; - -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.util.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-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 4610150d72c2..ce3fb8f92283 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 234e86372b1d..0735f8eadf90 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 b643c31afa08..5e657ba966dc 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 2eddd2cc5857..0fbc68e8b4d4 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/pom.xml b/sdk/clientcore/http-stress/pom.xml index ea803e4af436..92c0ba46e3f5 100644 --- a/sdk/clientcore/http-stress/pom.xml +++ b/sdk/clientcore/http-stress/pom.xml @@ -32,7 +32,7 @@ io.clientcore core - 1.0.0-beta.1 + 1.0.0-beta.2 io.clientcore 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 3953b1699418..9ed784836eb7 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 @@ -10,11 +10,11 @@ import io.clientcore.core.http.models.HttpMethod; import io.clientcore.core.http.models.HttpRequest; import io.clientcore.core.http.models.Response; -import io.clientcore.core.http.pipeline.HttpLoggingPolicy; +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.HttpRetryPolicy; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.http.okhttp3.OkHttpHttpClientProvider; import io.clientcore.http.stress.util.TelemetryHelper; import reactor.core.publisher.Mono; @@ -151,8 +151,8 @@ private HttpRequest createRequest() { private HttpPipelineBuilder getPipelineBuilder() { HttpLogOptions logOptions = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS); - HttpPipelineBuilder builder - = new HttpPipelineBuilder().policies(new HttpRetryPolicy(), new HttpLoggingPolicy(logOptions)); + HttpPipelineBuilder builder = new HttpPipelineBuilder().policies(new HttpRetryPolicy(), + new HttpInstrumentationPolicy(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 12b2fa87ac3f..192003ae4499 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 @@ -10,11 +10,11 @@ import io.clientcore.core.http.models.HttpMethod; import io.clientcore.core.http.models.HttpRequest; import io.clientcore.core.http.models.Response; -import io.clientcore.core.http.pipeline.HttpLoggingPolicy; +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.HttpRetryPolicy; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.core.util.binarydata.BinaryData; import io.clientcore.http.okhttp3.OkHttpHttpClientProvider; import io.clientcore.http.stress.util.TelemetryHelper; @@ -89,8 +89,8 @@ private HttpRequest createRequest() { private HttpPipelineBuilder getPipelineBuilder() { HttpLogOptions logOptions = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS); - HttpPipelineBuilder builder - = new HttpPipelineBuilder().policies(new HttpRetryPolicy(), new HttpLoggingPolicy(logOptions)); + HttpPipelineBuilder builder = new HttpPipelineBuilder().policies(new HttpRetryPolicy(), + new HttpInstrumentationPolicy(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/util/TelemetryHelper.java b/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/util/TelemetryHelper.java index 0aeaf6197400..871fecf765aa 100644 --- a/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/util/TelemetryHelper.java +++ b/sdk/clientcore/http-stress/src/main/java/io/clientcore/http/stress/util/TelemetryHelper.java @@ -5,7 +5,7 @@ import com.azure.monitor.opentelemetry.exporter.AzureMonitorExporter; import com.azure.monitor.opentelemetry.exporter.AzureMonitorExporterOptions; -import io.clientcore.core.util.ClientLogger; +import io.clientcore.core.instrumentation.logging.ClientLogger; import io.clientcore.http.stress.StressOptions; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; 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 59f801018231..157d476671c2 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. @@ -104,6 +101,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 @@ -141,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 @@ -151,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; @@ -159,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()); } } @@ -180,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/http/pipeline/HttpInstrumentationPolicyTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java index f08b4c8411c2..f6b4c6d12236 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; @@ -52,8 +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.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; @@ -314,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)) @@ -361,10 +337,10 @@ 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 +399,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 +442,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 +461,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/util/Slf4jLoggerShimIT.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/instrumentation/Slf4jLoggerShimIT.java similarity index 76% 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 0ef00ce83e8d..5a53c0d982cd 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; @@ -28,25 +28,27 @@ public class Slf4jLoggerShimIT { @BeforeEach public void setupLogLevels() { - slf4jLoggerShimITLogLevel = System - .setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.util.Slf4jLoggerShimIT", "debug"); - slf4jLoggerShimLogLevel - = System.setProperty(LOG_LEVEL_PREFIX + "io.clientcore.core.implementation.util.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.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/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 81ec9c97058e..558b76125c68 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}. 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 4a61f5717dcf..d603fdf3c0ea 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,36 +146,17 @@ 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()); - } - - @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")); + InstrumentationContext extracted = contextPropagator.extract(null, carrier, GETTER); + assertFalse(extracted.isValid()); } 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/InstrumentationTests.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/InstrumentationTests.java index 308f05dac42d..aa766c1a51e5 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 9316de90b044..0787d9bc7435 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(); @@ -188,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).setAttribute("key", "valueOuter").startSpan(); + Span outerSpan = 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(); + Span innerSpan = tracer.spanBuilder("innerSpan", innerKind, options.getInstrumentationContext()) + .setAttribute("key", "valueInner") + .startSpan(); // sanity check - this should not throw innerSpan.setAttribute("anotherKey", "anotherValue"); @@ -282,8 +281,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,10 +314,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(); - // TODO (limolkova): should we have addContext(k, v) on options? - 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")); @@ -334,9 +332,10 @@ 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 0c229702c142..6b9cd3c528e0 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(); @@ -191,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),