Skip to content

Commit

Permalink
Merge pull request #2073 from DataDog/mconstantin/rum-3438/support-pa…
Browse files Browse the repository at this point in the history
…rent-otel-span-in-interceptor

RUM-3438 Add the OkHttp Otel extensions module
  • Loading branch information
mariusc83 authored Jun 7, 2024
2 parents ee23810 + eb5ffe8 commit 8805d24
Show file tree
Hide file tree
Showing 27 changed files with 534 additions and 8 deletions.
16 changes: 16 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,22 @@ publish:release-okhttp:
- export OSSRH_PASSWORD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.signing.ossrh_password --with-decryption --query "Parameter.Value" --out text)
- ./gradlew :integrations:dd-sdk-android-okhttp:publishToSonatype --stacktrace --no-daemon

publish:release-okhttp-otel:
tags: [ "arch:amd64" ]
only:
- tags
- develop
image: $CI_IMAGE_DOCKER
stage: publish
timeout: 30m
script:
- aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties
- export GPG_PRIVATE_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.signing.gpg_private_key --with-decryption --query "Parameter.Value" --out text)
- export GPG_PASSWORD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.signing.gpg_passphrase --with-decryption --query "Parameter.Value" --out text)
- export OSSRH_USERNAME=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.signing.ossrh_username --with-decryption --query "Parameter.Value" --out text)
- export OSSRH_PASSWORD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.signing.ossrh_password --with-decryption --query "Parameter.Value" --out text)
- ./gradlew :integrations:dd-sdk-android-okhttp-otel:publishToSonatype --stacktrace --no-daemon

publish:release-trace:
tags: [ "arch:amd64" ]
only:
Expand Down
2 changes: 2 additions & 0 deletions detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ datadog:
- "java.lang.System.arraycopy(kotlin.Any, kotlin.Int, kotlin.Any, kotlin.Int, kotlin.Int):java.lang.IndexOutOfBoundsException,java.lang.ArrayStoreException,java.lang.NullPointerException"
- "java.lang.System.loadLibrary(kotlin.String?):java.lang.SecurityException,java.lang.UnsatisfiedLinkError,java.lang.NullPointerException"
- "java.lang.reflect.Field.get(kotlin.Any):java.lang.IllegalAccessException,java.lang.IllegalArgumentException,java.lang.NullPointerException,java.lang.ExceptionInInitializerError"
- "java.math.BigInteger.constructor(kotlin.String?, kotlin.Int):java.lang.NumberFormatException,java.lang.ArithmeticException"
- "java.net.URL.constructor(kotlin.String?):java.net.MalformedURLException"
- "java.security.MessageDigest.getInstance(kotlin.String?):java.security.NoSuchAlgorithmException"
- "java.text.SimpleDateFormat.constructor(kotlin.String, java.util.Locale):java.lang.NullPointerException"
Expand Down Expand Up @@ -286,6 +287,7 @@ datadog:
- "okhttp3.Request.Builder.post(okhttp3.RequestBody):java.lang.NullPointerException,java.lang.IllegalArgumentException"
- "okhttp3.Request.Builder.method(kotlin.String, okhttp3.RequestBody?):java.lang.NullPointerException,java.lang.IllegalArgumentException"
- "okhttp3.Request.Builder.tag(java.lang.Class, io.opentracing.Span?):java.lang.NullPointerException"
- "okhttp3.Request.Builder.tag(java.lang.Class, com.datadog.android.okhttp.TraceContext?):java.lang.ClassCastException"
- "okhttp3.Request.Builder.url(kotlin.String):java.lang.NullPointerException,java.lang.IllegalArgumentException"
- "okhttp3.Interceptor.Chain.proceed(okhttp3.Request):java.io.IOException"
- "okhttp3.RequestBody.writeTo(okio.BufferedSink):java.io.IOException"
Expand Down
18 changes: 18 additions & 0 deletions integrations/dd-sdk-android-okhttp-otel/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Built application files
*.apk
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/

# Gradle files
build/
7 changes: 7 additions & 0 deletions integrations/dd-sdk-android-okhttp-otel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Datadog SDK OpenTelemetry extensions for OkHttp

See the dedicated [Datadog SDK instrumentation for OkHttp documentation][1] to learn how track network requests made by the `OkHttp` library automatically.
See the dedicated [Datadog SDK Android for OpenTelemetry documentation][2] to learn how to add a parent span to network requests made by the `OkHttp` library.

[1]: https://docs.datadoghq.com/real_user_monitoring/android/advanced_configuration/#automatically-track-network-requests
[2]: https://docs.datadoghq.com/tracing/trace_collection/custom_instrumentation/android?tab=kotlin
1 change: 1 addition & 0 deletions integrations/dd-sdk-android-okhttp-otel/api/apiSurface
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fun okhttp3.Request.Builder.addParentSpan(io.opentelemetry.api.trace.Span): okhttp3.Request.Builder
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public final class com/datadog/android/okhttp/otel/OkHttpExtKt {
public static final fun addParentSpan (Lokhttp3/Request$Builder;Lio/opentelemetry/api/trace/Span;)Lokhttp3/Request$Builder;
}

