OpenTelemetry API를 사용한 Android 및 Android TV 커스텀 계측
{% alert level="info" %} Datadog에서 OpenTelemetry를 언제 사용해야 할지 잘 모르겠나요? OpenTelemetry API로 커스텀 계측부터 시작해 자세히 알아보세요. {% /alert %}
출처: 문서
본문
개요
OpenTelemetry API로 애플리케이션을 수동 계측해야 하는 이유는 몇 가지가 있어요:
- Datadog 지원 라이브러리 계측을 사용하고 있지 않을 때
ddtrace라이브러리의 기능을 확장하고 싶을 때- 애플리케이션 계측을 더 세밀하게 제어해야 할 때
ddtrace 라이브러리는 이러한 목표를 달성하는 데 도움이 되는 여러 기법을 제공해요. 다음 섹션에서는 Datadog에서 사용할 커스텀 계측에 OpenTelemetry API를 활용하는 방법을 보여줘요.
요구 사항 및 제한 사항
- 2.11.0+ 버전부터 dd-sdk-android-trace와 dd-sdk-android-trace-otel 의존성을 다운로드해야 해요
설정
- 애플리케이션 모듈의
build.gradle파일에 Android Trace와 Android Trace OpenTelemetry 의존성을 추가하세요:
android {
//(...)
}
dependencies {
implementation "com.datadoghq:dd-sdk-android-trace:x.x.x"
implementation "com.datadoghq:dd-sdk-android-trace-otel:x.x.x"
//(...)
}
참고: Android API 레벨 24 미만을 대상으로 한다면 build.gradle 파일에 다음 줄을 추가해 desugaring을 활성화하세요:
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
// ...
}
dependencies {
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:[latest_version]"
// ...
}
}
애플리케이션 컨텍스트, 추적 동의(tracking consent), Datadog 클라이언트 토큰으로 Datadog SDK를 초기화하세요. 보안상의 이유로 Datadog SDK 구성에는 Datadog API 키가 아니라 클라이언트 토큰을 사용해야 해요.
{% callout %}
다음 Datadog 사이트 사용자를 위한 중요 참고 사항: app.datadoghq.com
{% tab title="Kotlin" %}
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)
}
}
{% /tab %}
{% tab title="Java" %}
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);
}
}
{% /tab %}
{% /callout %}
{% callout %}
다음 Datadog 사이트 사용자를 위한 중요 참고 사항: app.datadoghq.eu
{% tab title="Kotlin" %}
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)
}
}
{% /tab %}
{% tab title="Java" %}
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);
}
}
{% /tab %}
{% /callout %}
{% callout %}
다음 Datadog 사이트 사용자를 위한 중요 참고 사항: us3.datadoghq.com
{% tab title="Kotlin" %}
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)
}
}
{% /tab %}
{% tab title="Java" %}
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);
}
}
{% /tab %}
{% /callout %}
{% callout %}
다음 Datadog 사이트 사용자를 위한 중요 참고 사항: us5.datadoghq.com
{% tab title="Kotlin" %}
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)
}
}
{% /tab %}
{% tab title="Java" %}
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);
}
}
{% /tab %}
{% /callout %}
{% callout %}
다음 Datadog 사이트 사용자를 위한 중요 참고 사항: app.ddog-gov.com
{% tab title="Kotlin" %}
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)
}
}
{% /tab %}
{% tab title="Java" %}
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);
}
}
{% /tab %}
{% /callout %}
{% callout %}
다음 Datadog 사이트 사용자를 위한 중요 참고 사항: us2.ddog-gov.com
{% tab title="Kotlin" %}
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
val configuration = Configuration.Builder(
clientToken = <CLIENT_TOKEN>,
env = <ENV_NAME>,
variant = <APP_VARIANT_NAME>
)
.useSite(DatadogSite.US2_FED)
.build()
Datadog.initialize(this, configuration, trackingConsent)
}
}
{% /tab %}
{% tab title="Java" %}
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.US2_FED)
.build();
Datadog.initialize(this, configuration, trackingConsent);
}
}
{% /tab %}
{% /callout %}
{% callout %}
다음 Datadog 사이트 사용자를 위한 중요 참고 사항: ap1.datadoghq.com
{% tab title="Kotlin" %}
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)
}
}
{% /tab %}
{% tab title="Java" %}
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);
}
}
{% /tab %}
{% /callout %}
{% callout %}
다음 Datadog 사이트 사용자를 위한 중요 참고 사항: ap2.datadoghq.com
{% tab title="Kotlin" %}
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)
}
}
{% /tab %}
{% tab title="Java" %}
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);
}
}
{% /tab %}
{% /callout %}
{% callout %}
다음 Datadog 사이트 사용자를 위한 중요 참고 사항: uk1.datadoghq.com
{% tab title="Kotlin" %}
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
val configuration = Configuration.Builder(
clientToken = <CLIENT_TOKEN>,
env = <ENV_NAME>,
variant = <APP_VARIANT_NAME>
)
.useSite(DatadogSite.UK1)
.build()
Datadog.initialize(this, configuration, trackingConsent)
}
}
{% /tab %}
{% tab title="Java" %}
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.UK1)
.build();
Datadog.initialize(this, configuration, trackingConsent);
}
}
{% /tab %}
{% /callout %}
GDPR을 준수하려면 SDK는 초기화 시 추적 동의 값을 요구해요. 추적 동의는 다음 값 중 하나일 수 있어요 (추적 동의 참고):
TrackingConsent.PENDING: SDK는 데이터 수집과 일괄 처리(batch)를 시작하지만 데이터 수집 엔드포인트로는 보내지 않아요. SDK는 새 추적 동의 값을 기다렸다가 일괄 처리된 데이터를 어떻게 처리할지 결정해요.TrackingConsent.GRANTED: SDK는 데이터 수집을 시작하고 데이터 수집 엔드포인트로 보내요.TrackingConsent.NOT_GRANTED: SDK는 어떤 데이터도 수집하지 않아요. 로그, 트레이스, RUM 이벤트를 수동으로 보낼 수 없어요.
SDK 초기화 후 추적 동의를 갱신하려면 Datadog.setTrackingConsent(<NEW CONSENT>)를 호출하세요. SDK는 새 동의에 따라 동작을 변경해요. 예를 들어 현재 추적 동의가 TrackingConsent.PENDING이고 다음으로 갱신한다면:
TrackingConsent.GRANTED: SDK는 현재 일괄 처리된 모든 데이터와 향후 데이터를 데이터 수집 엔드포인트로 직접 보내요.TrackingConsent.NOT_GRANTED: SDK는 일괄 처리된 모든 데이터를 삭제하고 향후 데이터를 수집하지 않아요.
isInitialized 유틸리티 메서드를 사용해 SDK가 제대로 초기화되었는지 확인하세요:
if (Datadog.isInitialized()) {
// your code here
}
애플리케이션을 작성할 때 setVerbosity 메서드를 호출해 개발 로그를 활성화할 수 있어요. 라이브러리의 모든 내부 메시지 중 제공된 레벨보다 높거나 같은 우선순위의 메시지는 Android의 Logcat에 기록돼요:
Datadog.setVerbosity(Log.INFO)
Trace 기능을 구성하고 활성화하세요: {% tab title="Kotlin" %}
val traceConfig = TraceConfiguration.Builder().build()
Trace.enable(traceConfig)
{% /tab %}
{% tab title="Java" %}
final TraceConfiguration traceConfig = TraceConfiguration.Builder().build();
Trace.enable(traceConfig);
{% /tab %}
Datadog SDK는 OpenTelemetry 표준을 구현해요. onCreate() 메서드에서 OtelTracerProvider를 만들고 GlobalOpenTelemetry에 OpenTelemetrySdk를 등록하세요:
{% tab title="Kotlin" %}
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>")
{% /tab %}
{% tab title="Java" %}
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>");
{% /tab %}
참고: GlobalOpenTelemetry.set API가 프로세스당 한 번만 호출되도록 하세요. 그렇지 않으면 TracerProvider를 만들어 프로젝트에서 싱글톤으로 사용할 수 있어요.
참고: setService 메서드는 트레이서 프로바이더의 서비스 이름을 설정하는 데 사용돼요. 이 서비스 이름은 Datadog UI에서 애플리케이션을 식별하는 데 사용돼요. GlobalOpenTelemetry를 사용해 TracerProvider의 단일 인스턴스를 보유하거나, 필요에 따라 애플리케이션 코드에서 자체 인스턴스를 만들어 사용할 수 있어요.
OpenTelemetry API로 코드를 계측하세요: {% tab title="Kotlin" %}
val span = tracer.spanBuilder(spanName = "<span_name>").startSpan()
// do something you want to measure ...
// ... then, when the operation is finished:
span.end()
{% /tab %}
{% tab title="Java" %}
final Span span = tracer.spanBuilder("<span_name>").startSpan();
// do something you want to measure ...
// ... then, when the operation is finished:
span.end();
{% /tab %}
(선택 사항) span 사이의 부모-자식 관계를 설정하세요: {% tab title="Kotlin" %}
val childSpan = tracer.spanBuilder(spanName = "response decoding")
.setParent(Context.current().with(parentSpan)) // make it child of parent span
.startSpan()
// ... do your logic here ...
childSpan.end()
{% /tab %}
{% tab title="Java" %}
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();
{% /tab %}
(선택 사항) span과 함께 추가 속성을 제공하세요: {% tab title="Kotlin" %}
tracer.spanBuilder(spanName = "<span_name>").setAttribute(key = "<key_name>", value = <key_value>).startSpan()
{% /tab %}
{% tab title="Java" %}
tracer.spanBuilder("<span_name>").setAttribute("<key_name>", <key_value>).startSpan();
{% /tab %}
(선택 사항) span에 오류를 첨부하세요: {% tab title="Kotlin" %}
span.setStatus(StatusCode.ERROR, description = "<error_description>")
// or if you want to set an exception
span.recordException(exception)
{% /tab %}
{% tab title="Java" %}
span.setStatus(StatusCode.ERROR, "<error_description>");
// or if you want to set an exception
span.recordException(exception)
{% /tab %}
(선택 사항) span에 span 링크를 추가하세요: {% tab title="Kotlin" %}
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()
{% /tab %}
{% tab title="Java" %}
final Span linkedSpan = tracer.spanBuilder("linked span").startSpan();
linkedSpan.end();
final Span spanWithLinks = tracer.spanBuilder("span with links")
.addLink(linkedSpan.getSpanContext())
.startSpan();
spanWithLinks.end();
{% /tab %}
(선택 사항) RUM의 OkHttp 요청 주변에서 생성된 span에 로컬 부모 span을 추가하세요: 먼저 프로젝트 의존성에 OpenTelemetry OkHttp 확장 모듈을 추가해야 해요:
android {
//(...)
}
dependencies {
implementation "com.datadoghq:dd-sdk-android-okhttp:x.x.x"
implementation "com.datadoghq:dd-sdk-android-okhttp-otel:x.x.x"
//(...)
}
OkHttp Request를 만든 후 요청에 부모 span을 첨부할 수 있어요:
{% tab title="Kotlin" %}
val parentSpan = tracer.spanBuilder(spanName = "parent span").startSpan()
parentSpan.end()
val request = Request.Builder()
.url("<URL>")
.addParentSpan(parentSpan)
.build()
{% /tab %}
{% tab title="Java" %}
final Span parentSpan = tracer.spanBuilder("parent span").startSpan();
parentSpan.end();
final Request request = new Request.Builder()
.url("<URL>")
.addParentSpan(parentSpan)
.build();
{% /tab %}
(선택 사항) RxJava RxJava 스트림 안에서 연속적인 트레이스를 제공하려면 다음 단계를 따르세요:
- OpenTelemetry for RxJava 의존성을 프로젝트에 추가하고 Readme 파일의 지침을 따르세요. 예를 들어 연속 트레이스를 위해 다음을 추가하면 돼요:
TracingAssembly.enable() - 그런 다음 프로젝트에서 Observable이 구독될 때 scope를 열고 완료될 때 닫으세요. 스트림 연산자 안에서 생성된 모든 span은 이 scope(부모 span) 안에 표시돼요:
{% tab title="Kotlin" %}
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()
}
{% /tab %}
{% tab title="Java" %}
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();
}
});
{% /tab %}
(선택 사항) RxJava + Retrofit 네트워크 요청에 Retrofit을 사용하는 RxJava 스트림 안에서 연속 트레이스를 위해:
- Datadog Interceptor를 구성하세요
- 네트워크 요청에 동기 Observable을 사용하려면 Retrofit RxJava 어댑터를 사용하세요:
{% tab title="Kotlin" %}
Retrofit.Builder()
.baseUrl("<YOUR_URL>")
.addCallAdapterFactory(RxJava3CallAdapterFactory.createSynchronous())
.client(okHttpClient)
.build()
{% /tab %}
{% tab title="Java" %}
new Retrofit.Builder()
.baseUrl("<YOUR_URL>")
.addCallAdapterFactory(RxJava3CallAdapterFactory.createSynchronous())
.client(okHttpClient)
.build();
{% /tab %}
- Rx 스트림 주변에서 다음과 같이 scope를 여세요:
{% tab title="Kotlin" %}
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()
}
{% /tab %}
{% tab title="Java" %}
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();
}
});
{% /tab %}
더 알아보기 (Learn more)
추가로 도움이 되는 문서, 링크, 아티클: