Compartilhar via


Adição da instrumentação de rastreamento distribuído

Este artigo se aplica 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 System.Diagnostics.Activity para produzir telemetria de rastreamento distribuído. Algumas instrumentações são incorporadas às bibliotecas padrão do .NET, mas talvez você queira 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 essa instrumentação.

Pré-requisitos

Criar aplicativo inicial

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

dotnet new console

Aplicativos direcionados ao .NET 5 e posteriores já têm as APIs de rastreamento distribuídas necessárias incluídas. Para aplicativos direcionados 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 NuGet OpenTelemetry e OpenTelemetry.Export.Console, que serão usados para coletar a telemetria.

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

Substitua o conteúdo do arquivo Program.cs gerado pelo código 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 a serem exibidas:

> dotnet run
Example work done

Práticas recomendadas

Somente os desenvolvedores de aplicativos precisam fazer referência a uma biblioteca opcional de terceiros para coletar a telemetria de rastreamento distribuído, como o OpenTelemetry neste exemplo. Os autores da biblioteca do .NET podem contar exclusivamente com as APIs no System.Diagnostics.DiagnosticSource, que fazem parte do runtime do .NET. Isso garante que as bibliotecas sejam executadas em diversos 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 classes System.Diagnostics.ActivitySource e System.Diagnostics.Activity.

ActivitySource

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

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

Práticas recomendadas

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

  • O nome da fonte passado para o construtor deve ser exclusivo para evitar conflitos com outras fontes. Se houver várias fontes no 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 deverá 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 em caso de várias versões da biblioteca e faça alterações na telemetria instrumentada.

Observação

O OpenTelemetry usa os termos alternativos 'Rastreamento' e 'Extensão'. No .NET, 'ActivitySource' é a implementação de Rastreamento e Atividade é a implementação de 'Extensão'. O tipo de atividade do NET precede a especificação OpenTelemetry e a nomenclatura original do .NET foi preservada para manter a consistência no ecossistema do .NET e a compatibilidade do aplicativo .NET.

Atividade

Use o objeto ActivitySource para os objetos Iniciar e Parar atividade em unidades significativas de trabalho. Atualize DoSomeWork() com o código exibido 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 o registro da nova atividade:

> 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

Observações

  • ActivitySource.StartActivity cria e inicia a atividade ao mesmo tempo. O padrão de código listado está usando o bloco using, que descarta automaticamente o objeto Atividade criado após a execução do bloco. O descarte do objeto Atividade irá interrompê-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á null e evitará a criação do objeto Atividade. Essa é uma otimização de desempenho para que o padrão de código ainda possa ser usado em funções chamadas com frequência.

Opcional: Preencher marcações

As atividades dão suporte a dados chave-valor chamados marcações, normalmente 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

Práticas recomendadas

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

  • Se você estiver instrumentando funções com requisitos de alto desempenho, Activity.IsAllDataRequested será uma dica que indica se algum dos códigos que escutam Atividades pretende ler informações auxiliares, como Marcas. Se nenhum ouvinte o ler, não será necessário que o código instrumentado gaste 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

Práticas recomendadas

  • 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 pequeno número de eventos. Para um volume grande ou desassociado de eventos, é melhor usar uma API de registro em log focada nessa tarefa, como iLogger. O ILogger também garante que as informações de registro em log estejam disponíveis independentemente do desenvolvedor do aplicativo optar por usar o rastreamento distribuído. O ILogger dá suporte à captura automática das IDs de atividade ativas 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/falha do trabalho. No momento, o .NET não tem uma API fortemente tipada para essa finalidade, mas há uma convenção estabelecida usando marcações:

  • otel.status_code é o nome da marcação usado para armazenar StatusCode. Os valores da marcação StatusCode devem ser uma das cadeias de caracteres "UNSET", "OK" ou "ERROR", que correspondem respectivamente às enumerações Unset, Ok e Error do StatusCode.
  • otel.status_description é o nome da marcação usado para armazenar o Description opcional

Atualizar 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 de trabalho maior. Isso pode ser valioso em partes de código que podem não ser executadas rapidamente ou para localizar melhor as falhas provenientes de dependências externas específicas. Embora este exemplo use uma atividade em todos os métodos, isso ocorre apenas porque o código extra foi minimizado. Em um projeto maior e mais realista, o uso de uma atividade em cada método produziria rastreamentos extremamente detalhados e, portanto, não é recomendado.

Atualizar StepOne e StepTwo para adicionar mais rastreamento em torno dessas 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 uma ParentId que se refere a SomeWork. O console não é uma grande visualização de árvores aninhadas de trabalho, mas muitos visualizadores de GUI, como Zipkin, podem exibir isso como um gráfico de Gantt:

Zipkin Gantt chart

Opcional: ActivityKind

As atividades têm uma propriedade Activity.Kind, 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 em um aplicativo sem pai ou filho remoto. Outros tipos podem ser definidos usando o parâmetro de tipo 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 tendo sua própria ID de rastreamento. Embora a atividade seja restrita a ter um único pai, ela pode vincular as IDs de rastreamento adicionais usando System.Diagnostics.ActivityLink. Cada ActivityLink é populado com ActivityContext que armazena informações da ID sobre a atividade que está sendo vinculada. ActivityContext pode ser recuperado de objetos de atividade em processo usando Activity.Context ou pode ser analisado a partir de informações de ID serializadas usando ActivityContext.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 dos eventos e marcações que podem ser adicionados sob demanda, os links devem ser adicionados durante StartActivity() e podem ser imutáveis posteriormente.