68 changes: 68 additions & 0 deletions integrations/dd-sdk-android-okhttp-otel/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.
*/

import com.datadog.gradle.config.androidLibraryConfig
import com.datadog.gradle.config.dependencyUpdateConfig
import com.datadog.gradle.config.javadocConfig
import com.datadog.gradle.config.junitConfig
import com.datadog.gradle.config.kotlinConfig
import com.datadog.gradle.config.publishingConfig
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
// Build
id("com.android.library")
kotlin("android")

// Publish
`maven-publish`
signing
id("org.jetbrains.dokka")

// Analysis tools
id("com.github.ben-manes.versions")

// Tests
id("org.jetbrains.kotlinx.kover")

// Internal Generation
id("thirdPartyLicences")
id("apiSurface")
id("transitiveDependencies")
id("binary-compatibility-validator")
}

android {
namespace = "com.datadog.android.okhttp.otel"
}

dependencies {
implementation(project(":integrations:dd-sdk-android-okhttp"))
implementation(project(":features:dd-sdk-android-trace-otel"))
implementation(libs.okHttp)
implementation(libs.kotlin)

testImplementation(project(":tools:unit")) {
attributes {
attribute(
com.android.build.api.attributes.ProductFlavorAttr.of("platform"),
objects.named("jvm")
)
}
}
testImplementation(libs.bundles.jUnit5)
testImplementation(libs.bundles.testTools)
testImplementation(libs.okHttpMock)
}

