Condividi tramite


Aggiunta della strumentazione di traccia distribuita

Questo articolo si applica a: ✔️ .NET Core 2.1 e versioni successive ✔️ .NET Framework 4.5 e versioni successive

Le applicazioni .NET possono essere instrumentate usando l'API System.Diagnostics.Activity per produrre dati di telemetria di traccia distribuita. Alcune strumentazioni sono integrate in librerie .NET standard, ma è consigliabile aggiungerne altre per rendere il codice più facilmente diagnosticabile. In questa esercitazione si aggiungerà nuova strumentazione di traccia distribuita personalizzata. Per altre informazioni sulla registrazione dei dati di telemetria prodotti da questa strumentazione, vedere l'esercitazione sulla raccolta.

Prerequisiti

Creare un'app iniziale

Prima di tutto si creerà un'app di esempio, che raccoglie i dati di telemetria usando OpenTelemetry, ma che non dispone ancora di strumentazione.

dotnet new console

Le applicazioni destinate a .NET 5 e versioni successive includono già le API di traccia distribuita necessarie. Per le app destinate a versioni precedenti di .NET, aggiungi il pacchetto NuGet System.Diagnostics.DiagnosticSource versione 5 o successiva.

dotnet add package System.Diagnostics.DiagnosticSource

Aggiungere i pacchetti NuGet OpenTelemetry e OpenTelemetry.Exporter.Console, che verranno usati per raccogliere i dati di telemetria.

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

Sostituisci il contenuto del Program.cs generato con l'origine di esempio seguente:

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

L'app non dispone ancora di strumentazione, quindi non sono disponibili informazioni di traccia da visualizzare:

> dotnet run
Example work done

Procedure consigliate

Solo gli sviluppatori di app devono fare riferimento a una libreria facoltativa di terze parti per raccogliere i dati di telemetria della traccia distribuita, come è il caso di OpenTelemetry in questo esempio. Gli autori di librerie .NET possono basarsi esclusivamente sulle API in System.Diagnostics.DiagnosticSource, che fa parte del runtime .NET. Ciò garantisce che le librerie vengano eseguite in un'ampia gamma di applicazioni .NET, indipendentemente dalle preferenze dello sviluppatore dell'app riguardo a quale libreria o fornitore utilizzare per la raccolta dei dati di telemetria.

Aggiungere la strumentazione di base

Le applicazioni e le librerie aggiungono strumentazione di traccia distribuita usando le classi System.Diagnostics.ActivitySource e System.Diagnostics.Activity.

ActivitySource

Creare prima di tutto un'istanza di ActivitySource. ActivitySource fornisce API per creare e avviare oggetti Activity. Aggiungere la variabile statica ActivitySource su Main() e using System.Diagnostics; alle istruzioni di utilizzo.

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

Procedure consigliate

  • Creare ActivitySource una sola volta, archiviarla in una variabile statica e usarla in caso di necessità. Ogni libreria o sottocomponente di libreria può (e spesso deve) creare la propria origine. Valutare la possibilità di creare una nuova origine anziché riutilizzarne una esistente se si prevede che gli sviluppatori di applicazioni apprezzino la possibilità di abilitare e disabilitare i dati di telemetria delle attività nelle origini in modo indipendente.

  • Il nome di origine passato al costruttore deve essere univoco al fine di evitare i conflitti con qualsiasi altra origine. Se sono presenti più origini all'interno dello stesso assembly, usare un nome gerarchico contenente il nome dell'assembly e, facoltativamente, un nome di componente, ad esempio Microsoft.AspNetCore.Hosting. Se un assembly aggiunge strumentazione per il codice in un secondo assembly indipendente, il nome deve essere basato sull'assembly che definisce ActivitySource, non sull'assembly il cui codice viene instrumentato.

  • Il parametro versione è facoltativo. È consigliabile fornire la versione nel caso in cui vengano rilasciate più versioni della libreria e si apportino modifiche ai dati di telemetria instrumentati.

Nota

OpenTelemetry usa i termini alternativi “Tracer” e “Span”. In .NET, “ActivitySource” è l'implementazione di Tracer mentre Activity è l'implementazione di “Span”. Il tipo di attività di .NET è precedente alle specifiche di OpenTelemetry e la denominazione originale di .NET è stata mantenuta per coerenza all'interno dell'ecosistema .NET e della compatibilità delle applicazioni .NET.

Impegno

Utilizzare l'oggetto ActivitySource per avviare e arrestare gli oggetti Activity intorno a unità di lavoro significative. Aggiornare DoSomeWork() con il codice illustrato di seguito:

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

L'esecuzione dell'app mostra ora la nuova attività registrata:

> 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

Note

  • ActivitySource.StartActivity crea e avvia l'attività contemporaneamente. Il modello di codice elencato usa il blocco using, che elimina automaticamente l'oggetto Activity creato dopo l'esecuzione del blocco. L'eliminazione dell'oggetto Activity lo arresterà in modo che il codice non debba richiamare Activity.Stop() in modo esplicito. Ciò semplifica il modello di codifica.

  • ActivitySource.StartActivity determina internamente se sono presenti listener che registrano l'attività. Se non sono presenti listener registrati oppure sono presenti listener che non sono interessati, StartActivity() restituirà null come risultato ed eviterà di creare l'oggetto Activity. Si tratta di un'ottimizzazione delle prestazioni in modo che il modello di codice possa ancora essere usato nelle funzioni richiamate di frequente.

