Share via


분산 추적 계측 추가

이 문서의 적용 대상: ✔️ .NET Core 2.1 이상 버전 ✔️ .NET Framework 4.5 이상 버전

System.Diagnostics.Activity API를 통해 .NET 애플리케이션을 계측하여 분산 추적 원격 분석을 생성할 수 있습니다. 일부 계측은 표준 .NET 라이브러리에 기본 제공되지만, 코드를 더 쉽게 진단할 수 있도록 더 많은 계측을 추가하려고 할 수 있습니다. 이 자습서에서는 새 사용자 지정 분산 추적 계측을 추가합니다. 이 계측에서 생성된 원격 분석을 기록하는 방법에 관한 자세한 내용은 컬렉션 자습서를 참조하세요.

필수 조건

초기 앱 만들기

먼저 OpenTelemetry를 사용하여 원격 분석을 수집하지만 아직 계측이 없는 샘플 앱을 만듭니다.

dotnet new console

.NET 5 이상을 대상으로 하는 애플리케이션에는 이미 필요한 분산 추적 API가 포함되어 있습니다. 이전 .NET 버전을 대상으로 하는 앱의 경우 System.Diagnostics.DiagnosticSource NuGet 패키지 버전 5 이상을 추가합니다.

dotnet add package System.Diagnostics.DiagnosticSource

원격 분석을 수집하는 데 사용할 OpenTelemetryOpenTelemetry.Exporter.Console NuGet 패키지를 추가합니다.

dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.Console

생성된 Program.cs의 콘텐츠를 다음 예제 소스로 바꿉니다.

using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System;
using System.Threading.Tasks;

namespace Sample.DistributedTracing
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using var tracerProvider = Sdk.CreateTracerProviderBuilder()
                .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MySample"))
                .AddSource("Sample.DistributedTracing")
                .AddConsoleExporter()
                .Build();

            await DoSomeWork("banana", 8);
            Console.WriteLine("Example work done");
        }

        // All the functions below simulate doing some arbitrary work
        static async Task DoSomeWork(string foo, int bar)
        {
            await StepOne();
            await StepTwo();
        }

        static async Task StepOne()
        {
            await Task.Delay(500);
        }

        static async Task StepTwo()
        {
            await Task.Delay(1000);
        }
    }
}

앱에 아직 계측이 없으므로 표시할 추적 정보가 없습니다.

> dotnet run
Example work done

모범 사례

이 예제에서 OpenTelemetry와 같은 분산 추적 원격 분석을 수집하기 위해 앱 개발자만 선택적 타사 라이브러리를 참조해야 합니다. .NET 라이브러리 작성자는 .NET 런타임에 포함된 System.Diagnostics.DiagnosticSource의 API만 사용할 수 있습니다. 이렇게 하면 원격 분석 수집에 사용할 라이브러리나 공급업체에 관한 앱 개발자의 기본 설정과 관계없이 라이브러리는 광범위한 .NET 앱에서 실행됩니다.

기본 계측 추가

애플리케이션 및 라이브러리는 System.Diagnostics.ActivitySourceSystem.Diagnostics.Activity 클래스를 사용하여 분산 추적 계측을 추가합니다.

ActivitySource

먼저 ActivitySource의 인스턴스를 만듭니다. ActivitySource는 Activity 개체를 만들고 시작하는 API를 제공합니다. Main() 위에 정적 ActivitySource 변수를 추가하고 using 문에 using System.Diagnostics;를 추가합니다.

using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System;
using System.Diagnostics;
using System.Threading.Tasks;

namespace Sample.DistributedTracing
{
    class Program
    {
        private static ActivitySource source = new ActivitySource("Sample.DistributedTracing", "1.0.0");

