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
+
+
+
+
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