diff --git a/detekt_custom.yml b/detekt_custom.yml index 154a6342ae..c2a08accfb 100644 --- a/detekt_custom.yml +++ b/detekt_custom.yml @@ -1257,6 +1257,7 @@ datadog: # endregion # region Opentelemetry - "io.opentelemetry.api.trace.TracerBuilder.build()" + - "io.opentelemetry.api.trace.SpanBuilder.setAttribute(kotlin.String?, kotlin.String?)" - "io.opentelemetry.api.trace.TracerBuilder.setInstrumentationVersion(kotlin.String?)" # endregion # region RxJava diff --git a/features/dd-sdk-android-trace/api/apiSurface b/features/dd-sdk-android-trace/api/apiSurface index 57a580faeb..663b590088 100644 --- a/features/dd-sdk-android-trace/api/apiSurface +++ b/features/dd-sdk-android-trace/api/apiSurface @@ -15,7 +15,7 @@ class com.datadog.android.trace.AndroidTracer : com.datadog.opentracing.DDTracer fun logThrowable(io.opentracing.Span, Throwable) fun logErrorMessage(io.opentracing.Span, String) class com.datadog.android.trace.OtelTracerProvider : io.opentelemetry.api.trace.TracerProvider - constructor(com.datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI, com.datadog.android.api.InternalLogger) + constructor(com.datadog.android.api.feature.FeatureSdkCore, com.datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI, com.datadog.android.api.InternalLogger, Boolean) override fun get(String): io.opentelemetry.api.trace.Tracer override fun get(String, String): io.opentelemetry.api.trace.Tracer override fun tracerBuilder(String): io.opentelemetry.api.trace.TracerBuilder @@ -27,6 +27,7 @@ class com.datadog.android.trace.OtelTracerProvider : io.opentelemetry.api.trace. fun setPartialFlushThreshold(Int): Builder fun addTag(String, String): Builder fun setSampleRate(Double): Builder + fun setBundleWithRumEnabled(Boolean): Builder override fun toString(): String companion object fun io.opentracing.Span.setError(Throwable) diff --git a/features/dd-sdk-android-trace/api/dd-sdk-android-trace.api b/features/dd-sdk-android-trace/api/dd-sdk-android-trace.api index feaa89a383..021b20d50f 100644 --- a/features/dd-sdk-android-trace/api/dd-sdk-android-trace.api +++ b/features/dd-sdk-android-trace/api/dd-sdk-android-trace.api @@ -28,7 +28,7 @@ public final class com/datadog/android/trace/AndroidTracer$Companion { public final class com/datadog/android/trace/OtelTracerProvider : io/opentelemetry/api/trace/TracerProvider { public static final field Companion Lcom/datadog/android/trace/OtelTracerProvider$Companion; - public fun (Lcom/datadog/trace/bootstrap/instrumentation/api/AgentTracer$TracerAPI;Lcom/datadog/android/api/InternalLogger;)V + public fun (Lcom/datadog/android/api/feature/FeatureSdkCore;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentTracer$TracerAPI;Lcom/datadog/android/api/InternalLogger;Z)V public fun get (Ljava/lang/String;)Lio/opentelemetry/api/trace/Tracer; public fun get (Ljava/lang/String;Ljava/lang/String;)Lio/opentelemetry/api/trace/Tracer; public fun toString ()Ljava/lang/String; @@ -41,6 +41,7 @@ public final class com/datadog/android/trace/OtelTracerProvider$Builder { public synthetic fun (Lcom/datadog/android/api/SdkCore;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addTag (Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/trace/OtelTracerProvider$Builder; public final fun build ()Lcom/datadog/android/trace/OtelTracerProvider; + public final fun setBundleWithRumEnabled (Z)Lcom/datadog/android/trace/OtelTracerProvider$Builder; public final fun setPartialFlushThreshold (I)Lcom/datadog/android/trace/OtelTracerProvider$Builder; public final fun setSampleRate (D)Lcom/datadog/android/trace/OtelTracerProvider$Builder; public final fun setService (Ljava/lang/String;)Lcom/datadog/android/trace/OtelTracerProvider$Builder; @@ -928,11 +929,12 @@ public class com/datadog/opentelemetry/trace/OtelSpanLink : com/datadog/trace/bo public class com/datadog/opentelemetry/trace/OtelTracer : io/opentelemetry/api/trace/Tracer { public fun (Ljava/lang/String;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentTracer$TracerAPI;Lcom/datadog/android/api/InternalLogger;)V + public fun (Ljava/lang/String;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentTracer$TracerAPI;Lcom/datadog/android/api/InternalLogger;Ljava/util/function/Function;)V public fun spanBuilder (Ljava/lang/String;)Lio/opentelemetry/api/trace/SpanBuilder; } public class com/datadog/opentelemetry/trace/OtelTracerBuilder : io/opentelemetry/api/trace/TracerBuilder { - public fun (Ljava/lang/String;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentTracer$TracerAPI;Lcom/datadog/android/api/InternalLogger;)V + public fun (Ljava/lang/String;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentTracer$TracerAPI;Lcom/datadog/android/api/InternalLogger;Ljava/util/function/Function;)V public fun build ()Lio/opentelemetry/api/trace/Tracer; public fun setInstrumentationVersion (Ljava/lang/String;)Lio/opentelemetry/api/trace/TracerBuilder; public fun setSchemaUrl (Ljava/lang/String;)Lio/opentelemetry/api/trace/TracerBuilder; diff --git a/features/dd-sdk-android-trace/src/main/java/com/datadog/opentelemetry/trace/OtelTracer.java b/features/dd-sdk-android-trace/src/main/java/com/datadog/opentelemetry/trace/OtelTracer.java index 8c1e686674..b8e2a1fa5e 100644 --- a/features/dd-sdk-android-trace/src/main/java/com/datadog/opentelemetry/trace/OtelTracer.java +++ b/features/dd-sdk-android-trace/src/main/java/com/datadog/opentelemetry/trace/OtelTracer.java @@ -11,10 +11,15 @@ import com.datadog.android.api.InternalLogger; import com.datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.util.function.Function; + import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.Tracer; public class OtelTracer implements Tracer { + + private static final Function NO_OP_DECORATOR = spanBuilder -> spanBuilder; + @NonNull private final AgentTracer.TracerAPI tracer; @NonNull @@ -22,19 +27,33 @@ public class OtelTracer implements Tracer { @NonNull private final InternalLogger logger; + @NonNull + private final Function spanBuilderDecorator; + public OtelTracer( @NonNull String instrumentationScopeName, @NonNull AgentTracer.TracerAPI tracer, - @NonNull InternalLogger logger) { + @NonNull InternalLogger logger, + @NonNull Function spanBuilderDecorator) { this.instrumentationScopeName = instrumentationScopeName; this.tracer = tracer; this.logger = logger; + this.spanBuilderDecorator = spanBuilderDecorator; + } + + public OtelTracer( + @NonNull String instrumentationScopeName, + @NonNull AgentTracer.TracerAPI tracer, + @NonNull InternalLogger logger) { + this(instrumentationScopeName, tracer, logger, NO_OP_DECORATOR); } @Override public SpanBuilder spanBuilder(String spanName) { AgentTracer.SpanBuilder delegate = - this.tracer.buildSpan(instrumentationScopeName, OtelConventions.SPAN_KIND_INTERNAL).withResourceName(spanName); - return new OtelSpanBuilder(delegate, logger); + this.tracer + .buildSpan(instrumentationScopeName, OtelConventions.SPAN_KIND_INTERNAL) + .withResourceName(spanName); + return spanBuilderDecorator.apply(new OtelSpanBuilder(delegate, logger)); } } diff --git a/features/dd-sdk-android-trace/src/main/java/com/datadog/opentelemetry/trace/OtelTracerBuilder.java b/features/dd-sdk-android-trace/src/main/java/com/datadog/opentelemetry/trace/OtelTracerBuilder.java index a1c3768fe5..711efacabe 100644 --- a/features/dd-sdk-android-trace/src/main/java/com/datadog/opentelemetry/trace/OtelTracerBuilder.java +++ b/features/dd-sdk-android-trace/src/main/java/com/datadog/opentelemetry/trace/OtelTracerBuilder.java @@ -12,6 +12,9 @@ import com.datadog.android.api.InternalLogger; import com.datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.util.function.Function; + +import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.TracerBuilder; @@ -25,13 +28,19 @@ public class OtelTracerBuilder implements TracerBuilder { @NonNull private final InternalLogger logger; + @NonNull + private final Function spanBuilderDecorator; + + public OtelTracerBuilder( @NonNull String instrumentationScopeName, @NonNull AgentTracer.TracerAPI coreTracer, - @NonNull InternalLogger logger) { + @NonNull InternalLogger logger, + @NonNull Function spanBuilderDecorator) { this.coreTracer = coreTracer; this.instrumentationScopeName = instrumentationScopeName; this.logger = logger; + this.spanBuilderDecorator = spanBuilderDecorator; } @Override @@ -48,6 +57,6 @@ public TracerBuilder setInstrumentationVersion(String instrumentationScopeVersio @Override public Tracer build() { - return new OtelTracer(this.instrumentationScopeName, this.coreTracer, this.logger); + return new OtelTracer(this.instrumentationScopeName, this.coreTracer, this.logger, this.spanBuilderDecorator); } } \ No newline at end of file diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/OtelTracerProvider.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/OtelTracerProvider.kt index 79e116c8bb..7f3d576e5e 100644 --- a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/OtelTracerProvider.kt +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/OtelTracerProvider.kt @@ -12,6 +12,7 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.SdkCore import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.log.LogAttributes import com.datadog.android.trace.internal.TracingFeature import com.datadog.android.trace.internal.data.NoOpOtelWriter import com.datadog.opentelemetry.trace.OtelTracerBuilder @@ -19,6 +20,7 @@ import com.datadog.trace.api.IdGenerationStrategy import com.datadog.trace.api.config.TracerConfig import com.datadog.trace.bootstrap.instrumentation.api.AgentTracer import com.datadog.trace.core.CoreTracer +import io.opentelemetry.api.trace.SpanBuilder import io.opentelemetry.api.trace.Tracer import io.opentelemetry.api.trace.TracerBuilder import io.opentelemetry.api.trace.TracerProvider @@ -34,8 +36,10 @@ import java.util.Properties * */ class OtelTracerProvider( + private val sdkCore: FeatureSdkCore, private val coreTracer: AgentTracer.TracerAPI, - private val internalLogger: InternalLogger + private val internalLogger: InternalLogger, + private val bundleWithRumEnabled: Boolean ) : TracerProvider { private val tracers: MutableMap = mutableMapOf() @@ -76,15 +80,12 @@ class OtelTracerProvider( /** @inheritDoc */ override fun tracerBuilder(instrumentationScopeName: String): TracerBuilder { val resolvedInstrumentationScopeName = resolveInstrumentationScopeName(instrumentationScopeName) - return OtelTracerBuilder(resolvedInstrumentationScopeName, coreTracer, internalLogger) - } - - private fun resolveInstrumentationScopeName(instrumentationScopeName: String): String { - return if (instrumentationScopeName.trim { it <= ' ' }.isEmpty()) { - DEFAULT_TRACER_NAME - } else { - instrumentationScopeName - } + return OtelTracerBuilder( + resolvedInstrumentationScopeName, + coreTracer, + internalLogger, + resolveSpanBuilderDecorator() + ) } /** @@ -112,6 +113,7 @@ class OtelTracerProvider( } private var partialFlushThreshold = DEFAULT_PARTIAL_MIN_FLUSH private val globalTags: MutableMap = mutableMapOf() + private var bundleWithRumEnabled: Boolean = true /** * @param sdkCore SDK instance to bind to. If not provided, default instance will be used. @@ -134,6 +136,15 @@ class OtelTracerProvider( { TRACING_NOT_ENABLED_ERROR_MESSAGE } ) } + val rumFeature = sdkCore.getFeature(Feature.RUM_FEATURE_NAME) + if (bundleWithRumEnabled && rumFeature == null) { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { RUM_NOT_ENABLED_ERROR_MESSAGE } + ) + bundleWithRumEnabled = false + } val coreTracer = CoreTracer.CoreTracerBuilder(sdkCore.internalLogger) .withProperties(properties()) .serviceName(serviceName) @@ -141,7 +152,7 @@ class OtelTracerProvider( .partialFlushMinSpans(partialFlushThreshold) .idGenerationStrategy(IdGenerationStrategy.fromName("SECURE_RANDOM", false)) .build() - return OtelTracerProvider(coreTracer, sdkCore.internalLogger) + return OtelTracerProvider(sdkCore, coreTracer, sdkCore.internalLogger, bundleWithRumEnabled) } /** @@ -194,6 +205,17 @@ class OtelTracerProvider( return this } + /** + * Enables the trace bundling with the current active View. If this feature is enabled all + * the spans from this moment on will be bundled with the current view information and you + * will be able to see all the traces sent during a specific view in the RUM Explorer. + * @param enabled true by default + */ + fun setBundleWithRumEnabled(enabled: Boolean): Builder { + bundleWithRumEnabled = enabled + return this + } + internal fun properties(): Properties { val properties = Properties() properties.setProperty( @@ -228,11 +250,59 @@ class OtelTracerProvider( // endregion } + // region Internal + + private fun resolveInstrumentationScopeName(instrumentationScopeName: String): String { + return if (instrumentationScopeName.trim { it <= ' ' }.isEmpty()) { + DEFAULT_TRACER_NAME + } else { + instrumentationScopeName + } + } + + private fun resolveSpanBuilderDecorator(): (SpanBuilder) -> SpanBuilder { + return if (bundleWithRumEnabled) { + resolveSpanBuilderDecoratorFromContext() + } else { + NO_OP_SPAN_BUILDER_DECORATOR + } + } + + private fun resolveSpanBuilderDecoratorFromContext(): (SpanBuilder) -> SpanBuilder = { spanBuilder -> + val rumContext = sdkCore.getFeatureContext(Feature.RUM_FEATURE_NAME) + val applicationId = rumContext[RUM_APPLICATION_ID_KEY] as? String + val sessionId = rumContext[RUM_SESSION_ID_KEY] as? String + val viewId = rumContext[RUM_VIEW_ID_KEY] as? String + val actionId = rumContext[RUM_ACTION_ID_KEY] as? String + if (applicationId != null && sessionId != null && viewId != null) { + spanBuilder.setAttribute(LogAttributes.RUM_APPLICATION_ID, applicationId) + spanBuilder.setAttribute(LogAttributes.RUM_SESSION_ID, sessionId) + spanBuilder.setAttribute(LogAttributes.RUM_VIEW_ID, viewId) + if (actionId != null) { + spanBuilder.setAttribute(LogAttributes.RUM_ACTION_ID, actionId) + } + } else { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { RUM_CONTEXT_MISSING_ERROR_MESSAGE } + ) + } + spanBuilder + } + override fun toString(): String { return "OtelTracerProvider/${super.toString()}" } +// endregion + companion object { + internal const val RUM_APPLICATION_ID_KEY = "application_id" + internal const val RUM_SESSION_ID_KEY = "session_id" + internal const val RUM_VIEW_ID_KEY = "view_id" + internal const val RUM_ACTION_ID_KEY = "action_id" + internal val NO_OP_SPAN_BUILDER_DECORATOR: (SpanBuilder) -> SpanBuilder = { it } internal const val TRACER_ALREADY_EXISTS_WARNING_MESSAGE = "Tracer for %s already exists. Returning existing instance." internal const val DEFAULT_TRACER_NAME = "android" @@ -245,6 +315,13 @@ class OtelTracerProvider( internal const val DEFAULT_SERVICE_NAME_IS_MISSING_ERROR_MESSAGE = "Default service name is missing during" + " OtelTracerProvider creation, did you initialize SDK?" + internal const val RUM_NOT_ENABLED_ERROR_MESSAGE = + "You're trying to bundle the traces with a RUM context, " + + "but the RUM feature was disabled in your Configuration. " + + "No RUM context will be attached to your traces in this case." + internal const val RUM_CONTEXT_MISSING_ERROR_MESSAGE = + "You are trying to bundle the traces with a RUM context, " + + "but the RUM context is missing. No RUM context will be attached to your traces in this case." // the minimum closed spans required for triggering a flush and deliver // everything to the writer diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/OtelTracerBuilderProviderTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/OtelTracerBuilderProviderTest.kt index 5622dab01c..733d8ca93d 100644 --- a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/OtelTracerBuilderProviderTest.kt +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/OtelTracerBuilderProviderTest.kt @@ -10,6 +10,7 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.log.LogAttributes import com.datadog.android.trace.internal.TracingFeature import com.datadog.android.trace.internal.data.NoOpOtelWriter import com.datadog.android.trace.utils.verifyLog @@ -39,12 +40,20 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doReturnConsecutively +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.times +import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -82,12 +91,33 @@ internal class OtelTracerBuilderProviderTest { @Mock lateinit var mockTraceWriter: Writer + lateinit var fakeRumContext: MutableMap + + @StringForgery(type = StringForgeryType.HEXADECIMAL) + lateinit var fakeApplicationId: String + + @StringForgery(type = StringForgeryType.HEXADECIMAL) + lateinit var fakeSessionId: String + + @StringForgery(type = StringForgeryType.HEXADECIMAL) + lateinit var fakeViewId: String + + @StringForgery(type = StringForgeryType.HEXADECIMAL) + lateinit var fakeActionId: String + @BeforeEach fun `set up`(forge: Forge) { fakeServiceName = forge.anAlphabeticalString() whenever( mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME) ) doReturn mockTracingFeatureScope + fakeRumContext = mutableMapOf( + OtelTracerProvider.RUM_APPLICATION_ID_KEY to fakeApplicationId, + OtelTracerProvider.RUM_SESSION_ID_KEY to fakeSessionId, + OtelTracerProvider.RUM_VIEW_ID_KEY to fakeViewId, + OtelTracerProvider.RUM_ACTION_ID_KEY to fakeActionId + ) + whenever(mockSdkCore.getFeatureContext(Feature.RUM_FEATURE_NAME)) doReturn fakeRumContext whenever(mockTracingFeatureScope.unwrap()) doReturn mockTracingFeature whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mock() whenever(mockSdkCore.service) doReturn fakeServiceName @@ -577,4 +607,225 @@ internal class OtelTracerBuilderProviderTest { } // endregion + + // region bundle with RUM + + @Test + fun `M build a Span with RUM context W startSpan`() { + // Given + val tracer = testedOtelTracerProviderBuilder + .build() + .tracerBuilder(fakeInstrumentationName) + .build() + + // When + val span = tracer + .spanBuilder(fakeOperationName) + .startSpan() + val delegateSpan: DDSpan = span.getFieldValue("delegate") + val context = delegateSpan.context() + span.end() + + // Then + assertThat(context.tags).containsEntry(LogAttributes.RUM_APPLICATION_ID, fakeApplicationId) + assertThat(context.tags).containsEntry(LogAttributes.RUM_SESSION_ID, fakeSessionId) + assertThat(context.tags).containsEntry(LogAttributes.RUM_VIEW_ID, fakeViewId) + assertThat(context.tags).containsEntry(LogAttributes.RUM_ACTION_ID, fakeActionId) + } + + @Test + fun `M bundle the span with current RUM context W startSpan`(forge: Forge) { + // Given + val fakeApppicationId1: String = forge.anHexadecimalString() + val fakeSessionId1: String = forge.anHexadecimalString() + val fakeViewId1: String = forge.anHexadecimalString() + val fakeActionId1: String = forge.anHexadecimalString() + val fakeApppicationId2: String = forge.anHexadecimalString() + val fakeSessionId2: String = forge.anHexadecimalString() + val fakeViewId2: String = forge.anHexadecimalString() + val fakeActionId2: String = forge.anHexadecimalString() + val fakeRumContext1 = mutableMapOf( + OtelTracerProvider.RUM_APPLICATION_ID_KEY to fakeApppicationId1, + OtelTracerProvider.RUM_SESSION_ID_KEY to fakeSessionId1, + OtelTracerProvider.RUM_VIEW_ID_KEY to fakeViewId1, + OtelTracerProvider.RUM_ACTION_ID_KEY to fakeActionId1 + ) + val fakeRumContext2 = mutableMapOf( + OtelTracerProvider.RUM_APPLICATION_ID_KEY to fakeApppicationId2, + OtelTracerProvider.RUM_SESSION_ID_KEY to fakeSessionId2, + OtelTracerProvider.RUM_VIEW_ID_KEY to fakeViewId2, + OtelTracerProvider.RUM_ACTION_ID_KEY to fakeActionId2 + ) + whenever(mockSdkCore.getFeatureContext(Feature.RUM_FEATURE_NAME)) doReturnConsecutively + listOf(fakeRumContext1, fakeRumContext2) + val tracer = testedOtelTracerProviderBuilder + .build() + .tracerBuilder(fakeInstrumentationName) + .build() + + // When + val span1 = tracer + .spanBuilder(fakeOperationName) + .startSpan() + val delegateSpan1: DDSpan = span1.getFieldValue("delegate") + val context1 = delegateSpan1.context() + span1.end() + + // Then + assertThat(context1.tags).containsEntry(LogAttributes.RUM_APPLICATION_ID, fakeApppicationId1) + assertThat(context1.tags).containsEntry(LogAttributes.RUM_SESSION_ID, fakeSessionId1) + assertThat(context1.tags).containsEntry(LogAttributes.RUM_VIEW_ID, fakeViewId1) + assertThat(context1.tags).containsEntry(LogAttributes.RUM_ACTION_ID, fakeActionId1) + + // When + val span2 = tracer + .spanBuilder(fakeOperationName) + .startSpan() + val delegateSpan2: DDSpan = span2.getFieldValue("delegate") + val context2 = delegateSpan2.context() + span2.end() + + // Then + assertThat(context2.tags).containsEntry(LogAttributes.RUM_APPLICATION_ID, fakeApppicationId2) + assertThat(context2.tags).containsEntry(LogAttributes.RUM_SESSION_ID, fakeSessionId2) + assertThat(context2.tags).containsEntry(LogAttributes.RUM_VIEW_ID, fakeViewId2) + assertThat(context2.tags).containsEntry(LogAttributes.RUM_ACTION_ID, fakeActionId2) + } + + @Test + fun `M build a Span without RUM context W startSpan { bundleWithRum = false }`() { + // Given + val tracer = testedOtelTracerProviderBuilder + .setBundleWithRumEnabled(false) + .build() + .tracerBuilder(fakeInstrumentationName) + .build() + + // When + val span = tracer + .spanBuilder(fakeOperationName) + .startSpan() + val delegateSpan: DDSpan = span.getFieldValue("delegate") + val context = delegateSpan.context() + span.end() + + // Then + assertThat(context.tags).doesNotContainKey(LogAttributes.RUM_APPLICATION_ID) + assertThat(context.tags).doesNotContainKey(LogAttributes.RUM_SESSION_ID) + assertThat(context.tags).doesNotContainKey(LogAttributes.RUM_VIEW_ID) + assertThat(context.tags).doesNotContainKey(LogAttributes.RUM_ACTION_ID) + } + + @Test + fun `M send a warning log W build { bundle with RUM and no RUM feature }`() { + // Given + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + + // When + val tracerProvider = testedOtelTracerProviderBuilder.build() + + // Then + assertThat(tracerProvider).isNotNull + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + OtelTracerProvider.RUM_NOT_ENABLED_ERROR_MESSAGE + ) + } + + @ParameterizedTest + @MethodSource("brokenRumContextProvider") + fun `M send a warning log W startSpan { bundle with RUM and broken RUM context }`( + fakeBrokenRumContext: Map + ) { + // Given + whenever(mockSdkCore.getFeatureContext(Feature.RUM_FEATURE_NAME)) doReturn fakeBrokenRumContext + val tracer = testedOtelTracerProviderBuilder + .build() + .tracerBuilder(fakeInstrumentationName) + .build() + + // When + val span = tracer + .spanBuilder(fakeOperationName) + .startSpan() + val delegateSpan: DDSpan = span.getFieldValue("delegate") + val context = delegateSpan.context() + span.end() + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + OtelTracerProvider.RUM_CONTEXT_MISSING_ERROR_MESSAGE + ) + assertThat(context.tags).doesNotContainKey(LogAttributes.RUM_APPLICATION_ID) + assertThat(context.tags).doesNotContainKey(LogAttributes.RUM_SESSION_ID) + assertThat(context.tags).doesNotContainKey(LogAttributes.RUM_VIEW_ID) + assertThat(context.tags).doesNotContainKey(LogAttributes.RUM_ACTION_ID) + } + + @Test + fun `M send span and do not log W startSpan { bundle with RUM and no RUM action id }`() { + // Given + fakeRumContext.remove(OtelTracerProvider.RUM_ACTION_ID_KEY) + val tracer = testedOtelTracerProviderBuilder + .build() + .tracerBuilder(fakeInstrumentationName) + .build() + + // When + val span = tracer + .spanBuilder(fakeOperationName) + .startSpan() + val delegateSpan: DDSpan = span.getFieldValue("delegate") + val context = delegateSpan.context() + span.end() + + // Then + verify(mockInternalLogger, never()).log( + eq(InternalLogger.Level.WARN), + eq(listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY)), + eq { OtelTracerProvider.RUM_CONTEXT_MISSING_ERROR_MESSAGE }, + anyOrNull(), + any(), + anyOrNull() + ) + assertThat(context.tags).containsEntry(LogAttributes.RUM_APPLICATION_ID, fakeApplicationId) + assertThat(context.tags).containsEntry(LogAttributes.RUM_SESSION_ID, fakeSessionId) + assertThat(context.tags).containsEntry(LogAttributes.RUM_VIEW_ID, fakeViewId) + assertThat(context.tags).doesNotContainKey(LogAttributes.RUM_ACTION_ID) + } + + // endregion + + companion object { + + val forge = Forge() + + @JvmStatic + fun brokenRumContextProvider(): List> { + return listOf( + mapOf(), + mapOf( + OtelTracerProvider.RUM_SESSION_ID_KEY to forge.anAlphabeticalString(), + OtelTracerProvider.RUM_VIEW_ID_KEY to forge.anAlphabeticalString(), + OtelTracerProvider.RUM_ACTION_ID_KEY to forge.anAlphabeticalString() + ), + mapOf( + OtelTracerProvider.RUM_APPLICATION_ID_KEY to forge.anAlphabeticalString(), + OtelTracerProvider.RUM_VIEW_ID_KEY to forge.anAlphabeticalString(), + OtelTracerProvider.RUM_ACTION_ID_KEY to forge.anAlphabeticalString() + ), + mapOf( + OtelTracerProvider.RUM_APPLICATION_ID_KEY to forge.anAlphabeticalString(), + OtelTracerProvider.RUM_SESSION_ID_KEY to forge.anAlphabeticalString(), + OtelTracerProvider.RUM_ACTION_ID_KEY to forge.anAlphabeticalString() + ), + mapOf( + OtelTracerProvider.RUM_ACTION_ID_KEY to forge.anAlphabeticalString() + ) + ) + } + } } diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/opentelemetry/trace/OtelTracerTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/opentelemetry/trace/OtelTracerTest.kt new file mode 100644 index 0000000000..13bd0d1e48 --- /dev/null +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/opentelemetry/trace/OtelTracerTest.kt @@ -0,0 +1,88 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.opentelemetry.trace + +import com.datadog.android.api.InternalLogger +import com.datadog.android.utils.forge.Configurator +import com.datadog.trace.bootstrap.instrumentation.api.AgentTracer +import com.datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import io.opentelemetry.api.trace.SpanBuilder +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class OtelTracerTest { + + private lateinit var testedTracer: OtelTracer + + @Mock + lateinit var mockDelegateTracer: TracerAPI + + @StringForgery + lateinit var fakeIntstrumentationName: String + + @StringForgery + lateinit var fakeSpanName: String + + @Mock + lateinit var mockLogger: InternalLogger + + @Mock + lateinit var mockDelegateSpanBuilder: AgentTracer.SpanBuilder + + // region Unit Tests + + @BeforeEach + fun `set up`() { + whenever(mockDelegateSpanBuilder.withResourceName(any())).thenReturn(mockDelegateSpanBuilder) + whenever(mockDelegateTracer.buildSpan(any(), any())).thenReturn(mockDelegateSpanBuilder) + testedTracer = OtelTracer(fakeIntstrumentationName, mockDelegateTracer, mockLogger) + } + + @Test + fun `M build a SpanBuilder W spanBuilder() {`() { + // When + val builder = testedTracer.spanBuilder(fakeSpanName) + + // Then + assertThat(builder).isInstanceOf(OtelSpanBuilder::class.java) + } + + @Test + fun `M decorate the SpanBuilder W spanBuilder() { decorator provided }{`() { + // Given + val mockDecoratedSpanBuilder: SpanBuilder = mock() + val decorator: (SpanBuilder) -> SpanBuilder = { mockDecoratedSpanBuilder } + testedTracer = OtelTracer(fakeIntstrumentationName, mockDelegateTracer, mockLogger, decorator) + + // When + val builder = testedTracer.spanBuilder(fakeSpanName) + + // Then + assertThat(builder).isSameAs(mockDecoratedSpanBuilder) + } + + // endregion +}