Erstellen von Metriken

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

.NET-Anwendungen können mithilfe der System.Diagnostics.Metrics APIs instrumentiert werden, um wichtige Metriken nachzuverfolgen. Einige Metriken sind in .NET-Standardbibliotheken enthalten. Möglicherweise möchten Sie jedoch neue benutzerdefinierte Metriken hinzufügen, die für Ihre Anwendungen und Bibliotheken relevant sind. In diesem Tutorial fügen Sie neue Metriken hinzu und erfahren, welche Arten von Metriken zur Verfügung stehen.

Hinweis

.NET verfügt über einige ältere Metrik-APIs, d. h. EventCounters und System.Diagnostics.PerformanceCounter, die hier nicht behandelt werden. Weitere Informationen zu diesen Alternativen finden Sie unter Vergleichen von Metrik-APIs.

Erstellen einer benutzerdefinierten Metrik

Voraussetzungen: .NET Core 6 SDK oder eine höhere Version

Erstellen Sie eine neue Konsolenanwendung, die auf das NuGet-Paket System.Diagnostics.DiagnosticSource (Version 8 oder höher) verweist. Anwendungen, die für .NET 8 oder höher ausgelegt sind, enthalten diesen Verweis standardmäßig. Aktualisieren Sie dann den Code in Program.cs, sodass folgendes erfüllt ist:

> dotnet new console
> dotnet add package System.Diagnostics.DiagnosticSource
using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each second that sells 4 hats
            Thread.Sleep(1000);
            s_hatsSold.Add(4);
        }
    }
}

Der System.Diagnostics.Metrics.Meter-Typ ist der Einstiegspunkt für eine Bibliothek zum Erstellen einer benannten Gruppe von Instrumenten. Instrumente zeichnen die numerischen Messungen auf, die zum Berechnen von Metriken erforderlich sind. Hier haben wir CreateCounter genutzt, um ein Zähler-Instrument mit der Bezeichnung „hatco.store.hats_sold“ zu erstellen. Während jeder angenommenen Transaktion ruft der Code Add auf, um die Messung der verkauften Mützen aufzuzeichnen, in diesem Fall 4. Das Instrument „hatco.store.hats_sold“ definiert implizit einige Metriken, die aus diesen Messungen berechnet werden können, z. B. die Gesamtzahl der verkauften Mützen oder der verkauften Mützen pro Sekunde. Letztendlich ist es Aufgabe der Metriksammlungstools zu bestimmen, welche Metriken berechnet werden sollen und wie diese Berechnungen ausgeführt werden, aber jedes Instrument verfügt über einige Standardkonventionen, die die Absicht des jeweiligen Entwicklers oder der jeweiligen Entwicklerin widerspiegeln. Bei Zähler-Instrumenten ist die Konvention, dass Sammlungstools die Gesamtanzahl und/oder die Rate anzeigen, in der sich die Anzahl erhöht.

Der generische Parameter int für Counter<int> und CreateCounter<int>(...) definiert, dass dieser Zähler Werte bis Int32.MaxValue speichern können muss. Abhängig von der Größe der zu speichernden Datengröße bzw. der Tatsache, ob Teilwerte benötigt werden, können Sie byte, short, int, long, float, double oder decimal verwenden.

Führen Sie die App aus und lassen Sie sie zunächst laufen. Als Nächstes werfen wir einen Blick auf die Metriken.

> dotnet run
Press any key to exit

