Freigeben über


Hinzufügen einer verteilten Ablaufverfolgungsinstrumentierung

Dieser Artikel bezieht sich auf: ✔️ .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 Ablaufverfolgungs-Telemetrie zu erzeugen. Einige Instrumentierungen sind in .NET-Standardbibliotheken integriert, aber Möglicherweise möchten Sie mehr hinzufügen, um Ihren Code einfacher zu diagnostizieren. In diesem Tutorial fügen Sie eine neue benutzerdefinierte verteilte Ablaufverfolgungsinstrumentierung hinzu. Weitere Informationen zum Aufzeichnen der Telemetrie, die von dieser Instrumentierung erzeugt wird, finden Sie im Sammlungslernprogramm .

Voraussetzungen

Erstellen der ersten App

Zuerst erstellen Sie eine Beispiel-App, die Telemetrie mithilfe von OpenTelemetry sammelt, aber noch keine Instrumentierung hat.

dotnet new console

Anwendungen, die auf .NET 5 und höher abzielen, enthalten bereits die erforderlichen verteilten Ablaufverfolgungs-APIs. Fügen Sie für Apps, die auf ältere .NET-Versionen abzielen, das Paket "System.Diagnostics.DiagnosticSource NuGet" , Version 5 oder höher, hinzu. Für Bibliotheken, die auf netstandard abzielen, empfehlen wir, auf die älteste Version des Pakets zu verweisen, die weiterhin unterstützt wird und die APIs enthält, die Ihre Bibliothek benötigt.

dotnet add package System.Diagnostics.DiagnosticSource

Fügen Sie die Pakete OpenTelemetry und OpenTelemetry.Exporter.Console NuGet hinzu, die zum Sammeln der Telemetrie verwendet werden.

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

Ersetzen Sie den Inhalt der generierten Program.cs durch diese Beispielquelle:

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 verfügt noch über keine Instrumentierung, sodass keine Ablaufverfolgungsinformationen angezeigt werden:

> dotnet run
Example work done

Bewährte Methoden

Nur App-Entwickler müssen auf eine optionale Drittanbieterbibliothek verweisen, um die verteilte Ablaufverfolgungs-Telemetrie zu sammeln, z. B. OpenTelemetry in diesem Beispiel. .NET-Bibliotheksautoren können sich ausschließlich auf APIs in System.Diagnostics.DiagnosticSource verlassen, die Teil der .NET-Laufzeit ist. Dadurch wird sichergestellt, dass Bibliotheken in einer breiten Palette von .NET-Apps ausgeführt werden, unabhängig von den Einstellungen des App-Entwicklers, welche Bibliothek oder welcher Anbieter zum Sammeln von Telemetrie 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 zuerst eine Instanz von ActivitySource. ActivitySource stellt APIs zum Erstellen und Starten von Activity-Objekten bereit. Fügen Sie die statische ActivitySource-Variable oben Main() und using System.Diagnostics; den using Direktiven 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 einmal, speichern Sie sie in einer statischen Variablen, und verwenden Sie diese Instanz nach Bedarf. Jede Bibliotheks- oder Bibliotheksunterkomponente kann (und sollte häufig) eine eigene Quelle erstellen. Erwägen Sie, eine neue Quelle zu erstellen, anstatt eine vorhandene Quelle wiederzuverwenden, wenn Sie davon ausgehen, dass App-Entwickler die Aktivitäts-Telemetrie in den Quellen unabhängig voneinander aktivieren und deaktivieren können.

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

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

Hinweis

OpenTelemetry verwendet alternative Begriffe "Tracer" und "Span". In .NET ist 'ActivitySource' die Implementierung von Tracer und Activity ist die Implementierung von 'Span'. Der .NET-Aktivitätstyp geht der OpenTelemetry-Spezifikation weit voraus, und die ursprüngliche .NET-Benennung wurde beibehalten, um Konsistenz innerhalb des .NET-Ökosystems und der .NET-Anwendungskompatibilität zu gewährleisten.

Aktivität

Verwenden Sie das ActivitySource-Objekt, um Aktivitätsobjekte zu starten und zu stoppen, die sich auf sinnvolle Arbeitseinheiten beziehen. Aktualisieren Sie DoSomeWork() mit dem hier gezeigten 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 jetzt die neue Protokollierte Aktivität angezeigt:

> 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 gleichzeitig. Das aufgelistete Codemuster verwendet den using Block, der das erstellte Activity-Objekt nach der Ausführung des Blocks automatisch entfernt. Wenn Sie das Activity-Objekt verwerfen, wird es beendet, sodass der Code nicht explizit Activity.Stop() aufrufen muss. Das vereinfacht das Codierungsmuster.

  • ActivitySource.StartActivity ermittelt intern, ob Listener die Aktivität aufzeichnen. Wenn keine registrierten Listener vorhanden sind oder manche Listener nicht interessiert sind, wird StartActivity()null zurückgeben und das Erstellen des Activity-Objekts vermeiden. Dies ist eine Leistungsoptimierung, sodass das Codemuster weiterhin in Funktionen verwendet werden kann, die häufig aufgerufen werden.

Optional: Auffüllen von Tags

Aktivitäten unterstützen Schlüsselwertdaten namens Tags, die häufig zum Speichern von Parametern der Arbeit 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-koaleszierende Operator ?. in C# ist eine praktische Kurzschreibweise, um Activity.SetTag nur aufzurufen, wenn activity nicht null ist. Das Verhalten ist identisch mit dem Schreiben:
if(activity != null)
{
    activity.SetTag("foo", foo);
}
  • OpenTelemetry bietet eine Reihe empfohlener Konventionen zum Festlegen von Tags für Aktivitäten, die allgemeine Arten von Anwendungsarbeit darstellen.

  • Wenn Sie Funktionen mit hohen Leistungsanforderungen instrumentieren, ist Activity.IsAllDataRequested ein Hinweis, der angibt, ob der Code, der Aktivitäten lauscht, Hilfsinformationen wie Tags lesen möchte. Wenn kein Listener es liest, ist es nicht erforderlich, dass der instrumentierte Code CPU-Zyklen dafür aufwendet, es zu füllen. Aus Gründen der Einfachheit wendet dieses Beispiel diese Optimierung nicht an.

Optional: Hinzufügen von Ereignissen

Ereignisse sind zeitstempelte 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 Speicherliste gespeichert, bis sie übertragen werden können, wodurch dieser Mechanismus nur für die Aufzeichnung einer geringen Anzahl von Ereignissen geeignet ist. Für ein großes oder ungebundenes Ereignisvolumen ist es besser, eine protokollierungs-API zu verwenden, die sich auf diese Aufgabe konzentriert, z. B. ILogger. ILogger stellt außerdem sicher, dass die Protokollierungsinformationen verfügbar sind, unabhängig davon, ob der App-Entwickler die verteilte Ablaufverfolgung verwendet. ILogger unterstützt die automatische Erfassung der aktiven Aktivitäts-IDs, sodass über diese API protokollierte Nachrichten weiterhin mit der verteilten Ablaufverfolgung korreliert werden können.

Optional: Status hinzufügen

OpenTelemetry ermöglicht es jeder Aktivität, einen Status zu melden, der das Pass-/Fail-Ergebnis der Arbeit darstellt. .NET verfügt zu diesem Zweck über eine stark typierte API:

Die ActivityStatusCode Werte werden als entweder Unset, Ok und Error dargestellt.

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?.SetStatus(ActivityStatusCode.Error, "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 für Teile von Code hilfreich sein, die möglicherweise nicht schnell ausgeführt werden, oder um Fehler zu lokalisieren, die aus bestimmten externen Abhängigkeiten stammen. Obwohl in diesem Beispiel eine Aktivität in jeder Methode verwendet wird, liegt dies ausschließlich daran, dass zusätzlicher 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 enthalten, die sich auf SomeWork bezieht. Die Konsole ist keine großartige Visualisierung geschachtelter Arbeitsstrukturen, aber viele GUI-Viewer wie Zipkin können dies als Gantt-Diagramm anzeigen:

Zipkin Gantt-Diagramm

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 Typparameters auf ActivitySource.StartActivity festgelegt werden. Weitere 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 Element gefüllt, mit dem ID-Informationen zu der Aktivität gespeichert werden, mit der eine Verknüpfung besteht. 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.

Wichtig

Gemäß der OpenTelemetry-Spezifikation beträgt der vorgeschlagene Grenzwert für die Anzahl der Verknüpfungen 128. Es ist jedoch wichtig zu beachten, dass dieser Grenzwert nicht erzwungen wird.