Android and Android TV Custom Instrumentation using OpenTelemetry API

Unsure when to use OpenTelemetry with Datadog? Start with Custom Instrumentation with the OpenTelemetry API to learn more.

Overview

There are a few reasons to manually instrument your applications with the OpenTelemetry API:

  • You are not using Datadog supported library instrumentation.
  • You want to extend the ddtrace library’s functionality.
  • You need finer control over instrumenting your applications.

The ddtrace library provides several techniques to help you achieve these goals. The following sections demonstrate how to use the OpenTelemetry API for custom instrumentation to use with Datadog.

Requirements and limitations

Setup

  1. Add Android Trace and Android Trace OpenTelemetry dependencies to your application module’s build.gradle file:
android {
    //(...)
}
dependencies {
    implementation "com.datadoghq:dd-sdk-android-trace:x.x.x"
    implementation "com.datadoghq:dd-sdk-android-trace-otel:x.x.x" 
    //(...)
}

Note: If you are targeting Android API level lower than 24, enable desugaring by adding the following lines to your build.gradle file:

android {

    compileOptions {
        isCoreLibraryDesugaringEnabled = true
        // ...
    }

    dependencies {
        coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:[latest_version]"
        // ...
    }
}
  1. Initialize Datadog SDK with your application context, tracking consent, and Datadog client token. For security reasons, you must use a client token, not Datadog API keys, to configure Datadog SDK.

class SampleApplication : Application() {
     override fun onCreate() {
         super.onCreate()
         val configuration = Configuration.Builder(
             clientToken = <CLIENT_TOKEN>,
             env = <ENV_NAME>,
             variant = <APP_VARIANT_NAME>
         ).build()
         Datadog.initialize(this, configuration, trackingConsent)
     }
 }
public class SampleApplication extends Application {
     @Override
     public void onCreate() {
         super.onCreate();
         Configuration configuration =
                 new Configuration.Builder(<CLIENT_TOKEN>, <ENV_NAME>, <APP_VARIANT_NAME>)
                         .build();
         Datadog.initialize(this, configuration, trackingConsent);
     }
 }

class SampleApplication : Application() {
     override fun onCreate() {
         super.onCreate()
         val configuration = Configuration.Builder(
                 clientToken = <CLIENT_TOKEN>,
                 env = <ENV_NAME>,
                 variant = <APP_VARIANT_NAME>
             )
             .useSite(DatadogSite.EU1)
             .build()
         Datadog.initialize(this, configuration, trackingConsent)
     }
 }
public class SampleApplication extends Application {
     @Override
     public void onCreate() {
         super.onCreate();
         Configuration configuration =
                 new Configuration.Builder(<CLIENT_TOKEN>, <ENV_NAME>, <APP_VARIANT_NAME>)
                         .useSite(DatadogSite.EU1)
                         .build();
         Datadog.initialize(this, configuration, trackingConsent);
     }
 }

class SampleApplication : Application() {
     override fun onCreate() {
         super.onCreate()
         val configuration = Configuration.Builder(
                 clientToken = <CLIENT_TOKEN>,
                 env = <ENV_NAME>,
                 variant = <APP_VARIANT_NAME>
             )
             .useSite(DatadogSite.US3)
             .build()
         Datadog.initialize(this, configuration, trackingConsent)
     }
 }
public class SampleApplication extends Application {
     @Override
     public void onCreate() {
         super.onCreate();
         Configuration configuration =
                 new Configuration.Builder(<CLIENT_TOKEN>, <ENV_NAME>, <APP_VARIANT_NAME>)
                         .useSite(DatadogSite.US3)
                         .build();
         Datadog.initialize(this, configuration, trackingConsent);
     }
 }

class SampleApplication : Application() {
     override fun onCreate() {
         super.onCreate()
         val configuration = Configuration.Builder(
                 clientToken = <CLIENT_TOKEN>,
                 env = <ENV_NAME>,
                 variant = <APP_VARIANT_NAME>
             )
             .useSite(DatadogSite.US5)
             .build()
         Datadog.initialize(this, configuration, trackingConsent)
     }
 }