Bewährte Methoden

  • Erstellen Sie für Code, der nicht für die Verwendung in einem DI-Container (Dependency Injection, Abhängigkeitseinschleusung) vorgesehen ist, die Verbrauchseinheit einmal, und speichern Sie sie in einer statischen Variablen. Für die Verwendung in DI-fähigen Bibliotheken gelten statische Variablen als Antimuster, und im folgenden DI-Beispiel wird ein idiomatischerer Ansatz gezeigt. Jede Bibliothek oder Bibliotheksunterkomponente kann (und sollte häufig) eine eigene Meter erstellen. Ziehen Sie in Erwägung, eine neue Verbrauchseinheit zu erstellen, anstatt eine vorhandene wiederzuverwenden, wenn Sie davon ausgehen, dass App-Entwickler*innen es schätzen würden, die Gruppen von Metriken einfach einzeln aktivieren und deaktivieren zu können.

  • Der an den Meter-Konstruktor übergebene Name sollte eindeutig sein, um ihn von anderen Verbrauchseinheiten zu unterscheiden. Es wird empfohlen, OpenTelemetry-Benennungsrichtlinien zu verwenden, die gepunktete hierarchische Namen verwenden. Assemblynamen oder Namespacenamen für instrumentierten Code sind in der Regel eine gute Wahl. Wenn eine Assembly eine Instrumentierung für Code in einer zweiten unabhängigen Assembly hinzufügt, sollte der Name auf der Assembly basieren, die die Verbrauchseinheit definiert, nicht auf der Assembly, deren Code instrumentiert wird.

  • .NET erzwingt kein Benennungsschema für Instrumente, es wird jedoch empfohlen, die OpenTelemetry-Benennungsrichtlinien zu befolgen, die gepunktete hierarchische Namen in Kleinbuchstaben und einen Unterstrich (_) als Trennzeichen zwischen mehreren Wörtern im selben Element verwenden. Nicht alle Metriktools behalten den Namen der Verbrauchseinheit als Teil des endgültigen Metriknamens bei, daher ist es vorteilhaft, einen global eindeutigen Instrumentnamen festzulegen.

    Beispielinstrumentnamen:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • Die APIs zum Erstellen von Instrumenten und Aufzeichnen von Messungen sind threadsicher. In .NET-Bibliotheken erfordern die meisten Instanzmethoden eine Synchronisierung, wenn sie für dasselbe Objekt aus mehreren Threads aufgerufen werden. In diesem Fall ist dies jedoch nicht erforderlich.

  • Die Instrumenten-APIs zum Aufzeichnen von Messungen (in diesem Beispiel Add) werden in der Regel in <10 ns ausgeführt, wenn keine Daten erfasst werden, oder in zehn bis einigen hundert Nanosekunden, wenn Messungen von einer extrem leistungsstarken Sammlungsbibliothek oder einem extrem leistungsstarken Sammlungstool erfasst werden. Dadurch können diese APIs in den meisten Fällen freizügig verwendet werden, aber achten Sie auf Code, der äußerst leistungssensitiv ist.

Anzeigen der neuen Metrik

Es gibt viele Optionen zum Speichern und Anzeigen von Metriken. In diesem Tutorial wird das Tool dotnet-counters verwendet, das für Ad-hoc-Analysen nützlich ist. Weitere Alternativen finden Sie auch im Tutorial zur Metriksammlung. Wenn das Tool dotnet-counters noch nicht installiert ist, verwenden Sie das SDK, um es zu installieren:

> dotnet tool update -g dotnet-counters
You can invoke the tool using the following command: dotnet-counters
Tool 'dotnet-counters' (version '7.0.430602') was successfully installed.

Während die Beispiel-App noch ausgeführt wird, verwenden Sie „dotnet-counters“, um den neuen Zähler zu überwachen:

> dotnet-counters monitor -n metric-demo.exe --counters HatCo.Store
Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold (Count / 1 sec)                          4

Wie erwartet, können Sie sehen, dass der HatCo-Store dauerhaft 4 Mützen pro Sekunde verkauft.

Abrufen einer Verbrauchseinheit über die Abhängigkeitseinschleusung

Im vorherigen Beispiel wurde die Verbrauchseinheit durch Konstruieren mit new und Zuweisen zu einem statischen Feld abgerufen. Eine derartige Nutzung von statischen Elementen ist bei Verwendung von Abhängigkeitseinschleusung (Dependency Injection, DI) kein guter Ansatz. Erstellen Sie in Code, der DI verwendet ( z. B. ASP.NET Core oder Apps mit generischem Host), das Objekt für die Verbrauchseinheit mithilfe von IMeterFactory. Ab .NET 8 registrieren Hosts IMeterFactory automatisch im Dienstcontainer, oder Sie können den Typ durch Aufrufen von AddMetrics manuell in einem beliebigen IServiceCollection-Element registrieren. Die Verbrauchseinheitenfactory integriert Metriken in DI, wobei Verbrauchseinheiten in unterschiedlichen Dienstsammlungen voneinander isoliert bleiben, auch wenn sie einen identischen Namen verwenden. Dies ist besonders hilfreich für Tests, sodass mehrere parallel ausgeführte Tests nur Messungen berücksichtigen, die im selben Testfall generiert wurden.

Um eine Verbrauchseinheit in einem für DI entwickelten Typ abzurufen, fügen Sie dem Konstruktor einen IMeterFactory-Parameter hinzu, und rufen Sie dann Create auf. Dieses Beispiel zeigt die Verwendung von IMeterFactory in einer ASP.NET Core-App.