        static async Task Main(string[] args)
        {
            ...

모범 사례

  • ActivitySource를 한 번 만들고 정적 변수에 저장하고 필요에 따라 해당 인스턴스를 사용합니다. 각 라이브러리 또는 라이브러리 하위 구성 요소는 자체 소스를 만들 수 있으며 만들어야 하는 경우도 있습니다. 앱 개발자가 소스의 활동 원격 분석을 독립적으로 사용 및 사용 안 함으로 설정하는 기능의 진가를 알아본다면 기존 소스를 다시 사용하는 것보다 새 소스를 만드는 것이 좋습니다.

  • 생성자에 전달된 소스 이름은 다른 소스와의 충돌을 방지하기 위해 고유해야 합니다. 같은 어셈블리 내에 여러 소스가 있는 경우 어셈블리 이름과 선택적으로 구성 요소 이름을 포함하는 계층 구조 이름을 사용합니다(예: Microsoft.AspNetCore.Hosting). 어셈블리가 두 번째 독립 어셈블리에서 코드의 계측을 추가하는 경우 이름은 해당 코드가 계측되고 있는 어셈블리가 아니라 ActivitySource를 정의하는 어셈블리를 기반으로 해야 합니다.

  • 버전 매개 변수는 선택 사항입니다. 여러 버전의 라이브러리를 릴리스하고 계측된 원격 분석을 변경하는 경우 버전을 제공하는 것이 좋습니다.

참고 항목

OpenTelemetry는 대체 용어 ‘추적 프로그램’ 및 ‘범위’를 사용합니다. .NET ‘ActivitySource’는 추적 프로그램의 구현이며 활동은 ‘범위’의 구현입니다. .NET의 활동 형식 long은 OpenTelemetry 사양보다 오래되었으며 원본 .NET 이름 지정은 .NET 에코시스템 및 .NET 애플리케이션 호환성 내에서 일관성을 위해 유지되었습니다.

활동

ActivitySource 개체를 사용하여 의미 있는 작업 단위를 중심으로 Activity 개체를 시작하고 중지합니다. DoSomeWork()를 여기에 표시된 코드로 업데이트합니다.

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                await StepOne();
                await StepTwo();
            }
        }

이제 앱을 실행하면 로그되는 새 활동이 표시됩니다.

> dotnet run
Activity.Id:          00-f443e487a4998c41a6fd6fe88bae644e-5b7253de08ed474f-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:36:51.4720202Z
Activity.Duration:    00:00:01.5025842
Resource associated with Activity:
    service.name: MySample
    service.instance.id: 067f4bb5-a5a8-4898-a288-dec569d6dbef

주의

  • ActivitySource.StartActivity는 동시에 활동을 만들고 시작합니다. 나열된 코드 패턴에서는 블록을 실행한 후 만든 Activity 개체를 자동으로 삭제하는 using 블록을 사용합니다. Activity 개체를 삭제하면 개체가 중지되므로 코드가 Activity.Stop()을 명시적으로 호출할 필요가 없습니다. 그러면 코딩 패턴이 단순해집니다.

  • ActivitySource.StartActivity는 활동을 기록하는 수신기가 있는지 내부적으로 확인합니다. 등록된 수신기가 없거나 필요하지 않은 수신기가 있는 경우 StartActivity()null을 반환하며 Activity 개체를 만드는 것을 방지합니다. 이는 자주 호출되는 함수에서 코드 패턴이 계속 사용될 수 있도록 하는 성능 최적화입니다.

선택 사항: 태그 채우기

활동은 진단에 유용할 수 있는 작업의 모든 매개 변수를 저장하는 데 일반적으로 사용되는 태그라는 키-값 데이터를 지원합니다. DoSomeWork()를 업데이트하여 태그를 포함합니다.

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                activity?.SetTag("foo", foo);
                activity?.SetTag("bar", bar);
                await StepOne();
                await StepTwo();
            }
        }
> dotnet run
Activity.Id:          00-2b56072db8cb5a4496a4bfb69f46aa06-7bc4acda3b9cce4d-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:37:31.4949570Z
Activity.Duration:    00:00:01.5417719
Activity.TagObjects:
    foo: banana
    bar: 8
Resource associated with Activity:
    service.name: MySample
    service.instance.id: 25bbc1c3-2de5-48d9-9333-062377fea49c

Example work done

모범 사례

  • 앞에서 설명한 대로 ActivitySource.StartActivity에서 반환된 activity는 null일 수 있습니다. C#의 null 병합 연산자 ?.activity가 null이 아닌 경우에만 Activity.SetTag를 호출하는 편리한 줄임 표기입니다. 동작은 쓰기와 동일합니다.
if(activity != null)
{
    activity.SetTag("foo", foo);
}
  • OpenTelemetry는 일반적인 유형의 애플리케이션 작업을 나타내는 활동에서 태그를 설정하기 위한 권장 규칙 세트를 제공합니다.

  • 고성능 요구 사항이 있는 함수를 계측하는 경우 Activity.IsAllDataRequested는 활동을 수신 대기하는 코드가 태그와 같은 보조 정보를 읽으려고 하는지를 나타내는 힌트입니다. 수신기가 보조 정보를 읽지 않는 경우 계측된 코드가 보조 정보를 채우는 데 CPU 주기를 사용할 필요가 없습니다. 편의상 이 샘플에서는 해당 최적화를 적용하지 않습니다.

선택 사항: 이벤트 추가

이벤트는 추가 진단 데이터의 임의 스트림을 활동에 연결할 수 있는 타임스탬프 메시지입니다. 활동에 일부 이벤트를 추가합니다.

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                activity?.SetTag("foo", foo);
                activity?.SetTag("bar", bar);
                await StepOne();
                activity?.AddEvent(new ActivityEvent("Part way there"));
                await StepTwo();
                activity?.AddEvent(new ActivityEvent("Done now"));
            }
        }
> dotnet run
Activity.Id:          00-82cf6ea92661b84d9fd881731741d04e-33fff2835a03c041-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:39:10.6902609Z
Activity.Duration:    00:00:01.5147582
Activity.TagObjects:
    foo: banana
    bar: 8
Activity.Events:
    Part way there [3/18/2021 10:39:11 AM +00:00]
    Done now [3/18/2021 10:39:12 AM +00:00]
Resource associated with Activity:
    service.name: MySample
    service.instance.id: ea7f0fcb-3673-48e0-b6ce-e4af5a86ce4f

Example work done

모범 사례

  • 이벤트는 전송될 수 있을 때까지 메모리 내 목록에 저장되므로 이 메커니즘은 적당한 수의 이벤트를 기록하는 경우에만 적합합니다. 대량 또는 무제한 양의 이벤트의 경우 ILogger와 같이 이 작업에 초점을 맞춘 로깅 API를 사용하는 것이 더 좋습니다. 또한 ILogger를 사용하면 앱 개발자가 분산 추적을 사용하도록 선택하는지와 관계없이 로깅 정보를 사용할 수 있습니다. ILogger는 활성 활동 ID를 자동으로 캡처하므로 해당 API를 통해 로그된 메시지는 여전히 분산 추적과 상호 연결될 수 있습니다.

선택 사항: 상태 추가

OpenTelemetry를 사용하면 각 활동이 작업의 성공/실패 결과를 나타내는 상태를 보고할 수 있습니다. .NET에는 현재 이 목적을 위한 강력한 형식의 API가 없지만 태그를 사용하여 설정된 규칙이 있습니다.

  • otel.status_codeStatusCode를 저장하는 데 사용되는 태그 이름입니다. StatusCode 태그 값은 문자열 “UNSET”, “OK” 또는 “ERROR” 중 하나여야 하며 각 문자열은 열거형 Unset, Ok, Error에 해당합니다.
  • otel.status_description은 선택적 Description을 저장하는 데 사용되는 태그 이름입니다.

DoSomeWork()를 업데이트하여 상태를 설정합니다.

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                activity?.SetTag("foo", foo);
                activity?.SetTag("bar", bar);
                await StepOne();
                activity?.AddEvent(new ActivityEvent("Part way there"));
                await StepTwo();
                activity?.AddEvent(new ActivityEvent("Done now"));

                // Pretend something went wrong
                activity?.SetTag("otel.status_code", "ERROR");
                activity?.SetTag("otel.status_description", "Use this text give more information about the error");
            }
        }

선택 사항: 다른 활동 추가

활동을 중첩하여 더 큰 작업 단위 부분을 설명할 수 있습니다. 이 기능은 빠르게 실행되지 않을 수 있는 코드 부분의 경우 또는 특정 외부 종속성에서 발생하는 오류를 더 효과적으로 지역화하려는 경우 유용할 수 있습니다. 이 샘플에서는 모든 메서드에서 활동을 사용하지만 이는 단지 추가 코드가 최소화되었기 때문입니다. 모든 메서드에서 활동을 사용하는 더 크고 더 현실적인 프로젝트는 극히 자세한 추적을 생성하므로 이 기능이 권장되지 않습니다.

