Partilhar via


Adicionando instrumentação de rastreamento distribuído

Este artigo aplica-se a: ✔️ .NET Core 2.1 e versões posteriores .NET Framework 4.5 e versões ✔️ posteriores

Os aplicativos .NET podem ser instrumentados usando a API para produzir telemetria System.Diagnostics.Activity de rastreamento distribuído. Alguma instrumentação é incorporada em bibliotecas .NET padrão, mas você pode querer adicionar mais para tornar seu código mais facilmente diagnosticável. Neste tutorial, você adicionará uma nova instrumentação de rastreamento distribuído personalizada. Consulte o tutorial da coleção para saber mais sobre como gravar a telemetria produzida por esta instrumentação.

Pré-requisitos

Criar aplicativo inicial

Primeiro, você criará um aplicativo de exemplo que coleta telemetria usando OpenTelemetry, mas ainda não tem nenhuma instrumentação.

dotnet new console

Os aplicativos destinados ao .NET 5 e posteriores já têm as APIs de rastreamento distribuído necessárias incluídas. Para aplicativos destinados a versões mais antigas do .NET, adicione o pacote NuGet System.Diagnostics.DiagnosticSource versão 5 ou superior.

dotnet add package System.Diagnostics.DiagnosticSource

Adicione os pacotes OpenTelemetry e OpenTelemetry.Exporter.Console NuGet, que serão usados para coletar a telemetria.

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

Substitua o conteúdo do Programa gerado.cs por esta fonte de exemplo:

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

O aplicativo ainda não tem instrumentação, portanto, não há informações de rastreamento para exibir:

> dotnet run
Example work done

Melhores práticas

Somente os desenvolvedores de aplicativos precisam fazer referência a uma biblioteca de terceiros opcional para coletar a telemetria de rastreamento distribuída, como OpenTelemetry neste exemplo. Os autores da biblioteca .NET podem confiar exclusivamente em APIs em System.Diagnostics.DiagnosticSource, que faz parte do tempo de execução do .NET. Isso garante que as bibliotecas serão executadas em uma ampla variedade de aplicativos .NET, independentemente das preferências do desenvolvedor do aplicativo sobre qual biblioteca ou fornecedor usar para coletar telemetria.

Adicionar instrumentação básica

Aplicativos e bibliotecas adicionam instrumentação de rastreamento distribuído usando as System.Diagnostics.ActivitySource classes and System.Diagnostics.Activity .

ActivitySource