Definieren Sie einen Typ für die Instrumente:

public class HatCoMetrics
{
    private readonly Counter<int> _hatsSold;

    public HatCoMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("HatCo.Store");
        _hatsSold = meter.CreateCounter<int>("hatco.store.hats_sold");
    }

    public void HatsSold(int quantity)
    {
        _hatsSold.Add(quantity);
    }
}

Registrieren Sie den Typ beim DI-Container in Program.cs.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<HatCoMetrics>();

Fügen Sie den Metriktyp ein, und erfassen Sie Werte nach Bedarf. Durch die Registrierung des Metriktyps bei DI kann er mit MVC-Controllern, mit minimalen APIs oder mit einem beliebigen anderen Typ verwendet werden, der von DI erstellt wird:

app.MapPost("/complete-sale", ([FromBody] SaleModel model, HatCoMetrics metrics) =>
{
    // ... business logic such as saving the sale to a database ...

    metrics.HatsSold(model.QuantitySold);
});

Bewährte Methoden

  • System.Diagnostics.Metrics.Meter implementiert IDisposable, aber IMeterFactory verwaltet automatisch die Lebensdauer aller davon erstellten Meter-Objekte und entfernt sie, wenn der DI-Container verworfen wird. Es ist unnötig, zusätzlichen Code hinzuzufügen, um Dispose() für Meter aufzurufen, und er hat keine Auswirkung.

Typen von Instrumenten

Bisher haben wir nur ein Counter<T>-Instrument gezeigt, allerdings sind noch mehr Instrumententypen verfügbar. Die Instrumente unterscheiden sich auf zwei Arten:

  • Standardmäßige Metrikberechnungen: Tools, die die Instrumentenmessungen erfassen und analysieren, berechnen je nach Instrument unterschiedliche Standardmetriken.
  • Speicher mit aggregierten Daten: Für Metriken, die vielseitig nutzbar sind, müssen Daten aus vielen Messungen aggregiert werden. Eine Möglichkeit besteht darin, dass der Aufrufer einzelne Messungen zu beliebigen Zeiten bereitstellt und das Sammlungstool die Aggregation verwaltet. Alternativ kann der Aufrufer die aggregierten Messungen verwalten und bei Bedarf in einem Rückruf bereitstellen.