public class SampleApplication extends Application {
     @Override
     public void onCreate() {
         super.onCreate();
         Configuration configuration =
                 new Configuration.Builder(<CLIENT_TOKEN>, <ENV_NAME>, <APP_VARIANT_NAME>)
                         .useSite(DatadogSite.US5)
                         .build();
         Datadog.initialize(this, configuration, trackingConsent);
     }
 }

class SampleApplication : Application() {
     override fun onCreate() {
         super.onCreate()
         val configuration = Configuration.Builder(
                 clientToken = <CLIENT_TOKEN>,
                 env = <ENV_NAME>,
                 variant = <APP_VARIANT_NAME>
             )
             .useSite(DatadogSite.US1_FED)
             .build()
         Datadog.initialize(this, configuration, trackingConsent)
     }
 }
public class SampleApplication extends Application {
     @Override
     public void onCreate() {
         super.onCreate();
         Configuration configuration =
                 new Configuration.Builder(<CLIENT_TOKEN>, <ENV_NAME>, <APP_VARIANT_NAME>)
                         .useSite(DatadogSite.US1_FED)
                         .build();
         Datadog.initialize(this, configuration, trackingConsent);
     }
 }

class SampleApplication : Application() {
     override fun onCreate() {
         super.onCreate()
         val configuration = Configuration.Builder(
                 clientToken = <CLIENT_TOKEN>,
                 env = <ENV_NAME>,
                 variant = <APP_VARIANT_NAME>
             )
             .useSite(DatadogSite.AP1)
             .build()
         Datadog.initialize(this, configuration, trackingConsent)
     }
 }
public class SampleApplication extends Application {
     @Override
     public void onCreate() {
         super.onCreate();
         Configuration configuration =
                 new Configuration.Builder(<CLIENT_TOKEN>, <ENV_NAME>, <APP_VARIANT_NAME>)
                         .useSite(DatadogSite.AP1)
                         .build();
         Datadog.initialize(this, configuration, trackingConsent);
     }
 }

To be GDPR compliant, the SDK requires the tracking consent value at initialization. The tracking consent can be one of the following values see Tracking Consent:

  • TrackingConsent.PENDING: The SDK starts collecting and batching the data but does not send it to the data collection endpoint. The SDK waits for the new tracking consent value to decide what to do with the batched data.
  • TrackingConsent.GRANTED: The SDK starts collecting the data and sends it to the data collection endpoint.
  • TrackingConsent.NOT_GRANTED: The SDK does not collect any data. You will not be able to manually send any logs, traces, or RUM events.

To update the tracking consent after the SDK is initialized, call: Datadog.setTrackingConsent(<NEW CONSENT>). The SDK changes its behavior according to the new consent. For example, if the current tracking consent is TrackingConsent.PENDING and you update it to:

  • TrackingConsent.GRANTED: The SDK sends all current batched data and future data directly to the data collection endpoint.
  • TrackingConsent.NOT_GRANTED: The SDK wipes all batched data and does not collect any future data.

Use the utility method isInitialized to check if the SDK is properly initialized:

 if (Datadog.isInitialized()) {
     // your code here
 }

When writing your application, you can enable development logs by calling the setVerbosity method. All internal messages in the library with a priority equal to or higher than the provided level are then logged to Android’s Logcat:

Datadog.setVerbosity(Log.INFO)
  1. Configure and enable Trace feature:
val traceConfig = TraceConfiguration.Builder().build()
Trace.enable(traceConfig)
final TraceConfiguration traceConfig = TraceConfiguration.Builder().build();
Trace.enable(traceConfig);
  1. Datadog tracer implements the OpenTelemetry standard. Create OtelTracerProvider and register OpenTelemetrySdk in GlobalOpenTelemetry in your onCreate() method:
GlobalOpenTelemetry.set(object : OpenTelemetry {
    private val tracerProvider = OtelTracerProvider.Builder()
        .setService([BuildConfig.APPLICATION_ID])
        .build()

    override fun getTracerProvider(): TracerProvider {
        return tracerProvider
    }

    override fun getPropagators(): ContextPropagators {
        return ContextPropagators.noop()
    }
})
// and later on if you want to access the tracer provider
val tracerProvider = GlobalOpenTelemetry.get().getTracer(instrumentationName = "<instrumentation_name>")
GlobalOpenTelemetry.set(new OpenTelemetry() {
    private final TracerProvider tracerProvider = new OtelTracerProvider.Builder()
            .setService(BuildConfig.APPLICATION_ID)
            .build();

    @Override
    public TracerProvider getTracerProvider() {
        return tracerProvider;
    }

    @Override
    public ContextPropagators getPropagators() {
        return ContextPropagators.noop();
    }
};
// and later on if you want to access the tracer provider
final TracerProvider tracerProvider = GlobalOpenTelemetry.get().getTracer("<instrumentation_name>");       

Note: Ensure GlobalOpenTelemetry.set API is only called once per process. Otherwise, you can create a TracerProvider and use it as a singleton in your project.

Note: The setService method is used to set the service name for the tracer provider. The service name is used to identify the application in the Datadog UI. You can either use the GlobalOpenTelemetry to hold a single instance of the TracerProvider create your own instance and use it in your application code as needed.

  1. Instrument your code with the OpenTelemetry API:
val span = tracer.spanBuilder(spanName = "<span_name>").startSpan()
// do something you want to measure ...
// ... then, when the operation is finished:
span.end()
final Span span = tracer.spanBuilder("<span_name>").startSpan();
// do something you want to measure ...
// ... then, when the operation is finished:
span.end();
  1. (Optional) Set child-parent relationship between your spans:
let childSpan = tracer.spanBuilder(spanName = "response decoding")
    .setParent(Context.current().with(parentSpan)) // make it child of parent span
    .startSpan()

// ... do your logic here ...
childSpan.end()
final Span childSpan = tracer.spanBuilder("<span_name>")
    .setParent(Context.current().with(parentSpan)) // make it child of parent span
    .startSpan();

// ... do your logic here ...
childSpan.end();
  1. (Optional) Provide additional attributes alongside your span:
tracer.spanBuilder(spanName = "<span_name>").setAttribute(key = "<key_name>", value = <key_value>).startSpan()
tracer.spanBuilder("<span_name>").setAttribute("<key_name>", <key_value>).startSpan();
  1. (Optional) Attach an error to a span:
span.setStatus(StatusCode.ERROR, description = "<error_description>")

// or if you want to set an exception

span.recordException(exception)
span.setStatus(StatusCode.ERROR, "<error_description>")

// or if you want to set an exception

span.recordException(exception)
  1. (Optional) Add span links to your span:
val linkedSpan = tracer.spanBuilder(spanName = "linked span").startSpan()
linkedSpan.end()

val spanWithLinks = tracer.spanBuilder(spanName = "span with links")
    .addLink(spanContext = linkedSpan.spanContext)
    .startSpan()
spanWithLinks.end()
final Span linkedSpan = tracer.spanBuilder("linked span").startSpan();
linkedSpan.end();

final Span spanWithLinks = tracer.spanBuilder("span with links")
        .addLink(linkedSpan.getSpanContext())
        .startSpan();
spanWithLinks.end();
  1. (Optional) Add local parent span to the span generated around the OkHttp request in RUM:

First, you need to add the OpenTelemetry OkHttp extension module to your project dependencies:

android {
    //(...)
}
dependencies {
    implementation "com.datadoghq:dd-sdk-android-okhttp:x.x.x"
    implementation "com.datadoghq:dd-sdk-android-okhttp-otel:x.x.x"
    //(...)
}

After you create an OkHttp Request, you can attach a parent span to the request:

val parentSpan = tracer.spanBuilder(spanName = "parent span").startSpan()
parentSpan.end()
val request = Request.Builder()
    .url("<URL>")
    .addParentSpan(parentSpan)
    .build()
final Span parentSpan = tracer.spanBuilder("parent span").startSpan();
parentSpan.end()
final Request:request = new Request.Builder()
    .url("<URL>")
    .addParentSpan(parentSpan)
    .build();

Further reading