StepOne 및 StepTwo를 업데이트하여 다음 개별 단계를 중심으로 더 많은 추적을 추가합니다.

        static async Task StepOne()
        {
            using (Activity activity = source.StartActivity("StepOne"))
            {
                await Task.Delay(500);
            }
        }

        static async Task StepTwo()
        {
            using (Activity activity = source.StartActivity("StepTwo"))
            {
                await Task.Delay(1000);
            }
        }
> dotnet run
Activity.Id:          00-9d5aa439e0df7e49b4abff8d2d5329a9-39cac574e8fda44b-01
Activity.ParentId:    00-9d5aa439e0df7e49b4abff8d2d5329a9-f16529d0b7c49e44-01
Activity.DisplayName: StepOne
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:40:51.4278822Z
Activity.Duration:    00:00:00.5051364
Resource associated with Activity:
    service.name: MySample
    service.instance.id: e0a8c12c-249d-4bdd-8180-8931b9b6e8d0

Activity.Id:          00-9d5aa439e0df7e49b4abff8d2d5329a9-4ccccb6efdc59546-01
Activity.ParentId:    00-9d5aa439e0df7e49b4abff8d2d5329a9-f16529d0b7c49e44-01
Activity.DisplayName: StepTwo
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:40:51.9441095Z
Activity.Duration:    00:00:01.0052729
Resource associated with Activity:
    service.name: MySample
    service.instance.id: e0a8c12c-249d-4bdd-8180-8931b9b6e8d0

Activity.Id:          00-9d5aa439e0df7e49b4abff8d2d5329a9-f16529d0b7c49e44-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:40:51.4256627Z
Activity.Duration:    00:00:01.5286408
Activity.TagObjects:
    foo: banana
    bar: 8
    otel.status_code: ERROR
    otel.status_description: Use this text give more information about the error
Activity.Events:
    Part way there [3/18/2021 10:40:51 AM +00:00]
    Done now [3/18/2021 10:40:52 AM +00:00]
Resource associated with Activity:
    service.name: MySample
    service.instance.id: e0a8c12c-249d-4bdd-8180-8931b9b6e8d0

Example work done

StepOne 및 StepTwo에는 둘 다 SomeWork를 참조하는 ParentId가 포함됩니다. 콘솔은 중첩된 작업 트리를 멋지게 시각화하는 것이 아니며 Zipkin과 같은 많은 GUI 뷰어는 이를 Gantt 차트로 표시할 수 있습니다.

Zipkin Gantt chart

선택 사항: ActivityKind

활동에는 활동, 해당 부모, 해당 자식 간 관계를 설명하는 Activity.Kind 속성이 있습니다. 기본적으로 모든 새 활동은 Internal로 설정되며, 이 설정은 원격 부모 또는 자식이 없는 애플리케이션 내 내부 작업인 활동에 적합합니다. 다른 종류는 ActivitySource.StartActivity에서 kind 매개 변수를 사용하여 설정할 수 있습니다. 다른 옵션의 경우 System.Diagnostics.ActivityKind를 참조하세요.

작업이 일괄 처리 시스템에서 발생하는 경우 단일 활동은 동시에 여러 요청을 대신하여 작업을 나타낼 수 있습니다. 각 요청에는 각각 자체 추적 ID가 있습니다. 활동은 하나의 부모가 있도록 제한되지만, System.Diagnostics.ActivityLink를 사용하여 추가 추적 ID에 연결할 수 있습니다. 각 ActivityLink는 연결되는 활동에 관한 ID 정보를 저장하는 ActivityContext로 채워집니다. ActivityContext는 Activity.Context를 사용하여 In-process Activity 개체에서 검색하거나, ActivityContext.Parse(String, String)를 사용하여 직렬화된 ID 정보에서 구문 분석할 수 있습니다.

void DoBatchWork(ActivityContext[] requestContexts)
{
    // Assume each context in requestContexts encodes the trace-id that was sent with a request
    using(Activity activity = s_source.StartActivity(name: "BigBatchOfWork",
                                                     kind: ActivityKind.Internal,
                                                     parentContext: default,
                                                     links: requestContexts.Select(ctx => new ActivityLink(ctx))
    {
        // do the batch of work here
    }
}

요청 시 추가할 수 있는 이벤트 및 태그와는 달리 링크는 StartActivity() 중에 추가해야 하며 나중에 변경할 수 없습니다.

Important

OpenTelemetry 사양에 따르면 링크 수는 기본적으로 128개로 제한됩니다.