Android and Android TV Custom Instrumentation using the OpenTelemetry API
이 페이지는 아직 영어로 제공되지 않습니다. 번역 작업 중입니다.
현재 번역 프로젝트에 대한 질문이나 피드백이 있으신 경우
언제든지 연락주시기 바랍니다.개요
다음의 경우 OpenTelemetry API를 사용해 애플리케이션을 수동으로 계측해야 합니다.
- Datadog이 지원하는 라이브러리 계측을 사용하고 있지 않습니다.
ddtrace
라이브러리의 기능을 확장하고 싶습니다.- 애플리케이션 계측을 보다 세밀하게 제어해야 합니다.
ddtrace
라이브러리는 이러한 목표를 달성하는 데 도움이 됩니다. 다음 섹션에서는 커스텀 계측을 위해 Datadog과 OpenTelemetry API를 함께 사용하는 방법을 다룹니다.
Requirements and limitations
Setup
- 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]"
// ...
}
}
- 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);
}
}
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);
}
}
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)
- Configure and enable Trace feature:
val traceConfig = TraceConfiguration.Builder().build()
Trace.enable(traceConfig)
final TraceConfiguration traceConfig = TraceConfiguration.Builder().build();
Trace.enable(traceConfig);
- 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
val tracer = 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
final Tracer tracer = 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
or create your own instance and use it in your application code as needed.
- 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();
- (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();
- (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();
- (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)
- (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();
- (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();
- (Optional) RxJava
To provide a continuous trace inside a RxJava stream you need to follow the steps below:
- Add the OpenTelemetry for RxJava dependency into your project and follow the Readme file
for instructions. For example, for a continuous trace you would add:
- Then, in your project, open a scope when the Observable is subscribed and close it when it completes. Any span
created inside the stream operators is displayed inside this scope (parent Span):
var spanScope: Scope? = null
Single.fromSupplier { }
.subscribeOn(Schedulers.io())
.map {
val span = GlobalOpenTelemetry.get().getTracer("<TRACER_NAME>")
.spanBuilder("<YOUR_OP_NAME>")
.startSpan()
// ...
span.end()
}
.doOnSubscribe {
val span = GlobalOpenTelemetry.get().getTracer("<TRACER_NAME>")
.spanBuilder("<YOUR_OP_NAME>")
.startSpan()
spanScope = span.makeCurrent()
}
.doFinally {
Span.current()?.end()
spanScope?.close()
}
ThreadLocal<Scope> scopeStorage = new ThreadLocal<>();
// ...
Single.fromSupplier({})
.subscribeOn(Schedulers.io())
.map(data -> {
final Span span = GlobalOpenTelemetry.get().getTracer("<TRACER_NAME>")
.spanBuilder("<YOUR_OP_NAME>")
.startSpan();
// ...
span.end();
// ...
})
.doOnSubscribe(disposable -> {
final Span span = GlobalOpenTelemetry.get().getTracer("<TRACER_NAME>")
.spanBuilder("<YOUR_OP_NAME>")
.startSpan();
Scope spanScope = span.makeCurrent();
scopeStorage.set(spanScope);
})
.doFinally(() -> {
final Span activeSpan = Span.current();
if (activeSpan != null) {
activeSpan.end();
}
Scope spanScope = scopeStorage.get();
if (spanScope != null) {
spanScope.close();
scopeStorage.remove();
}
});
- (Optional) RxJava + Retrofit
For a continuous trace inside a RxJava stream that uses Retrofit for the network requests:
Retrofit.Builder()
.baseUrl("<YOUR_URL>")
.addCallAdapterFactory(RxJava3CallAdapterFactory.createSynchronous())
.client(okHttpClient)
.build()
new Retrofit.Builder()
.baseUrl("<YOUR_URL>")
.addCallAdapterFactory(RxJava3CallAdapterFactory.createSynchronous())
.client(okHttpClient)
.build();
- Open a scope around your Rx stream as follows:
var spanScope: Scope? = null
remoteDataSource.getData(query)
.subscribeOn(Schedulers.io())
.map {
// ...
}
.doOnSuccess {
localDataSource.persistData(it)
}
.doOnSubscribe {
val span = GlobalOpenTelemetry.get().getTracer("...")
.spanBuilder("<YOUR_OP_NAME>")
.startSpan()
spanScope = span.makeCurrent()
}
.doFinally {
Span.current()?.end()
spanScope?.close()
}
ThreadLocal<Scope> scopeStorage = new ThreadLocal<>();
// ...
remoteDataSource.getData(query)
.subscribeOn(Schedulers.io())
.map(data -> {
// ...
})
.doOnSuccess(data -> {
localDataSource.persistData(data);
})
.doOnSubscribe(disposable -> {
final Span span = GlobalOpenTelemetry.get().getTracer("...")
.spanBuilder("<YOUR_OP_NAME>")
.startSpan();
Scope spanScope = span.makeCurrent();
scopeStorage.set(spanScope);
})
.doFinally(() -> {
final Span activeSpan = Span.current();
if (activeSpan != null) {
activeSpan.end();
}
Scope spanScope = scopeStorage.get();
if (spanScope != null) {
spanScope.close();
scopeStorage.remove();
}
});
Further reading