kotlinConfig(jvmBytecodeTarget = JvmTarget.JVM_11)
androidLibraryConfig()
junitConfig()
javadocConfig()
dependencyUpdateConfig()
publishingConfig(
"An OkHttp collection of extensions to be used in conjunction with OpenTelemetry Datadog SDK."
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.android.okhttp.otel

import com.datadog.android.okhttp.TraceContext
import com.datadog.trace.api.sampling.PrioritySampling
import io.opentelemetry.api.trace.Span
import okhttp3.Request

/**
* Add the current span context as the parent span for the distributed trace created around the Request.
* @param span the parent span to add to the request.
* @return the modified Request.Builder instance
*/
fun Request.Builder.addParentSpan(span: Span): Request.Builder {
val context = span.spanContext
val prioritySampling = if (context.isSampled) PrioritySampling.USER_KEEP.toInt() else PrioritySampling.UNSET.toInt()
@Suppress("UnsafeThirdPartyFunctionCall") // the context will always be a TraceContext
tag(TraceContext::class.java, TraceContext(context.traceId, context.spanId, prioritySampling))
return this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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.android.okhttp.otel

import com.datadog.android.okhttp.TraceContext
import com.datadog.tools.unit.forge.BaseConfigurator
import com.datadog.trace.api.sampling.PrioritySampling
import fr.xgouchet.elmyr.annotation.BoolForgery
import fr.xgouchet.elmyr.annotation.StringForgery
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
import fr.xgouchet.elmyr.junit5.ForgeExtension
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.SpanContext
import okhttp3.Request
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.mock
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class)
)
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(BaseConfigurator::class)
internal class OkHttpExtTest {

@StringForgery(regex = "[a-d0-9]{16}")
internal lateinit var fakeSpanId: String

@StringForgery(regex = "[a-d0-9]{32}")
internal lateinit var fakeTraceId: String

@BoolForgery
internal var fakeIsSampled: Boolean = false

@StringForgery(regex = "http://[a-z0-9_]{8}\\.[a-z]{3}/")
internal lateinit var fakeUrl: String

@Mock
internal lateinit var mockSpan: Span

private var expectedPrioritySampling: Int = 0

@BeforeEach
fun `set up`() {
val spanContext: SpanContext = mock {
on { spanId }.thenReturn(fakeSpanId)
on { traceId }.thenReturn(fakeTraceId)
on { isSampled }.thenReturn(fakeIsSampled)
}
expectedPrioritySampling =
if (fakeIsSampled) PrioritySampling.USER_KEEP.toInt() else PrioritySampling.UNSET.toInt()
whenever(mockSpan.spanContext).thenReturn(spanContext)
}

@Test
fun `M set the parentSpan through the Request builder W addParentSpan`() {
// When
val request = Request.Builder().url(fakeUrl).addParentSpan(mockSpan).build()

// Then
val taggedContext = request.tag(TraceContext::class.java)
checkNotNull(taggedContext)
assertThat(taggedContext.spanId).isEqualTo(fakeSpanId)
assertThat(taggedContext.traceId).isEqualTo(fakeTraceId)
assertThat(taggedContext.samplingPriority).isEqualTo(expectedPrioritySampling)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mock-maker-inline
14 changes: 14 additions & 0 deletions integrations/dd-sdk-android-okhttp-otel/transitiveDependencies
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Dependencies List

com.squareup.okhttp3:okhttp:4.11.0 : 768 Kb
com.squareup.okio:okio-jvm:3.2.0 : 337 Kb
io.opentelemetry:opentelemetry-api:1.4.0 : 78 Kb
io.opentelemetry:opentelemetry-context:1.4.0 : 42 Kb
org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 : 216 Kb
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 : 963 b
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 : 969 b
org.jetbrains.kotlin:kotlin-stdlib:1.8.22 : 1631 Kb
org.jetbrains:annotations:13.0 : 17 Kb

Total transitive dependencies size : 3 Mb

2 changes: 2 additions & 0 deletions integrations/dd-sdk-android-okhttp/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ open class com.datadog.android.okhttp.DatadogInterceptor : com.datadog.android.o
override fun onRequestIntercepted(com.datadog.android.api.feature.FeatureSdkCore, okhttp3.Request, io.opentracing.Span?, okhttp3.Response?, Throwable?)
override fun canSendSpan(): Boolean
override fun onSdkInstanceReady(com.datadog.android.core.InternalSdkCore)
data class com.datadog.android.okhttp.TraceContext
constructor(String, String, Int)
fun okhttp3.Request.Builder.parentSpan(io.opentracing.Span): okhttp3.Request.Builder
interface com.datadog.android.okhttp.trace.TracedRequestListener
fun onRequestIntercepted(okhttp3.Request, io.opentracing.Span, okhttp3.Response?, Throwable?)
Expand Down
15 changes: 15 additions & 0 deletions integrations/dd-sdk-android-okhttp/api/dd-sdk-android-okhttp.api
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ public class com/datadog/android/okhttp/DatadogInterceptor : com/datadog/android
protected fun onRequestIntercepted (Lcom/datadog/android/api/feature/FeatureSdkCore;Lokhttp3/Request;Lio/opentracing/Span;Lokhttp3/Response;Ljava/lang/Throwable;)V
}

public final class com/datadog/android/okhttp/TraceContext {
public fun <init> (Ljava/lang/String;Ljava/lang/String;I)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()I
public final fun copy (Ljava/lang/String;Ljava/lang/String;I)Lcom/datadog/android/okhttp/TraceContext;
public static synthetic fun copy$default (Lcom/datadog/android/okhttp/TraceContext;Ljava/lang/String;Ljava/lang/String;IILjava/lang/Object;)Lcom/datadog/android/okhttp/TraceContext;
public fun equals (Ljava/lang/Object;)Z
public final fun getSamplingPriority ()I
public final fun getSpanId ()Ljava/lang/String;
public final fun getTraceId ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/datadog/android/okhttp/trace/OkHttpRequestExtKt {
public static final fun parentSpan (Lokhttp3/Request$Builder;Lio/opentracing/Span;)Lokhttp3/Request$Builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.android.okhttp

import com.datadog.android.lint.InternalApi

/**
* The context of a trace to be propagated through the OkHttp requests for Datadog tracing.
*/
@InternalApi
data class TraceContext(
/**
* The trace id.
*/
val traceId: String,
/**
* The span id.
*/
val spanId: String,
/**
* The sampling priority.
*/
val samplingPriority: Int
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.android.okhttp.internal.otel

import com.datadog.android.okhttp.TraceContext
import com.datadog.opentracing.propagation.ExtractedContext
import io.opentracing.SpanContext
import java.math.BigInteger

private const val BASE_16_RADIX = 16

internal fun TraceContext.toOpenTracingContext(): SpanContext {
val traceIdAsBigInteger = parseToBigInteger(traceId)
val spanIdAsBigInteger = parseToBigInteger(spanId)
return ExtractedContext(
traceIdAsBigInteger,
spanIdAsBigInteger,
samplingPriority,
null,
emptyMap(),
emptyMap()
)
}

@Suppress("SwallowedException")
private fun parseToBigInteger(value: String): BigInteger {
// just in case but theoretically it should never happen as we are controlling the way the ID is
// generated in the CoreTracer.
return try {
BigInteger(value, BASE_16_RADIX)
} catch (e: NumberFormatException) {
BigInteger.ZERO
} catch (e: ArithmeticException) {
BigInteger.ZERO
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeReso
import com.datadog.android.core.internal.utils.loggableStackTrace
import com.datadog.android.core.sampling.RateBasedSampler
import com.datadog.android.core.sampling.Sampler
import com.datadog.android.okhttp.TraceContext
import com.datadog.android.okhttp.internal.otel.toOpenTracingContext
import com.datadog.android.trace.AndroidTracer
import com.datadog.android.trace.TracingHeaderType
import com.datadog.legacy.trace.api.DDTags
Expand Down Expand Up @@ -432,6 +434,7 @@ internal constructor(

private fun extractParentContext(tracer: Tracer, request: Request): SpanContext? {
val tagContext = request.tag(Span::class.java)?.context()
?: request.tag(TraceContext::class.java)?.toOpenTracingContext()

val headerContext = tracer.extract(
Format.Builtin.TEXT_MAP_EXTRACT,
Expand Down
Loading

0 comments on commit 8805d24

Please sign in to comment.