Derzeit verfügbare Instrumententypen:

  • Zähler (CreateCounter): Dieses Instrument verfolgt einen Wert nach, der im Laufe der Zeit zunimmt, und der Aufrufer meldet die Inkremente mithilfe von Add. Die meisten Tools berechnen die Summe und die Änderungsrate in der Summe. Für Tools, die nur ein Element zeigen, wird die Änderungsrate empfohlen. Nehmen Sie beispielsweise an, dass der Aufrufer Add() einmal pro Sekunde mit den aufeinander folgenden Werten 1, 2, 4, 5, 4, 3 aufruft. Wenn das Sammlungstool alle drei Sekunden aktualisiert wird, beträgt die Summe nach drei Sekunden 1+2+4=7 und nach sechs Sekunden 1+2+4+5+4+3=19. Die Änderungsrate ist (Summe_aktuell – Summe_zuvor), sodass das Tool nach drei Sekunden 7-0 = 7 und nach sechs Sekunden 19-7 = 12 meldet.

  • UpDownCounter (CreateUpDownCounter): Dieses Instrument verfolgt einen Wert nach, der sich im Laufe der Zeit erhöhen oder verringern kann. Der Aufrufer meldet die Inkremente und Dekremente mithilfe von Add. Nehmen Sie beispielsweise an, dass der Aufrufer Add() einmal pro Sekunde mit den aufeinander folgenden Werten 1, 5, -2, 3, -1, -3 aufruft. Wenn das Sammlungstool alle drei Sekunden aktualisiert wird, beträgt die Summe nach drei Sekunden 1+5-2=4 und nach sechs Sekunden 1+5-2+3-1-3=3.

  • ObservableCounter (CreateObservableCounter): Dieses Instrument ähnelt dem Zähler, außer dass der Aufrufer jetzt für die Verwaltung der aggregierten Summe verantwortlich ist. Der Aufrufer stellt einen Rückrufdelegaten bereit, wenn der ObservableCounter erstellt wird, und der Rückruf wird immer dann aufgerufen, wenn Tools die aktuelle Summe überwachen müssen. Wenn ein Sammlungstool beispielsweise alle drei Sekunden aktualisiert wird, wird die Rückruffunktion ebenfalls alle drei Sekunden aufgerufen. Für die meisten Tools sind sowohl die Summe als auch die Änderungsrate in der Summe verfügbar. Wenn nur ein Wert angezeigt werden kann, wird die Änderungsrate empfohlen. Wenn der Rückruf beim ersten Aufruf 0 zurückgibt, 7 beim erneuten Aufruf nach drei Sekunden und 19 beim Aufruf nach sechs Sekunden, meldet das Tool diese Werte unverändert als Summen. Für die Änderungsrate zeigt das Tool nach drei Sekunden 7-0=7 und nach sechs Sekunden 19-7=12 an.

  • ObservableUpDownCounter (CreateObservableUpDownCounter): Dieses Instrument ähnelt dem Zähler, außer dass der Aufrufer jetzt für die Verwaltung der aggregierten Summe verantwortlich ist. Der Aufrufer stellt einen Rückrufdelegaten bereit, wenn der ObservableUpDownCounter erstellt wird, und der Rückruf wird immer dann aufgerufen, wenn Tools die aktuelle Summe überwachen müssen. Wenn ein Sammlungstool beispielsweise alle drei Sekunden aktualisiert wird, wird die Rückruffunktion ebenfalls alle drei Sekunden aufgerufen. Jeder vom Rückruf zurückgegebene Wert wird im Auflistungstool unverändert als Summe angezeigt.

  • ObservableGauge (CreateObservableGauge): Mit diesem Instrument kann der Aufrufer einen Rückruf bereitstellen, bei dem der gemessene Wert direkt als Metrik übergeben wird. Jedes Mal, wenn das Sammlungstool aktualisiert wird, wird der Rückruf aufgerufen, und der vom Rückruf zurückgegebene Wert wird im Tool angezeigt.

  • Histogramm (CreateHistogram): Dieses Instrument verfolgt die Messverteilung nach. Es gibt keine einzige kanonische Möglichkeit, einen Satz von Messungen zu beschreiben. Es wird jedoch empfohlen, als Tools Histogramme oder berechnete Perzentile zu verwenden. Nehmen Sie beispielsweise an, dass der Aufrufer Record aufgerufen hat, um diese Messungen während des Aktualisierungsintervalls des Sammlungstools aufzuzeichnen: 1,5,2,3,10,9,7,4,6,8. Ein Sammlungstool meldet möglicherweise, dass die 50., 90. und 95. Perzentile dieser Messungen 5, 9 bzw. 9 sind.

Bewährte Methoden bei der Auswahl eines Instrumententyps

  • Verwenden Sie im Hinblick auf Zähler oder andere Werte, die im Laufe der Zeit ausschließlich zunehmen, die Instrumente Zähler oder ObservableCounter. Wählen Sie zwischen Zähler und ObservableCounter, je nachdem, was dem vorhandenen Code einfacher hinzugefügt werden kann: entweder ein API-Aufruf für jeden Inkrementvorgang oder ein Rückruf, der die aktuelle Summe aus einer Variablen liest, die der Code verwaltet. In extrem heißen Codepfaden, bei denen Leistung wichtig ist und die Verwendung von Add mehr als eine Million Aufrufe pro Sekunde pro Thread erzeugen würde, bietet die Verwendung von ObservableCounter gegebenenfalls mehr Möglichkeiten zur Optimierung.

  • Für Zeitsteuerungen wird in der Regel das Histogramm bevorzugt. Häufig ist es hilfreich, das Ende dieser Verteilungen (90., 95., 99. Perzentil) anstelle von Durchschnittswerten oder der Summen zu verstehen.

  • Andere häufige Fälle wie Cachetrefferraten oder Größen von Caches, Warteschlangen und Dateien eignen sich in der Regel gut für UpDownCounter oder ObservableUpDownCounter. Wählen Sie zwischen ihnen, je nachdem, was dem vorhandenen Code einfacher hinzugefügt werden kann: entweder ein API-Aufruf für jeden Inkrement- und Dekrementvorgang, oder ein Rückruf, der den aktuellen Wert aus einer Variablen liest, die der Code verwaltet.

Hinweis

Wenn Sie eine ältere Version von .NET oder ein DiagnosticSource NuGet-Paket verwenden, das UpDownCounter und ObservableUpDownCounter (vor Version 7) nicht unterstützt, ist ObservableGauge häufig ein guter Ersatz.