Primeiro, crie uma instância de ActivitySource. ActivitySource fornece APIs para criar e iniciar objetos Activity. Adicione a variável estática ActivitySource acima de Main() e using System.Diagnostics; às instruções using.

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)
        {
            ...

Melhores práticas

  • Crie o ActivitySource uma vez, armazene-o em uma variável estática e use essa instância o tempo necessário. Cada biblioteca ou subcomponente de biblioteca pode (e muitas vezes deve) criar sua própria fonte. Considere criar uma nova fonte em vez de reutilizar uma existente se você antecipar que os desenvolvedores de aplicativos gostariam de poder habilitar e desabilitar a telemetria de atividade nas fontes de forma independente.

  • O nome de origem passado para o construtor tem que ser exclusivo para evitar os conflitos com quaisquer outras fontes. Se houver várias fontes dentro do mesmo assembly, use um nome hierárquico que contenha o nome do assembly e, opcionalmente, um nome de componente, por exemplo, Microsoft.AspNetCore.Hosting. Se um assembly estiver adicionando instrumentação para código em um segundo assembly independente, o nome deve ser baseado no assembly que define o ActivitySource, não no assembly cujo código está sendo instrumentado.

  • O parâmetro version é opcional. Recomendamos que você forneça a versão caso libere várias versões da biblioteca e faça alterações na telemetria instrumentada.

Nota

OpenTelemetry usa termos alternativos 'Tracer' e 'Span'. No .NET 'ActivitySource' é a implementação do Tracer e Activity é a implementação do 'Span'. . O tipo de atividade da NET é muito anterior à especificação OpenTelemetry e a nomenclatura original do .NET foi preservada para consistência dentro do ecossistema .NET e compatibilidade de aplicativos .NET.

Atividade

Use o objeto ActivitySource para iniciar e parar objetos Activity em torno de unidades significativas de trabalho. Atualize DoSomeWork() com o código mostrado aqui:

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

A execução do aplicativo agora mostra a nova atividade sendo registrada:

> 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

Notas

  • ActivitySource.StartActivity cria e inicia a atividade ao mesmo tempo. O padrão de código listado está usando o bloco, que descarta automaticamente o using objeto Activity criado após a execução do bloco. Eliminar o objeto Activity irá pará-lo para que o código não precise chamar Activity.Stop()explicitamente . Isso simplifica o padrão de codificação.

  • ActivitySource.StartActivity determina internamente se há ouvintes gravando a Atividade. Se não houver ouvintes registrados ou se houver ouvintes que não estejam interessados, StartActivity() retornarão null e evitarão criar o objeto Activity. Esta é uma otimização de desempenho para que o padrão de código ainda possa ser usado em funções que são chamadas com freqüência.

Opcional: preencher tags

As atividades suportam dados de chave-valor chamados Tags, comumente usados para armazenar quaisquer parâmetros do trabalho que possam ser úteis para diagnósticos. Atualize DoSomeWork() para incluí-los:

        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

Melhores práticas

  • Como mencionado acima, activity devolvido por ActivitySource.StartActivity pode ser nulo. O operador ?. null-coalescing em C# é uma abreviação conveniente para invocar Activity.SetTag apenas se activity não for null. O comportamento é idêntico à escrita:
if(activity != null)
{
    activity.SetTag("foo", foo);
}
  • OpenTelemetry fornece um conjunto de convenções recomendadas para definir Tags em Atividades que representam tipos comuns de trabalho de aplicativo.

  • Se você estiver instrumentando funções com requisitos de alto desempenho, é uma dica que indica se algum dos códigos ouvindo Atividades pretende ler informações auxiliares, Activity.IsAllDataRequested como Tags. Se nenhum ouvinte o ler, não há necessidade de o código instrumentado passar ciclos de CPU preenchendo-o. Para simplificar, este exemplo não aplica essa otimização.

Opcional: Adicionar eventos

Os eventos são mensagens com carimbo de data/hora que podem anexar um fluxo arbitrário de dados de diagnóstico adicionais às Atividades. Adicione alguns eventos à Atividade:

        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

Melhores práticas

  • Os eventos são armazenados em uma lista na memória até que possam ser transmitidos, o que torna esse mecanismo adequado apenas para registrar um número modesto de eventos. Para um volume grande ou ilimitado de eventos, é melhor usar uma API de log focada nessa tarefa, como o ILogger. O ILogger também garante que as informações de log estarão disponíveis independentemente de o desenvolvedor do aplicativo optar por usar o rastreamento distribuído. O ILogger suporta a captura automática dos IDs de atividade ativos para que as mensagens registradas por meio dessa API ainda possam ser correlacionadas com o rastreamento distribuído.

Opcional: Adicionar status

O OpenTelemetry permite que cada atividade relate um Status que representa o resultado de aprovação/reprovação do trabalho. Atualmente, o .NET não tem uma API fortemente tipada para essa finalidade, mas há uma convenção estabelecida usando Tags:

  • otel.status_code é o nome da tag usado para armazenar StatusCode. Os valores para a tag StatusCode devem ser uma das cadeias de caracteres "UNSET", "OK" ou "ERROR", que correspondem respectivamente aos enums Unset, Oke Error de StatusCode.
  • otel.status_description é o nome da tag usado para armazenar o opcional Description

Atualize DoSomeWork() para definir o status:

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

Opcional: Adicionar atividades adicionais

As atividades podem ser aninhadas para descrever partes de uma unidade maior de trabalho. Isso pode ser valioso em torno de partes de código que podem não ser executadas rapidamente ou para localizar melhor falhas provenientes de dependências externas específicas. Embora este exemplo use uma atividade em cada método, isso ocorre apenas porque o código extra foi minimizado. Em um projeto maior e mais realista, usar uma atividade em todos os métodos produziria traços extremamente detalhados, por isso não é recomendado.

Atualize o StepOne e o StepTwo para adicionar mais rastreamento em torno destas etapas separadas:

        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

Observe que StepOne e StepTwo incluem um ParentId que se refere a SomeWork. O console não é uma ótima visualização de árvores aninhadas de trabalho, mas muitos visualizadores de GUI, como o Zipkin, podem mostrar isso como um gráfico de Gantt:

Zipkin Gantt chart

Opcional: ActivityKind

As Atividades têm uma Activity.Kind propriedade, que descreve a relação entre a Atividade, seu pai e seus filhos. Por padrão, todas as novas Atividades são definidas como Internal, o que é apropriado para Atividades que são uma operação interna dentro de um aplicativo sem pai ou filhos remotos. Outros tipos podem ser definidos usando o parâmetro kind em ActivitySource.StartActivity. Para outras opções, consulte System.Diagnostics.ActivityKind.

Quando o trabalho ocorre em sistemas de processamento em lote, uma única atividade pode representar o trabalho em nome de muitas solicitações diferentes simultaneamente, cada uma das quais tem seu próprio trace-id. Embora a atividade seja restrita a ter um único pai, ela pode ser vinculada a rastreamentos adicionais usando System.Diagnostics.ActivityLinko . Cada ActivityLink é preenchido com um que armazena informações de ActivityContext ID sobre a Atividade à qual está sendo vinculada. ActivityContext pode ser recuperado de objetos Activity em processo usando ou pode ser analisado a partir de informações de ID serializadas usando Activity.ContextActivityContext.Parse(String, String).

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

Ao contrário de eventos e tags que podem ser adicionados sob demanda, os links devem ser adicionados durante StartActivity() e são imutáveis posteriormente.