Facoltativo: popola tag

Le attività supportano i dati chiave-valore denominati Tag, comunemente usati per archiviare i parametri del lavoro che possono risultare utili per la diagnostica. Aggiornare DoSomeWork() per includerli:

        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

Procedure consigliate

  • Come indicato in precedenza, il risultato activity restituito da ActivitySource.StartActivity può essere null. L'operatore di coalescenza di valori null ?. in C# è una comoda modalità abbreviata per chiamare in causa Activity.SetTag solo se activity non è null. Il comportamento è identico alla scrittura:
if(activity != null)
{
    activity.SetTag("foo", foo);
}
  • OpenTelemetry fornisce un set di convenzioni consigliate per l'impostazione dei tag nelle attività che rappresentano tipi comuni di lavoro dell'applicazione.

  • Se si instrumentano funzioni con requisiti a prestazioni elevate, Activity.IsAllDataRequested è un suggerimento che indica se uno dei codici in ascolto delle attività intende leggere informazioni ausiliarie, come i tag. Se nessun listener lo leggerà, non è necessario che il codice instrumentato utilizzi cicli di CPU popolandolo. Per semplicità, questo esempio non applica tale ottimizzazione.

Facoltativo: aggiungere eventi

Gli eventi sono messaggi con timestamp che possono collegare un flusso arbitrario di dati di diagnostica aggiuntivi alle attività. Aggiungere alcuni eventi all'attività:

        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

Procedure consigliate

  • Gli eventi vengono archiviati in un elenco in memoria fino a quando non possono essere trasmessi, il che rende tale meccanismo adatto solo per la registrazione di un numero ridotto di eventi. Per un volume di eventi elevato o non associato, è preferibile utilizzare un'API di registrazione incentrata su questa particolare attività, ad esempio ILogger. ILogger garantisce inoltre che le informazioni di registrazione saranno disponibili indipendentemente dal fatto che lo sviluppatore dell'applicazione scelga di usare la traccia distribuita. ILogger supporta l'acquisizione automatica degli ID attività attivi, in modo che i messaggi registrati tramite tale API possano comunque essere correlati alla traccia distribuita.

Facoltativo: aggiungere stato

OpenTelemetry consente a ogni attività di segnalare uno stato indicativo del risultato di esito positivo/negativo del lavoro. .NET attualmente non dispone di un'API fortemente tipizzata a questo scopo, ma esiste una convenzione stabilita usando i tag:

  • otel.status_code è il nome del tag utilizzato per archiviare StatusCode. I valori per il tag StatusCode devono essere una delle stringhe "UNSET", "OK" o "ERROR", che corrispondono rispettivamente alle enumerazioni Unset, Ok e Error di StatusCode.
  • otel.status_description è il nome del tag utilizzato per archiviare il valore facoltativo Description

Aggiornare DoSomeWork() per impostare lo stato:

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

Facoltativo: aggiungere altre attività

Le attività possono essere annidate per descrivere parti di un'unità di lavoro più grande. Ciò può essere utile per parti di codice che potrebbero non essere eseguite rapidamente o per localizzare meglio gli errori provenienti da dipendenze esterne specifiche. Anche se in questo esempio viene usata un'attività in ogni metodo, ciò è dovuto esclusivamente al fatto che il codice aggiuntivo è stato ridotto a icona. In un progetto più ampio e più realistico, l'uso di un'attività in ogni metodo produce tracce estremamente dettagliate, cosa che lo rende non consigliabile.

Aggiornare StepOne e StepTwo per aggiungere altre tracce relative a questi passaggi separati:

        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

Si noti che StepOne e StepTwo includono un ParentId che fa riferimento a SomeWork. La console non è un'ottima visualizzazione degli alberi di lavoro annidati, ma molti visualizzatori GUI, ad esempio Zipkin, possono visualizzare questo elemento come diagramma di Gantt:

Zipkin Gantt chart

Facoltativo: ActivityKind

Le attività hanno una proprietà Activity.Kind che descrive la relazione tra l'attività, il relativo padre e i relativi elementi figlio. Per impostazione predefinita, tutte le nuove attività sono impostate su Internal, che è appropriato per le attività che sono un'operazione interna nell’ambito di un'applicazione priva di elementi padre o figli remoti. È possibile impostare altri tipi usando il parametro tipologia in ActivitySource.StartActivity. Per altre opzioni, vedere System.Diagnostics.ActivityKind.

Quando il lavoro avviene in sistemi di elaborazione batch, una singola attività può rappresentare il lavoro per conto di molte richieste diverse contemporaneamente, ognuna delle quali ha il proprio trace-id. Anche se l'attività è limitata per avere un singolo elemento padre, può collegarsi a id di traccia aggiuntivi tramite System.Diagnostics.ActivityLink. Ogni ActivityLink viene popolato con un oggetto ActivityContext che archivia le informazioni sull'ID relative all'attività alle quali è collegato. ActivityContext può essere recuperato da oggetti Activity in-process usando Activity.Context oppure può essere analizzato da informazioni sull'ID serializzate 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
    }
}

A differenza di eventi e tag che possono essere aggiunti su richiesta, i collegamenti devono essere aggiunti durante StartActivity() e non sono modificabili in seguito.

Importante

In base alla specifica OpenTelemetry, il numero di collegamenti è limitato a 128 per impostazione predefinita.