Beispiel für verschiedene Instrumententypen

Beenden Sie den zuvor gestarteten Beispielprozess und ersetzen Sie den Beispielcode in Program.cs durch:

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");
    static Histogram<double> s_orderProcessingTime = s_meter.CreateHistogram<double>("hatco.store.order_processing_time");
    static int s_coatsSold;
    static int s_ordersPending;

    static Random s_rand = new Random();

    static void Main(string[] args)
    {
        s_meter.CreateObservableCounter<int>("hatco.store.coats_sold", () => s_coatsSold);
        s_meter.CreateObservableGauge<int>("hatco.store.orders_pending", () => s_ordersPending);

        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has one transaction each 100ms that each sell 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);

            // Pretend we also sold 3 coats. For an ObservableCounter we track the value in our variable and report it
            // on demand in the callback
            s_coatsSold += 3;

            // Pretend we have some queue of orders that varies over time. The callback for the orders_pending gauge will report
            // this value on-demand.
            s_ordersPending = s_rand.Next(0, 20);

            // Last we pretend that we measured how long it took to do the transaction (for example we could time it with Stopwatch)
            s_orderProcessingTime.Record(s_rand.Next(0.005, 0.015));
        }
    }
}

Führen Sie den neuen Prozess aus und verwenden Sie dotnet-counters wie zuvor in einer zweiten Shell, um die Metriken anzuzeigen:

> dotnet-counters monitor -n metric-demo.exe --counters HatCo.Store
Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.coats_sold (Count / 1 sec)                                27
    hatco.store.hats_sold (Count / 1 sec)                                 36
    hatco.store.order_processing_time
        Percentile=50                                                      0.012
        Percentile=95                                                      0.014
        Percentile=99                                                      0.014
    hatco.store.orders_pending                                             5

In diesem Beispiel werden einige zufällig generierte Zahlen verwendet, sodass Ihre Werte etwas variieren. Sie können sehen, dass hatco.store.hats_sold (Zähler) und hatco.store.coats_sold (observableCounter) beide als Rate angezeigt werden. ObservableGauge, hatco.store.orders_pending, wird als absoluter Wert angezeigt. dotnet-counters rendert Histogramminstrumente als Statistiken mit drei Perzentilen (50., 95. und 99.), aber andere Tools können die Verteilung anders zusammenfassen oder mehr Konfigurationsoptionen bieten.

Bewährte Methoden

  • Histogramme speichern in der Regel viel mehr Daten im Arbeitsspeicher als andere Metriktypen. Die genaue Speicherauslastung wird jedoch durch das verwendete Sammlungstool bestimmt. Wenn Sie eine große Anzahl (>100) von Histogrammmetriken definieren, müssen Sie Benutzer möglicherweise darauf hinweisen, dass sie diese nicht alle gleichzeitig aktivieren. Zudem sollten Benutzer ihre Tools so konfigurieren, dass durch Verringern der Genauigkeit Arbeitsspeicher gespart wird. Für einige Sammlungstools gelten im Hinblick auf die Anzahl gleichzeitig überwachter Histogramme möglicherweise harte Grenzwerte, um eine übermäßige Speicherauslastung zu vermeiden.

  • Rückrufe für alle überwachbaren Instrumente werden nacheinander aufgerufen, sodass lange dauernde Rückrufe die Erfassung aller Metriken verzögern oder verhindern können. Bevorzugen Sie das schnelle Lesen eines zwischengespeicherten Werts, kein Zurückgeben von Messungen oder auch das Auslösen einer Ausnahme gegenüber der Ausführung eines potenziell lang andauernden oder blockierenden Vorgangs.

  • Die Rückrufe „ObservableCounter“, „ObservableUpDownCounter“ und „ObservableGauge“ treten in einem Thread auf, der normalerweise nicht mit dem Code synchronisiert wird, der die Werte aktualisiert. Es liegt in Ihrer Verantwortung, entweder den Speicherzugriff zu synchronisieren oder die inkonsistenten Werte zu akzeptieren, die sich aus der Verwendung nicht synchronisierter Zugriff ergeben können. Häufige Ansätze für die Synchronisierung des Zugriffs sind die Verwendung einer Sperre oder eines Anrufs Volatile.Read und Volatile.Write.

  • Die Funktionen CreateObservableGauge und CreateObservableCounter geben ein Instrumentenobjekt zurück. In den meisten Fällen müssen Sie dies allerdings nicht in einer Variablen speichern, da keine weitere Interaktion mit dem Objekt erforderlich ist. Die Zuweisung zu einer statischen Variable wie bei den anderen Instrumenten ist zulässig, aber fehleranfällig, da die statische C#-Initialisierung verzögert ist und in der Regel nie auf die Variable verwiesen wird. Hier sehen Sie ein Beispiel für das Problem:

    using System;
    using System.Diagnostics.Metrics;
    
    class Program
    {
        // BEWARE! Static initializers only run when code in a running method refers to a static variable.
        // These statics will never be initialized because none of them were referenced in Main().
        //
        static Meter s_meter = new Meter("HatCo.Store");
        static ObservableCounter<int> s_coatsSold = s_meter.CreateObservableCounter<int>("hatco.store.coats_sold", () => s_rand.Next(1,10));
        static Random s_rand = new Random();
    
        static void Main(string[] args)
        {
            Console.ReadLine();
        }
    }
    

Beschreibungen und Einheiten

Instrumente können optionale Beschreibungen und Einheiten angeben. Diese Werte sind für alle Metrikberechnungen nicht transparent, können aber auf der Benutzeroberfläche des Sammlungstools angezeigt werden, damit Techniker verstehen, wie die Daten interpretiert werden. Beenden Sie den zuvor gestarteten Beispielprozess und ersetzen Sie den Beispielcode in Program.cs durch:

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>(name: "hatco.store.hats_sold",
                                                                unit: "{hats}",
                                                                description: "The number of hats sold in our store");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each 100ms that sells 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);
        }
    }
}

Führen Sie den neuen Prozess aus und verwenden Sie dotnet-counters wie zuvor in einer zweiten Shell, um die Metriken anzuzeigen:

Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold ({hats} / 1 sec)                                40

dotnet-counters verwendet derzeit nicht den Beschreibungstext auf der Benutzeroberfläche, zeigt jedoch die Einheit an, wenn diese bereitgestellt wird. In diesem Fall sehen Sie, dass „{hats}“ den generischen Begriff „Zählwert“ ersetzt hat, der in vorherigen Beschreibungen sichtbar ist.

Bewährte Methoden

  • .NET-APIs ermöglichen die Verwendung einer beliebigen Zeichenfolge als Einheit, wir empfehlen jedoch die Verwendung von UCUM, einem internationalen Standard für Einheitennamen. Die geschweiften Klammern um „{hats}“ sind Teil des UCUM-Standards und weisen darauf hin, dass es sich um eine beschreibende Anmerkung und nicht um einen Einheitennamen mit einer standardisierten Bedeutung wie Sekunden oder Bytes handelt.

  • Die im Konstruktor angegebene Einheit sollte die Einheiten beschreiben, die für eine einzelne Messung geeignet sind. Dies unterscheidet sich manchmal von den Einheiten in der abschließenden Metrik. In diesem Beispiel ist jede Messung eine bestimmte Anzahl von Mützen, daher ist „{hats}“ die geeignete Einheit, die an den Konstruktor zu übergeben ist. Das Sammlungstool hat eine Rate berechnet und selbständig abgeleitet, dass die entsprechende Einheit für die berechnete Metrik {hats}/Sek. ist.

  • Bei der Aufzeichnung von Zeitmessungen bevorzugen Sie Einheiten von Sekunden, die als Gleitkomma- oder Double-Wert aufgezeichnet werden.

Mehrdimensionale Metriken

Messungen können auch Schlüssel-Wert-Paaren zugeordnet werden, die als Tags bezeichnet werden und für die Analyse die Kategorisierung von Daten ermöglichen. Beispielsweise kann es sein, dass HatCo nicht nur die Anzahl der verkauften Mützen, sondern auch die Größe und Farbe aufzeichnen möchte. Bei der späteren Analyse der Daten können HatCo-Techniker die Summen nach Größe, Farbe oder einer beliebigen Kombination aus beiden aufschlüsseln.

Indikator- und Histogrammtags können in Überladungen von Add und Recordangegeben werden, die ein oder mehrere KeyValuePair Argumente verwenden. Beispiel:

s_hatsSold.Add(2,
               new KeyValuePair<string, object>("product.color", "red"),
               new KeyValuePair<string, object>("product.size", 12));

Ersetzen Sie den Code von Program.cs und führen Sie die App und dotnet-counters wie zuvor erneut aus:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction, every 100ms, that sells two size 12 red hats, and one size 19 blue hat.
            Thread.Sleep(100);
            s_hatsSold.Add(2,
                           new KeyValuePair<string,object>("product.color", "red"),
                           new KeyValuePair<string,object>("product.size", 12));
            s_hatsSold.Add(1,
                           new KeyValuePair<string,object>("product.color", "blue"),
                           new KeyValuePair<string,object>("product.size", 19));
        }
    }
}

dotnet-counters zeigt nun eine grundlegende Kategorisierung:

Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold (Count / 1 sec)
        product.color=blue,product.size=19                                 9
        product.color=red,product.size=12                                 18

Für ObservableCounter und ObservableGauge können markierte Messungen im Rückruf bereitgestellt werden, der an den Konstruktor übergeben wird:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");

    static void Main(string[] args)
    {
        s_meter.CreateObservableGauge<int>("hatco.store.orders_pending", GetOrdersPending);
        Console.WriteLine("Press any key to exit");
        Console.ReadLine();
    }

    static IEnumerable<Measurement<int>> GetOrdersPending()
    {
        return new Measurement<int>[]
        {
            // pretend these measurements were read from a real queue somewhere
            new Measurement<int>(6, new KeyValuePair<string,object>("customer.country", "Italy")),
            new Measurement<int>(3, new KeyValuePair<string,object>("customer.country", "Spain")),
            new Measurement<int>(1, new KeyValuePair<string,object>("customer.country", "Mexico")),
        };
    }
}

Wenn sie wie zuvor mit dotnet-counters ausgeführt werden, ist das Ergebnis:

Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.orders_pending
        customer.country=Italy                                             6
        customer.country=Mexico                                            1
        customer.country=Spain                                             3

Bewährte Methoden

  • Obwohl die API die Verwendung beliebiger Objekte als Tagwert zulässt, werden numerische Typen und Zeichenfolgen von den Sammlungstools erwartet. Andere Typen können von einem bestimmten Sammlungstool unterstützt werden.

  • Es wird empfohlen, bei Tagnamen die OpenTelemetry-Benennungsrichtlinien zu befolgen, bei denen gepunktete hierarchische Namen in Kleinbuchstaben mit Unterstrichen (_) verwendet werden, um mehrere Wörter im selben Element zu trennen. Wenn Tagnamen in unterschiedlichen Metriken oder anderen Telemetriedatensätzen wiederverwendet werden, sollten sie überall dort, wo sie verwendet werden, dieselbe Bedeutung und dieselben gültigen Werte haben.

    Beispiele für Tagnamen:

    • customer.country
    • store.payment_method
    • store.purchase_result
  • Beachten Sie, dass in der Praxis sehr große oder ungebundene Kombinationen von Tagwerten aufgezeichnet werden können. Dies sollten Sie vermeiden. Obwohl die .NET-API-Implementierung dies verarbeiten kann, weisen Sammlungstools wahrscheinlich Speicher für Metrikdaten zu, die jeder Tagkombination zugeordnet sind. Dieser kann sehr groß werden. So ist es beispielsweise in Ordnung, wenn HatCo 10 verschiedene Mützenfarben und 25 Mützengrößen hat, denn dann sind insgesamt bis zu 10*25=250 Verkäufe nachzuverfolgen. Wenn HatCo jedoch ein drittes Tag, z. B. eine Kunden-ID für den Verkauf, hinzugefügt hat und die Mützen an 100 Millionen Kunden weltweit verkauft werden, werden jetzt wahrscheinlich Milliarden verschiedener Tagkombinationen aufgezeichnet. Die meisten Sammlungstools für Metriken legen entweder Daten ab, um innerhalb der technischen Grenzwerte zu bleiben, oder es können hohe Kosten für die Datenspeicherung und -verarbeitung entstehen. Die Implementierung der einzelnen Sammlungstools bestimmt die jeweiligen Grenzwerte, aber wahrscheinlich sind weniger als 1.000 Kombinationen für ein Instrument als sicher anzusehen. Alles über 1.000 Kombinationen setzt voraus, dass das Sammlungstool einen Filter anwendet oder auf einen hohen Umfang ausgelegt wird. Histogrammimplementierungen verwenden tendenziell viel mehr Arbeitsspeicher als andere Metriken, sodass die sicheren Grenzwerte 10-100 mal niedriger sein können. Wenn Sie eine große Anzahl von eindeutigen Tagkombinationen erwarten, sind Protokolle, Transaktionsdatenbanken oder Big Data-Verarbeitungssysteme möglicherweise besser geeignet, um den erforderlichen Umfang zu bewältigen.

  • Bei Instrumenten mit einer sehr großen Anzahl von Tagkombinationen sollten Sie einen kleineren Speichertyp verwenden, um den Arbeitsspeicheraufwand zu reduzieren. Beispielsweise belegt das Speichern von short für eine Counter<short> nur 2 Bytes pro Tagkombination, während eine double für Counter<double> 8 Bytes pro Tagkombination belegt.

  • Sammlungstools sollten für Code optimiert werden, der für jeden Aufruf die gleiche Reihe von Tagnamen in der gleichen Reihenfolge angibt, um Messungen auf demselben Instrument aufzuzeichnen. Für Hochleistungscode, der häufig Add und Record aufrufen muss, sollten Sie für jeden Aufruf die gleiche Sequenz von Tagnamen verwenden.

  • Die .NET-API ist so optimiert, dass sie für Aufrufe von Add und Record ohne Zuordnung ist, bei denen drei oder weniger Tags einzeln angegeben sind. Verwenden Sie TagList, um Zuordnungen mit einer größeren Anzahl von Tags zu vermeiden. Im Allgemeinen steigt der Leistungsaufwand dieser Aufrufe, wenn mehr Tags verwendet werden.

