본문 바로가기
WIKI 기술 지식 베이스

OpenTelemetry API를 사용한 Android 및 Android TV 커스텀 계측

원문 보기 위키 갱신

{% alert level="info" %} Datadog에서 OpenTelemetry를 언제 사용해야 할지 잘 모르겠나요? OpenTelemetry API로 커스텀 계측부터 시작해 자세히 알아보세요. {% /alert %}

출처: 문서

본문

개요

OpenTelemetry API로 애플리케이션을 수동 계측해야 하는 이유는 몇 가지가 있어요:

  • Datadog 지원 라이브러리 계측을 사용하고 있지 않을 때
  • ddtrace 라이브러리의 기능을 확장하고 싶을 때
  • 애플리케이션 계측을 더 세밀하게 제어해야 할 때

ddtrace 라이브러리는 이러한 목표를 달성하는 데 도움이 되는 여러 기법을 제공해요. 다음 섹션에서는 Datadog에서 사용할 커스텀 계측에 OpenTelemetry API를 활용하는 방법을 보여줘요.

요구 사항 및 제한 사항

설정

  1. 애플리케이션 모듈의 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 스트림 안에서 연속적인 트레이스를 제공하려면 다음 단계를 따르세요:

  1. OpenTelemetry for RxJava 의존성을 프로젝트에 추가하고 Readme 파일의 지침을 따르세요. 예를 들어 연속 트레이스를 위해 다음을 추가하면 돼요:
    TracingAssembly.enable()
    
  2. 그런 다음 프로젝트에서 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)

추가로 도움이 되는 문서, 링크, 아티클: