Envía trazas a Datadog desde tus aplicaciones Android con la biblioteca de rastreo del cliente dd-sdk-android-trace de Datadog y aprovecha las siguientes características:

  • Crea tramos (spans) personalizados para las operaciones de tu aplicación.
  • Añade context y atributos personalizados adicionales a cada tramo enviado.
  • Uso de red optimizado con envíos masivos automáticos.
Datadog charges for ingested and indexed spans sent from your Android applications, but does not charge for the underlying devices. Learn more in the APM billing documentation.

The Datadog Tracer implements the OpenTelemetry standard, and Datadog recommends using it as an interface for tracing your application because it’s vendor-neutral, supports many languages and frameworks, and unifies traces, metrics, and logs under one standard. See instructions on setting up OpenTelemetry integration with the SDK.

Note: The OpenTelemetry specification library requires desugaring to be enabled for projects with a minSdk < 26. If you cannot enable desugaring in your project, you can still use the Trace product with the Datadog API instead.

Note: The Datadog API implementation helps you transition from OpenTracing to OpenTelemetry.

Configuración

  1. Añade la dependencia de Gradle declarando la biblioteca como dependencia en tu archivo build.gradle:
dependencies {
    implementation "com.datadoghq:dd-sdk-android-trace:x.x.x"
}
  1. Inicializa el SDK de Datadog con el contexto de tu aplicación, el consentimiento de seguimiento y el token de cliente Datadog. Por motivos de seguridad, debes utilizar un token de cliente: no puedes utilizar claves de API Datadog para configurar el SDK de Datadog, ya que quedaría expuesto al cliente en el código de bytes APK de la aplicación Android. Para obtener más información sobre cómo configurar un token de cliente, consulta la documentación sobre tokens de cliente:

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);
  }
}

class SampleApplication : Application() {
  override fun onCreate() {
    super.onCreate()
    val configuration = Configuration.Builder(
      clientToken = "<CLIENT_TOKEN>",
      env = "<ENV_NAME>",
      variant = "<APP_VARIANT_NAME>"
    ).useSite(DatadogSite.AP2)
      .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.AP2)
      .build();

    Datadog.initialize(this, configuration, trackingConsent);
  }
}

Para cumplir con la normativa del RGPD, el SDK requiere el valor de consentimiento de seguimiento durante la inicialización. El consentimiento de seguimiento puede ser uno de los siguientes valores:

  • TrackingConsent.PENDING: El SDK comienza a recopilar y procesar en lotes los datos, pero no los envía al endpoint de recopilación de datos. El SDK espera el nuevo valor de consentimiento de seguimiento para decidir qué hacer con los datos en lotes.
  • TrackingConsent.GRANTED: El SDK comienza a recopilar los datos y los envía al endpoint de recopilación de datos.
  • TrackingConsent.NOT_GRANTED: El SDK no recopila ningún dato. No podrás enviar logs, trazas o eventos RUM manualmente. evento de RUM.

Para actualizar el consentimiento de seguimiento después de inicializar el SDK, llama a Datadog.setTrackingConsent(<NEW CONSENT>). El SDK cambia de comportamiento de acuerdo con el nuevo consentimiento. Por ejemplo, si el consentimiento de seguimiento actual es TrackingConsent.PENDING y lo actualizas a:

  • TrackingConsent.GRANTED: El SDK envía todos los datos actuales en lotes y los datos futuros directamente al endpoint de recopilación de datos.
  • TrackingConsent.NOT_GRANTED: El SDK elimina todos los datos en lotes y no recopila ningún dato futuro.

Nota: En las credenciales necesarias para la inicialización, tu nombre de variante de aplicación también es necesario y debe utilizar tu valor BuildConfig.FLAVOR (o una cadena vacía si no tienes variantes). Esto es importante, ya que permite que el archivo mapping.txt ProGuard correcto se cargue automáticamente en tiempo de compilación para poder ver las trazas de stack tecnológico de errores RUM desofuscadas. Para obtener más información, consulta la guía para cargar archivos de asignación de fuentes de Android.

Utiliza el método de utilidad isInitialized para comprobar si el SDK está correctamente inicializado:

if (Datadog.isInitialized()) {
  // tu código aquí
}

Cuando escribas tu aplicación, puedes habilitar logs de desarrollo llamando al método setVerbosity. Todos los mensajes internos en la biblioteca con una prioridad igual o superior al nivel proporcionado se registran en el Logcat de Android:

Datadog.setVerbosity(Log.INFO)
  1. Configura y habilita la función de rastreo:
val traceConfig = TraceConfiguration.Builder().build()
Trace.enable(traceConfig)
TraceConfiguration traceConfig = new TraceConfiguration.Builder().build();
Trace.enable(traceConfig);
  1. Configura y registra el DatadogTracer. Solo tendrás que hacerlo una vez, normalmente en el método onCreate() de tu aplicación:
