Compartir a través de


Adición de instrumentación de seguimiento distribuido

Este artículo se aplica a: ✔️.NET Core 2.1 y versiones posteriores ✔️ .NET Framework 4.5 y versiones posteriores

Las aplicaciones .NET se pueden instrumentar con la API System.Diagnostics.Activity para generar telemetría de seguimiento distribuido. Parte de la instrumentación se integra en bibliotecas estándar de .NET, pero es posible que quiera agregar más para que el código sea más fácil de diagnosticar. En este tutorial, agregará una nueva instrumentación de seguimiento distribuido personalizada. Vea el tutorial sobre colecciones para obtener más información sobre cómo registrar la telemetría generada por esta instrumentación.

Prerrequisitos

Creación de una aplicación inicial

En primer lugar, creará una aplicación de ejemplo que recopila la telemetría mediante OpenTelemetry, pero que todavía no tiene ninguna instrumentación.

dotnet new console

Las aplicaciones destinadas a .NET 5 y versiones posteriores ya tienen las API de seguimiento distribuido necesarias. En el caso de las aplicaciones destinadas a versiones anteriores de .NET, agregue el paquete NuGet System.Diagnostics.DiagnosticSource versión 5 o superior.

dotnet add package System.Diagnostics.DiagnosticSource

Agregue los paquetes NuGet OpenTelemetry y OpenTelemetry.Exporter.Console, que se usarán para recopilar la telemetría.

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

Reemplace el contenido del archivo Program.cs generado con este código de ejemplo:

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

La aplicación todavía no tiene instrumentación, por lo que no hay información de seguimiento para mostrar:

> dotnet run
Example work done

Procedimientos recomendados

Solo los desarrolladores de aplicaciones deben hacer referencia a una biblioteca de terceros opcional para recopilar la telemetría de seguimiento distribuido, como OpenTelemetry en este ejemplo. Los creadores de bibliotecas de .NET pueden confiar exclusivamente en las API de System.Diagnostics.DiagnosticSource, que forma parte del entorno de ejecución de .NET. Esto garantiza que las bibliotecas se ejecutarán en una amplia gama de aplicaciones .NET, independientemente de las preferencias del desarrollador de la aplicación sobre qué biblioteca o proveedor usar para recopilar la telemetría.

Adición de instrumentación básica

Las aplicaciones y las bibliotecas agregan instrumentación de seguimiento distribuido mediante las clases System.Diagnostics.ActivitySource y System.Diagnostics.Activity.

ActivitySource

