Hinzufügen einer verteilten Ablaufverfolgungsinstrumentierung

Dieser Artikel gilt für: ✔️ .NET Core 2.1 und höhere Versionen ✔️ .NET Framework 4.5 und höhere Versionen

.NET-Anwendungen können mithilfe der System.Diagnostics.Activity-API instrumentiert werden, um verteilte Ablaufverfolgungstelemetrie zu generieren. Einige Instrumentierungen sind in .NET-Standardbibliotheken integriert, aber Sie sollten weitere hinzufügen, um Ihren Code leichter diagnostizieren zu können. In diesem Tutorial fügen Sie eine neue benutzerdefinierte verteilte Ablaufverfolgungsinstrumentierung hinzu. Weitere Informationen zum Aufzeichnen der Telemetriedaten, die von dieser Instrumentierung generiert werden, finden Sie im Tutorial zur Erfassung.

Voraussetzungen

Erstellen der ursprünglichen App

Zunächst erstellen Sie eine Beispiel-App, die Telemetriedaten mit OpenTelemetry erfasst, aber noch keine Instrumentierung aufweist.

dotnet new console

Anwendungen, die auf .NET 5 und höher ausgerichtet sind, verfügen bereits über die erforderlichen verteilten Ablaufverfolgungs-APIs. Fügen Sie für Apps, die auf ältere .NET-Versionen abzielen, das NuGet-Paket System.Diagnostics.DiagnosticSource (Version 5 oder höher) hinzu.

dotnet add package System.Diagnostics.DiagnosticSource

Fügen Sie die NuGet-Pakete OpenTelemetry und OpenTelemetry.Exporter.Console hinzu, die zum Erfassen der Telemetriedaten verwendet werden.

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

Ersetzen Sie den Inhalt der generierten Datei „Program.cs“ durch diesen Beispielquellcode:

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

Die App weist noch keine Instrumentierung auf, daher können keine Ablaufverfolgungsinformationen angezeigt werden:

> dotnet run
Example work done

Bewährte Methoden

Nur App-Entwickler müssen auf eine optionale Bibliothek eines Drittanbieters zum Erfassen der verteilten Ablaufverfolgungstelemetrie verweisen, etwa auf OpenTelemetry in diesem Beispiel. Autoren von .NET-Bibliotheken können exklusiv APIs in System.Diagnostics.DiagnosticSource verwenden, die Teil der .NET-Runtime sind. Dadurch wird sichergestellt, dass Bibliotheken in einer Vielzahl von .NET-Apps ausgeführt werden, unabhängig davon, welche Bibliothek oder welcher Anbieter für die Erfassung von Telemetriedaten verwendet werden soll.

Hinzufügen grundlegender Instrumentierung

Anwendungen und Bibliotheken fügen eine verteilte Ablaufverfolgungsinstrumentierung mithilfe der Klassen System.Diagnostics.ActivitySource und System.Diagnostics.Activity hinzu.

ActivitySource

Erstellen Sie zunächst eine Instanz von ActivitySource. ActivitySource stellt APIs zum Erstellen und Starten von Aktivitätsobjekten bereit. Fügen Sie die statische ActivitySource-Variable oberhalb von Main() sowie using System.Diagnostics; den using-Anweisungen hinzu.

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

Bewährte Methoden

  • Erstellen Sie die ActivitySource ein Mal, speichern Sie sie in einer statischen Variablen, und verwenden Sie diese Instanz so lange wie erforderlich. Jede Bibliothek oder Bibliotheksunterkomponente kann (und sollte häufig) eine eigene Quelle erstellen. Ziehen Sie in Erwägung, eine neue Quelle zu erstellen, anstatt eine vorhandene wiederzuverwenden, wenn Sie davon ausgehen, dass App-Entwickler es schätzen würden, die Aktivitätstelemetrie in den Quellen unabhängig aktivieren und deaktivieren zu können.

  • Der an den Konstruktor übergebene Quellname muss eindeutig sein, um Konflikte mit anderen Quellen zu vermeiden. Es wird empfohlen, einen hierarchischen Namen zu verwenden, der den Assemblynamen und optional einen Komponentennamen enthält, wenn es mehrere Quellen innerhalb derselben Assembly gibt, z. B. Microsoft.AspNetCore.Hosting. Wenn eine Assembly Instrumentierung für Code in einer zweiten unabhängigen Assembly hinzufügt, sollte der Name auf der Assembly basieren, die ActivitySource definiert, nicht auf der Assembly, deren Code instrumentiert wird.

  • Der Versionsparameter ist optional. Es wird empfohlen, die Version anzugeben, falls Sie mehrere Versionen der Bibliothek veröffentlichen und Änderungen an der instrumentierten Telemetrie vornehmen.

Hinweis

OpenTelemetry verwendet die alternativen Begriffe „Tracer“ und „Span“. In .NET ist „ActivitySource“ die Implementierung von „Tracer“ und „Activity“ die Implementierung von „Span“. Der Activity-Typ von .NET ist lange vor der OpenTelemetry-Spezifikation entstanden, und die ursprüngliche .NET-Benennung wurde aus Gründen der Konsistenz innerhalb des .NET-Ökosystems und der Kompatibilität mit .NET-Anwendungen beibehalten.

Aktivität

Verwenden Sie das ActivitySource-Objekt, um Activity-Objekte um sinnvolle Arbeitseinheiten zu starten und zu beenden. Aktualisieren Sie DoSomeWork() mit dem folgenden Code:

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

Beim Ausführen der App wird nun die neue Aktivität angezeigt, die protokolliert wird:

> 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

Hinweise

  • ActivitySource.StartActivity erstellt und startet die Aktivität zur gleichen Zeit. Das aufgelistete Codemuster verwendet den using-Block, der das erstellte Activity-Objekt nach der Ausführung des Blocks automatisch freigibt. Wenn Sie das Activity-Objekt verwerfen, wird es beendet, sodass der Code nicht explizit Activity.Stop() aufrufen muss. Dies vereinfacht das Codierungsmuster.

  • ActivitySource.StartActivity bestimmt intern, ob Listener die Aktivität aufzeichnen. Wenn keine registrierten Listener vorhanden sind oder es Listener gibt, die nicht interessiert sind, gibt StartActivity()null zurück und vermeidet die Erstellung des Activity-Objekts. Dies ist eine Leistungsoptimierung, sodass das Codemuster weiterhin in Funktionen verwendet werden kann, die häufig aufgerufen werden.

Optional: Auffüllen von Tags mit Daten

Aktivitäten unterstützen Schlüssel-Wert-Daten, die als Tags bezeichnet werden und häufig zum Speichern von Parametern der Aufgaben verwendet werden, die für die Diagnose nützlich sein können. Aktualisieren Sie DoSomeWork(), um sie einzuschließen:

        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

Bewährte Methoden

  • Wie bereits erwähnt, kann das activity-Element, das von ActivitySource.StartActivity zurückgegeben wird, NULL sein. Der NULL-Sammeloperator ?. in C# eignet sich ideal, wenn Activity.SetTag nur aufgerufen werden soll, wenn activity nicht NULL ist. Das Verhalten ist mit dem folgenden Code identisch:
if(activity != null)
{
    activity.SetTag("foo", foo);
}
  • OpenTelemetry bietet eine Reihe empfohlener Konventionen für das Festlegen von Tags für Aktivitäten, die allgemeine Typen von Anwendungsaufgaben darstellen.

  • Wenn Sie Funktionen mit hohen Leistungsanforderungen instrumentieren, ist Activity.IsAllDataRequested ein Hinweis, der angibt, ob der Code, der auf Aktivitäten lauscht, Hilfsinformationen wie z. B. Tags lesen soll. Wenn kein Listener diese liest, ist es nicht erforderlich, dass der instrumentierte Code CPU-Zyklen damit verbringt, sie mit Daten aufzufüllen. Der Einfachheit halber wird diese Optimierung in diesem Beispiel nicht angewendet.

Optional: Hinzufügen von Ereignissen

Ereignisse sind mit einem Zeitstempel versehene Nachrichten, die einen beliebigen Datenstrom zusätzlicher Diagnosedaten an Aktivitäten anfügen können. Fügen Sie der Aktivität einige Ereignisse hinzu:

        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

Bewährte Methoden

  • Ereignisse werden in einer In-Memory-Liste gespeichert, bis sie übertragen werden können, sodass dieser Mechanismus nur für die Aufzeichnung einer bescheidenen Anzahl von Ereignissen geeignet ist. Für eine große oder unbegrenzte Anzahl von Ereignissen ist die Verwendung einer auf diese Aufgabe ausgerichteten Protokollierungs-API wie ILogger die bessere Wahl. ILogger stellt außerdem sicher, dass die Protokollierungsinformationen unabhängig davon zur Verfügung stehen, ob sich der App-Entwickler für die Verwendung von verteilter Ablaufverfolgung entscheidet. ILogger unterstützt das automatische Erfassen der aktiven Aktivitäts-IDs, sodass über diese API protokollierte Nachrichten weiterhin mit der verteilten Ablaufverfolgung korreliert werden können.

Optional: Hinzufügen eines Status

OpenTelemetry ermöglicht es, für jede Aktivität einen Status zu melden, der das Ergebnis der Arbeit als bestanden/nicht bestanden darstellt. .Net verfügt zurzeit über keine stark typisierte API für diesen Zweck, aber es gibt eine etablierte Konvention mit Tags:

  • otel.status_code ist der Tagname, der zum Speichern von StatusCode verwendet wird. Werte für das StatusCode-Tag müssen eine der Zeichenfolgen "UNSET", "OK" oder "ERROR" sein, die jeweils den Enumerationen Unset, Ok und Error von StatusCode entsprechen.
  • otel.status_description ist der Tagname, der zum Speichern der optionalen Description verwendet wird.

Aktualisieren Sie DoSomeWork(), um den Status festzulegen:

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

Optional: Hinzufügen zusätzlicher Aktivitäten

Aktivitäten können geschachtelt werden, um Teile einer größeren Arbeitseinheit zu beschreiben. Dies kann wertvoll sein, wenn Teile des Codes nicht schnell ausgeführt werden können oder um Fehler besser zu lokalisieren, die aus bestimmten externen Abhängigkeiten stammen. Obwohl dieses Beispiel in jeder Methode eine Aktivität verwendet, liegt das lediglich daran, dass der zusätzliche Code minimiert wurde. In einem größeren und realistischeren Projekt würde die Verwendung einer Aktivität in jeder Methode extrem ausführliche Ablaufverfolgungen generieren, weshalb dies nicht empfohlen wird.

Aktualisieren Sie StepOne und StepTwo, um mehr Ablaufverfolgung um diese separaten Schritte hinzuzufügen:

        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

Beachten Sie, dass sowohl StepOne als auch StepTwo eine ParentId enthält, die auf SomeWork verweist. Die Konsole bietet keine gute Visualisierung von geschachtelten Arbeitsstrukturen, aber viele GUI-Viewer wie Zipkin können dies als Gantt-Diagramm darstellen:

Zipkin Gantt chart

Optional: ActivityKind

Aktivitäten verfügen über eine Activity.Kind-Eigenschaft, die die Beziehung zwischen der Aktivität, ihrem übergeordneten Element und ihren untergeordneten Elementen beschreibt. Standardmäßig werden alle neuen Aktivitäten auf Internal festgelegt. Dies ist für Aktivitäten geeignet, die ein interner Vorgang innerhalb einer Anwendung ohne übergeordnetes oder untergeordnetes Remoteelement sind. Andere Arten können mithilfe des kind-Parameters für ActivitySource.StartActivity festgelegt werden. Informationen zu anderen Optionen finden Sie unter System.Diagnostics.ActivityKind.

Wenn die Verarbeitung in Batchverarbeitungssystemen stattfindet, kann eine einzelne Activity-Instanz die Arbeit im Namen vieler verschiedener Anforderungen gleichzeitig darstellen, von denen jede ihre eigene Nachverfolgungs-ID (trace-id) aufweist. Obwohl die Activity-Instanz nur ein einziges übergeordnetes Element haben kann, kann sie über System.Diagnostics.ActivityLink mit weiteren Nachverfolgungs-IDs (trace-ids) verknüpft werden. Jeder ActivityLink wird mit einem ActivityContext aufgefüllt, der ID-Informationen zu der Aktivität speichert, mit der er verknüpft ist. ActivityContext kann mit Activity.Context aus prozessinternen Activity-Objekten abgerufen oder mit ActivityContext.Parse(String, String) aus serialisierten ID-Informationen analysiert werden.

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

Im Gegensatz zu Ereignissen und Tags, die bei Bedarf hinzugefügt werden können, müssen Links während StartActivity() hinzugefügt werden und sind anschließend unveränderlich.