Hinweis

OpenTelemetry bezieht sich auf Tags als „Attribute“. Dabei handelt es sich um zwei verschiedene Namen für dieselbe Funktionalität.

Testen benutzerdefinierter Metriken

Sie können alle benutzerdefinierten Metriken, die Sie hinzufügen, mithilfe von MetricCollector<T> testen. Dieser Typ macht es einfach, die Messungen aus bestimmten Instrumenten aufzuzeichnen und die Richtigkeit der Werte zu bestätigen.

Testen mit Abhängigkeitseinschleusung

Der folgende Code zeigt einen Beispieltestfall für Codekomponenten, die Abhängigkeitseinschleusung und IMeterFactory verwenden.

public class MetricTests
{
    [Fact]
    public void SaleIncrementsHatsSoldCounter()
    {
        // Arrange
        var services = CreateServiceProvider();
        var metrics = services.GetRequiredService<HatCoMetrics>();
        var meterFactory = services.GetRequiredService<IMeterFactory>();
        var collector = new MetricCollector<int>(meterFactory, "HatCo.Store", "hatco.store.hats_sold");

        // Act
        metrics.HatsSold(15);

        // Assert
        var measurements = collector.GetMeasurementSnapshot();
        Assert.Equal(1, measurements.Count);
        Assert.Equal(15, measurements[0].Value);
    }

    // Setup a new service provider. This example creates the collection explicitly but you might leverage
    // a host or some other application setup code to do this as well.
    private static IServiceProvider CreateServiceProvider()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddMetrics();
        serviceCollection.AddSingleton<HatCoMetrics>();
        return serviceCollection.BuildServiceProvider();
    }
}

Jedes MetricCollector-Objekt zeichnet alle Messungen für ein Instrument auf. Wenn Sie Messungen aus mehreren Instrumenten überprüfen müssen, erstellen Sie für jedes ein MetricCollector-Objekt

Testen ohne Abhängigkeitseinschleusung

Es ist auch möglich, Code zu testen, der ein freigegebenes globales Meter-Objekt in einem statischen Feld verwendet. Stellen Sie jedoch sicher, dass solche Tests nicht parallel ausgeführt werden. Da das Meter-Objekt freigegeben wird, berücksichtigt MetricCollector in einem Test die Messungen, die von anderen parallel ausgeführten Tests generiert werden.

class HatCoMetricsWithGlobalMeter
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    public void HatsSold(int quantity)
    {
        s_hatsSold.Add(quantity);
    }
}

public class MetricTests
{
    [Fact]
    public void SaleIncrementsHatsSoldCounter()
    {
        // Arrange
        var metrics = new HatCoMetricsWithGlobalMeter();
        // Be careful specifying scope=null. This binds the collector to a global Meter and tests
        // that use global state should not be configured to run in parallel.
        var collector = new MetricCollector<int>(null, "HatCo.Store", "hatco.store.hats_sold");

        // Act
        metrics.HatsSold(15);

        // Assert
        var measurements = collector.GetMeasurementSnapshot();
        Assert.Equal(1, measurements.Count);
        Assert.Equal(15, measurements[0].Value);
    }
}