diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000000..d20066c8ffac --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +**/.idea \ No newline at end of file diff --git a/eng/.docsettings.yml b/eng/.docsettings.yml index 12064f887d6a..c8846a66660b 100644 --- a/eng/.docsettings.yml +++ b/eng/.docsettings.yml @@ -108,6 +108,7 @@ known_content_issues: - ['sdk/clientcore/README.md', '#3113'] - ['sdk/clientcore/core/README.md', '#3113'] - ['sdk/clientcore/http-okhttp3/README.md', '#3113'] + - ['sdk/clientcore/optional-dependency-tests/README.md', '#3113'] - ['sdk/core/azure-core-experimental/README.md', '#3113'] - ['sdk/cosmos/faq/README.md', '#3113'] - ['sdk/cosmos/azure-cosmos-benchmark/README.md', '#3113'] diff --git a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml index 963955ca0c2d..d71656bff33d 100644 --- a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml +++ b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml @@ -184,4 +184,17 @@ + + + + + + + + + + + + + diff --git a/sdk/clientcore/ci.yml b/sdk/clientcore/ci.yml index 749cf3528092..9089ac38c77f 100644 --- a/sdk/clientcore/ci.yml +++ b/sdk/clientcore/ci.yml @@ -50,3 +50,6 @@ extends: groupId: io.clientcore safeName: httpokhttp3 releaseInBatch: ${{ parameters.release_clientcorehttpokhttp3 }} + AdditionalModules: + - name: optional-dependency-tests + groupId: io.clientcore diff --git a/sdk/clientcore/core/checkstyle-suppressions.xml b/sdk/clientcore/core/checkstyle-suppressions.xml index b9e79ae1d076..d5b026d94c0c 100644 --- a/sdk/clientcore/core/checkstyle-suppressions.xml +++ b/sdk/clientcore/core/checkstyle-suppressions.xml @@ -37,4 +37,5 @@ + diff --git a/sdk/clientcore/core/pom.xml b/sdk/clientcore/core/pom.xml index defde15f9e53..25d191c6d96a 100644 --- a/sdk/clientcore/core/pom.xml +++ b/sdk/clientcore/core/pom.xml @@ -37,8 +37,8 @@ UTF-8 - 0.60 - 0.60 + 0.50 + 0.50 --add-opens io.clientcore.core/io.clientcore.core.annotation=ALL-UNNAMED @@ -161,14 +161,6 @@ org.apache.maven.plugins maven-failsafe-plugin 3.5.1 - - - - org.slf4j - slf4j-simple - 1.7.36 - - @@ -183,6 +175,23 @@ + + com.azure.tools + codesnippet-maven-plugin + 1.0.0-beta.10 + + + + ../optional-dependency-tests/src/samples/java + **/*.java + + + src/test + **/*.java + + + + diff --git a/sdk/clientcore/core/spotbugs-exclude.xml b/sdk/clientcore/core/spotbugs-exclude.xml index c7ba52b2cb6f..c0e54b82136b 100644 --- a/sdk/clientcore/core/spotbugs-exclude.xml +++ b/sdk/clientcore/core/spotbugs-exclude.xml @@ -73,6 +73,7 @@ + @@ -393,4 +394,12 @@ + + + + + + + + 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 f44b9e12711e..16095265d6a8 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 @@ -323,6 +323,26 @@ public RequestOptions setContext(Context context) { return this; } + /** + * Adds a key-value pair to the request context associated with this request. + * + * @param key The key to add to the context. + * @param value The value to add to the context. + * @return The updated {@link RequestOptions} object. + * + * @see #setContext(Context) + */ + public RequestOptions putContext(Object key, Object value) { + if (locked) { + throw LOGGER.logThrowableAsError( + new IllegalStateException("This instance of RequestOptions is immutable. Cannot set context.")); + } + + this.context = this.context.put(key, value); + + return this; + } + /** * Sets the configuration indicating how the body of the resulting HTTP response should be handled. If {@code null}, * the response body will be handled based on the content type of the response. 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 new file mode 100644 index 000000000000..bcc9370c62ca --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicy.java @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.http.pipeline; + +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.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.LibraryInstrumentationOptions; +import io.clientcore.core.instrumentation.InstrumentationOptions; +import io.clientcore.core.instrumentation.tracing.SpanBuilder; +import io.clientcore.core.instrumentation.tracing.TracingScope; +import io.clientcore.core.instrumentation.tracing.Span; +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 java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +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.instrumentation.tracing.SpanKind.CLIENT; + +/** + * The {@link HttpInstrumentationPolicy} is responsible for instrumenting the HTTP request and response with distributed tracing + * and (in the future) metrics following + * OpenTelemetry Semantic Conventions. + *

+ * 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 + * in the scope of the span. + *

+ * The policy supports basic customizations using {@link InstrumentationOptions} and {@link HttpLogOptions}. + *

+ * If your client library needs a different approach to distributed tracing, + * you can create a custom policy and use it instead of the {@link HttpInstrumentationPolicy}. If you want to enrich instrumentation + * policy spans with additional attributes, you can create a custom policy and add it under the {@link HttpInstrumentationPolicy} + * so that it's executed in the scope of the span created by the {@link HttpInstrumentationPolicy}. + * + *

Configure instrumentation policy:

+ * + *
+ *
+ * HttpPipeline pipeline = new HttpPipelineBuilder()
+ *     .policies(
+ *         new HttpRetryPolicy(),
+ *         new HttpInstrumentationPolicy(instrumentationOptions, logOptions),
+ *         new HttpLoggingPolicy(logOptions))
+ *     .build();
+ *
+ * 
+ * + * + *

Customize instrumentation policy:

+ * + *
+ *
+ * // You can configure URL sanitization to include additional query parameters to preserve
+ * // in `url.full` attribute.
+ * HttpLogOptions logOptions = new HttpLogOptions();
+ * logOptions.addAllowedQueryParamName("documentId");
+ *
+ * HttpPipeline pipeline = new HttpPipelineBuilder()
+ *     .policies(
+ *         new HttpRetryPolicy(),
+ *         new HttpInstrumentationPolicy(instrumentationOptions, logOptions),
+ *         new HttpLoggingPolicy(logOptions))
+ *     .build();
+ *
+ * 
+ * + * + *

Enrich HTTP spans with additional attributes:

+ * + *
+ *
+ * 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));
+ *     }
+ *
+ *     return next.process();
+ * };
+ *
+ * HttpPipeline pipeline = new HttpPipelineBuilder()
+ *     .policies(
+ *         new HttpRetryPolicy(),
+ *         new HttpInstrumentationPolicy(instrumentationOptions, logOptions),
+ *         enrichingPolicy,
+ *         new HttpLoggingPolicy(logOptions))
+ *     .build();
+ *
+ *
+ * 
+ * + * + */ +public final class HttpInstrumentationPolicy implements HttpPipelinePolicy { + private static final ClientLogger LOGGER = new ClientLogger(HttpInstrumentationPolicy.class); + private static final HttpLogOptions DEFAULT_LOG_OPTIONS = new HttpLogOptions(); + private static final String LIBRARY_NAME; + private static final String LIBRARY_VERSION; + private static final LibraryInstrumentationOptions LIBRARY_OPTIONS; + private static final TraceContextSetter SETTER + = (headers, name, value) -> headers.set(HttpHeaderName.fromString(name), value); + + static { + Map properties = getProperties("core.properties"); + LIBRARY_NAME = properties.getOrDefault("name", "unknown"); + LIBRARY_VERSION = properties.getOrDefault("version", "unknown"); + LibraryInstrumentationOptions libOptions + = new LibraryInstrumentationOptions(LIBRARY_NAME).setLibraryVersion(LIBRARY_VERSION) + .setSchemaUrl("https://opentelemetry.io/schemas/1.29.0"); + + // HTTP tracing is special - we suppress nested public API spans, but + // preserve nested HTTP ones. + // We might want to make it configurable for other cases, but let's hide the API for now. + LibraryInstrumentationOptionsAccessHelper.disableSpanSuppression(libOptions); + + 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 final Tracer tracer; + private final TraceContextPropagator traceContextPropagator; + private final Set allowedQueryParameterNames; + + /** + * Creates a new instrumentation policy. + * @param instrumentationOptions Application telemetry options. + * @param logOptions Http log options. TODO: we should merge this with telemetry options. + */ + public HttpInstrumentationPolicy(InstrumentationOptions instrumentationOptions, HttpLogOptions logOptions) { + Instrumentation instrumentation = Instrumentation.create(instrumentationOptions, LIBRARY_OPTIONS); + this.tracer = instrumentation.getTracer(); + this.traceContextPropagator = instrumentation.getW3CTraceContextPropagator(); + + HttpLogOptions logOptionsToUse = logOptions == null ? DEFAULT_LOG_OPTIONS : logOptions; + this.allowedQueryParameterNames = logOptionsToUse.getAllowedQueryParamNames(); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("try") + @Override + public Response process(HttpRequest request, HttpPipelineNextPolicy next) { + if (!isTracingEnabled(request)) { + return next.process(); + } + + String sanitizedUrl = getRedactedUri(request.getUri(), allowedQueryParameterNames); + Span span = startHttpSpan(request, sanitizedUrl); + + if (request.getRequestOptions() == RequestOptions.none()) { + request = request.setRequestOptions(new RequestOptions()); + } + + Context context = request.getRequestOptions().getContext().put(TRACE_CONTEXT_KEY, span); + request.getRequestOptions().setContext(context); + propagateContext(context, request.getHeaders()); + + try (TracingScope scope = span.makeCurrent()) { + Response response = next.process(); + + addDetails(request, response, span); + + span.end(); + return response; + } catch (Throwable t) { + span.end(unwrap(t)); + throw t; + } + } + + 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()); + maybeSetServerPort(spanBuilder, request.getUri()); + return spanBuilder.startSpan(); + } + + /** + * Does the best effort to capture the server port with minimum perf overhead. + * If port is not set, we check scheme for "http" and "https" (case-sensitive). + * If scheme is not one of those, we don't set the port. + * + * @param spanBuilder span builder + * @param uri request URI + */ + private static void maybeSetServerPort(SpanBuilder spanBuilder, URI uri) { + int port = uri.getPort(); + if (port != -1) { + spanBuilder.setAttribute(SERVER_PORT, port); + } else { + switch (uri.getScheme()) { + case "http": + spanBuilder.setAttribute(SERVER_PORT, 80); + break; + + case "https": + spanBuilder.setAttribute(SERVER_PORT, 443); + break; + + default: + break; + } + } + } + + private void addDetails(HttpRequest request, Response response, Span span) { + if (!span.isRecording()) { + return; + } + + span.setAttribute(HTTP_RESPONSE_STATUS_CODE, (long) response.getStatusCode()); + + int tryCount = HttpRequestAccessHelper.getTryCount(request); + if (tryCount > 0) { + span.setAttribute(HTTP_REQUEST_RESEND_COUNT, (long) tryCount); + } + + String userAgent = request.getHeaders().getValue(HttpHeaderName.USER_AGENT); + if (userAgent != null) { + span.setAttribute(USER_AGENT_ORIGINAL, userAgent); + } + + if (response.getStatusCode() >= 400) { + span.setError(String.valueOf(response.getStatusCode())); + } + // 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) { + while (t.getCause() != null) { + t = t.getCause(); + } + return t; + } + + private void propagateContext(Context context, HttpHeaders headers) { + traceContextPropagator.inject(context, headers, SETTER); + } + + private static Map getProperties(String propertiesFileName) { + try (InputStream inputStream + = HttpInstrumentationPolicy.class.getClassLoader().getResourceAsStream(propertiesFileName)) { + if (inputStream != null) { + Properties properties = new Properties(); + properties.load(inputStream); + return Collections.unmodifiableMap(properties.entrySet() + .stream() + .collect(Collectors.toMap(entry -> (String) entry.getKey(), entry -> (String) entry.getValue()))); + } + } catch (IOException ex) { + LOGGER.atWarning() + .addKeyValue("propertiesFileName", propertiesFileName) + .log("Failed to read properties.", ex); + } + + return Collections.emptyMap(); + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java index be575faa8ffb..373498d34fe5 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/pipeline/HttpLoggingPolicy.java @@ -11,22 +11,20 @@ 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.ImplUtils; 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.net.URI; import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.Map; 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; /** @@ -234,53 +232,6 @@ private static boolean canLogBody(BinaryData data) { return data != null && data.getLength() != null && data.getLength() > 0 && data.getLength() < MAX_BODY_LOG_SIZE; } - /** - * Generates the redacted URI for logging. - * - * @param uri URI where the request is being sent. - * @param allowedQueryParameterNames Query parameters that are allowed to be logged. - * @return A URI with query parameters redacted based on configurations in this policy. - */ - private static String getRedactedUri(URI uri, Set allowedQueryParameterNames) { - String query = uri.getQuery(); - StringBuilder uriBuilder = new StringBuilder(); - - // Add the protocol, host and port to the uriBuilder - uriBuilder.append(uri.getScheme()).append("://").append(uri.getHost()); - - if (uri.getPort() != -1) { - uriBuilder.append(":").append(uri.getPort()); - } - - // Add the path to the uriBuilder - uriBuilder.append(uri.getPath()); - - if (query != null && !query.isEmpty()) { - uriBuilder.append("?"); - - // Parse and redact the query parameters - boolean firstQueryParam = true; - for (Map.Entry kvp : new ImplUtils.QueryParameterIterable(query)) { - if (!firstQueryParam) { - uriBuilder.append('&'); - } - - uriBuilder.append(kvp.getKey()); - uriBuilder.append('='); - - if (allowedQueryParameterNames.contains(kvp.getKey().toLowerCase(Locale.ROOT))) { - uriBuilder.append(kvp.getValue()); - } else { - uriBuilder.append(REDACTED_PLACEHOLDER); - } - - firstQueryParam = false; - } - } - - return uriBuilder.toString(); - } - /** * Adds HTTP headers into the StringBuilder that is generating the log message. * diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/MethodHandleReflectiveInvoker.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/MethodHandleReflectiveInvoker.java index fc67af504041..2913b88a8911 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/MethodHandleReflectiveInvoker.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/MethodHandleReflectiveInvoker.java @@ -42,6 +42,71 @@ public Object invokeWithArguments(Object target, Object... args) throws Exceptio } } + @Override + public Object invoke() throws Exception { + try { + return methodHandle.invoke(); + } catch (Throwable throwable) { + if (throwable instanceof Error) { + throw (Error) throwable; + } else { + throw (Exception) throwable; + } + } + } + + @Override + public Object invoke(Object argOrTarget) throws Exception { + try { + return methodHandle.invoke(argOrTarget); + } catch (Throwable throwable) { + if (throwable instanceof Error) { + throw (Error) throwable; + } else { + throw (Exception) throwable; + } + } + } + + @Override + public Object invoke(Object argOrTarget, Object arg1) throws Exception { + try { + return methodHandle.invoke(argOrTarget, arg1); + } catch (Throwable throwable) { + if (throwable instanceof Error) { + throw (Error) throwable; + } else { + throw (Exception) throwable; + } + } + } + + @Override + public Object invoke(Object argOrTarget, Object arg1, Object arg2) throws Exception { + try { + return methodHandle.invoke(argOrTarget, arg1, arg2); + } catch (Throwable throwable) { + if (throwable instanceof Error) { + throw (Error) throwable; + } else { + throw (Exception) throwable; + } + } + } + + @Override + public Object invoke(Object argOrTarget, Object arg1, Object arg2, Object arg3) throws Exception { + try { + return methodHandle.invoke(argOrTarget, arg1, arg2, arg3); + } catch (Throwable throwable) { + if (throwable instanceof Error) { + throw (Error) throwable; + } else { + throw (Exception) throwable; + } + } + } + @Override public int getParameterCount() { return methodHandle.type().parameterCount(); diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/ReflectionUtils.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/ReflectionUtils.java index ed24898b9269..8e675fe00845 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/ReflectionUtils.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/ReflectionUtils.java @@ -135,6 +135,31 @@ public Object invokeWithArguments(Object target, Object... args) { return null; } + @Override + public Object invoke() { + return null; + } + + @Override + public Object invoke(Object argOrTarget) { + return null; + } + + @Override + public Object invoke(Object argOrTarget, Object arg1) { + return null; + } + + @Override + public Object invoke(Object argOrTarget, Object arg1, Object arg2) { + return null; + } + + @Override + public Object invoke(Object argOrTarget, Object arg1, Object arg2, Object arg3) { + return null; + } + @Override public int getParameterCount() { return 0; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/ReflectiveInvoker.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/ReflectiveInvoker.java index ceda707b0459..624584b4e4b4 100644 --- a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/ReflectiveInvoker.java +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/ReflectiveInvoker.java @@ -10,6 +10,11 @@ public interface ReflectiveInvoker { * Invokes an API that doesn't have a target. *

* APIs without a target are constructors and static methods. + *

+ * This method provides convenience for invoking static APIs with a variable number of arguments. + * Don't use this method in performance-sensitive scenarios. Use {@code + * invoke()} and other invoke overloads on the hot path. + *

* * @return The result of invoking the API. * @param args The arguments to pass to the API. @@ -19,6 +24,11 @@ public interface ReflectiveInvoker { /** * Invokes the API on the target object with the provided arguments. + *

+ * This method provides convenience for invoking APIs with a variable number of arguments. + * Don't use this method in performance-sensitive scenarios. Use {@code + * invoke(Object argOrTarget)} and other invoke overloads on the hot path. + *

* * @param target The target object to invoke the API on. * @param args The arguments to pass to the API. @@ -27,6 +37,56 @@ public interface ReflectiveInvoker { */ Object invokeWithArguments(Object target, Object... args) throws Exception; + /** + * Invokes the static API with no arguments. + * + * @return The result of invoking the API. + * @throws Exception If the API invocation fails. + */ + Object invoke() throws Exception; + + /** + * Invokes the API with the provided argument or on the provided target. + * + * @param argOrTarget The argument to pass to the API or the target object to invoke the API on. + * @return The result of invoking the API. + * @throws Exception If the API invocation fails. + */ + Object invoke(Object argOrTarget) throws Exception; + + /** + * Invokes the API with the provided arguments or on the provided target. + * + * @param argOrTarget The argument to pass to the API or the target object to invoke the API on. + * @param arg1 The second argument to pass to the API. + * @return The result of invoking the API. + * @throws Exception If the API invocation fails. + */ + Object invoke(Object argOrTarget, Object arg1) throws Exception; + + /** + * Invokes the API with the provided arguments or on the provided target. + * + * @param argOrTarget The argument to pass to the API or the target object to invoke the API on. + * @param arg1 The second argument to pass to the API. + * @param arg2 The third argument to pass to the API. + * @return The result of invoking the API. + * @throws Exception If the API invocation fails. + */ + Object invoke(Object argOrTarget, Object arg1, Object arg2) throws Exception; + + /** + * Invokes the API with the provided arguments or on the provided target. + * + * @param argOrTarget The argument to pass to the API or the target object to invoke the API on. + * @param arg1 The second argument to pass to the API. + * @param arg2 The third argument to pass to the API. + * @param arg3 The fourth argument to pass to the API. + * @return The result of invoking the API. + * @throws Exception If the API invocation fails. + */ + Object invoke(Object argOrTarget, Object arg1, Object arg2, Object arg3) throws Exception; + /** * Gets the number of parameters the API takes. * diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/UrlRedactionUtil.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/UrlRedactionUtil.java new file mode 100644 index 000000000000..ad309692634f --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/UrlRedactionUtil.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation; + +import io.clientcore.core.implementation.util.ImplUtils; + +import java.net.URI; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Utility class for URL redaction. + */ +public final class UrlRedactionUtil { + private static final String REDACTED_PLACEHOLDER = "REDACTED"; + + /** + * Generates the redacted URI for logging. + * + * @param uri URI where the request is being sent. + * @param allowedQueryParameterNames Set of query parameter names that are allowed to be logged. + * @return A URI with query parameters redacted based on provided allow-list. + */ + public static String getRedactedUri(URI uri, Set allowedQueryParameterNames) { + String query = uri.getQuery(); + + int estimatedUriLength = uri.toString().length() + 128; + StringBuilder uriBuilder = new StringBuilder(estimatedUriLength); + + // Add the protocol, host and port to the uriBuilder + uriBuilder.append(uri.getScheme()).append("://").append(uri.getHost()); + + if (uri.getPort() != -1) { + uriBuilder.append(":").append(uri.getPort()); + } + + // Add the path to the uriBuilder + uriBuilder.append(uri.getPath()); + + if (query != null && !query.isEmpty()) { + uriBuilder.append("?"); + + // Parse and redact the query parameters + boolean firstQueryParam = true; + for (Map.Entry kvp : new ImplUtils.QueryParameterIterable(query)) { + if (!firstQueryParam) { + uriBuilder.append('&'); + } + + uriBuilder.append(kvp.getKey()); + uriBuilder.append('='); + + if (allowedQueryParameterNames.contains(kvp.getKey().toLowerCase(Locale.ROOT))) { + uriBuilder.append(kvp.getValue()); + } else { + uriBuilder.append(REDACTED_PLACEHOLDER); + } + + firstQueryParam = false; + } + } + + return uriBuilder.toString(); + } + + private UrlRedactionUtil() { + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LibraryInstrumentationOptionsAccessHelper.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LibraryInstrumentationOptionsAccessHelper.java new file mode 100644 index 000000000000..21409d97d9c9 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/LibraryInstrumentationOptionsAccessHelper.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation; + +import io.clientcore.core.instrumentation.LibraryInstrumentationOptions; + +/** + * Helper class to access package-private members of {@link LibraryInstrumentationOptions}. + */ +public final class LibraryInstrumentationOptionsAccessHelper { + + private static LibraryInstrumentationOptionsAccessor accessor; + + /** + * Defines the methods that can be called on an instance of {@link LibraryInstrumentationOptions}. + */ + public interface LibraryInstrumentationOptionsAccessor { + + /** + * Disables span suppression for the given options. + * @param options the options to disable span suppression for + * @return the options with span suppression disabled + */ + LibraryInstrumentationOptions disableSpanSuppression(LibraryInstrumentationOptions options); + + /** + * Checks if span suppression is disabled for the given options. + * @param options the options to check + * @return true if span suppression is disabled, false otherwise + */ + boolean isSpanSuppressionDisabled(LibraryInstrumentationOptions options); + } + + /** + * Disables span suppression for the given options. + * @param options the options to disable span suppression for + * @return the options with span suppression disabled + */ + public static LibraryInstrumentationOptions disableSpanSuppression(LibraryInstrumentationOptions options) { + return accessor.disableSpanSuppression(options); + } + + /** + * Checks if span suppression is disabled for the given options. + * @param options the options to check + * @return true if span suppression is disabled, false otherwise + */ + public static boolean isSpanSuppressionDisabled(LibraryInstrumentationOptions options) { + return accessor.isSpanSuppressionDisabled(options); + } + + /** + * Sets the accessor. + * @param accessor the accessor + */ + public static void setAccessor(LibraryInstrumentationOptionsAccessor accessor) { + LibraryInstrumentationOptionsAccessHelper.accessor = accessor; + } + + private LibraryInstrumentationOptionsAccessHelper() { + } +} 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 new file mode 100644 index 000000000000..72d85b0da7fd --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/FallbackInvoker.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.otel; + +import io.clientcore.core.implementation.ReflectiveInvoker; +import io.clientcore.core.util.ClientLogger; + +/** + * A wrapper around a {@link ReflectiveInvoker} that provides a fallback value if the invocation fails, + * reports the error and suppresses exceptions. + */ +public class FallbackInvoker { + private final ReflectiveInvoker inner; + private final Object fallback; + private final ClientLogger logger; + + /** + * Creates a new instance of {@link FallbackInvoker}. + * @param inner the inner invoker + * @param logger the logger to log error on. + */ + public FallbackInvoker(ReflectiveInvoker inner, ClientLogger logger) { + this(inner, null, logger); + } + + /** + * Creates a new instance of {@link FallbackInvoker}. + * + * @param inner the inner invoker + * @param fallback the fallback value + * @param logger the logger to log error on. + */ + public FallbackInvoker(ReflectiveInvoker inner, Object fallback, ClientLogger logger) { + this.inner = inner; + this.fallback = fallback; + this.logger = logger; + } + + /** + * 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() { + try { + return inner.invoke(); + } catch (Throwable t) { + OTelInitializer.runtimeError(logger, t); + } + return fallback; + } + + /** + * 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 + */ + public Object invoke(Object argOrTarget) { + try { + return inner.invoke(argOrTarget); + } catch (Throwable t) { + OTelInitializer.runtimeError(logger, t); + } + return fallback; + } + + /** + * 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 + */ + public Object invoke(Object argOrTarget, Object arg1) { + try { + return inner.invoke(argOrTarget, arg1); + } catch (Throwable t) { + OTelInitializer.runtimeError(logger, t); + } + return fallback; + } + + /** + * 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 + * @return the result of the invocation or the fallback value + */ + public Object invoke(Object argOrTarget, Object arg1, Object arg2) { + try { + return inner.invoke(argOrTarget, arg1, arg2); + } catch (Throwable t) { + OTelInitializer.runtimeError(logger, t); + } + return fallback; + } + + /** + * Invokes the inner invoker and returns the fallback value if the invocation fails. + * @param argOrTarget the argument or target + * @param arg1 the first argument + * @param arg2 the second argument + * @param arg3 the third argument + * @return the result of the invocation or the fallback value + */ + public Object invoke(Object argOrTarget, Object arg1, Object arg2, Object arg3) { + try { + return inner.invoke(argOrTarget, arg1, arg2, arg3); + } catch (Throwable t) { + OTelInitializer.runtimeError(logger, t); + } + return fallback; + } +} 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 new file mode 100644 index 000000000000..4e1b1932aa1c --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelAttributeKey.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.otel; + +import io.clientcore.core.implementation.ReflectiveInvoker; +import io.clientcore.core.util.ClientLogger; + +import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.ATTRIBUTE_KEY_CLASS; + +/** + * Helper class to create OTel attribute keys. + */ +public class OTelAttributeKey { + private static final ClientLogger LOGGER = new ClientLogger(OTelAttributeKey.class); + private static final FallbackInvoker CREATE_STRING_KEY_INVOKER; + private static final FallbackInvoker CREATE_BOOLEAN_KEY_INVOKER; + private static final FallbackInvoker CREATE_LONG_KEY_INVOKER; + private static final FallbackInvoker CREATE_DOUBLE_KEY_INVOKER; + + static { + ReflectiveInvoker createStringKeyInvoker = null; + ReflectiveInvoker createBooleanKeyInvoker = null; + ReflectiveInvoker createLongKeyInvoker = null; + ReflectiveInvoker createDoubleKeyInvoker = null; + + try { + createStringKeyInvoker + = getMethodInvoker(ATTRIBUTE_KEY_CLASS, ATTRIBUTE_KEY_CLASS.getMethod("stringKey", String.class)); + createBooleanKeyInvoker + = getMethodInvoker(ATTRIBUTE_KEY_CLASS, ATTRIBUTE_KEY_CLASS.getMethod("booleanKey", String.class)); + createLongKeyInvoker + = getMethodInvoker(ATTRIBUTE_KEY_CLASS, ATTRIBUTE_KEY_CLASS.getMethod("longKey", String.class)); + createDoubleKeyInvoker + = getMethodInvoker(ATTRIBUTE_KEY_CLASS, ATTRIBUTE_KEY_CLASS.getMethod("doubleKey", String.class)); + } catch (Throwable t) { + OTelInitializer.initError(LOGGER, t); + } + + CREATE_STRING_KEY_INVOKER = new FallbackInvoker(createStringKeyInvoker, LOGGER); + CREATE_BOOLEAN_KEY_INVOKER = new FallbackInvoker(createBooleanKeyInvoker, LOGGER); + CREATE_LONG_KEY_INVOKER = new FallbackInvoker(createLongKeyInvoker, LOGGER); + CREATE_DOUBLE_KEY_INVOKER = new FallbackInvoker(createDoubleKeyInvoker, LOGGER); + } + + /** + * Creates an OTel attribute key. + * + * @param key the key name + * @param value the value + * @return the OTel attribute key + */ + public static Object getKey(String key, Object value) { + if (OTelInitializer.isInitialized()) { + if (value instanceof Boolean) { + return CREATE_BOOLEAN_KEY_INVOKER.invoke(key); + } else if (value instanceof String) { + return CREATE_STRING_KEY_INVOKER.invoke(key); + } else if (value instanceof Long) { + return CREATE_LONG_KEY_INVOKER.invoke(key); + } else if (value instanceof Integer) { + return CREATE_LONG_KEY_INVOKER.invoke(key); + } else if (value instanceof Double) { + return CREATE_DOUBLE_KEY_INVOKER.invoke(key); + } else { + LOGGER.atVerbose() + .addKeyValue("key", key) + .addKeyValue("type", value.getClass().getName()) + .log("Could not populate attribute. Type is not supported."); + return null; + } + } + + return null; + } + + /** + * Casts the attribute value to the correct type. + * + * @param value the value + * @return the casted value + */ + public static Object castAttributeValue(Object value) { + if (value instanceof Integer) { + return ((Integer) value).longValue(); + } + + return value; + } +} 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 new file mode 100644 index 000000000000..7b0e5eb1b2a2 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInitializer.java @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.otel; + +import io.clientcore.core.util.ClientLogger; + +/** + * This class is used to initialize OpenTelemetry. + */ +public final class OTelInitializer { + private static final ClientLogger LOGGER = new ClientLogger(OTelInitializer.class); + private static final OTelInitializer INSTANCE; + + public static final Class ATTRIBUTE_KEY_CLASS; + public static final Class ATTRIBUTES_CLASS; + public static final Class ATTRIBUTES_BUILDER_CLASS; + + public static final Class CONTEXT_CLASS; + public static final Class CONTEXT_KEY_CLASS; + public static final Class CONTEXT_PROPAGATORS_CLASS; + public static final Class OTEL_CLASS; + public static final Class GLOBAL_OTEL_CLASS; + + public static final Class SCOPE_CLASS; + public static final Class SPAN_BUILDER_CLASS; + public static final Class SPAN_CONTEXT_CLASS; + public static final Class SPAN_KIND_CLASS; + public static final Class SPAN_CLASS; + + public static final Class STATUS_CODE_CLASS; + + public static final Class TEXT_MAP_PROPAGATOR_CLASS; + public static final Class TEXT_MAP_GETTER_CLASS; + public static final Class TEXT_MAP_SETTER_CLASS; + + public static final Class TRACE_FLAGS_CLASS; + public static final Class TRACE_STATE_CLASS; + public static final Class TRACER_CLASS; + public static final Class TRACER_BUILDER_CLASS; + public static final Class TRACER_PROVIDER_CLASS; + + public static final Class W3C_PROPAGATOR_CLASS; + + private volatile boolean initialized; + + static { + Class attributeKeyClass = null; + Class attributesClass = null; + Class attributesBuilderClass = null; + + Class contextClass = null; + Class contextKeyClass = null; + Class contextPropagatorsClass = null; + + Class otelClass = null; + Class globalOtelClass = null; + + Class scopeClass = null; + Class spanClass = null; + Class spanBuilderClass = null; + Class spanContextClass = null; + Class spanKindClass = null; + Class statusCodeClass = null; + + Class textMapPropagatorClass = null; + Class textMapGetterClass = null; + Class textMapSetterClass = null; + + Class traceFlagsClass = null; + Class traceStateClass = null; + Class tracerClass = null; + Class tracerBuilderClass = null; + Class tracerProviderClass = null; + + Class w3cPropagatorClass = null; + + OTelInitializer instance = null; + try { + ClassLoader classLoader = OTelInitializer.class.getClassLoader(); + attributeKeyClass = Class.forName("io.opentelemetry.api.common.AttributeKey", true, classLoader); + attributesClass = Class.forName("io.opentelemetry.api.common.Attributes", true, classLoader); + attributesBuilderClass = Class.forName("io.opentelemetry.api.common.AttributesBuilder", true, classLoader); + + contextClass = Class.forName("io.opentelemetry.context.Context", true, classLoader); + contextKeyClass = Class.forName("io.opentelemetry.context.ContextKey", true, classLoader); + contextPropagatorsClass + = Class.forName("io.opentelemetry.context.propagation.ContextPropagators", true, classLoader); + + otelClass = Class.forName("io.opentelemetry.api.OpenTelemetry", true, classLoader); + globalOtelClass = Class.forName("io.opentelemetry.api.GlobalOpenTelemetry", true, classLoader); + + scopeClass = Class.forName("io.opentelemetry.context.Scope", true, classLoader); + + spanClass = Class.forName("io.opentelemetry.api.trace.Span", true, classLoader); + spanBuilderClass = Class.forName("io.opentelemetry.api.trace.SpanBuilder", true, classLoader); + spanContextClass = Class.forName("io.opentelemetry.api.trace.SpanContext", true, classLoader); + spanKindClass = Class.forName("io.opentelemetry.api.trace.SpanKind", true, classLoader); + statusCodeClass = Class.forName("io.opentelemetry.api.trace.StatusCode", true, classLoader); + + textMapPropagatorClass + = Class.forName("io.opentelemetry.context.propagation.TextMapPropagator", true, classLoader); + textMapGetterClass = Class.forName("io.opentelemetry.context.propagation.TextMapGetter", true, classLoader); + textMapSetterClass = Class.forName("io.opentelemetry.context.propagation.TextMapSetter", true, classLoader); + + traceFlagsClass = Class.forName("io.opentelemetry.api.trace.TraceFlags", true, classLoader); + traceStateClass = Class.forName("io.opentelemetry.api.trace.TraceState", true, classLoader); + tracerClass = Class.forName("io.opentelemetry.api.trace.Tracer", true, classLoader); + tracerBuilderClass = Class.forName("io.opentelemetry.api.trace.TracerBuilder", true, classLoader); + tracerProviderClass = Class.forName("io.opentelemetry.api.trace.TracerProvider", true, classLoader); + w3cPropagatorClass + = Class.forName("io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator", true, classLoader); + + instance = new OTelInitializer(true); + } catch (Throwable t) { + LOGGER.atVerbose().log("OpenTelemetry was not initialized.", t); + instance = new OTelInitializer(false); + } + + ATTRIBUTE_KEY_CLASS = attributeKeyClass; + ATTRIBUTES_CLASS = attributesClass; + ATTRIBUTES_BUILDER_CLASS = attributesBuilderClass; + + CONTEXT_CLASS = contextClass; + CONTEXT_KEY_CLASS = contextKeyClass; + CONTEXT_PROPAGATORS_CLASS = contextPropagatorsClass; + + OTEL_CLASS = otelClass; + GLOBAL_OTEL_CLASS = globalOtelClass; + + SCOPE_CLASS = scopeClass; + SPAN_CLASS = spanClass; + SPAN_BUILDER_CLASS = spanBuilderClass; + SPAN_CONTEXT_CLASS = spanContextClass; + SPAN_KIND_CLASS = spanKindClass; + STATUS_CODE_CLASS = statusCodeClass; + + TEXT_MAP_PROPAGATOR_CLASS = textMapPropagatorClass; + TEXT_MAP_GETTER_CLASS = textMapGetterClass; + TEXT_MAP_SETTER_CLASS = textMapSetterClass; + + TRACE_FLAGS_CLASS = traceFlagsClass; + TRACE_STATE_CLASS = traceStateClass; + TRACER_CLASS = tracerClass; + TRACER_BUILDER_CLASS = tracerBuilderClass; + TRACER_PROVIDER_CLASS = tracerProviderClass; + + W3C_PROPAGATOR_CLASS = w3cPropagatorClass; + + INSTANCE = instance; + } + + private OTelInitializer(boolean initialized) { + this.initialized = initialized; + } + + /** + * Disables OTel and logs OTel initialization error. + * @param logger the logger + * @param t the error + */ + public static void initError(ClientLogger logger, Throwable t) { + logger.atVerbose().log("OpenTelemetry version is incompatible.", t); + INSTANCE.initialized = false; + } + + /** + * Disables OTel and logs runtime error when using OTel. + * + * @param logger the logger + * @param t the error + */ + public static void runtimeError(ClientLogger logger, Throwable t) { + if (INSTANCE.initialized) { + logger.atWarning().log("Unexpected error when invoking OpenTelemetry, turning tracing off.", t); + } + + INSTANCE.initialized = false; + } + + /** + * Checks if OTel initialization was successful. + * + * @return true if OTel is initialized successfully, false otherwise + */ + 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 new file mode 100644 index 000000000000..6edbbb51a4eb --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/OTelInstrumentation.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.otel; + +import io.clientcore.core.implementation.ReflectiveInvoker; +import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelTraceContextPropagator; +import io.clientcore.core.implementation.instrumentation.otel.tracing.OTelTracer; +import io.clientcore.core.instrumentation.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 static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; +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.TRACER_PROVIDER_CLASS; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.W3C_PROPAGATOR_CLASS; + +/** + * A {@link Instrumentation} implementation that uses OpenTelemetry. + */ +public class OTelInstrumentation implements Instrumentation { + private static final FallbackInvoker GET_PROVIDER_INVOKER; + private static final FallbackInvoker GET_GLOBAL_OTEL_INVOKER; + + private static final Object NOOP_PROVIDER; + private static final OTelTraceContextPropagator W3C_PROPAGATOR_INSTANCE; + private static final ClientLogger LOGGER = new ClientLogger(OTelInstrumentation.class); + static { + ReflectiveInvoker getProviderInvoker = null; + ReflectiveInvoker getGlobalOtelInvoker = null; + + Object noopProvider = null; + Object w3cPropagatorInstance = null; + + if (OTelInitializer.isInitialized()) { + try { + getProviderInvoker = getMethodInvoker(OTEL_CLASS, OTEL_CLASS.getMethod("getTracerProvider")); + getGlobalOtelInvoker = getMethodInvoker(GLOBAL_OTEL_CLASS, GLOBAL_OTEL_CLASS.getMethod("get")); + + ReflectiveInvoker noopProviderInvoker + = getMethodInvoker(TRACER_PROVIDER_CLASS, TRACER_PROVIDER_CLASS.getMethod("noop")); + noopProvider = noopProviderInvoker.invoke(); + + ReflectiveInvoker w3cPropagatorInvoker + = getMethodInvoker(W3C_PROPAGATOR_CLASS, W3C_PROPAGATOR_CLASS.getMethod("getInstance")); + w3cPropagatorInstance = w3cPropagatorInvoker.invoke(); + + } catch (Throwable t) { + OTelInitializer.initError(LOGGER, t); + } + } + + GET_PROVIDER_INVOKER = new FallbackInvoker(getProviderInvoker, LOGGER); + GET_GLOBAL_OTEL_INVOKER = new FallbackInvoker(getGlobalOtelInvoker, LOGGER); + NOOP_PROVIDER = noopProvider; + + W3C_PROPAGATOR_INSTANCE = new OTelTraceContextPropagator(w3cPropagatorInstance); + } + + private final Object otelInstance; + private final LibraryInstrumentationOptions libraryOptions; + private final boolean isTracingEnabled; + + /** + * Creates a new instance of {@link OTelInstrumentation}. + * + * @param applicationOptions the application options + * @param libraryOptions the library options + */ + public OTelInstrumentation(InstrumentationOptions applicationOptions, + LibraryInstrumentationOptions libraryOptions) { + Object explicitOTel = applicationOptions == null ? null : applicationOptions.getProvider(); + if (explicitOTel != null && !OTEL_CLASS.isInstance(explicitOTel)) { + throw LOGGER.atError() + .addKeyValue("expectedProvider", OTEL_CLASS.getName()) + .addKeyValue("actualProvider", explicitOTel.getClass().getName()) + .log("Unexpected telemetry provider type.", + new IllegalArgumentException("Telemetry provider is not an instance of " + OTEL_CLASS.getName())); + } + + this.otelInstance = explicitOTel; + this.libraryOptions = libraryOptions; + this.isTracingEnabled = applicationOptions == null || applicationOptions.isTracingEnabled(); + } + + /** + * {@inheritDoc} + */ + @Override + public Tracer getTracer() { + if (isTracingEnabled && OTelInitializer.isInitialized()) { + Object otelTracerProvider = GET_PROVIDER_INVOKER.invoke(getOtelInstance()); + + if (otelTracerProvider != null && otelTracerProvider != NOOP_PROVIDER) { + return new OTelTracer(otelTracerProvider, libraryOptions); + } + } + + return OTelTracer.NOOP; + } + + /** + * {@inheritDoc} + */ + @Override + public TraceContextPropagator getW3CTraceContextPropagator() { + return OTelInitializer.isInitialized() ? W3C_PROPAGATOR_INSTANCE : OTelTraceContextPropagator.NOOP; + } + + 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 new file mode 100644 index 000000000000..ead18dbf6d52 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the implementation of the OpenTelemetry telemetry provider. + */ +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 new file mode 100644 index 000000000000..1d7219bc52a8 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelContext.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.otel.tracing; + +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.tracing.TracingScope; +import io.clientcore.core.instrumentation.tracing.SpanKind; +import io.clientcore.core.util.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.CONTEXT_KEY_CLASS; + +class OTelContext { + private static final ClientLogger LOGGER = new ClientLogger(OTelContext.class); + private static final TracingScope NOOP_SCOPE = () -> { + }; + private static final FallbackInvoker CURRENT_INVOKER; + private static final FallbackInvoker MAKE_CURRENT_INVOKER; + private static final FallbackInvoker WITH_INVOKER; + private static final FallbackInvoker GET_INVOKER; + + // this context key will indicate if the span is created by client core + // AND has client or internal kind (logical client operation) + // this is used to suppress multiple spans created for the same logical operation + // such as convenience API on top of protocol methods when both as instrumented. + // We might need to suppress logical server (consumer) spans in the future, but that + // was not necessary so far + private static final Object HAS_CLIENT_SPAN_CONTEXT_KEY; + + static { + ReflectiveInvoker currentInvoker = null; + ReflectiveInvoker makeCurrentInvoker = null; + ReflectiveInvoker withInvoker = null; + ReflectiveInvoker getInvoker = null; + Object hasClientSpanContextKey = null; + Object rootContext = null; + + if (OTelInitializer.isInitialized()) { + try { + currentInvoker = getMethodInvoker(CONTEXT_CLASS, CONTEXT_CLASS.getMethod("current")); + makeCurrentInvoker = getMethodInvoker(CONTEXT_CLASS, CONTEXT_CLASS.getMethod("makeCurrent")); + withInvoker + = getMethodInvoker(CONTEXT_CLASS, CONTEXT_CLASS.getMethod("with", CONTEXT_KEY_CLASS, Object.class)); + getInvoker = getMethodInvoker(CONTEXT_CLASS, CONTEXT_CLASS.getMethod("get", CONTEXT_KEY_CLASS)); + + ReflectiveInvoker contextKeyNamedInvoker + = getMethodInvoker(CONTEXT_KEY_CLASS, CONTEXT_KEY_CLASS.getMethod("named", String.class)); + + hasClientSpanContextKey = contextKeyNamedInvoker.invoke("client-core-call"); + + ReflectiveInvoker rootInvoker = getMethodInvoker(CONTEXT_CLASS, CONTEXT_CLASS.getMethod("root")); + rootContext = rootInvoker.invoke(); + } catch (Throwable t) { + OTelInitializer.initError(LOGGER, t); + } + } + + CURRENT_INVOKER = new FallbackInvoker(currentInvoker, rootContext, LOGGER); + 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; + } + + static Object getCurrent() { + Object currentContext = CURRENT_INVOKER.invoke(); + assert CONTEXT_CLASS.isInstance(currentContext); + return currentContext; + } + + static AutoCloseable makeCurrent(Object context) { + assert CONTEXT_CLASS.isInstance(context); + Object scope = MAKE_CURRENT_INVOKER.invoke(context); + assert scope instanceof AutoCloseable; + return (AutoCloseable) scope; + } + + static Object markCoreSpan(Object context, SpanKind spanKind) { + 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; + } + + static boolean hasClientCoreSpan(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); + } +} 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 new file mode 100644 index 000000000000..52a31d0f42e3 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpan.java @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.otel.tracing; + +import io.clientcore.core.implementation.ReflectiveInvoker; +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.tracing.TracingScope; +import io.clientcore.core.instrumentation.tracing.Span; +import io.clientcore.core.instrumentation.tracing.SpanKind; +import io.clientcore.core.util.ClientLogger; + +import java.util.Objects; + +import static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; +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; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CONTEXT_CLASS; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.STATUS_CODE_CLASS; +import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelContext.markCoreSpan; +import static io.clientcore.core.implementation.instrumentation.otel.tracing.OTelSpanContext.INVALID_OTEL_SPAN_CONTEXT; + +/** + * OpenTelemetry implementation of {@link Span}. + */ +public class OTelSpan implements Span { + private static final ClientLogger LOGGER = new ClientLogger(OTelSpan.class); + private static final TracingScope NOOP_SCOPE = () -> { + }; + private static final FallbackInvoker SET_ATTRIBUTE_INVOKER; + private static final FallbackInvoker SET_STATUS_INVOKER; + private static final FallbackInvoker END_INVOKER; + private static final FallbackInvoker GET_SPAN_CONTEXT_INVOKER; + private static final FallbackInvoker IS_RECORDING_INVOKER; + private static final FallbackInvoker STORE_IN_CONTEXT_INVOKER; + private static final FallbackInvoker FROM_CONTEXT_INVOKER; + private static final FallbackInvoker WRAP_INVOKER; + private static final Object ERROR_STATUS_CODE; + private final Object otelSpan; + private final Object otelContext; + private final boolean isRecording; + private String errorType; + + static { + ReflectiveInvoker setAttributeInvoker = null; + ReflectiveInvoker setStatusInvoker = null; + ReflectiveInvoker endInvoker = null; + ReflectiveInvoker getSpanContextInvoker = null; + ReflectiveInvoker isRecordingInvoker = null; + ReflectiveInvoker storeInContextInvoker = null; + ReflectiveInvoker fromContextInvoker = null; + ReflectiveInvoker wrapInvoker = null; + + Object errorStatusCode = null; + + if (OTelInitializer.isInitialized()) { + try { + setAttributeInvoker = getMethodInvoker(SPAN_CLASS, + SPAN_CLASS.getMethod("setAttribute", ATTRIBUTE_KEY_CLASS, Object.class)); + + setStatusInvoker + = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("setStatus", STATUS_CODE_CLASS, String.class)); + endInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("end")); + + isRecordingInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("isRecording")); + + getSpanContextInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("getSpanContext")); + + storeInContextInvoker + = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("storeInContext", CONTEXT_CLASS)); + fromContextInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("fromContext", CONTEXT_CLASS)); + + wrapInvoker = getMethodInvoker(SPAN_CLASS, SPAN_CLASS.getMethod("wrap", SPAN_CONTEXT_CLASS)); + errorStatusCode = STATUS_CODE_CLASS.getField("ERROR").get(null); + } catch (Throwable t) { + OTelInitializer.initError(LOGGER, t); + } + } + + SET_ATTRIBUTE_INVOKER = new FallbackInvoker(setAttributeInvoker, LOGGER); + SET_STATUS_INVOKER = new FallbackInvoker(setStatusInvoker, LOGGER); + END_INVOKER = new FallbackInvoker(endInvoker, LOGGER); + GET_SPAN_CONTEXT_INVOKER = new FallbackInvoker(getSpanContextInvoker, INVALID_OTEL_SPAN_CONTEXT, LOGGER); + IS_RECORDING_INVOKER = new FallbackInvoker(isRecordingInvoker, false, LOGGER); + STORE_IN_CONTEXT_INVOKER = new FallbackInvoker(storeInContextInvoker, LOGGER); + FROM_CONTEXT_INVOKER = new FallbackInvoker(fromContextInvoker, LOGGER); + WRAP_INVOKER = new FallbackInvoker(wrapInvoker, LOGGER); + + ERROR_STATUS_CODE = errorStatusCode; + } + + OTelSpan(Object otelSpan, Object otelParentContext, SpanKind spanKind) { + this.otelSpan = otelSpan; + this.isRecording = otelSpan != null && (boolean) IS_RECORDING_INVOKER.invoke(otelSpan); + + Object contextWithSpan = otelSpan != null ? storeInContext(otelSpan, otelParentContext) : otelParentContext; + this.otelContext = markCoreSpan(contextWithSpan, spanKind); + } + + /** + * {@inheritDoc} + */ + @Override + public OTelSpan setAttribute(String key, Object value) { + if (isInitialized() && isRecording) { + SET_ATTRIBUTE_INVOKER.invoke(otelSpan, OTelAttributeKey.getKey(key, value), + OTelAttributeKey.castAttributeValue(value)); + } + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public Span setError(String errorType) { + this.errorType = errorType; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public void end(Throwable throwable) { + Objects.requireNonNull(throwable, "'throwable' cannot be null"); + endSpan(throwable); + } + + /** + * {@inheritDoc} + */ + @Override + 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} + */ + @Override + public boolean isRecording() { + return isRecording; + } + + /** + * {@inheritDoc} + */ + @Override + public TracingScope makeCurrent() { + return isInitialized() ? wrapOTelScope(OTelContext.makeCurrent(otelContext)) : NOOP_SCOPE; + } + + static Object createPropagatingSpan(Object otelContext) { + assert CONTEXT_CLASS.isInstance(otelContext); + + Object span = FROM_CONTEXT_INVOKER.invoke(otelContext); + assert SPAN_CLASS.isInstance(span); + + Object spanContext = GET_SPAN_CONTEXT_INVOKER.invoke(span); + assert SPAN_CONTEXT_CLASS.isInstance(spanContext); + + Object propagatingSpan = WRAP_INVOKER.invoke(spanContext); + assert SPAN_CLASS.isInstance(propagatingSpan); + + return propagatingSpan; + } + + Object getOtelContext() { + return otelContext; + } + + private void endSpan(Throwable throwable) { + if (isInitialized()) { + if (errorType != null || throwable != null) { + setAttribute("error.type", errorType != null ? errorType : throwable.getClass().getCanonicalName()); + SET_STATUS_INVOKER.invoke(otelSpan, ERROR_STATUS_CODE, + throwable == null ? null : throwable.getMessage()); + } + + END_INVOKER.invoke(otelSpan); + } + } + + private static TracingScope wrapOTelScope(AutoCloseable otelScope) { + return () -> { + try { + otelScope.close(); + } catch (Exception e) { + OTelInitializer.runtimeError(LOGGER, e); + } + }; + } + + 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 new file mode 100644 index 000000000000..ccd5e25dfb1a --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanBuilder.java @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.otel.tracing; + +import io.clientcore.core.implementation.ReflectiveInvoker; +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.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 static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; +import static io.clientcore.core.implementation.instrumentation.otel.OTelAttributeKey.castAttributeValue; +import static io.clientcore.core.implementation.instrumentation.otel.OTelAttributeKey.getKey; +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_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")); + + 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; + private static final FallbackInvoker START_SPAN_INVOKER; + private static final Object INTERNAL_KIND; + private static final Object SERVER_KIND; + private static final Object CLIENT_KIND; + private static final Object PRODUCER_KIND; + private static final Object CONSUMER_KIND; + + private final Object otelSpanBuilder; + private final boolean suppressNestedSpans; + private final SpanKind spanKind; + private final Context context; + + static { + ReflectiveInvoker setParentInvoker = null; + ReflectiveInvoker setAttributeInvoker = null; + ReflectiveInvoker setSpanKindInvoker = null; + ReflectiveInvoker startSpanInvoker = null; + + Object internalKind = null; + Object serverKind = null; + Object clientKind = null; + Object producerKind = null; + Object consumerKind = null; + OTelSpan noopSpan = null; + + if (OTelInitializer.isInitialized()) { + try { + setParentInvoker + = getMethodInvoker(SPAN_BUILDER_CLASS, SPAN_BUILDER_CLASS.getMethod("setParent", CONTEXT_CLASS)); + + setAttributeInvoker = getMethodInvoker(SPAN_BUILDER_CLASS, + SPAN_BUILDER_CLASS.getMethod("setAttribute", ATTRIBUTE_KEY_CLASS, Object.class)); + + setSpanKindInvoker = getMethodInvoker(SPAN_BUILDER_CLASS, + SPAN_BUILDER_CLASS.getMethod("setSpanKind", SPAN_KIND_CLASS)); + + startSpanInvoker = getMethodInvoker(SPAN_BUILDER_CLASS, SPAN_BUILDER_CLASS.getMethod("startSpan")); + + internalKind = SPAN_KIND_CLASS.getField("INTERNAL").get(null); + serverKind = SPAN_KIND_CLASS.getField("SERVER").get(null); + 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); + INTERNAL_KIND = internalKind; + SERVER_KIND = serverKind; + CLIENT_KIND = clientKind; + PRODUCER_KIND = producerKind; + CONSUMER_KIND = consumerKind; + + } + + OTelSpanBuilder(Object otelSpanBuilder, SpanKind kind, Context parent, + LibraryInstrumentationOptions libraryOptions) { + this.otelSpanBuilder = otelSpanBuilder; + this.suppressNestedSpans = libraryOptions == null + || !LibraryInstrumentationOptionsAccessHelper.isSpanSuppressionDisabled(libraryOptions); + this.spanKind = kind; + this.context = parent; + } + + /** + * {@inheritDoc} + */ + @Override + public SpanBuilder setAttribute(String key, Object value) { + if (isInitialized()) { + SET_ATTRIBUTE_INVOKER.invoke(otelSpanBuilder, getKey(key, value), castAttributeValue(value)); + } + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public Span startSpan() { + if (isInitialized()) { + Object otelParentContext = getOTelContext(context); + SET_PARENT_INVOKER.invoke(otelSpanBuilder, otelParentContext); + SET_SPAN_KIND_INVOKER.invoke(otelSpanBuilder, toOtelSpanKind(spanKind)); + Object otelSpan = shouldSuppress(otelParentContext) + ? OTelSpan.createPropagatingSpan(otelParentContext) + : START_SPAN_INVOKER.invoke(otelSpanBuilder); + if (otelSpan != null) { + return new OTelSpan(otelSpan, otelParentContext, this.spanKind); + } + } + + return NOOP_SPAN; + } + + private boolean shouldSuppress(Object parentContext) { + return suppressNestedSpans + && (this.spanKind == SpanKind.CLIENT || this.spanKind == SpanKind.INTERNAL) + && OTelContext.hasClientCoreSpan(parentContext); + } + + private Object toOtelSpanKind(SpanKind spanKind) { + switch (spanKind) { + case SERVER: + return SERVER_KIND; + + case CLIENT: + return CLIENT_KIND; + + case PRODUCER: + return PRODUCER_KIND; + + case CONSUMER: + return CONSUMER_KIND; + + default: + return INTERNAL_KIND; + } + } + + private boolean isInitialized() { + return otelSpanBuilder != null && OTelInitializer.isInitialized(); + } +} 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 new file mode 100644 index 000000000000..9433464c2281 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelSpanContext.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.otel.tracing; + +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 static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.SPAN_CONTEXT_CLASS; + +/** + * Wrapper around OpenTelemetry SpanContext. + */ +public class OTelSpanContext { + public static final Object INVALID_OTEL_SPAN_CONTEXT; + private static final String INVALID_TRACE_ID = "00000000000000000000000000000000"; + private static final String INVALID_SPAN_ID = "0000000000000000"; + private static final String INVALID_TRACE_FLAGS = "00"; + private static final OTelSpanContext INVALID; + private static final ClientLogger LOGGER = new ClientLogger(OTelSpanContext.class); + 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 final Object otelSpanContext; + static { + ReflectiveInvoker getSpanIdInvoker = null; + ReflectiveInvoker getTraceIdInvoker = null; + ReflectiveInvoker getTraceFlagsInvoker = null; + + Object invalidInstance = null; + + if (OTelInitializer.isInitialized()) { + try { + getTraceIdInvoker = getMethodInvoker(SPAN_CONTEXT_CLASS, SPAN_CONTEXT_CLASS.getMethod("getTraceId")); + getSpanIdInvoker = getMethodInvoker(SPAN_CONTEXT_CLASS, SPAN_CONTEXT_CLASS.getMethod("getSpanId")); + getTraceFlagsInvoker + = getMethodInvoker(SPAN_CONTEXT_CLASS, SPAN_CONTEXT_CLASS.getMethod("getTraceFlags")); + ReflectiveInvoker getInvalidInvoker + = getMethodInvoker(SPAN_CONTEXT_CLASS, SPAN_CONTEXT_CLASS.getMethod("getInvalid")); + + invalidInstance = getInvalidInvoker.invoke(); + } catch (Throwable t) { + OTelInitializer.initError(LOGGER, t); + } + } + + INVALID_OTEL_SPAN_CONTEXT = invalidInstance; + INVALID = new OTelSpanContext(invalidInstance); + 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); + } + + OTelSpanContext(Object otelSpanContext) { + this.otelSpanContext = otelSpanContext; + } + + static OTelSpanContext getInvalid() { + return INVALID; + } + + /** + * Gets trace id. + * + * @return the trace id. + */ + public String getTraceId() { + return isInitialized() ? (String) GET_TRACE_ID_INVOKER.invoke(otelSpanContext) : INVALID_TRACE_ID; + } + + /** + * Gets span id. + * + * @return the span id. + */ + public String getSpanId() { + return isInitialized() ? (String) GET_SPAN_ID_INVOKER.invoke(otelSpanContext) : INVALID_SPAN_ID; + } + + /** + * Gets trace flags. + * + * @return the trace flags. + */ + public String getTraceFlags() { + if (isInitialized()) { + Object traceFlags = GET_TRACE_FLAGS_INVOKER.invoke(otelSpanContext); + if (traceFlags != null) { + return traceFlags.toString(); + } + } + + return INVALID_TRACE_FLAGS; + } + + private boolean isInitialized() { + return otelSpanContext != null && OTelInitializer.isInitialized(); + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java new file mode 100644 index 000000000000..eb673edc9fa6 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTraceContextPropagator.java @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation.instrumentation.otel.tracing; + +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.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 java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Map; + +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.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}. + */ +public class OTelTraceContextPropagator implements TraceContextPropagator { + public static final TraceContextPropagator NOOP = new OTelTraceContextPropagator(null); + + private static final ClientLogger LOGGER = new ClientLogger(OTelTraceContextPropagator.class); + private static final FallbackInvoker INJECT_INVOKER; + private static final FallbackInvoker EXTRACT_INVOKER; + + static { + ReflectiveInvoker injectInvoker = null; + ReflectiveInvoker extractInvoker = null; + if (OTelInitializer.isInitialized()) { + try { + injectInvoker = getMethodInvoker(TEXT_MAP_PROPAGATOR_CLASS, + TEXT_MAP_PROPAGATOR_CLASS.getMethod("inject", CONTEXT_CLASS, Object.class, TEXT_MAP_SETTER_CLASS)); + + extractInvoker = getMethodInvoker(TEXT_MAP_PROPAGATOR_CLASS, + TEXT_MAP_PROPAGATOR_CLASS.getMethod("extract", CONTEXT_CLASS, Object.class, TEXT_MAP_GETTER_CLASS)); + } catch (Throwable t) { + OTelInitializer.initError(LOGGER, t); + } + } + + INJECT_INVOKER = new FallbackInvoker(injectInvoker, LOGGER); + EXTRACT_INVOKER = new FallbackInvoker(extractInvoker, LOGGER); + } + + private final Object otelPropagator; + + /** + * Creates a new instance of {@link OTelTraceContextPropagator}. + * + * @param otelPropagator the OpenTelemetry propagator + */ + public OTelTraceContextPropagator(Object otelPropagator) { + this.otelPropagator = otelPropagator; + } + + /** + * {@inheritDoc} + */ + @Override + public void inject(Context context, C carrier, TraceContextSetter setter) { + if (isInitialized()) { + INJECT_INVOKER.invoke(otelPropagator, getOTelContext(context), carrier, Setter.toOTelSetter(setter)); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Context extract(Context context, C carrier, TraceContextGetter getter) { + if (isInitialized()) { + Object updatedContext + = EXTRACT_INVOKER.invoke(otelPropagator, getOTelContext(context), carrier, Getter.toOTelGetter(getter)); + if (updatedContext != null) { + return context.put(TRACE_CONTEXT_KEY, updatedContext); + } + } + return context; + } + + private boolean isInitialized() { + return otelPropagator != null && OTelInitializer.isInitialized(); + } + + private static final class Setter implements InvocationHandler { + private static final Class[] INTERFACES = new Class[] { TEXT_MAP_SETTER_CLASS }; + private static final Map, Object> PROXIES + = new java.util.concurrent.ConcurrentHashMap<>(); + private final TraceContextSetter setter; + + static Object toOTelSetter(TraceContextSetter setter) { + return PROXIES.computeIfAbsent(setter, + s -> Proxy.newProxyInstance(TEXT_MAP_SETTER_CLASS.getClassLoader(), INTERFACES, new Setter<>(s))); + } + + private Setter(TraceContextSetter setter) { + this.setter = setter; + } + + @Override + @SuppressWarnings("unchecked") + public Object invoke(Object proxy, Method method, Object[] args) { + if ("set".equals(method.getName())) { + assert args.length == 3; + setter.set((C) args[0], (String) args[1], (String) args[2]); + } + + return null; + } + } + + private static final class Getter implements InvocationHandler { + private static final Class[] INTERFACES = new Class[] { TEXT_MAP_GETTER_CLASS }; + private static final Map, Object> PROXIES + = new java.util.concurrent.ConcurrentHashMap<>(); + private final TraceContextGetter getter; + + static Object toOTelGetter(TraceContextGetter getter) { + return PROXIES.computeIfAbsent(getter, + g -> Proxy.newProxyInstance(TEXT_MAP_GETTER_CLASS.getClassLoader(), INTERFACES, new Getter<>(g))); + } + + private Getter(TraceContextGetter getter) { + this.getter = getter; + } + + @Override + @SuppressWarnings("unchecked") + public Object invoke(Object proxy, Method method, Object[] args) { + if ("get".equals(method.getName())) { + assert args.length == 2; + return getter.get((C) args[0], (String) args[1]); + } + + if ("keys".equals(method.getName())) { + assert args.length == 1; + return getter.keys((C) args[0]); + } + + return null; + } + } +} 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 new file mode 100644 index 000000000000..15948ab3005b --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelTracer.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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.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 static io.clientcore.core.implementation.ReflectionUtils.getMethodInvoker; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACER_BUILDER_CLASS; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACER_CLASS; +import static io.clientcore.core.implementation.instrumentation.otel.OTelInitializer.TRACER_PROVIDER_CLASS; + +/** + * OpenTelemetry implementation of {@link Tracer}. + */ +public final class OTelTracer implements Tracer { + public static final OTelTracer NOOP = new OTelTracer(); + private static final ClientLogger LOGGER = new ClientLogger(OTelTracer.class); + private static final FallbackInvoker SPAN_BUILDER_INVOKER; + private static final FallbackInvoker SET_INSTRUMENTATION_VERSION_INVOKER; + private static final FallbackInvoker BUILD_INVOKER; + private static final FallbackInvoker SET_SCHEMA_URL_INVOKER; + private static final FallbackInvoker GET_TRACER_BUILDER_INVOKER; + + private final Object otelTracer; + private final LibraryInstrumentationOptions libraryOptions; + + static { + ReflectiveInvoker spanBuilderInvoker = null; + ReflectiveInvoker setInstrumentationVersionInvoker = null; + ReflectiveInvoker buildInvoker = null; + ReflectiveInvoker setSchemaUrlInvoker = null; + ReflectiveInvoker getTracerBuilderInvoker = null; + + if (OTelInitializer.isInitialized()) { + try { + spanBuilderInvoker + = getMethodInvoker(TRACER_CLASS, TRACER_CLASS.getMethod("spanBuilder", String.class)); + + setInstrumentationVersionInvoker = getMethodInvoker(TRACER_BUILDER_CLASS, + TRACER_BUILDER_CLASS.getMethod("setInstrumentationVersion", String.class)); + + setSchemaUrlInvoker = getMethodInvoker(TRACER_BUILDER_CLASS, + TRACER_BUILDER_CLASS.getMethod("setSchemaUrl", String.class)); + + buildInvoker = getMethodInvoker(TRACER_BUILDER_CLASS, TRACER_BUILDER_CLASS.getMethod("build")); + + getTracerBuilderInvoker = getMethodInvoker(TRACER_PROVIDER_CLASS, + TRACER_PROVIDER_CLASS.getMethod("tracerBuilder", String.class)); + } catch (Throwable t) { + OTelInitializer.initError(LOGGER, t); + } + } + + SPAN_BUILDER_INVOKER = new FallbackInvoker(spanBuilderInvoker, LOGGER); + SET_INSTRUMENTATION_VERSION_INVOKER = new FallbackInvoker(setInstrumentationVersionInvoker, LOGGER); + SET_SCHEMA_URL_INVOKER = new FallbackInvoker(setSchemaUrlInvoker, LOGGER); + BUILD_INVOKER = new FallbackInvoker(buildInvoker, LOGGER); + GET_TRACER_BUILDER_INVOKER = new FallbackInvoker(getTracerBuilderInvoker, LOGGER); + } + + private OTelTracer() { + this.otelTracer = null; + this.libraryOptions = null; + } + + /** + * Creates a new instance of {@link OTelTracer}. + * @param otelTracerProvider the OpenTelemetry tracer provider + * @param libraryOptions the library options + */ + public OTelTracer(Object otelTracerProvider, LibraryInstrumentationOptions libraryOptions) { + Object tracerBuilder = GET_TRACER_BUILDER_INVOKER.invoke(otelTracerProvider, libraryOptions.getLibraryName()); + if (tracerBuilder != null) { + SET_INSTRUMENTATION_VERSION_INVOKER.invoke(tracerBuilder, libraryOptions.getLibraryVersion()); + SET_SCHEMA_URL_INVOKER.invoke(tracerBuilder, libraryOptions.getSchemaUrl()); + this.otelTracer = BUILD_INVOKER.invoke(tracerBuilder); + } else { + this.otelTracer = null; + } + this.libraryOptions = libraryOptions; + } + + /** + * {@inheritDoc} + */ + @Override + public SpanBuilder spanBuilder(String spanName, SpanKind spanKind, RequestOptions options) { + 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 OTelSpanBuilder.NOOP; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEnabled() { + return otelTracer != null && OTelInitializer.isInitialized(); + } +} 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 new file mode 100644 index 000000000000..1857512b82b4 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/OTelUtils.java @@ -0,0 +1,42 @@ +// 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/instrumentation/otel/tracing/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/package-info.java new file mode 100644 index 000000000000..29c2e1da2660 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/otel/tracing/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the implementation of the OpenTelemetry tracing. + */ +package io.clientcore.core.implementation.instrumentation.otel.tracing; diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/package-info.java new file mode 100644 index 000000000000..826d039b3967 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/instrumentation/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This package contains the optional OpenTelemetry implementation of the telemetry API. + */ +package io.clientcore.core.implementation.instrumentation; 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/util/Slf4jLoggerShim.java index b518f6c33f37..8ecd69e34f32 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/util/Slf4jLoggerShim.java @@ -185,22 +185,22 @@ public void performLogging(ClientLogger.LogLevel logLevel, String message, Throw try { switch (logLevel) { case VERBOSE: - LOGGER_VERBOSE.invokeWithArguments(localSlf4jLogger, message, throwable); + LOGGER_VERBOSE.invoke(localSlf4jLogger, message, throwable); break; case INFORMATIONAL: - LOGGER_INFO.invokeWithArguments(localSlf4jLogger, message, throwable); + LOGGER_INFO.invoke(localSlf4jLogger, message, throwable); break; case WARNING: - LOGGER_WARN.invokeWithArguments(localSlf4jLogger, message, throwable); + LOGGER_WARN.invoke(localSlf4jLogger, message, throwable); break; case ERROR: - LOGGER_ERROR.invokeWithArguments(localSlf4jLogger, message, throwable); + LOGGER_ERROR.invoke(localSlf4jLogger, message, throwable); break; 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 new file mode 100644 index 000000000000..a5ca91ac6e09 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/Instrumentation.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation; + +import io.clientcore.core.implementation.instrumentation.otel.OTelInitializer; +import io.clientcore.core.implementation.instrumentation.otel.OTelInstrumentation; +import io.clientcore.core.instrumentation.tracing.TraceContextPropagator; +import io.clientcore.core.instrumentation.tracing.Tracer; + +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. + *

+ * Tracer lifetime should usually match the client lifetime. Avoid creating new tracers for each request. + * + *

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

+ * + * + *
+     *
+     * LibraryInstrumentationOptions libraryOptions = new LibraryInstrumentationOptions("sample")
+     *     .setLibraryVersion("1.0.0")
+     *     .setSchemaUrl("https://opentelemetry.io/schemas/1.29.0");
+     *
+     * InstrumentationOptions<?> instrumentationOptions = new InstrumentationOptions<>();
+     *
+     * Tracer tracer = Instrumentation.create(instrumentationOptions, libraryOptions).getTracer();
+     *
+     * 
+ * + * + * @return The tracer. + */ + Tracer getTracer(); + + /** + * Gets the implementation of W3C Trace Context propagator. + * + * @return The context propagator. + */ + TraceContextPropagator getW3CTraceContextPropagator(); + + /** + * Gets the singleton instance of the resolved telemetry provider. + * + * @param applicationOptions Telemetry collection options provided by the application. + * @param libraryOptions Library-specific telemetry collection options. + * @return The instance of telemetry provider implementation. + */ + static Instrumentation create(InstrumentationOptions applicationOptions, + LibraryInstrumentationOptions libraryOptions) { + Objects.requireNonNull(libraryOptions, "'libraryOptions' cannot be null"); + if (OTelInitializer.isInitialized()) { + return new OTelInstrumentation(applicationOptions, libraryOptions); + } else { + return NOOP_PROVIDER; + } + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationOptions.java new file mode 100644 index 000000000000..230f6068ce4a --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/InstrumentationOptions.java @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation; + +import io.clientcore.core.http.pipeline.HttpInstrumentationPolicy; + +/** + * Telemetry options describe application-level configuration and can be configured on specific + * client instances via the corresponding client builder. + *

+ * + * Library should use them on all instance of {@link io.clientcore.core.instrumentation.tracing.Tracer} + * it creates and, if it sets up {@link HttpInstrumentationPolicy}, it should pass + * {@link InstrumentationOptions} to the policy. + * + * @param The type of the provider. Only {@code io.opentelemetry.api.OpenTelemetry} is supported. + */ +public class InstrumentationOptions { + private boolean isTracingEnabled = true; + private T provider = null; + + /** + * Enables or disables distributed tracing. Distributed tracing is enabled by default when + * OpenTelemetry is found on the classpath and is configured to export traces. + * + *

Disable distributed tracing on a specific client instance

+ * + * + *
+     *
+     * InstrumentationOptions<?> instrumentationOptions = new InstrumentationOptions<>()
+     *     .setTracingEnabled(false);
+     *
+     * SampleClient client = new SampleClientBuilder().instrumentationOptions(instrumentationOptions).build();
+     * client.clientCall();
+     *
+     * 
+ * + * + * @param isTracingEnabled true to enable distributed tracing, false to disable. + * @return The updated {@link InstrumentationOptions} object. + */ + public InstrumentationOptions setTracingEnabled(boolean isTracingEnabled) { + this.isTracingEnabled = isTracingEnabled; + return this; + } + + /** + * Sets the provider to use for telemetry. Only {@code io.opentelemetry.api.OpenTelemetry} and + * derived classes are supported. + *

+ * + * When provider is not passed explicitly, clients will attempt to use global OpenTelemetry instance. + * + *

Pass configured OpenTelemetry instance explicitly

+ * + * + *
+     *
+     * OpenTelemetry openTelemetry =  AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk();
+     * InstrumentationOptions<OpenTelemetry> instrumentationOptions = new InstrumentationOptions<OpenTelemetry>()
+     *     .setProvider(openTelemetry);
+     *
+     * SampleClient client = new SampleClientBuilder().instrumentationOptions(instrumentationOptions).build();
+     * client.clientCall();
+     *
+     * 
+ * + * + * @param provider The provider to use for telemetry. + * @return The updated {@link InstrumentationOptions} object. + */ + public InstrumentationOptions setProvider(T provider) { + this.provider = provider; + return this; + } + + /** + * Returns true if distributed tracing is enabled, false otherwise. + * + * @return true if distributed tracing is enabled, false otherwise. + */ + public boolean isTracingEnabled() { + return isTracingEnabled; + } + + /** + * Returns the telemetry provider. + * + * @return The telemetry provider instance. + */ + public T getProvider() { + return provider; + } + + /** + * Creates an instance of {@link InstrumentationOptions}. + */ + public InstrumentationOptions() { + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/LibraryInstrumentationOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/LibraryInstrumentationOptions.java new file mode 100644 index 000000000000..d7e4393a4306 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/LibraryInstrumentationOptions.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation; + +import io.clientcore.core.implementation.instrumentation.LibraryInstrumentationOptionsAccessHelper; + +import java.util.Objects; + +/** + * Options for configuring library-specific telemetry settings. + * + *

This class is intended to be used by the client libraries only. Library options must not be provided or modified + * by application code

+ * + * Library options describe the client library - it's name, version, and schema URL. + * Schema URL describes telemetry schema and version. + *

+ * If your client library adds any attributes (links, events, etc.) to the spans, + * these properties SHOULD follow specific version of OpenTelemetry Semantic Conventions. + * And provide the corresponding schema URL. + *

+ * The {@link LibraryInstrumentationOptions} are usually static and shared across all instances of the client. + * Application developers are not expected to change them. + */ +public final class LibraryInstrumentationOptions { + private final String libraryName; + private String libraryVersion; + private String schemaUrl; + private boolean disableSpanSuppression; + + static { + LibraryInstrumentationOptionsAccessHelper + .setAccessor(new LibraryInstrumentationOptionsAccessHelper.LibraryInstrumentationOptionsAccessor() { + @Override + public LibraryInstrumentationOptions disableSpanSuppression(LibraryInstrumentationOptions options) { + return options.disableSpanSuppression(true); + } + + @Override + public boolean isSpanSuppressionDisabled(LibraryInstrumentationOptions options) { + return options.isSpanSuppressionDisabled(); + } + }); + } + + /** + * Creates an instance of {@link LibraryInstrumentationOptions}. + * + * @param libraryName The client library name. + */ + public LibraryInstrumentationOptions(String libraryName) { + this.libraryName = Objects.requireNonNull(libraryName, "'libraryName' cannot be null."); + } + + /** + * Sets the client library version. + * + * @param libraryVersion The client library version. + * @return The updated {@link LibraryInstrumentationOptions} object. + */ + public LibraryInstrumentationOptions setLibraryVersion(String libraryVersion) { + this.libraryVersion = libraryVersion; + return this; + } + + /** + * Sets the schema URL describing specific schema and version of the telemetry + * the library emits. + * + * @param schemaUrl The schema URL. + * @return The updated {@link LibraryInstrumentationOptions} object. + */ + public LibraryInstrumentationOptions setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + return this; + } + + /** + * Gets the client library name. + * + * @return The client library name. + */ + public String getLibraryName() { + return libraryName; + } + + /** + * Gets the client library version. + * + * @return The client library version. + */ + public String getLibraryVersion() { + return libraryVersion; + } + + /** + * Gets the schema URL describing specific schema and version of the telemetry + * the library emits. + * + * @return The schema URL. + */ + public String getSchemaUrl() { + return schemaUrl; + } + + LibraryInstrumentationOptions disableSpanSuppression(boolean disableSpanSuppression) { + this.disableSpanSuppression = disableSpanSuppression; + return this; + } + + boolean isSpanSuppressionDisabled() { + return disableSpanSuppression; + } +} 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 new file mode 100644 index 000000000000..53205494031c --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/NoopInstrumentation.java @@ -0,0 +1,86 @@ +// 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/instrumentation/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/package-info.java new file mode 100644 index 000000000000..61f58befec5a --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/package-info.java @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Package containing core observability primitives and configuration options. + *

+ * + * These primitives are used by the client libraries to emit distributed traces and correlate logs. + * Instrumentation is not operational without proper configuration of the OpenTelemetry SDK. + *

+ * + * Application developers who want to consume traces created by the client libraries should + * use OpenTelemetry-compatible java agent or configure the OpenTelemetry SDK. + *

+ * + * Follow the https://opentelemetry.io/docs/languages/java/configuration/ for more details. + *

+ * + * Client libraries auto-discover global OpenTelemetry SDK instance configured by the java agent or + * in the application code. Just create a client instance as usual as shown in the following code snippet: + * + *

Clients auto-discover global OpenTelemetry

+ * + * + *
+ *
+ * AutoConfiguredOpenTelemetrySdk.initialize();
+ *
+ * SampleClient client = new SampleClientBuilder().build();
+ * client.clientCall();
+ *
+ * 
+ * + *

+ * + * Alternatively, application developers can pass OpenTelemetry SDK instance explicitly to the client libraries. + * + *

Pass configured OpenTelemetry instance explicitly

+ * + * + *
+ *
+ * OpenTelemetry openTelemetry =  AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk();
+ * InstrumentationOptions<OpenTelemetry> instrumentationOptions = new InstrumentationOptions<OpenTelemetry>()
+ *     .setProvider(openTelemetry);
+ *
+ * SampleClient client = new SampleClientBuilder().instrumentationOptions(instrumentationOptions).build();
+ * client.clientCall();
+ *
+ * 
+ * + *

+ * + * To correlate application and client library telemetry, application developers should + * leverage implicit context propagation feature of OpenTelemetry API: + * + *

Make application spans current to correlate them with library telemetry

+ * + * + *
+ *
+ * Tracer tracer = GlobalOpenTelemetry.getTracer("sample");
+ * Span span = tracer.spanBuilder("my-operation")
+ *     .startSpan();
+ * SampleClient client = new SampleClientBuilder().build();
+ *
+ * try (Scope scope = span.makeCurrent()) {
+ *     // Client library will create span for the clientCall operation
+ *     // and will use current span (my-operation) as a parent.
+ *     client.clientCall();
+ * } finally {
+ *     span.end();
+ * }
+ *
+ * 
+ * + *

+ * + * Implicit context propagation works best in synchronous code. Implicit context propagation may not work in + * asynchronous scenarios depending on the async framework used by the application, implementation details, + * and OpenTelemetry instrumentation's used. + * + * When writing asynchronous code, it's recommended to use explicit context propagation. + * + *

Pass context explicitly to correlate them with library telemetry in async code

+ * + * + *
+ *
+ * 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()
+ *     .setContext(io.clientcore.core.util.Context.of(TRACE_CONTEXT_KEY, Context.current().with(span)));
+ *
+ * // run on another thread
+ * client.clientCall(options);
+ *
+ * 
+ * + */ +package io.clientcore.core.instrumentation; 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 new file mode 100644 index 000000000000..1a4ea4143515 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Span.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.tracing; + +/** + * 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

+ */ +public interface Span { + /** + * Sets an attribute on the span. + *

+ * When adding attributes, make sure to follow OpenTelemetry semantic conventions + * + *

+ * You may optionally guard this call with {@link #isRecording()} to avoid unnecessary work + * if the span is not sampled. + * + * @param key The key of the attribute. + * @param value The value of the attribute. Only {@link Boolean}, {@link String}, {@link Long}, {@link Integer}, + * and {@link Double} are supported. + * @return The updated {@link Span} object. + * + * @see SpanBuilder#setAttribute(String, Object) + */ + Span setAttribute(String key, Object value); + + /** + * Sets an error on the span. + * + * @param errorType The error type to set on the span. + * @return The updated {@link Span} object. + * + * @see #end(Throwable) + */ + Span setError(String errorType); + + /** + * Ends the span with exception. This should match the exception (or its cause) + * that will be thrown to the application code. + *

+ * Exceptions handled by the client library should not be passed to this method. + *

+ * + * It is important to record any exceptions that are about to be thrown + * to the user code including unchecked ones. + * @param throwable The exception to set on the span. + */ + void end(Throwable throwable); + + /** + * Ends the span. + *

+ * This method may be called multiple times. + */ + void end(); + + /** + * Checks if the span is recording. + * + * @return true if the span is recording, false otherwise. + */ + boolean isRecording(); + + /** + * Makes the context representing this span current. + *

+ * By making span current, we create a scope that's used to correlate all other telemetry reported under it + * such as other spans, logs, or metrics exemplars. + *

+ * The scope MUST be closed on the same thread that created it. + *

+ * Closing the scope does not end the span. + * + * @return The {@link TracingScope} object. + */ + TracingScope makeCurrent(); +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/SpanBuilder.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/SpanBuilder.java new file mode 100644 index 000000000000..0671a2d31721 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/SpanBuilder.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.tracing; + +/** + * Represents a span builder. + *

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

+ */ +public interface SpanBuilder { + + /** + * Sets attribute value under provided key. + *

+ * Attributes added on span builder are used to make sampling decisions, + * and if the span is sampled, they are added to the resulting span. + *

+ * When adding attributes, make sure to follow OpenTelemetry semantic conventions + * + * + * @param key The attribute key. + * @param value The value of the attribute. Only {@link Boolean}, {@link String}, {@link Long}, {@link Integer}, + * and {@link Double} are supported. + * @return Updated {@link SpanBuilder} object. + */ + SpanBuilder setAttribute(String key, Object value); + + /** + * Starts the span. + * + * @return The started {@link Span} instance. + */ + Span startSpan(); +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/SpanKind.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/SpanKind.java new file mode 100644 index 000000000000..5d8b22abd01a --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/SpanKind.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.tracing; + +/** + * Represents the span kind. + *

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

+ */ +public enum SpanKind { + /** + * Indicates that the span is used internally. + */ + INTERNAL, + + /** + * Indicates that the span covers the client-side wrapper around an RPC or other remote request. + */ + CLIENT, + + /** + * Indicates that the span covers server-side handling of an RPC or other remote request. + */ + SERVER, + + /** + * Indicates that the span describes producer sending a message to a broker. Unlike client and server, there is no + * direct critical path latency relationship between producer and consumer spans. + */ + PRODUCER, + + /** + * Indicates that the span describes consumer receiving a message from a broker. Unlike client and server, there is + * no direct critical path latency relationship between producer and consumer spans. + */ + CONSUMER +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextGetter.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextGetter.java new file mode 100644 index 000000000000..dd106479865c --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextGetter.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.tracing; + +/** + * A {@code TextMapGetter} retrieves context fields from a carrier, such as {@link io.clientcore.core.http.models.HttpRequest}. + *

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

+ * + * @param the type of the carrier. + */ +public interface TraceContextGetter { + /** + * Returns all the keys in the given carrier. + * + * @param carrier carrier of propagation fields, such as http request. + * + * @return all the keys in the given carrier. + */ + Iterable keys(C carrier); + + /** + * Returns the first value of the given propagation {@code key} or returns {@code null}. + * + * @param carrier carrier of propagation fields, such as http request. + * @param key the key of the field. + * @return the first value of the given propagation {@code key} or returns {@code null}. + */ + String get(C carrier, String key); +} 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 new file mode 100644 index 000000000000..d5bffecb6039 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextPropagator.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.tracing; + +import io.clientcore.core.util.Context; + +/** + * A {@link TraceContextPropagator} injects and extracts tracing context from a carrier, + * such as {@link io.clientcore.core.http.models.HttpRequest}. + *

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

+ */ +public interface TraceContextPropagator { + /** + * Injects the context into the carrier. + * + * @param context The context to inject. + * @param carrier The carrier to inject the context into. + * @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); + + /** + * Extracts the context from the carrier. + * + * @param context Initial context. + * @param carrier The carrier to extract the context from. + * @param getter The getter to use to extract the context from the carrier. + * @param The type of the carrier. + * + * @return The extracted context. + */ + Context extract(Context context, C carrier, TraceContextGetter getter); +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextSetter.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextSetter.java new file mode 100644 index 000000000000..2003b6fe1fa0 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TraceContextSetter.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.tracing; + +/** + * A {@code TextMapSetter} sets context fields on a carrier, such as {@link io.clientcore.core.http.models.HttpRequest}. + *

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

+ * + * @param the type of the carrier. + */ +public interface TraceContextSetter { + /** + * Sets the context property on the carrier. + * + * @param carrier The carrier to set the context property on. + * @param key The key of the context property. + * @param value The value of the context property. + */ + void set(C carrier, String key, String value); +} 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 new file mode 100644 index 000000000000..3223d411e358 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/Tracer.java @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.tracing; + +import io.clientcore.core.http.models.RequestOptions; + +/** + * Represents a tracer - a component that creates spans. + *

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

+ */ +public interface Tracer { + /** + * Creates a new span builder. + * + *

Make sure to follow OpenTelemetry semantic conventions + *

+ * + *

Basic tracing instrumentation for a service method:

+ * + *
+     *
+     * Span span = tracer.spanBuilder("{operationName}", SpanKind.CLIENT, requestOptions)
+     *     .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);
+     * }
+     *
+     * try (TracingScope scope = span.makeCurrent()) {
+     *     clientCall(requestOptions);
+     * } catch (Throwable t) {
+     *     // make sure to report any exceptions including unchecked ones.
+     *     span.end(t);
+     *     throw t;
+     * } finally {
+     *     // NOTE: closing the scope does not end the span, span should be ended explicitly.
+     *     span.end();
+     * }
+     *
+     * 
+ * + * + *

Adding attributes to spans:

+ * + *
+     *
+     * Span sendSpan = tracer.spanBuilder("send {queue-name}", SpanKind.PRODUCER, requestOptions)
+     *     // 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")
+     *     .setAttribute("messaging.destination.name", "{queue-name}")
+     *     .setAttribute("messaging.operations.name", "send")
+     *     .startSpan();
+     *
+     * try (TracingScope scope = sendSpan.makeCurrent()) {
+     *     if (sendSpan.isRecording()) {
+     *         sendSpan.setAttribute("messaging.message.id", "{message-id}");
+     *     }
+     *
+     *     clientCall(requestOptions);
+     * } catch (Throwable t) {
+     *     sendSpan.end(t);
+     *     throw t;
+     * } finally {
+     *     sendSpan.end();
+     * }
+     *
+     * 
+ * + * + * @param spanName The name of the span. + * @param spanKind The kind of the span. + * @param requestOptions The request options. + * @return The span builder. + */ + SpanBuilder spanBuilder(String spanName, SpanKind spanKind, RequestOptions requestOptions); + + /** + * Checks if the tracer is enabled. + * + * @return true if the tracer is enabled, false otherwise. + */ + default boolean isEnabled() { + return false; + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TracingScope.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TracingScope.java new file mode 100644 index 000000000000..4b906c58162d --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/TracingScope.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation.tracing; + +/** + * An {@code AutoCloseable} scope that controls implicit tracing context lifetime. + *

+ * + * The scope MUST be closed. It also MUST be closed on the same thread it was created. + * + *

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

+ */ +@FunctionalInterface +public interface TracingScope extends AutoCloseable { + @Override + void close(); +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/package-info.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/package-info.java new file mode 100644 index 000000000000..5645bed45c18 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/instrumentation/tracing/package-info.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Package containing core tracing primitives to be used by client libraries. + *

+ * + * Classes in this package are intended to be used by client libraries only. Application developers + * should use OpenTelemetry API directly + */ +package io.clientcore.core.instrumentation.tracing; diff --git a/sdk/clientcore/core/src/main/java/module-info.java b/sdk/clientcore/core/src/main/java/module-info.java index 37fb7355c4f7..36127c7b6cd6 100644 --- a/sdk/clientcore/core/src/main/java/module-info.java +++ b/sdk/clientcore/core/src/main/java/module-info.java @@ -26,6 +26,8 @@ exports io.clientcore.core.util.configuration; exports io.clientcore.core.util.serializer; exports io.clientcore.core.util.auth; + exports io.clientcore.core.instrumentation; + exports io.clientcore.core.instrumentation.tracing; uses io.clientcore.core.http.client.HttpClientProvider; diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java new file mode 100644 index 000000000000..0fc62912cf21 --- /dev/null +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/instrumentation/TracingForLibraryDevelopersJavaDocCodeSnippets.java @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation; + +import io.clientcore.core.http.models.HttpHeaderName; +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; +import io.clientcore.core.http.pipeline.HttpRetryPolicy; +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 static io.clientcore.core.instrumentation.Instrumentation.TRACE_CONTEXT_KEY; + +/** + * THESE CODE SNIPPETS ARE INTENDED FOR CLIENT LIBRARY DEVELOPERS ONLY. + *

+ * + * Application developers are expected to use OpenTelemetry API directly. + * Check out {@code TelemetryJavaDocCodeSnippets} for application-level samples. + */ +public class TracingForLibraryDevelopersJavaDocCodeSnippets { + private static final LibraryInstrumentationOptions LIBRARY_OPTIONS = new LibraryInstrumentationOptions("sample") + .setLibraryVersion("1.0.0") + .setSchemaUrl("https://opentelemetry.io/schemas/1.29.0"); + private static final HttpHeaderName CUSTOM_REQUEST_ID = HttpHeaderName.fromString("custom-request-id"); + + public void createTracer() { + + // BEGIN: io.clientcore.core.telemetry.tracing.createtracer + + LibraryInstrumentationOptions libraryOptions = new LibraryInstrumentationOptions("sample") + .setLibraryVersion("1.0.0") + .setSchemaUrl("https://opentelemetry.io/schemas/1.29.0"); + + InstrumentationOptions instrumentationOptions = new InstrumentationOptions<>(); + + Tracer tracer = Instrumentation.create(instrumentationOptions, libraryOptions).getTracer(); + + // END: io.clientcore.core.telemetry.tracing.createtracer + } + + /** + * This example shows minimal distributed tracing instrumentation. + */ + public void traceCall() { + + Tracer tracer = Instrumentation.create(null, LIBRARY_OPTIONS).getTracer(); + RequestOptions requestOptions = null; + + // BEGIN: io.clientcore.core.telemetry.tracing.tracecall + + Span span = tracer.spanBuilder("{operationName}", SpanKind.CLIENT, requestOptions) + .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); + } + + try (TracingScope scope = span.makeCurrent()) { + clientCall(requestOptions); + } catch (Throwable t) { + // make sure to report any exceptions including unchecked ones. + span.end(t); + throw t; + } finally { + // NOTE: closing the scope does not end the span, span should be ended explicitly. + span.end(); + } + + // END: io.clientcore.core.telemetry.tracing.tracecall + } + + /** + * This example shows full distributed tracing instrumentation that adds attributes. + */ + public void traceWithAttributes() { + + Tracer tracer = Instrumentation.create(null, LIBRARY_OPTIONS).getTracer(); + RequestOptions requestOptions = null; + + // BEGIN: io.clientcore.core.telemetry.tracing.tracewithattributes + + Span sendSpan = tracer.spanBuilder("send {queue-name}", SpanKind.PRODUCER, requestOptions) + // 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") + .setAttribute("messaging.destination.name", "{queue-name}") + .setAttribute("messaging.operations.name", "send") + .startSpan(); + + try (TracingScope scope = sendSpan.makeCurrent()) { + if (sendSpan.isRecording()) { + sendSpan.setAttribute("messaging.message.id", "{message-id}"); + } + + clientCall(requestOptions); + } catch (Throwable t) { + sendSpan.end(t); + throw t; + } finally { + sendSpan.end(); + } + + // END: io.clientcore.core.telemetry.tracing.tracewithattributes + } + + public void configureInstrumentationPolicy() { + InstrumentationOptions instrumentationOptions = new InstrumentationOptions<>(); + HttpLogOptions logOptions = new HttpLogOptions(); + + // BEGIN: io.clientcore.core.telemetry.tracing.instrumentationpolicy + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies( + new HttpRetryPolicy(), + new HttpInstrumentationPolicy(instrumentationOptions, logOptions), + new HttpLoggingPolicy(logOptions)) + .build(); + + // END: io.clientcore.core.telemetry.tracing.instrumentationpolicy + } + + public void customizeInstrumentationPolicy() { + InstrumentationOptions instrumentationOptions = new InstrumentationOptions<>(); + + // BEGIN: io.clientcore.core.telemetry.tracing.customizeinstrumentationpolicy + + // You can configure URL sanitization to include additional query parameters to preserve + // in `url.full` attribute. + HttpLogOptions logOptions = new HttpLogOptions(); + logOptions.addAllowedQueryParamName("documentId"); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies( + new HttpRetryPolicy(), + new HttpInstrumentationPolicy(instrumentationOptions, logOptions), + new HttpLoggingPolicy(logOptions)) + .build(); + + // END: io.clientcore.core.telemetry.tracing.customizeinstrumentationpolicy + } + + public void enrichInstrumentationPolicySpans() { + InstrumentationOptions instrumentationOptions = new InstrumentationOptions<>(); + HttpLogOptions logOptions = new HttpLogOptions(); + + // 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)); + } + + return next.process(); + }; + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies( + new HttpRetryPolicy(), + new HttpInstrumentationPolicy(instrumentationOptions, logOptions), + enrichingPolicy, + new HttpLoggingPolicy(logOptions)) + .build(); + + + // END: io.clientcore.core.telemetry.tracing.enrichhttpspans + } + + private void clientCall(RequestOptions options) { + } +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyNoopTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyNoopTests.java new file mode 100644 index 000000000000..38f13b79d5a4 --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyNoopTests.java @@ -0,0 +1,55 @@ +// 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.HttpHeaderName; +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.instrumentation.InstrumentationOptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class HttpInstrumentationPolicyNoopTests { + private static final InstrumentationOptions OPTIONS = new InstrumentationOptions<>(); + 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)) + .build(); + + // should not throw + try (Response response = pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost/"))) { + assertEquals(statusCode, response.getStatusCode()); + assertNull(response.getRequest().getHeaders().get(TRACESTATE)); + assertNull(response.getRequest().getHeaders().get(TRACEPARENT)); + } + } + + @Test + public void exceptionTracingDisabled() { + SocketException exception = new SocketException("test exception"); + HttpPipeline pipeline + = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(OPTIONS, null)).httpClient(request -> { + throw exception; + }).build(); + + assertThrows(UncheckedIOException.class, + () -> pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost/")).close()); + } +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/util/ClientLoggerTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/util/ClientLoggerTests.java index 80718c864420..7375c46827ed 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/util/ClientLoggerTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/util/ClientLoggerTests.java @@ -810,6 +810,7 @@ private void assertMessage(Map expectedMessage, String fullLog, LogLevel loggedLevel) { if (loggedLevel.compareTo(configuredLevel) >= 0) { // remove date/time/level/etc from fullMessage + String messageJson = fullLog.substring(fullLog.indexOf(" - ") + 3); System.out.println(messageJson); Map message = fromJson(messageJson); 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 6f365d14bb95..0aeaf6197400 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 @@ -101,7 +101,7 @@ public static void init() { AutoConfiguredOpenTelemetrySdkBuilder sdkBuilder = AutoConfiguredOpenTelemetrySdk.builder(); String applicationInsightsConnectionString = System.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"); if (applicationInsightsConnectionString != null) { - AzureMonitorExporter.customize(AutoConfiguredOpenTelemetrySdk.builder(), + AzureMonitorExporter.customize(sdkBuilder, new AzureMonitorExporterOptions().connectionString(applicationInsightsConnectionString)); } else { System.setProperty("otel.traces.exporter", "none"); diff --git a/sdk/clientcore/optional-dependency-tests/CHANGELOG.md b/sdk/clientcore/optional-dependency-tests/CHANGELOG.md new file mode 100644 index 000000000000..4144f75694a0 --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/CHANGELOG.md @@ -0,0 +1,3 @@ +# Release History + +## 1.0.0-beta.1 (Unreleased) diff --git a/sdk/clientcore/optional-dependency-tests/README.md b/sdk/clientcore/optional-dependency-tests/README.md new file mode 100644 index 000000000000..f8d099bea9ea --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/README.md @@ -0,0 +1,19 @@ +# Core Tests shared library for Java + +Tests that validate optional dependencies and features of the Core library. + +## Getting started + +## Key concepts + +## Examples + +## Troubleshooting + +## Next steps + +## Contributing + + + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-java%2Fsdk%clientcore%2Foptional-dependency-tests%2FREADME.png) diff --git a/sdk/clientcore/optional-dependency-tests/checkstyle-suppressions.xml b/sdk/clientcore/optional-dependency-tests/checkstyle-suppressions.xml new file mode 100644 index 000000000000..d262da508eb7 --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/checkstyle-suppressions.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/sdk/clientcore/optional-dependency-tests/pom.xml b/sdk/clientcore/optional-dependency-tests/pom.xml new file mode 100644 index 000000000000..00acb3e5820c --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/pom.xml @@ -0,0 +1,147 @@ + + + 4.0.0 + + io.clientcore + clientcore-parent + 1.0.0-beta.1 + ../../parents/clientcore-parent + + + io.clientcore + optional-dependency-tests + jar + 1.0.0-beta.1 + + Java Core library tests for optional dependencies and features. + Tests that validate optional dependencies and features of the Core library. + https://github.com/Azure/azure-sdk-for-java + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + https://github.com/Azure/azure-sdk-for-java + scm:git:https://github.com/Azure/azure-sdk-for-java.git + scm:git:https://github.com/Azure/azure-sdk-for-java.git + + + + UTF-8 + 0.60 + 0.60 + true + + + **/generated/**/*.java + + + + + + io.clientcore.core.json,com.azure.json,com.azure.xml,com.azure.core* + + + + + + io.clientcore + core + 1.0.0-beta.2 + + + + + io.clientcore + core + 1.0.0-beta.2 + test-jar + test + + + org.slf4j + slf4j-simple + 2.0.16 + test + + + io.opentelemetry + opentelemetry-sdk + 1.43.0 + test + + + io.opentelemetry + opentelemetry-sdk-testing + 1.43.0 + test + + + io.opentelemetry + opentelemetry-sdk-extension-autoconfigure + 1.43.0 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.11.2 + test + + + org.junit.jupiter + junit-jupiter-params + 5.11.2 + test + + + org.openjdk.jmh + jmh-generator-annprocess + 1.37 + test + + + + + + jmh-benchmark + + + jmh-benchmark + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + + + org.openjdk.jmh + jmh-generator-annprocess + 1.37 + + + + + + + + + diff --git a/sdk/clientcore/optional-dependency-tests/spotbugs-exclude.xml b/sdk/clientcore/optional-dependency-tests/spotbugs-exclude.xml new file mode 100644 index 000000000000..486f51077fd9 --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/spotbugs-exclude.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000000..59f801018231 --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/src/samples/java/io/clientcore/core/instrumentation/TelemetryJavaDocCodeSnippets.java @@ -0,0 +1,209 @@ +// 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.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. + *

+ * + * It can be done by + * 1. providing javaagent based on OpenTelemetry + * 2. setting configured OpenTelemetry SDK as global + * 3. setting up OpenTelemetry SDK and providing it to client libraries + * explicitly. + *

+ * + * Refer to OpenTelemetry documentation for + * the details on how to configure OpenTelemetry. + *

+ * + * Option 1 (javaagent) and Options 2 do not involve any code changes specific to + * client libraries which discover and use global OpenTelemetry instance. + *

+ * + * See {@link TelemetryJavaDocCodeSnippets#useGlobalOpenTelemetry()} for Option 2, + * {@link TelemetryJavaDocCodeSnippets#useExplicitOpenTelemetry()} for Option 3. + * + */ +public class TelemetryJavaDocCodeSnippets { + + /** + * This code snippet shows how to initialize global OpenTelemetry SDK + * and let client libraries discover it. + */ + public void useGlobalOpenTelemetry() { + // BEGIN: io.clientcore.core.telemetry.useglobalopentelemetry + + AutoConfiguredOpenTelemetrySdk.initialize(); + + SampleClient client = new SampleClientBuilder().build(); + client.clientCall(); + + // END: io.clientcore.core.telemetry.useglobalopentelemetry + } + + /** + * This code snippet shows how to pass OpenTelemetry SDK instance + * to client libraries explicitly. + */ + public void useExplicitOpenTelemetry() { + // BEGIN: io.clientcore.core.telemetry.useexplicitopentelemetry + + OpenTelemetry openTelemetry = AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk(); + InstrumentationOptions instrumentationOptions = new InstrumentationOptions() + .setProvider(openTelemetry); + + SampleClient client = new SampleClientBuilder().instrumentationOptions(instrumentationOptions).build(); + client.clientCall(); + + // END: io.clientcore.core.telemetry.useexplicitopentelemetry + } + + /** + * This code snippet shows how to disable distributed tracing + * for a specific instance of client. + */ + public void disableDistributedTracing() { + // BEGIN: io.clientcore.core.telemetry.disabledistributedtracing + + InstrumentationOptions instrumentationOptions = new InstrumentationOptions<>() + .setTracingEnabled(false); + + SampleClient client = new SampleClientBuilder().instrumentationOptions(instrumentationOptions).build(); + client.clientCall(); + + // END: io.clientcore.core.telemetry.disabledistributedtracing + } + + /** + * This code snippet shows how to correlate spans from + * client library with spans from application code + * using current context. + */ + public void correlationWithImplicitContext() { + // BEGIN: io.clientcore.core.telemetry.correlationwithimplicitcontext + + Tracer tracer = GlobalOpenTelemetry.getTracer("sample"); + Span span = tracer.spanBuilder("my-operation") + .startSpan(); + SampleClient client = new SampleClientBuilder().build(); + + try (Scope scope = span.makeCurrent()) { + // Client library will create span for the clientCall operation + // and will use current span (my-operation) as a parent. + client.clientCall(); + } finally { + span.end(); + } + + // END: io.clientcore.core.telemetry.correlationwithimplicitcontext + } + + /** + * This code snippet shows how to correlate spans from + * client library with spans from application code + * by passing context explicitly. + */ + public void correlationWithExplicitContext() { + // BEGIN: io.clientcore.core.telemetry.correlationwithexplicitcontext + + 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() + .setContext(io.clientcore.core.util.Context.of(TRACE_CONTEXT_KEY, Context.current().with(span))); + + // run on another thread + client.clientCall(options); + + // END: io.clientcore.core.telemetry.correlationwithexplicitcontext + } + + 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 InstrumentationPolicy(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) { + io.clientcore.core.instrumentation.tracing.Span span = tracer.spanBuilder("clientCall", SpanKind.CLIENT, options) + .startSpan(); + + if (options == null) { + options = new RequestOptions(); + } + + options.setContext(options.getContext().put(TRACE_CONTEXT_KEY, span)); + + 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/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 new file mode 100644 index 000000000000..f08b4c8411c2 --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/http/pipeline/HttpInstrumentationPolicyTests.java @@ -0,0 +1,522 @@ +// 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.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.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.LibraryInstrumentationOptions; +import io.clientcore.core.instrumentation.InstrumentationOptions; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.IdGenerator; +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; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.SocketException; +import java.net.URI; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; +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; +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; + +public class HttpInstrumentationPolicyTests { + private static final AttributeKey ERROR_TYPE = AttributeKey.stringKey("error.type"); + private static final AttributeKey HTTP_REQUEST_RESEND_COUNT + = AttributeKey.longKey("http.request.resend_count"); + private static final AttributeKey USER_AGENT_ORIGINAL = AttributeKey.stringKey("user_agent.original"); + private static final AttributeKey HTTP_REQUEST_METHOD = AttributeKey.stringKey("http.request.method"); + private static final AttributeKey URL_FULL = AttributeKey.stringKey("url.full"); + private static final AttributeKey SERVER_ADDRESS = AttributeKey.stringKey("server.address"); + private static final AttributeKey SERVER_PORT = AttributeKey.longKey("server.port"); + private static final AttributeKey HTTP_RESPONSE_STATUS_CODE + = AttributeKey.longKey("http.response.status_code"); + private static final HttpHeaderName TRACESTATE = HttpHeaderName.fromString("tracestate"); + private static final HttpHeaderName CUSTOM_REQUEST_ID = HttpHeaderName.fromString("custom-request-id"); + + private InMemorySpanExporter exporter; + private SdkTracerProvider tracerProvider; + private OpenTelemetry openTelemetry; + private InstrumentationOptions otelOptions; + + @BeforeEach + public void setUp() { + exporter = InMemorySpanExporter.create(); + tracerProvider = SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)).build(); + + openTelemetry = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); + otelOptions = new InstrumentationOptions().setProvider(openTelemetry); + } + + @AfterEach + public void tearDown() { + exporter.reset(); + tracerProvider.close(); + } + + @ParameterizedTest + @ValueSource(ints = { 200, 201, 206, 302 }) + public void simpleRequestIsRecorded(int statusCode) throws IOException { + AtomicReference current = new AtomicReference<>(); + + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) + .httpClient(request -> { + assertStartAttributes((ReadableSpan) Span.current(), request.getHttpMethod(), request.getUri()); + assertNull(request.getHeaders().get(TRACESTATE)); + assertEquals(traceparent(Span.current()), request.getHeaders().get(TRACEPARENT).getValue()); + current.set(Span.current()); + return new MockHttpResponse(request, statusCode); + }) + .build(); + + pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost/")).close(); + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(1, exporter.getFinishedSpanItems().size()); + + assertNotNull(current.get()); + SpanData exportedSpan = exporter.getFinishedSpanItems().get(0); + assertEquals(exportedSpan.getSpanId(), current.get().getSpanContext().getSpanId()); + assertEquals(exportedSpan.getTraceId(), current.get().getSpanContext().getTraceId()); + assertHttpSpan(exportedSpan, HttpMethod.GET, "https://localhost/", statusCode); + + assertNull(exportedSpan.getAttributes().get(HTTP_REQUEST_RESEND_COUNT)); + assertNull(exportedSpan.getAttributes().get(ERROR_TYPE)); + assertNull(exportedSpan.getAttributes().get(USER_AGENT_ORIGINAL)); + assertEquals(StatusCode.UNSET, exportedSpan.getStatus().getStatusCode()); + } + + @ParameterizedTest + @ValueSource(ints = { 400, 404, 500, 503 }) + public void errorResponseIsRecorded(int statusCode) throws IOException { + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) + .httpClient(request -> new MockHttpResponse(request, statusCode)) + .build(); + + pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost:8080/path/to/resource?query=param")).close(); + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(1, exporter.getFinishedSpanItems().size()); + + SpanData exportedSpan = exporter.getFinishedSpanItems().get(0); + assertHttpSpan(exportedSpan, HttpMethod.GET, "https://localhost:8080/path/to/resource?query=REDACTED", + statusCode); + assertNull(exportedSpan.getAttributes().get(HTTP_REQUEST_RESEND_COUNT)); + assertEquals(String.valueOf(statusCode), exportedSpan.getAttributes().get(ERROR_TYPE)); + assertEquals(StatusCode.ERROR, exportedSpan.getStatus().getStatusCode()); + assertEquals("", exportedSpan.getStatus().getDescription()); + } + + @SuppressWarnings("try") + @Test + public void tracingWithRetries() throws IOException { + Tracer testTracer = tracerProvider.get("test"); + Span testSpan = testTracer.spanBuilder("test").startSpan(); + try (Scope scope = testSpan.makeCurrent()) { + AtomicInteger count = new AtomicInteger(0); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(new HttpRetryPolicy(), new HttpInstrumentationPolicy(otelOptions, null)) + .httpClient(request -> { + assertEquals(traceparent(Span.current()), request.getHeaders().get(TRACEPARENT).getValue()); + if (count.getAndIncrement() == 0) { + throw new UnknownHostException("test exception"); + } else { + return new MockHttpResponse(request, 200); + } + }) + .build(); + + pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost:8080/path/to/resource?query=param")) + .close(); + + assertEquals(2, count.get()); + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(2, exporter.getFinishedSpanItems().size()); + + SpanData failedTry = exporter.getFinishedSpanItems().get(0); + assertHttpSpan(failedTry, HttpMethod.GET, "https://localhost:8080/path/to/resource?query=REDACTED", null); + assertNull(failedTry.getAttributes().get(HTTP_REQUEST_RESEND_COUNT)); + assertEquals(UnknownHostException.class.getCanonicalName(), failedTry.getAttributes().get(ERROR_TYPE)); + assertEquals(StatusCode.ERROR, failedTry.getStatus().getStatusCode()); + assertEquals("test exception", failedTry.getStatus().getDescription()); + + SpanData successfulTry = exporter.getFinishedSpanItems().get(1); + assertHttpSpan(successfulTry, HttpMethod.GET, "https://localhost:8080/path/to/resource?query=REDACTED", + 200); + assertEquals(1L, successfulTry.getAttributes().get(HTTP_REQUEST_RESEND_COUNT)); + assertNull(successfulTry.getAttributes().get(ERROR_TYPE)); + } finally { + testSpan.end(); + } + } + + @Test + public void unsampledSpan() throws IOException { + SdkTracerProvider sampleNone = SdkTracerProvider.builder() + .setSampler(Sampler.alwaysOff()) + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .build(); + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setTracerProvider(sampleNone).build(); + InstrumentationOptions otelOptions + = new InstrumentationOptions().setProvider(openTelemetry); + + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) + .httpClient(request -> { + assertTrue(Span.current().getSpanContext().isValid()); + assertEquals(traceparent(Span.current()), request.getHeaders().get(TRACEPARENT).getValue()); + return new MockHttpResponse(request, 200); + }) + .build(); + + pipeline.send(new HttpRequest(HttpMethod.GET, "http://localhost/")).close(); + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(0, exporter.getFinishedSpanItems().size()); + } + + @Test + @SuppressWarnings("try") + public void tracestateIsPropagated() throws IOException { + SpanContext parentContext + = SpanContext.create(IdGenerator.random().generateTraceId(), IdGenerator.random().generateSpanId(), + TraceFlags.getSampled(), TraceState.builder().put("key", "value").build()); + + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) + .httpClient(request -> { + assertEquals("key=value", request.getHeaders().get(TRACESTATE).getValue()); + assertEquals(traceparent(Span.current()), request.getHeaders().get(TRACEPARENT).getValue()); + return new MockHttpResponse(request, 200); + }) + .build(); + + try (Scope scope = Span.wrap(parentContext).makeCurrent()) { + pipeline.send(new HttpRequest(HttpMethod.GET, "http://localhost/")).close(); + } + + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(1, exporter.getFinishedSpanItems().size()); + } + + @Test + public void otelPropagatorIsIgnored() throws IOException { + TextMapPropagator testPropagator = new TextMapPropagator() { + @Override + public Collection fields() { + return Collections.singleton("foo"); + } + + @Override + public void inject(io.opentelemetry.context.Context context, C carrier, TextMapSetter setter) { + setter.set(carrier, "foo", "bar"); + } + + @Override + public io.opentelemetry.context.Context extract(io.opentelemetry.context.Context context, C carrier, + TextMapGetter getter) { + return context; + } + }; + + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .setPropagators(ContextPropagators.create(testPropagator)) + .build(); + + InstrumentationOptions otelOptions + = new InstrumentationOptions().setProvider(openTelemetry); + + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) + .httpClient(request -> { + assertEquals(traceparent(Span.current()), request.getHeaders().get(TRACEPARENT).getValue()); + return new MockHttpResponse(request, 200); + }) + .build(); + + pipeline.send(new HttpRequest(HttpMethod.GET, "http://localhost/")).close(); + } + + @Test + public void exceptionIsRecorded() { + SocketException exception = new SocketException("test exception"); + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) + .httpClient(request -> { + throw exception; + }) + .build(); + + assertThrows(UncheckedIOException.class, + () -> pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost/")).close()); + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(1, exporter.getFinishedSpanItems().size()); + + SpanData exportedSpan = exporter.getFinishedSpanItems().get(0); + assertHttpSpan(exportedSpan, HttpMethod.GET, "https://localhost/", null); + assertEquals(exception.getClass().getCanonicalName(), exportedSpan.getAttributes().get(ERROR_TYPE)); + assertEquals(StatusCode.ERROR, exportedSpan.getStatus().getStatusCode()); + assertEquals(exception.getMessage(), exportedSpan.getStatus().getDescription()); + } + + @Test + public void tracingIsDisabledOnInstance() throws IOException { + InstrumentationOptions options + = new InstrumentationOptions().setTracingEnabled(false).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/"); + pipeline.send(new HttpRequest(HttpMethod.GET, url)).close(); + assertNotNull(exporter.getFinishedSpanItems()); + 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)) + .httpClient(request -> new MockHttpResponse(request, 200)) + .build(); + + HttpRequest request = new HttpRequest(HttpMethod.GET, "https://localhost/"); + request.getHeaders().set(HttpHeaderName.USER_AGENT, "test-user-agent"); + pipeline.send(request).close(); + + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(1, exporter.getFinishedSpanItems().size()); + + SpanData exportedSpan = exporter.getFinishedSpanItems().get(0); + assertHttpSpan(exportedSpan, HttpMethod.GET, "https://localhost/", 200); + + assertEquals("test-user-agent", exportedSpan.getAttributes().get(USER_AGENT_ORIGINAL)); + } + + @Test + public void enrichSpans() throws IOException { + HttpLogOptions logOptions = new HttpLogOptions().setLogLevel(HttpLogOptions.HttpLogDetailLevel.HEADERS); + + 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)); + } + + return next.process(); + }; + + HttpPipeline pipeline = new HttpPipelineBuilder().policies(httpInstrumentationPolicy, enrichingPolicy) + .httpClient(request -> new MockHttpResponse(request, 200)) + .build(); + + HttpRequest request = new HttpRequest(HttpMethod.GET, "https://localhost/"); + request.getHeaders().set(CUSTOM_REQUEST_ID, "42"); + + pipeline.send(request).close(); + + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(1, exporter.getFinishedSpanItems().size()); + + SpanData exportedSpan = exporter.getFinishedSpanItems().get(0); + assertHttpSpan(exportedSpan, HttpMethod.GET, "https://localhost/", 200); + + assertEquals("42", exportedSpan.getAttributes().get(AttributeKey.stringKey("custom.request.id"))); + } + + @SuppressWarnings("try") + @Test + public void implicitParent() throws IOException { + Tracer testTracer = tracerProvider.get("test"); + Span testSpan = testTracer.spanBuilder("test").startSpan(); + + try (Scope scope = testSpan.makeCurrent()) { + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) + .httpClient(request -> new MockHttpResponse(request, 200)) + .build(); + + pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost:8080/path/to/resource?query=param")) + .close(); + } finally { + testSpan.end(); + } + + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(2, exporter.getFinishedSpanItems().size()); + + SpanData httpSpan = exporter.getFinishedSpanItems().get(0); + assertHttpSpan(httpSpan, HttpMethod.GET, "https://localhost:8080/path/to/resource?query=REDACTED", 200); + assertEquals(testSpan.getSpanContext().getSpanId(), httpSpan.getParentSpanContext().getSpanId()); + assertEquals(testSpan.getSpanContext().getTraceId(), httpSpan.getSpanContext().getTraceId()); + } + + @Test + public void explicitParent() throws IOException { + Tracer testTracer = tracerProvider.get("test"); + Span testSpan = testTracer.spanBuilder("test").startSpan(); + + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) + .httpClient(request -> new MockHttpResponse(request, 200)) + .build(); + + RequestOptions requestOptions = new RequestOptions().putContext(TRACE_CONTEXT_KEY, + io.opentelemetry.context.Context.current().with(testSpan)); + + pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost:8080/path/to/resource?query=param") + .setRequestOptions(requestOptions)).close(); + testSpan.end(); + + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(2, exporter.getFinishedSpanItems().size()); + + SpanData httpSpan = exporter.getFinishedSpanItems().get(0); + assertHttpSpan(httpSpan, HttpMethod.GET, "https://localhost:8080/path/to/resource?query=REDACTED", 200); + assertEquals(testSpan.getSpanContext().getSpanId(), httpSpan.getParentSpanContext().getSpanId()); + assertEquals(testSpan.getSpanContext().getTraceId(), httpSpan.getSpanContext().getTraceId()); + } + + @Test + public void customUrlRedaction() throws IOException { + HttpLogOptions logOptions = new HttpLogOptions().setAllowedQueryParamNames(Collections.singleton("key1")); + HttpPipeline pipeline + = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, logOptions)) + .httpClient(request -> new MockHttpResponse(request, 200)) + .build(); + + pipeline + .send(new HttpRequest(HttpMethod.GET, "https://localhost:8080/path/to/resource?query=param&key1=value1")) + .close(); + + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(1, exporter.getFinishedSpanItems().size()); + + SpanData httpSpan = exporter.getFinishedSpanItems().get(0); + assertHttpSpan(httpSpan, HttpMethod.GET, "https://localhost:8080/path/to/resource?query=REDACTED&key1=value1", + 200); + } + + @Test + public void explicitLibraryCallParent() throws IOException { + io.clientcore.core.instrumentation.tracing.Tracer tracer + = Instrumentation.create(otelOptions, new LibraryInstrumentationOptions("test-library")).getTracer(); + + RequestOptions requestOptions = new RequestOptions(); + io.clientcore.core.instrumentation.tracing.Span parent + = tracer.spanBuilder("parent", INTERNAL, requestOptions).startSpan(); + + requestOptions.putContext(TRACE_CONTEXT_KEY, parent); + + HttpPipeline pipeline = new HttpPipelineBuilder().policies(new HttpInstrumentationPolicy(otelOptions, null)) + .httpClient(request -> new MockHttpResponse(request, 200)) + .build(); + + pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost:8080/path/to/resource?query=param") + .setRequestOptions(requestOptions)).close(); + + parent.end(); + + assertNotNull(exporter.getFinishedSpanItems()); + assertEquals(2, exporter.getFinishedSpanItems().size()); + + SpanData httpSpan = exporter.getFinishedSpanItems().get(0); + assertHttpSpan(httpSpan, HttpMethod.GET, "https://localhost:8080/path/to/resource?query=REDACTED", 200); + + OTelSpanContext parentContext = ((OTelSpan) parent).getSpanContext(); + assertEquals(parentContext.getSpanId(), httpSpan.getParentSpanContext().getSpanId()); + assertEquals(parentContext.getTraceId(), httpSpan.getSpanContext().getTraceId()); + } + + private void assertStartAttributes(ReadableSpan span, HttpMethod method, URI url) { + assertEquals(url.toString(), span.getAttributes().get(URL_FULL)); + assertEquals(url.getHost(), span.getAttributes().get(SERVER_ADDRESS)); + assertEquals(url.getPort() == -1 ? 443L : url.getPort(), span.getAttributes().get(SERVER_PORT)); + assertEquals(method.toString(), span.getAttributes().get(HTTP_REQUEST_METHOD)); + } + + private void assertHttpSpan(SpanData span, HttpMethod method, String urlStr, Integer statusCode) { + URI url = URI.create(urlStr); + assertEquals(method.toString(), span.getName()); + assertEquals(url.toString(), span.getAttributes().get(URL_FULL)); + assertEquals(url.getHost(), span.getAttributes().get(SERVER_ADDRESS)); + assertEquals(url.getPort() == -1 ? 443L : url.getPort(), span.getAttributes().get(SERVER_PORT)); + assertEquals(method.toString(), span.getAttributes().get(HTTP_REQUEST_METHOD)); + if (statusCode != null) { + assertEquals(statusCode.longValue(), span.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + } else { + assertNull(span.getAttributes().get(HTTP_RESPONSE_STATUS_CODE)); + } + + assertEquals("core", span.getInstrumentationScopeInfo().getName()); + assertNotNull(span.getInstrumentationScopeInfo().getVersion()); + assertEquals("https://opentelemetry.io/schemas/1.29.0", span.getInstrumentationScopeInfo().getSchemaUrl()); + } + + private String traceparent(Span span) { + return String.format("00-%s-%s-%s", span.getSpanContext().getTraceId(), span.getSpanContext().getSpanId(), + span.getSpanContext().getTraceFlags()); + } +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/util/Slf4jLoggerShimIT.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/util/Slf4jLoggerShimIT.java similarity index 100% rename from sdk/clientcore/core/src/test/java/io/clientcore/core/implementation/util/Slf4jLoggerShimIT.java rename to sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/implementation/util/Slf4jLoggerShimIT.java 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 new file mode 100644 index 000000000000..4a61f5717dcf --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/ContextPropagationTests.java @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.IdGenerator; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +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; + +public class ContextPropagationTests { + private static final LibraryInstrumentationOptions DEFAULT_LIB_OPTIONS + = new LibraryInstrumentationOptions("test-library"); + 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); + } + }; + + private InMemorySpanExporter exporter; + private SdkTracerProvider tracerProvider; + private InstrumentationOptions otelOptions; + private Tracer tracer; + private TraceContextPropagator contextPropagator; + private Instrumentation instrumentation; + + @BeforeEach + public void setUp() { + exporter = InMemorySpanExporter.create(); + tracerProvider = SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)).build(); + + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); + otelOptions = new InstrumentationOptions().setProvider(openTelemetry); + instrumentation = Instrumentation.create(otelOptions, DEFAULT_LIB_OPTIONS); + tracer = instrumentation.getTracer(); + contextPropagator = instrumentation.getW3CTraceContextPropagator(); + } + + @AfterEach + public void tearDown() { + exporter.reset(); + tracerProvider.close(); + } + + @Test + 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); + + assertEquals(getTraceparent(span), carrier.get("traceparent")); + assertEquals(1, carrier.size()); + } + + @Test + public void testInjectReplaces() { + Span span = tracer.spanBuilder("test-span", INTERNAL, null).startSpan(); + + Map carrier = new HashMap<>(); + carrier.put("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); + contextPropagator.inject(Context.of(TRACE_CONTEXT_KEY, span), carrier, Map::put); + + assertEquals(getTraceparent(span), carrier.get("traceparent")); + assertEquals(1, carrier.size()); + } + + @Test + public void testInjectNoContext() { + Map carrier = new HashMap<>(); + contextPropagator.inject(Context.none(), carrier, Map::put); + + assertNull(carrier.get("traceparent")); + assertNull(carrier.get("tracestate")); + assertEquals(0, carrier.size()); + } + + @Test + public void testInjectWithTracestate() { + TraceState traceState = TraceState.builder().put("k1", "v1").put("k2", "v2").build(); + 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); + + assertEquals(getTraceparent(otelSpanContext), carrier.get("traceparent")); + assertEquals("k2=v2,k1=v1", carrier.get("tracestate")); + assertEquals(2, carrier.size()); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + 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); + + 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()); + } + + @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(); + assertFalse(extracted.isValid()); + } + + @Test + 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")); + } + + private String getTraceparent(Span span) { + OTelSpanContext spanContext = ((OTelSpan) span).getSpanContext(); + return "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-01"; + } + + private String getTraceparent(SpanContext spanContext) { + return "00-" + spanContext.getTraceId() + "-" + spanContext.getSpanId() + "-01"; + } +} 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 new file mode 100644 index 000000000000..308f05dac42d --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/InstrumentationTests.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation; + +import io.clientcore.core.instrumentation.tracing.Tracer; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.Isolated; + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Isolated +@Execution(ExecutionMode.SAME_THREAD) +public class InstrumentationTests { + private static final LibraryInstrumentationOptions DEFAULT_LIB_OPTIONS + = new LibraryInstrumentationOptions("test-library"); + private InMemorySpanExporter exporter; + private SdkTracerProvider tracerProvider; + + @BeforeEach + public void setup() { + GlobalOpenTelemetry.resetForTest(); + exporter = InMemorySpanExporter.create(); + tracerProvider = SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)).build(); + } + + @AfterEach + public void teardown() { + GlobalOpenTelemetry.resetForTest(); + } + + @Test + public void createTracerOTelNotConfigured() { + Tracer tracer = Instrumentation.create(null, DEFAULT_LIB_OPTIONS).getTracer(); + assertFalse(tracer.isEnabled()); + } + + @Test + public void createTracerTracingDisabled() { + OpenTelemetry otel = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal(); + + InstrumentationOptions options + = new InstrumentationOptions().setTracingEnabled(false).setProvider(otel); + + Tracer tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); + assertFalse(tracer.isEnabled()); + tracer.spanBuilder("test", INTERNAL, null).startSpan().end(); + + assertEquals(0, exporter.getFinishedSpanItems().size()); + } + + @SuppressWarnings("try") + @Test + public void createTracerGlobalOTel() throws Exception { + try (AutoCloseable otel + = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal()) { + + Tracer tracer = Instrumentation.create(null, DEFAULT_LIB_OPTIONS).getTracer(); + assertTrue(tracer.isEnabled()); + + tracer.spanBuilder("test", INTERNAL, null).startSpan().end(); + + assertEquals(1, exporter.getFinishedSpanItems().size()); + SpanData span = exporter.getFinishedSpanItems().get(0); + assertEquals("test", span.getName()); + assertEquals("test-library", span.getInstrumentationScopeInfo().getName()); + assertNull(span.getInstrumentationScopeInfo().getVersion()); + assertNull(span.getInstrumentationScopeInfo().getSchemaUrl()); + } + } + + @SuppressWarnings("try") + @Test + public void createTracerExplicitOTel() throws Exception { + try (AutoCloseable global + = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal()) { + + InMemorySpanExporter localExporter = InMemorySpanExporter.create(); + SdkTracerProvider localTracerProvider + = SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(localExporter)).build(); + + OpenTelemetry localOTel = OpenTelemetrySdk.builder().setTracerProvider(localTracerProvider).build(); + + Tracer tracer = Instrumentation + .create(new InstrumentationOptions().setProvider(localOTel), DEFAULT_LIB_OPTIONS) + .getTracer(); + assertTrue(tracer.isEnabled()); + + tracer.spanBuilder("test", INTERNAL, null).startSpan().end(); + + assertTrue(exporter.getFinishedSpanItems().isEmpty()); + assertEquals(1, localExporter.getFinishedSpanItems().size()); + assertEquals("test", localExporter.getFinishedSpanItems().get(0).getName()); + } + } + + @Test + public void createTracerBadArguments() { + InstrumentationOptions options + = new InstrumentationOptions().setProvider(tracerProvider); + + assertThrows(IllegalArgumentException.class, + () -> Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer()); + assertThrows(NullPointerException.class, () -> Instrumentation.create(null, null).getTracer()); + } + + @SuppressWarnings("try") + @Test + public void createTracerWithLibInfo() throws Exception { + try (AutoCloseable otel + = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal()) { + + LibraryInstrumentationOptions libOptions + = new LibraryInstrumentationOptions("test-library").setLibraryVersion("1.0.0") + .setSchemaUrl("https://opentelemetry.io/schemas/1.29.0"); + + Tracer tracer = Instrumentation.create(null, libOptions).getTracer(); + assertTrue(tracer.isEnabled()); + + tracer.spanBuilder("test", INTERNAL, null).startSpan().end(); + + SpanData span = exporter.getFinishedSpanItems().get(0); + assertEquals("test-library", span.getInstrumentationScopeInfo().getName()); + assertEquals("1.0.0", span.getInstrumentationScopeInfo().getVersion()); + assertEquals("https://opentelemetry.io/schemas/1.29.0", span.getInstrumentationScopeInfo().getSchemaUrl()); + } + } +} 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 new file mode 100644 index 000000000000..9316de90b044 --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/SuppressionTests.java @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation; + +import io.clientcore.core.http.MockHttpResponse; +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.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; +import io.clientcore.core.instrumentation.tracing.TracingScope; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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 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; +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class SuppressionTests { + + private static final LibraryInstrumentationOptions DEFAULT_LIB_OPTIONS + = new LibraryInstrumentationOptions("test-library"); + + private InMemorySpanExporter exporter; + private SdkTracerProvider tracerProvider; + private InstrumentationOptions otelOptions; + private Tracer tracer; + + @BeforeEach + public void setUp() { + exporter = InMemorySpanExporter.create(); + tracerProvider = SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)).build(); + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); + otelOptions = new InstrumentationOptions().setProvider(openTelemetry); + tracer = Instrumentation.create(otelOptions, DEFAULT_LIB_OPTIONS).getTracer(); + } + + @AfterEach + public void tearDown() { + exporter.reset(); + tracerProvider.close(); + } + + @Test + public void testNoSuppressionForSimpleMethod() { + HttpPipeline pipeline + = new HttpPipelineBuilder().httpClient(request -> new MockHttpResponse(request, 200)).build(); + SampleClient client = new SampleClient(pipeline, otelOptions); + + client.protocolMethod(new RequestOptions()); + + // test that one span is created for simple method + assertEquals(1, exporter.getFinishedSpanItems().size()); + SpanData span = exporter.getFinishedSpanItems().get(0); + assertEquals("protocolMethod", span.getName()); + } + + @Test + public void testNestedInternalSpanSuppression() { + HttpPipeline pipeline + = new HttpPipelineBuilder().httpClient(request -> new MockHttpResponse(request, 200)).build(); + SampleClient client = new SampleClient(pipeline, otelOptions); + + client.convenienceMethod(new RequestOptions()); + assertEquals(1, exporter.getFinishedSpanItems().size()); + SpanData span = exporter.getFinishedSpanItems().get(0); + assertEquals("convenienceMethod", span.getName()); + } + + @Test + public void testDisabledSuppression() { + Tracer outerTracer = tracer; + Tracer innerTracer = Instrumentation + .create(otelOptions, new LibraryInstrumentationOptions("test-library").disableSpanSuppression(true)) + .getTracer(); + + RequestOptions options = new RequestOptions(); + Span outerSpan = outerTracer.spanBuilder("outerSpan", CLIENT, options).startSpan(); + + options.putContext(TRACE_CONTEXT_KEY, outerSpan); + + Span innerSpan = innerTracer.spanBuilder("innerSpan", CLIENT, options).startSpan(); + innerSpan.end(); + outerSpan.end(); + + assertEquals(2, exporter.getFinishedSpanItems().size()); + SpanData outerSpanData = exporter.getFinishedSpanItems().get(1); + SpanData innerSpanData = exporter.getFinishedSpanItems().get(0); + + assertEquals("outerSpan", outerSpanData.getName()); + assertEquals("innerSpan", innerSpanData.getName()); + assertIsParentOf(outerSpanData, innerSpanData); + } + + @Test + public void disabledSuppressionDoesNotAffectChildren() { + Tracer outerTracer = Instrumentation + .create(otelOptions, new LibraryInstrumentationOptions("test-library").disableSpanSuppression(true)) + .getTracer(); + Tracer innerTracer = tracer; + + RequestOptions options = new RequestOptions(); + Span outerSpan = outerTracer.spanBuilder("outerSpan", CLIENT, options).startSpan(); + + options.putContext(TRACE_CONTEXT_KEY, outerSpan); + Span innerSpan = innerTracer.spanBuilder("innerSpan", CLIENT, options).startSpan(); + innerSpan.end(); + outerSpan.end(); + + assertEquals(1, exporter.getFinishedSpanItems().size()); + SpanData outerSpanData = exporter.getFinishedSpanItems().get(0); + + assertEquals("outerSpan", outerSpanData.getName()); + } + + @Test + @SuppressWarnings("try") + public void noSuppressionForSiblings() { + Span first = tracer.spanBuilder("first", CLIENT, null).startSpan(); + try (TracingScope outerScope = first.makeCurrent()) { + first.setAttribute("key", "valueOuter"); + } finally { + first.end(); + } + + tracer.spanBuilder("second", CLIENT, null).startSpan().end(); + assertEquals(2, exporter.getFinishedSpanItems().size()); + } + + @Test + public void multipleLayers() { + Tracer tracer = Instrumentation.create(otelOptions, DEFAULT_LIB_OPTIONS).getTracer(); + + RequestOptions options = new RequestOptions(); + + Span outer = tracer.spanBuilder("outer", CLIENT, options).startSpan(); + options.putContext(TRACE_CONTEXT_KEY, outer); + + Span inner = tracer.spanBuilder("inner", PRODUCER, options).startSpan(); + options.putContext(TRACE_CONTEXT_KEY, inner); + + Span suppressed = tracer.spanBuilder("suppressed", CLIENT, options).startSpan(); + suppressed.end(); + inner.end(); + outer.end(); + + assertEquals(2, exporter.getFinishedSpanItems().size()); + SpanData firstSpanData = exporter.getFinishedSpanItems().get(1); + SpanData secondSpanData = exporter.getFinishedSpanItems().get(0); + + assertEquals("outer", firstSpanData.getName()); + assertEquals("inner", secondSpanData.getName()); + assertIsParentOf(firstSpanData, secondSpanData); + assertSpanContextEquals(inner, suppressed); + } + + @ParameterizedTest + @MethodSource("suppressionTestCases") + @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(); + + options.putContext(TRACE_CONTEXT_KEY, outerSpan); + + Span innerSpan + = tracer.spanBuilder("innerSpan", innerKind, options).setAttribute("key", "valueInner").startSpan(); + // sanity check - this should not throw + innerSpan.setAttribute("anotherKey", "anotherValue"); + + if (expectedSpanCount == 1) { + // suppressed span should carry the original context + assertSpanContextEquals(outerSpan, innerSpan); + + // but should not be recording + assertFalse(innerSpan.isRecording()); + } + innerSpan.end(); + outerSpan.end(); + + assertEquals(expectedSpanCount, exporter.getFinishedSpanItems().size()); + SpanData outerSpanData = exporter.getFinishedSpanItems().get(expectedSpanCount - 1); + assertEquals("outerSpan", outerSpanData.getName()); + assertEquals("valueOuter", outerSpanData.getAttributes().get(AttributeKey.stringKey("key"))); + assertNull(outerSpanData.getAttributes().get(AttributeKey.stringKey("anotherKey"))); + + assertNotNull(innerSpan); + if (expectedSpanCount == 2) { + SpanData innerSpanData = exporter.getFinishedSpanItems().get(0); + assertEquals("innerSpan", innerSpanData.getName()); + assertEquals("valueInner", innerSpanData.getAttributes().get(AttributeKey.stringKey("key"))); + + assertIsParentOf(outerSpanData, innerSpanData); + } else { + assertSpanContextEquals(outerSpan, innerSpan); + } + } + + @ParameterizedTest + @MethodSource("suppressionTestCases") + @SuppressWarnings("try") + public void testSuppressionImplicitContext(SpanKind outerKind, SpanKind innerKind, int expectedSpanCount) { + Span outerSpan = tracer.spanBuilder("outerSpan", outerKind, null).setAttribute("key", "valueOuter").startSpan(); + Span innerSpan = null; + try (TracingScope outerScope = outerSpan.makeCurrent()) { + innerSpan = tracer.spanBuilder("innerSpan", innerKind, null).setAttribute("key", "valueInner").startSpan(); + io.opentelemetry.api.trace.Span outerCurrentSpan = io.opentelemetry.api.trace.Span.current(); + try (TracingScope innerScope = innerSpan.makeCurrent()) { + // sanity check - this should not throw + innerSpan.setAttribute("anotherKey", "anotherValue"); + + if (expectedSpanCount == 1) { + // suppressed span should carry the original context + io.opentelemetry.api.trace.Span innerCurrentSpan = io.opentelemetry.api.trace.Span.current(); + assertTrue(innerCurrentSpan.getSpanContext().isValid()); + + assertSpanContextEquals(outerCurrentSpan, innerCurrentSpan); + assertSpanContextEquals(outerSpan, innerSpan); + + // but should not be recording + assertFalse(innerCurrentSpan.isRecording()); + assertFalse(innerSpan.isRecording()); + } + } finally { + innerSpan.end(); + } + } finally { + outerSpan.end(); + } + + assertEquals(expectedSpanCount, exporter.getFinishedSpanItems().size()); + SpanData outerSpanData = exporter.getFinishedSpanItems().get(expectedSpanCount - 1); + assertEquals("outerSpan", outerSpanData.getName()); + assertEquals("valueOuter", outerSpanData.getAttributes().get(AttributeKey.stringKey("key"))); + assertNull(outerSpanData.getAttributes().get(AttributeKey.stringKey("anotherKey"))); + + assertNotNull(innerSpan); + if (expectedSpanCount == 2) { + SpanData innerSpanData = exporter.getFinishedSpanItems().get(0); + assertEquals("innerSpan", innerSpanData.getName()); + assertEquals("valueInner", innerSpanData.getAttributes().get(AttributeKey.stringKey("key"))); + + assertIsParentOf(outerSpanData, innerSpanData); + } else { + assertSpanContextEquals(outerSpan, innerSpan); + } + } + + private static void assertIsParentOf(SpanData parent, SpanData child) { + assertEquals(parent.getSpanContext().getSpanId(), child.getParentSpanContext().getSpanId()); + assertEquals(parent.getSpanContext().getTraceId(), child.getSpanContext().getTraceId()); + } + + private static void assertSpanContextEquals(Span first, Span second) { + OTelSpanContext firstContext = ((OTelSpan) first).getSpanContext(); + OTelSpanContext secondContext = ((OTelSpan) second).getSpanContext(); + assertEquals(firstContext.getTraceId(), secondContext.getTraceId()); + assertEquals(firstContext.getSpanId(), secondContext.getSpanId()); + assertSame(firstContext.getTraceFlags(), secondContext.getTraceFlags()); + } + + private static void assertSpanContextEquals(io.opentelemetry.api.trace.Span first, + io.opentelemetry.api.trace.Span second) { + assertEquals(first.getSpanContext().getTraceId(), second.getSpanContext().getTraceId()); + assertEquals(first.getSpanContext().getSpanId(), second.getSpanContext().getSpanId()); + assertSame(first.getSpanContext().getTraceFlags(), second.getSpanContext().getTraceFlags()); + } + + public static Stream suppressionTestCases() { + return Stream.of(Arguments.of(CLIENT, CLIENT, 1), Arguments.of(CLIENT, INTERNAL, 1), + Arguments.of(INTERNAL, CLIENT, 1), Arguments.of(INTERNAL, INTERNAL, 1), + Arguments.of(SpanKind.SERVER, CLIENT, 2), Arguments.of(SpanKind.SERVER, INTERNAL, 2), + Arguments.of(PRODUCER, CLIENT, 2), Arguments.of(INTERNAL, PRODUCER, 2), + Arguments.of(SpanKind.CONSUMER, CLIENT, 2), Arguments.of(SpanKind.CONSUMER, INTERNAL, 2)); + } + + static class SampleClient { + private final HttpPipeline pipeline; + private final Tracer tracer; + + SampleClient(HttpPipeline pipeline, InstrumentationOptions options) { + this.pipeline = pipeline; + this.tracer = Instrumentation.create(options, DEFAULT_LIB_OPTIONS).getTracer(); + } + + @SuppressWarnings("try") + public void protocolMethod(RequestOptions options) { + Span span = tracer.spanBuilder("protocolMethod", INTERNAL, options).startSpan(); + + // TODO (limolkova): should we have addContext(k, v) on options? + options.putContext(TRACE_CONTEXT_KEY, span); + + try (TracingScope scope = span.makeCurrent()) { + Response response = pipeline.send(new HttpRequest(HttpMethod.GET, "https://localhost")); + try { + response.close(); + } catch (IOException e) { + fail(e); + } + } finally { + span.end(); + } + } + + @SuppressWarnings("try") + public void convenienceMethod(RequestOptions options) { + Span span = tracer.spanBuilder("convenienceMethod", INTERNAL, options).startSpan(); + + options.putContext(TRACE_CONTEXT_KEY, span); + + try (TracingScope scope = span.makeCurrent()) { + protocolMethod(options); + } finally { + span.end(); + } + } + } +} 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 new file mode 100644 index 000000000000..0c229702c142 --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracerTests.java @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.instrumentation; + +import io.clientcore.core.http.models.RequestOptions; +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.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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 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 { + private static final LibraryInstrumentationOptions DEFAULT_LIB_OPTIONS + = new LibraryInstrumentationOptions("test-library"); + + private InMemorySpanExporter exporter; + private SdkTracerProvider tracerProvider; + private InstrumentationOptions otelOptions; + private Tracer tracer; + + @BeforeEach + public void setUp() { + exporter = InMemorySpanExporter.create(); + tracerProvider = SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)).build(); + + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); + otelOptions = new InstrumentationOptions().setProvider(openTelemetry); + tracer = Instrumentation.create(otelOptions, DEFAULT_LIB_OPTIONS).getTracer(); + } + + @AfterEach + public void tearDown() { + exporter.reset(); + tracerProvider.close(); + } + + @SuppressWarnings("try") + @Test + public void testSpan() { + Span span = tracer.spanBuilder("test-span", INTERNAL, null) + .setAttribute("builder-string-attribute", "string") + .setAttribute("builder-int-attribute", 42) + .setAttribute("builder-long-attribute", 4242L) + .setAttribute("builder-double-attribute", 42.42) + .setAttribute("builder-boolean-attribute", true) + .startSpan(); + + assertTrue(span.isRecording()); + + try (TracingScope scope = span.makeCurrent()) { + assertTrue(io.opentelemetry.api.trace.Span.current().getSpanContext().isValid()); + } + + span.setAttribute("span-string-attribute", "string") + .setAttribute("span-int-attribute", 42) + .setAttribute("span-long-attribute", 4242L) + .setAttribute("span-double-attribute", 42.42) + .setAttribute("span-boolean-attribute", true); + + span.end(); + + assertEquals(1, exporter.getFinishedSpanItems().size()); + SpanData spanData = exporter.getFinishedSpanItems().get(0); + assertEquals("test-span", spanData.getName()); + assertEquals(io.opentelemetry.api.trace.SpanKind.INTERNAL, spanData.getKind()); + + assertEquals("string", spanData.getAttributes().get(AttributeKey.stringKey("builder-string-attribute"))); + assertEquals("string", spanData.getAttributes().get(AttributeKey.stringKey("span-string-attribute"))); + + assertEquals(42, spanData.getAttributes().get(AttributeKey.longKey("builder-int-attribute"))); + assertEquals(42, spanData.getAttributes().get(AttributeKey.longKey("span-int-attribute"))); + + assertEquals(4242L, spanData.getAttributes().get(AttributeKey.longKey("builder-long-attribute"))); + assertEquals(4242L, spanData.getAttributes().get(AttributeKey.longKey("span-long-attribute"))); + + assertEquals(42.42, spanData.getAttributes().get(AttributeKey.doubleKey("span-double-attribute"))); + assertEquals(42.42, spanData.getAttributes().get(AttributeKey.doubleKey("builder-double-attribute"))); + + assertEquals(true, spanData.getAttributes().get(AttributeKey.booleanKey("builder-boolean-attribute"))); + assertEquals(true, spanData.getAttributes().get(AttributeKey.booleanKey("span-boolean-attribute"))); + + assertEquals(io.opentelemetry.api.trace.StatusCode.UNSET, spanData.getStatus().getStatusCode()); + } + + @Test + public void testEndWithErrorString() { + Span span = tracer.spanBuilder("test-span", INTERNAL, null).startSpan(); + + span.setError("cancelled"); + span.end(); + + assertEquals(1, exporter.getFinishedSpanItems().size()); + SpanData spanData = exporter.getFinishedSpanItems().get(0); + assertEquals("test-span", spanData.getName()); + + assertEquals("cancelled", spanData.getAttributes().get(AttributeKey.stringKey("error.type"))); + assertEquals(io.opentelemetry.api.trace.StatusCode.ERROR, spanData.getStatus().getStatusCode()); + assertEquals("", spanData.getStatus().getDescription()); + } + + @Test + public void testEndWithException() { + Span span = tracer.spanBuilder("test-span", INTERNAL, null).startSpan(); + + IOException exception = new IOException("test"); + span.end(exception); + + assertEquals(1, exporter.getFinishedSpanItems().size()); + SpanData spanData = exporter.getFinishedSpanItems().get(0); + assertEquals("test-span", spanData.getName()); + + assertEquals(IOException.class.getCanonicalName(), + spanData.getAttributes().get(AttributeKey.stringKey("error.type"))); + assertEquals(io.opentelemetry.api.trace.StatusCode.ERROR, spanData.getStatus().getStatusCode()); + assertEquals(exception.getMessage(), spanData.getStatus().getDescription()); + } + + @ParameterizedTest + @MethodSource("kindSource") + public void testKinds(SpanKind kind, io.opentelemetry.api.trace.SpanKind expectedKind) { + Span span = tracer.spanBuilder("test-span", kind, null).startSpan(); + + span.end(); + + assertEquals(1, exporter.getFinishedSpanItems().size()); + SpanData spanData = exporter.getFinishedSpanItems().get(0); + + assertEquals(expectedKind, spanData.getKind()); + } + + @SuppressWarnings("try") + @Test + public void implicitParent() throws Exception { + io.opentelemetry.api.trace.Tracer otelTracer = otelOptions.getProvider().getTracer("test"); + io.opentelemetry.api.trace.Span parent = otelTracer.spanBuilder("parent").startSpan(); + try (AutoCloseable scope = parent.makeCurrent()) { + Span child = tracer.spanBuilder("child", INTERNAL, null).startSpan(); + child.end(); + } + + parent.end(); + + assertEquals(2, exporter.getFinishedSpanItems().size()); + SpanData childData = exporter.getFinishedSpanItems().get(0); + SpanData parentData = exporter.getFinishedSpanItems().get(1); + + assertEquals("child", childData.getName()); + assertEquals(parentData.getTraceId(), childData.getTraceId()); + assertEquals(parentData.getSpanId(), childData.getParentSpanId()); + } + + @Test + 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(); + child.end(); + parent.end(); + + assertEquals(2, exporter.getFinishedSpanItems().size()); + SpanData childData = exporter.getFinishedSpanItems().get(0); + SpanData parentData = exporter.getFinishedSpanItems().get(1); + + assertEquals("child", childData.getName()); + assertEquals(parentData.getTraceId(), childData.getTraceId()); + 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), + Arguments.of(SpanKind.PRODUCER, io.opentelemetry.api.trace.SpanKind.PRODUCER), + Arguments.of(SpanKind.CONSUMER, io.opentelemetry.api.trace.SpanKind.CONSUMER), + Arguments.of(SpanKind.SERVER, io.opentelemetry.api.trace.SpanKind.SERVER)); + } +} diff --git a/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracingShimBenchmarks.java b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracingShimBenchmarks.java new file mode 100644 index 000000000000..8f029d16e5db --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/instrumentation/TracingShimBenchmarks.java @@ -0,0 +1,161 @@ +// 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.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.api.trace.StatusCode; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; +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.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 TracingShimBenchmarks { + + private Tracer shimTracer; + private Tracer shimTracerDisabled; + private io.opentelemetry.api.trace.Tracer otelTracer; + private io.opentelemetry.api.trace.Tracer otelTracerDisabled; + private OpenTelemetry openTelemetry; + + private static final AttributeKey STRING_ATTRIBUTE_KEY_1 = AttributeKey.stringKey("string1"); + private static final AttributeKey STRING_ATTRIBUTE_KEY_2 = AttributeKey.stringKey("string2"); + private static final AttributeKey ERROR_TYPE_ATTRIBUTE_KEY = AttributeKey.stringKey("error.type"); + private static final AttributeKey INT_ATTRIBUTE_KEY = AttributeKey.longKey("int"); + private static final AttributeKey LONG_ATTRIBUTE_KEY = AttributeKey.longKey("long"); + private static final AttributeKey DOUBLE_ATTRIBUTE_KEY = AttributeKey.doubleKey("double"); + private static final AttributeKey BOOLEAN_ATTRIBUTE_KEY = AttributeKey.booleanKey("boolean"); + + @Setup + public void setupOtel() { + openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(SdkTracerProvider.builder().addSpanProcessor(new NoopProcessor()).build()) + .build(); + + otelTracer = openTelemetry.getTracer("test"); + otelTracerDisabled = TracerProvider.noop().get("test"); + shimTracer + = Instrumentation + .create(new InstrumentationOptions().setProvider(openTelemetry), + new LibraryInstrumentationOptions("test")) + .getTracer(); + shimTracerDisabled + = Instrumentation + .create(new InstrumentationOptions().setProvider(OpenTelemetry.noop()), + new LibraryInstrumentationOptions("test")) + .getTracer(); + } + + @Benchmark + public void shimTracingDisabled(Blackhole blackhole) { + blackhole.consume(testShimSpan(shimTracerDisabled)); + } + + @Benchmark + public void directTracingDisabled(Blackhole blackhole) { + blackhole.consume(testOTelSpan(otelTracerDisabled)); + } + + @Benchmark + public void shimTracing(Blackhole blackhole) { + blackhole.consume(testShimSpan(shimTracer)); + } + + @Benchmark + public void directTracing(Blackhole blackhole) { + blackhole.consume(testOTelSpan(otelTracer)); + } + + @SuppressWarnings("try") + private Span testShimSpan(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; + } + + @SuppressWarnings("try") + private io.opentelemetry.api.trace.Span testOTelSpan(io.opentelemetry.api.trace.Tracer tracer) { + io.opentelemetry.api.trace.Span span = tracer.spanBuilder("test") + .setSpanKind(io.opentelemetry.api.trace.SpanKind.CLIENT) + .setAttribute(STRING_ATTRIBUTE_KEY_1, "test") + .startSpan(); + + if (span.isRecording()) { + span.setAttribute(STRING_ATTRIBUTE_KEY_2, "test"); + span.setAttribute(INT_ATTRIBUTE_KEY, 42); + span.setAttribute(LONG_ATTRIBUTE_KEY, 42L); + span.setAttribute(DOUBLE_ATTRIBUTE_KEY, 42.0); + span.setAttribute(BOOLEAN_ATTRIBUTE_KEY, true); + } + + try (io.opentelemetry.context.Scope scope = span.makeCurrent()) { + span.setAttribute(ERROR_TYPE_ATTRIBUTE_KEY, "canceled"); + span.setStatus(StatusCode.ERROR); + } + span.end(); + + return span; + } + + static class NoopProcessor implements SpanProcessor { + + @Override + public void onStart(io.opentelemetry.context.Context context, ReadWriteSpan readWriteSpan) { + + } + + @Override + public boolean isStartRequired() { + return false; + } + + @Override + public void onEnd(ReadableSpan readableSpan) { + + } + + @Override + public boolean isEndRequired() { + return false; + } + } +} 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/util/ClientLoggerSlf4JTests.java new file mode 100644 index 000000000000..81ec9c97058e --- /dev/null +++ b/sdk/clientcore/optional-dependency-tests/src/test/java/io/clientcore/core/util/ClientLoggerSlf4JTests.java @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.util; + +/** + * Tests for {@link ClientLogger}. + */ +public class ClientLoggerSlf4JTests extends ClientLoggerTests { +} diff --git a/sdk/clientcore/pom.xml b/sdk/clientcore/pom.xml index b27a9a86912b..7cb48778cd93 100644 --- a/sdk/clientcore/pom.xml +++ b/sdk/clientcore/pom.xml @@ -12,6 +12,7 @@ core http-okhttp3 + optional-dependency-tests http-stress diff --git a/sdk/parents/clientcore-parent/pom.xml b/sdk/parents/clientcore-parent/pom.xml index 4aaf0c56279e..d8ee1551d410 100644 --- a/sdk/parents/clientcore-parent/pom.xml +++ b/sdk/parents/clientcore-parent/pom.xml @@ -240,12 +240,6 @@ 0.8.12 test - - org.slf4j - slf4j-simple - 1.7.36 - test -