import com.datadog.android.trace.GlobalDatadogTracer
import com.datadog.android.trace.DatadogTracing

GlobalDatadogTracer.registerIfAbsent(
    DatadogTracing.newTracerBuilder()
        .build()
)
import com.datadog.android.trace.GlobalDatadogTracer;
import com.datadog.android.trace.DatadogTracing;

GlobalDatadogTracer.registerIfAbsent(
    DatadogTracing.newTracerBuilder(Datadog.getInstance())
        .build()
);
  1. (Opcional) - Configura el umbral de descarga parcial para optimizar la carga de trabajo del SDK en función del número de tramos que genere tu aplicación. La biblioteca espera hasta que el número de tramos terminados exceda el umbral, antes de escribirlos al disco. Configurar este valor como 1 escribe cada tramo tan pronto como termina.
val tracer = DatadogTracing.newTracerBuilder()
    .withPartialFlushMinSpans(10)
    .build()
DatadogTracer tracer = DatadogTracing.newTracerBuilder(Datadog.getInstance())
    .withPartialFlushMinSpans(10)
    .build();
  1. Inicia un tramo personalizado utilizando el siguiente método:
val tracer = GlobalDatadogTracer.get()
val span = tracer.buildSpan("<SPAN_NAME>").start()
// Do something ...
// ...
// Then when the span should be closed
span.finish()
DatadogTracer tracer = GlobalDatadogTracer.get();
DatadogSpan span = tracer.buildSpan("<SPAN_NAME>").start();
// Do something ...
// ...
// Then when the span should be closed
span.finish();
  1. Para utilizar contextos en llamadas síncronas:
val span = tracer.buildSpan("<SPAN_NAME1>").start()
try {
    val scope = tracer.activateSpan(span)
    scope?.use {
        // Do something ...
        // ...
        // Start a new Scope
        val childSpan = tracer.buildSpan("<SPAN_NAME2>").start()
        try {
            val innerScope = tracer.activateSpan(childSpan).use { innerScope ->

            }
        } catch (e: Throwable) {
            childSpan.logThrowable(e)
        } finally {
            childSpan.finish()
        }
    }
} catch (e: Error) {
}
DatadogSpan span = tracer.buildSpan("<SPAN_NAME1>").start();
try {
    DatadogScope scope = tracer.activateSpan(span);
    try {
        // Do something ...
        // ...
        // Start a new Scope
        DatadogSpan childSpan = tracer.buildSpan("<SPAN_NAME2>").start();
        try {
            DatadogScope innerScope = tracer.activateSpan(childSpan);
            try {
                // Do something ...
            } finally {
                innerScope.close();
            }
        } catch (Throwable e) {
            childSpan.logThrowable(e);
        } finally {
            childSpan.finish();
        }
    } finally {
        scope.close();
    }
} catch (Error e) {
}
  1. Para utilizar contextos en llamadas asíncronas:
val span = tracer.buildSpan("<SPAN_NAME1>").start()
try {
    val scope = tracer.activateSpan(span)
    scope.use {
        // Do something ...
        Thread {
            // Step 2: reactivate the Span in the worker thread
            tracer.activateSpan(span).use {
                // Do something ...
            }
        }.start()
    }
} catch(e: Throwable) {
    span.logThrowable(e)
} finally {
    span.finish()
}
DatadogSpan span = tracer.buildSpan("<SPAN_NAME1>").start();
try {
    DatadogScope scope = tracer.activateSpan(span);
    try {
        // Do something ...
        new Thread(() -> {
            // Step 2: reactivate the Span in the worker thread
            DatadogScope scopeContinuation = tracer.activateSpan(span);
            try {
                // Do something
            } finally {
                scope.close();
            }
        }).start();
    } finally {
        scope.close();
    }
} catch (Throwable e){
    span.logThrowable(e);
} finally {
    span.finish();
}
  1. (Opcional) Para distribuir manualmente trazas entre tus entornos, por ejemplo frontend a backend:

    a. Inyecta contexto del rastreador en la solicitud del cliente.

val tracer = GlobalDatadogTracer.get()
val span = tracer.buildSpan("<SPAN_NAME>").start()
val tracedRequestBuilder = Request.Builder()
tracer.propagate().inject<Request.Builder?>(
    span.context(),
    tracedRequestBuilder
) { builder, key, value ->
    builder?.addHeader(key, value)
}
val request = tracedRequestBuilder.build()
// Dispatch the request and finish the span after.
DatadogTracer tracer = GlobalDatadogTracer.get();
DatadogSpan span = tracer.buildSpan("<SPAN_NAME>").start();
Request.Builder tracedRequestBuilder = new Request.Builder();
tracer.propagate().inject(
    span.context(),
    tracedRequestBuilder,
    new Function3<Request.Builder,String,String,Unit>(){
        @Override
        public Unit invoke(Request.Builder builder, String key, String value) {
          builder.addHeader(key, value);
          return Unit.INSTANCE;
        }
    }
);
Request request = tracedRequestBuilder.build();
// Dispatch the request and finish the span after.

b. Extrae el contexto de rastreador del cliente de las cabeceras en el código del servidor.

val tracer = GlobalDatadogTracer.get()
val extractedContext = tracer.propagate()
    .extract(request) { carrier, classifier ->
        val headers = carrier.headers.toMultimap()
            .map { it.key to it.value.joinToString(";") }
            .toMap()

        for ((key, value) in headers) classifier(key, value)
    }

val serverSpan = tracer.buildSpan("<SERVER_SPAN_NAME>").withParentContext(extractedContext).start()
DatadogTracer tracer = GlobalDatadogTracer.get();
DatadogSpanContext extractedContext = tracer.propagate()
  .extract(request,
    new Function2<Request, Function2<? super String, ? super String, Boolean>, Unit>() {
      @Override
      public Unit invoke(
        Request carrier,
        Function2<? super String, ? super String, Boolean> classifier
      ) {
        request.headers().forEach(pair -> {
          String key = pair.component1();
          String value = pair.component2();

          classifier.invoke(key, value);
        });

        return Unit.INSTANCE;
      }
    });
DatadogSpan serverSpan = tracer.buildSpan("<SERVER_SPAN_NAME>").withParentContext(extractedContext).start();

Nota: Para las bases de código que utilizan el cliente OkHttp, Datadog proporciona la siguiente implementación.

  1. (Opcional) Para proporcionar etiquetas (tags) adicionales junto a tu tramo:
span.setTag("http.url", url)
  1. (Opcional) Para marcar un tramo como que tiene un error, regístralo utilizando los métodos correspondientes:
span.logThrowable(throwable)
span.logErrorMessage(message)
  1. Si necesitas modificar algunos atributos en eventos de tu tramo antes de la colocación en lotes, puedes hacerlo proporcionando una implementación de SpanEventMapper al habilitar la función de rastreo:
val traceConfig = TraceConfiguration.Builder()
  // ...
  .setEventMapper(spanEventMapper)
  .build()
TraceConfiguration config = new TraceConfiguration.Builder()
  // ...
  .setEventMapper(spanEventMapper)
  .build();

Extensiones de Kotlin

Ejecución de una Lambda dentro de un tramo

Para monitorizar el rendimiento de una Lambda dada, puedes utilizar el método withinSpan(). Por defecto, se creará un contexto para el tramo, pero puedes deshabilitar este comportamiento configurando el parámetro activate como false (falso).

import com.datadog.android.trace.withinSpan
import com.datadog.android.trace.api.span.DatadogSpan

withinSpan("<SPAN_NAME>", parentSpan, activate) {
   // Your code here
}

Rastreo de transacciones SQLite

Si estás utilizando SQLiteDatabase para conservar datos localmente, puedes rastrear la transacción de la base de datos utilizando el siguiente método:

import com.datadog.android.trace.sqlite.transactionTraced
import android.database.sqlite.SQLiteDatabase

sqliteDatabase.transactionTraced("<SPAN_NAME>", isExclusive) { database ->
  // Your queries here
  database.insert("<TABLE_NAME>", null, contentValues)

  // Decorate the Span
  setTag("<TAG_KEY>", "<TAG_VALUE>")
}

Se comporta como el método SQLiteDatabase.transaction proporcionado en el paquete de Android core-ktx y solo requiere un nombre de operación de tramo.

Integraciones

Además del rastreo manual, el SDK de Datadog proporciona las siguientes integraciones.

OkHttp

Si quieres rastrear tus solicitudes OkHttp, puedes añadir el interceptor proporcionado (que puedes encontrar en la biblioteca dd-sdk-android-okhttp) de la siguiente manera:

  1. Añade la dependencia de Gradle a la biblioteca dd-sdk-android-okhttp en el archivo build.gradle a nivel de módulo:
dependencies {
  implementation "com.datadoghq:dd-sdk-android-okhttp:x.x.x"
}
  1. Añade DatadogInterceptor a tu OkHttpClient:
val tracedHosts = listOf("example.com", "example.eu")
val okHttpClient = OkHttpClient.Builder()
  .addInterceptor(
    DatadogInterceptor.Builder(tracedHosts)
      .setTraceSampler(RateBasedSampler(20f))
      .build()
  )
  .build()
List<String> tracedHosts = Arrays.asList("example.com", "example.eu");
OkHttpClient okHttpClient = new OkHttpClient.Builder()
  .addInterceptor(
    new DatadogInterceptor.Builder(tracedHosts)
      .setTraceSampler(new RateBasedSampler(20f))
      .build()
  )
  .build();

Esto crea un tramo alrededor de cada solicitud procesada por el cliente OkHttp (coincidente con los hosts proporcionados), con toda la información relevante rellenada automáticamente (URL, método, código de estado, error), y propaga la información de rastreo a tu backend para obtener una traza unificada en Datadog.

Las trazas de red se muestrean con una frecuencia de muestreo ajustable. Por defecto se aplica un muestreo del 100%.

El interceptor rastrea solicitudes a nivel de la aplicación. Para obtener más detalles, también puedes añadir un TracingInterceptor a nivel de la red, por ejemplo cuando se siguen redirecciones.

val tracedHosts = listOf("example.com", "example.eu")
val okHttpClient =  OkHttpClient.Builder()
  .addInterceptor(
    DatadogInterceptor.Builder(tracedHosts)
      .setTraceSampler(RateBasedSampler(20f))
      .build()
  )
  .addNetworkInterceptor(
    TracingInterceptor.Builder(tracedHosts)
      .setTraceSampler(RateBasedSampler(100f))
      .build()
  )
  .build()
List<String> tracedHosts = Arrays.asList("example.com", "example.eu");
OkHttpClient okHttpClient = new OkHttpClient.Builder()
  .addInterceptor(
    new DatadogInterceptor.Builder(tracedHosts)
      .setTraceSampler(new RateBasedSampler(20f))
      .build()
  )
  .addNetworkInterceptor(
    new TracingInterceptor.Builder(tracedHosts)
      .setTraceSampler(new RateBasedSampler(20f))
      .build()
  )
  .build();

En este caso, la decisión de muestreo de tazas tomada por el interceptor ascendente para una solicitud concreta será respetada por el interceptor descendente.

Debido a la forma en que se ejecuta la solicitud OkHttp (utilizando un pool de threads), el tramo de la solicitud no se vinculará automáticamente con el tramo que ha activado la solicitud. Puedes proporcionar un tramo principal manualmente en el OkHttp Request.Builder, de la siguiente manera, mediante el método de extensión Request.Builder.parentSpan:

val request = Request.Builder()
  .url(requestUrl)
  .parentSpan(parentSpan)
  .build()
Request.Builder requestBuilder = new Request.Builder()
  .url(requestUrl)

Request request = OkHttpRequestExtKt
  .parentSpan(requestBuilder, parentSpan)
  .build();

Nota:

  • Si se utilizan varios interceptores, éste debe llamarse en primer lugar.
  • Si defines tipos de cabecera de rastreo personalizados en la configuración de Datadog y estás utilizando un rastreador registrado en GlobalDatadogTracer, asegúrate de que se definen los mismos tipos de cabecera de rastreo para el rastreador en uso.

Recopilación de lotes

Todos los tramos se almacenan primero en el dispositivo local por lotes. Cada lote sigue la especificación de admisión. Se envían en cuanto la red se encuentra disponible y la batería es lo suficientemente alta como para garantizar que el SDK de Datadog no afecte a la experiencia del usuario final. Si la red no se encuentra disponible mientras la aplicación está en primer plano, o si falla una carga de datos, el lote se guarda hasta que pueda enviarse correctamente.

Esto significa que aunque los usuarios abran tu aplicación estando desconectados, no se perderá ningún dato.

Los datos en disco se descartarán automáticamente si son demasiado antiguos, para garantizar que el SDK no utiliza demasiado espacio en disco.

Inicialización

Los siguientes métodos de DatadogTracerBuilder pueden utilizarse al inicializar el DatadogTracer:

MétodoDescripción
withServiceName(<SERVICE_NAME>)Configura el valor del service.
withPartialFlushMinSpans(<INT>)Cuando se alcanza este umbral (tienes una cantidad específica <INT> de tramos cerrados en espera), se activa el mecanismo de descarga y todos los tramos cerrados pendientes se procesan y se envían para su admisión.
withTag(<KEY>, <VALUE>)Configura un par de etiquetas <KEY>:<VALUE> que se añadirán a tramos creados por el rastreador.
setBundleWithRumEnabled(true)Configura como true para permitir que los tramos se enriquezcan con la información de la vista RUM actual. Esto te permite ver todos los tramos generados durante la vida útil de una vista específica en el Explorador RUM.
withSampleRate(<FLOAT>)Configura un valor 0-100 para definir el porcentaje de trazas a recopilar.
withTracingHeadersTypes(Set<TracingHeaderType>)Define los estilos de cabeceras de rastreo que pueden ser inyectados por el rastreador.
setTraceRateLimit(<INT>)Define el límite de frecuencia de rastreo. Es el número máximo de trazas por segundo que se aceptarán.

Referencias adicionales