En primer lugar cree una instancia de ActivitySource. ActivitySource proporciona API para crear e iniciar objetos Activity. Agregue la variable estática ActivitySource por encima de Main() y using System.Diagnostics; a las instrucciones 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)
        {
            ...

Procedimientos recomendados

  • Cree la clase ActivitySource una vez, almacénela en una variable estática y use esa instancia siempre que sea necesario. Cada biblioteca o subcomponente de biblioteca puede (y debería) crear su propio origen. Considere la posibilidad de crear un origen, en lugar de volver a usar uno existente, si prevé que los desarrolladores de aplicaciones apreciarán la posibilidad de habilitar y deshabilitar la telemetría de la actividad en los orígenes de forma independiente.

  • El nombre de origen que se pasa al constructor debe ser único para evitar conflictos con otros orígenes. Si hay varios orígenes en el mismo ensamblado, use un nombre jerárquico que contenga el nombre de ensamblado y, opcionalmente, un nombre de componente, por ejemplo, Microsoft.AspNetCore.Hosting. Si un ensamblado agrega instrumentación para el código en un segundo ensamblado independiente, el nombre debe basarse en el ensamblado que defina la instancia de ActivitySource, no en el ensamblado cuyo código se va a instrumentar.

  • El parámetro de versión es opcional. Se recomienda proporcionar la versión en caso de que se publiquen varias versiones de la biblioteca y se realicen cambios en la telemetría instrumentada.

Nota:

OpenTelemetry usa los términos alternativos "Tracer" y "Span". En .NET, "ActivitySource" es la implementación de "Tracer" y "Activity", la de "Span". El tipo Activity de .NET es muy anterior a la especificación de OpenTelemetry y la nomenclatura original de .NET se ha conservado por motivos de coherencia dentro del ecosistema de .NET y para la compatibilidad con las aplicaciones .NET.

Actividad

Use el objeto ActivitySource para iniciar y detener los objetos Activity en torno a unidades de trabajo significativas. Actualice DoSomeWork() con este código:

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

Ahora, al ejecutar la aplicación, se muestra el registro del nuevo objeto Activity:

> 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 crea e inicia la actividad al mismo tiempo. El patrón de código enumerado usa el bloque using, que desecha automáticamente el objeto Activity creado después de ejecutar el bloque. Al desechar el objeto Activity, se detendrá, por lo que el código no necesita llamar a Activity.Stop() de forma explícita. Esto simplifica el patrón de codificación.

  • ActivitySource.StartActivity determina internamente si hay clientes de escucha que registran la actividad. Si no hay clientes de escucha registrados, o los que hay no están interesados, StartActivity() devolverá null y evitará crear el objeto Activity. Se trata de una optimización del rendimiento para que el patrón de código se pueda seguir usando en funciones a las que se llama con frecuencia.

Opcional: Rellenado de etiquetas

Las actividades admiten datos de clave-valor denominados etiquetas, que se suelen usar para almacenar los parámetros del trabajo que pueden ser útiles para el diagnóstico. Actualice DoSomeWork() para incluirlas:

        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

Procedimientos recomendados

  • Como se ha mencionado antes, el valor activity devuelto por ActivitySource.StartActivity puede ser NULL. El operador de fusión de NULL ?. en C# es una forma abreviada y cómoda de invocar solo Activity.SetTag si activity no es NULL. El comportamiento es idéntico a escribir lo siguiente:
if(activity != null)
{
    activity.SetTag("foo", foo);
}
  • OpenTelemetry proporciona un conjunto de convenciones recomendadas para establecer etiquetas en las actividades que representan los tipos comunes de trabajo de la aplicación.

  • Si va a instrumentar funciones con requisitos de alto rendimiento, Activity.IsAllDataRequested es una sugerencia que indica si alguno de los códigos que escuchan las actividades pretende leer información auxiliar, como etiquetas. Si ningún cliente de escucha la va a leer, no es necesario que el código instrumentado dedique ciclos de CPU a rellenarla. Para simplificar, en este ejemplo no se aplica esa optimización.

Opcional: adición de eventos

Los eventos son mensajes con marca de tiempo que pueden adjuntar un flujo arbitrario de datos de diagnóstico adicionales a las actividades. Agregue algunos eventos a la actividad:

        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

Procedimientos recomendados

  • Los eventos se almacenan en una lista en memoria hasta que se pueden transmitir, lo que hace que este mecanismo solo sea adecuado para registrar un número reducido de eventos. Para un volumen grande o ilimitado de eventos, es mejor usar una API de registro centrada en esta tarea, como ILogger. ILogger también garantiza que la información de registro estará disponible independientemente de si el desarrollador de la aplicación opta por usar el seguimiento distribuido. ILogger admite la captura automática de los identificadores de actividad activos, por lo que los mensajes registrados por medio de esa API todavía se pueden correlacionar con el seguimiento distribuido.

Opcional: adición de estado

OpenTelemetry permite que cada actividad notifique un Estado que representa el resultado de la superación o el error del trabajo. En la actualidad .NET no tiene una API fuertemente tipada para este propósito, pero hay una convención establecida mediante etiquetas:

  • otel.status_code es el nombre de etiqueta que se usa para almacenar StatusCode. Los valores de la etiqueta StatusCode deben ser una de las cadenas "UNSET", "OK" o "ERROR", que se corresponden respectivamente a las enumeraciones Unset, Ok y Error de StatusCode.
  • otel.status_description es el nombre de etiqueta que se usa para almacenar el objeto Description opcional.

Actualice DoSomeWork() para establecer el estado:

        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: adición de otras actividades

Las actividades se pueden anidar para describir partes de una unidad de trabajo mayor. Esto puede ser útil en algunas partes del código que podrían no ejecutarse rápidamente o para localizar mejor los errores que provienen de dependencias externas concretas. Aunque en este ejemplo se usa una actividad en cada método, es solo porque se ha minimizado el código adicional. En un proyecto más grande y realista, el uso de una actividad en cada método generaría seguimientos muy detallados, por lo que no se recomienda.

Actualice StepOne y StepTwo para agregar más seguimiento en torno a estos pasos independientes:

        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

Tenga en cuenta que StepOne y StepTwo incluyen un valor ParentId que hace referencia a SomeWork. La consola no es la mejor forma de visualizar árboles de trabajo anidados, pero muchos visores de GUI como Zipkin lo pueden mostrar como un diagrama de Gantt:

Zipkin Gantt chart

Opcional: ActivityKind

Las actividades tienen una propiedad Activity.Kind, que describe la relación entre la actividad, su elemento primario y sus elementos secundarios. De forma predeterminada, todas las actividades nuevas se establecen en Internal, lo que es adecuado para las que son una operación interna dentro de una aplicación sin elementos primarios o secundarios remotos. Se pueden establecer otros tipos mediante el parámetro kind en ActivitySource.StartActivity. Para conocer otras opciones, consulte System.Diagnostics.ActivityKind.

Cuando se desarrolla trabajo en sistemas de procesamiento por lotes, es posible que se represente por medio de una sola actividad en nombre de muchas solicitudes diferentes simultáneamente, cada una con un identificador de seguimiento propio. Aunque la actividad está restringida a tener un único elemento primario, se puede vincular a identificadores de seguimiento adicionales mediante System.Diagnostics.ActivityLink. Cada elemento ActivityLink se rellena con un objeto ActivityContext en el que se almacena la información de identificador sobre la actividad a la que se vincula. ActivityContext se puede recuperar de los objetos Activity en proceso mediante Activity.Context, o bien se puede analizar a partir de la información de identificador serializada mediante 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
    }
}

A diferencia de los eventos y las etiquetas que se pueden agregar a petición, los vínculos se deben agregar durante StartActivity() y, después, son inmutables.