Udostępnij za pośrednictwem


Tworzenie metryk

Ten artykuł dotyczy: ✔️ .NET Core 6 i nowsze wersje ✔️ .NET Framework 4.6.1 lub nowsze wersje

Aplikacje platformy .NET można instrumentować przy użyciu interfejsów API System.Diagnostics.Metrics do śledzenia ważnych metryk. Niektóre metryki są uwzględniane w standardowych bibliotekach .NET, ale warto dodać nowe metryki niestandardowe, które są istotne dla aplikacji i bibliotek. W tym samouczku dodasz nowe metryki i dowiesz się, jakie typy metryk są dostępne.

Notatka

Platforma .NET ma pewne starsze interfejsy API metryk, a mianowicie EventCounters i System.Diagnostics.PerformanceCounter, które nie zostały tutaj omówione. Aby dowiedzieć się więcej o tych alternatywach, zobacz Porównanie interfejsów API metryk.

Utwórz metrykę niestandardową

Wymagania wstępne: .NET Core 6 SDK lub nowsza wersja

Utwórz nową aplikację konsolową odwołującą się do pakietu NuGet System.Diagnostics.DiagnosticSource w wersji 8 lub nowszej. Aplikacje przeznaczone dla platformy .NET 8+ domyślnie zawierają to odwołanie. Następnie zaktualizuj kod w Program.cs, aby pasował do:

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

Typ System.Diagnostics.Metrics.Meter to punkt początkowy dla biblioteki do tworzenia nazwanej grupy instrumentów. Instrumenty rejestrują pomiary liczbowe potrzebne do obliczenia metryk. W tym miejscu użyliśmy CreateCounter do utworzenia instrumentu counter o nazwie "hatco.store.hats_sold". Podczas każdej transakcji pozornej kod wywołuje Add, aby zarejestrować liczbę sprzedanych kapeluszy, która wynosi 4 w tym przypadku. Instrument "hatco.store.hats_sold" niejawnie definiuje niektóre metryki, które można obliczyć na podstawie tych pomiarów, takie jak łączna liczba sprzedanych kapeluszy lub sprzedanych kapeluszy na sekundę. Ostatecznie do narzędzi do zbierania metryk należy określić, które metryki mają być obliczane i jak wykonywać te obliczenia, ale każdy instrument ma pewne domyślne konwencje, które przekazują intencję dewelopera. W przypadku liczników konwencja polega na tym, że narzędzia do zbierania pokazują łączną liczbę i/lub tempo, w jakim liczba rośnie.

Ogólny parametr int w Counter<int> i CreateCounter<int>(...) definiuje, że ten licznik musi mieć możliwość przechowywania wartości do Int32.MaxValue. Możesz użyć dowolnego z byte, short, int, long, float, doublelub decimal w zależności od rozmiaru danych, które należy przechowywać i czy są potrzebne wartości ułamkowe.

Uruchom aplikację i pozostaw ją uruchomioną na razie. Następnie wyświetlimy metryki.

> dotnet run
Press any key to exit

Najlepsze rozwiązania

  • W przypadku kodu, który nie jest przeznaczony do użycia w kontenerze DI (wstrzykiwania zależności), utwórz miernik raz i zapisz go w zmiennej statycznej. W przypadku użycia w bibliotekach obsługujących wstrzykiwanie zależności, zmienne statyczne są uważane za antywzorzec, a poniższy przykład DI przedstawia bardziej konwencjonalne podejście. Każda biblioteka lub podkomponent biblioteki może (i często powinna) tworzyć własne Meter. Rozważ utworzenie nowego miernika zamiast ponownego użycia istniejącego, jeśli przewidujesz, że deweloperzy aplikacji docenią możliwość łatwego włączania i wyłączania grup metryk oddzielnie.

  • Nazwa przekazana do konstruktora Meter powinna być unikalna, aby odróżnić ją od innych liczników. Zalecamy wytycznych dotyczących nazewnictwa biblioteki OpenTelemetry, które używają nazw hierarchicznych kropkowanych. Nazwy zestawów lub nazwy przestrzeni nazw dla instrumentowanego kodu są zwykle dobrym wyborem. Jeśli zestaw dodaje instrumentację dla kodu w drugim, niezależnym zestawie, nazwa powinna być oparta na zestawie, który definiuje miernik, a nie zestaw, którego kod jest instrumentowany.

  • Platforma .NET nie wymusza żadnego schematu nazewnictwa dla instrumentów, ale zalecamy stosowanie wytycznych dotyczących nazewnictwa openTelemetrii, które używają małych liter kropkowanych nazw hierarchicznych i podkreślenia ('_') jako separatora między wieloma słowami w tym samym elemecie. Nie wszystkie narzędzia metryk zachowują nazwę miernika jako część końcowej nazwy metryki, dlatego korzystne jest, aby nazwa narzędzia była globalnie unikatowa samodzielnie.

    Przykładowe nazwy instrumentów:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • Interfejsy API do tworzenia instrumentów i zapisywania pomiarów są bezpieczne wątkowo. W bibliotekach platformy .NET większość metod wystąpień wymaga synchronizacji w przypadku wywołania na tym samym obiekcie z wielu wątków, ale nie jest to wymagane w tym przypadku.

  • Interfejsy Instrument APIs do rejestrowania pomiarów (Add w tym przykładzie) zwykle działają w <10 ns, gdy nie są zbierane żadne dane, lub od dziesięciu do kilkuset nanosekund, gdy pomiary są zbierane przez bibliotekę lub narzędzie o wysokiej wydajności. Dzięki temu te interfejsy API mogą być używane liberalnie w większości przypadków, ale dbają o kod, który jest bardzo wrażliwy na wydajność.

Wyświetlanie nowej metryki

Istnieje wiele opcji przechowywania i wyświetlania metryk. W tym samouczku jest używane narzędzie dotnet-counters, które jest przydatne do analizy ad hoc. Możesz również zapoznać się z samouczkiem dotyczącym zbierania metryk , aby poznać inne możliwości. Jeśli narzędzie dotnet-counters nie jest jeszcze zainstalowane, użyj SDK, aby je zainstalować.

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

Mimo że przykładowa aplikacja jest nadal uruchomiona, użyj liczników dotnet-counter do monitorowania nowego licznika:

> 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

Zgodnie z oczekiwaniami widać, że sklep HatCo stale sprzedaje 4 kapelusze na sekundę.

Pobieranie miernika za pośrednictwem wstrzykiwania zależności

W poprzednim przykładzie miernik został uzyskany przez utworzenie go za pomocą new i przypisanie go do pola statycznego. Użycie statycznych elementów w ten sposób nie jest dobrym podejściem podczas korzystania z wstrzykiwania zależności (DI). W kodzie, który używa di, takich jak ASP.NET Core lub aplikacji z ogólnego hosta, utwórz obiekt Miernik przy użyciu IMeterFactory. Począwszy od platformy .NET 8, hosty będą automatycznie rejestrować IMeterFactory w kontenerze usług, lub można ręcznie zarejestrować typ w dowolnym IServiceCollection przez wywołanie AddMetrics. Fabryka mierników integruje metryki z DI, utrzymując mierniki w różnych kolekcjach usług odseparowane od siebie, nawet jeśli używają identycznej nazwy. Jest to szczególnie przydatne w przypadku testowania, dzięki czemu wiele testów uruchomionych równolegle obserwuje tylko pomiary generowane z poziomu tego samego przypadku testowego.

Aby uzyskać miernik w typie zaprojektowanym dla DI, dodaj parametr IMeterFactory do konstruktora. Następnie wywołaj Create. W tym przykładzie pokazano użycie funkcji IMeterFactory w aplikacji ASP.NET Core.

Zdefiniuj typ do przechowywania instrumentów:

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

Zarejestruj typ w kontenerze DI w Program.cs.

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

Należy wdrożyć typ metryk i zanotować wartości tam, gdzie to konieczne. Ponieważ typ metryk jest zarejestrowany w di, może być używany z kontrolerami MVC, minimalnymi interfejsami API lub dowolnym innym typem utworzonym przez di:

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

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

Najlepsze rozwiązania

  • System.Diagnostics.Metrics.Meter implementuje interfejs IDisposable, ale IMeterFactory automatycznie zarządza cyklem życia wszystkich tworzonych obiektów Meter, usuwając je po usunięciu kontenera DI. Nie ma potrzeby dodawania dodatkowego kodu do wywołania Dispose() na Meter, ponieważ nie będzie to miało żadnego wpływu.

Typy instrumentów

Do tej pory przedstawiliśmy tylko instrument Counter<T>, ale dostępnych jest więcej typów instrumentów. Instrumenty różnią się na dwa sposoby:

  • domyślne obliczenia metryk — narzędzia, które zbierają i analizują pomiary instrumentów, będą obliczać różne domyślne metryki w zależności od instrumentu.
  • Przechowywanie zagregowanych danych — najbardziej przydatne metryki wymagają agregowania danych z wielu pomiarów. Jedną z opcji jest to, że inicjator wywołania dostarcza poszczególne pomiary w dowolnych momentach, a narzędzie zbierające zarządza agregacją. Alternatywnie obiekt wywołujący może zarządzać zagregowanymi pomiarami i dostarczać je na żądanie w wywołaniu zwrotnym.

Obecnie dostępne typy instrumentów:

  • Counter (CreateCounter) — ten instrument śledzi wartość, która wzrasta wraz z upływem czasu, a obiekt wywołujący zgłasza przyrosty przy użyciu Add. Większość narzędzi oblicza sumę i współczynnik zmian w sumie. W przypadku narzędzi, które pokazują tylko jedną rzecz, zalecana jest szybkość zmian. Załóżmy na przykład, że obiekt wywołujący wywołuje Add() raz na sekundę z kolejnymi wartościami 1, 2, 4, 5, 4, 3. Jeśli narzędzie kolekcji jest aktualizowane co trzy sekundy, suma po trzech sekundach wynosi 1+2+4=7, a suma po sześciu sekundach wynosi 1+2+4+5+4+3=19. Współczynnik zmian to (obecna_suma - poprzednia_suma), więc po trzech sekundach narzędzie podaje 7-0=7, a po sześciu sekundach pokazuje 19-7=12.

  • UpDownCounter (CreateUpDownCounter) - to urządzenie śledzi wartość, która może wzrosnąć lub zmniejszyć się w czasie. Obiekt wywołujący zgłasza przyrosty i dekrementy przy użyciu Add. Załóżmy na przykład, że obiekt wywołujący wywołuje Add() raz na sekundę z kolejnymi wartościami 1, 5, -2, 3, -1, -3. Jeśli narzędzie kolekcji aktualizuje co trzy sekundy, suma po trzech sekundach wynosi 1+5-2=4, a suma po sześciu sekundach wynosi 1+5-2+3-1-3=3.

  • ObservableCounter (CreateObservableCounter) — to narzędzie jest podobne do Counter, z tą różnicą, że obiekt wywołujący jest teraz odpowiedzialny za utrzymanie zagregowanej sumy. Obiekt wywołujący udostępnia delegata wywołania zwrotnego podczas tworzenia elementu ObservableCounter, a wywołanie zwrotne jest aktywowane za każdym razem, gdy narzędzia muszą monitorować bieżącą sumę. Jeśli na przykład narzędzie do zbierania jest aktualizowane co trzy sekundy, funkcja wywołania zwrotnego będzie również wywoływana co trzy sekundy. Większość narzędzi będzie mieć zarówno łączną, jak i stawkę zmian w sumie dostępnej. Jeśli można wyświetlić tylko jeden, zalecana jest szybkość zmian. Jeśli wywołanie zwrotne zwraca wartość 0 w początkowym wywołaniu, 7, gdy jest wywoływana ponownie po trzech sekundach, a 19 po wywołaniu po sześciu sekundach, narzędzie zgłosi te wartości bez zmian jako sumy. W przypadku współczynnika zmian narzędzie będzie pokazywać wartość 7-0=7 po trzech sekundach i 19-7=12 po sześciu sekundach.

  • ObservableUpDownCounter (CreateObservableUpDownCounter) — to narzędzie jest podobne do UpDownCounter, z tą różnicą, że obiekt wywołujący jest teraz odpowiedzialny za utrzymanie zagregowanej sumy. Obiekt wywołujący udostępnia delegat wywołania zwrotnego po utworzeniu elementu ObservableUpDownCounter, a wywołanie zwrotne jest wywoływane za każdym razem, gdy narzędzia muszą obserwować bieżącą sumę. Jeśli na przykład narzędzie do zbierania jest aktualizowane co trzy sekundy, funkcja wywołania zwrotnego będzie również wywoływana co trzy sekundy. Dowolna wartość zwracana przez funkcję zwrotną będzie wyświetlana w narzędziu zbiorczym bez zmian jako całkowita.

  • miernik (CreateGauge) — to narzędzie umożliwia wywołującemu ustawienie bieżącej wartości metryki przy użyciu metody Record. Wartość można zaktualizować w dowolnym momencie, wywołując ponownie metodę, a narzędzie do zbierania metryk wyświetli dowolną ostatnio ustawioną wartość.

  • ObservableGauge (CreateObservableGauge) — to narzędzie umożliwia użytkownikowi podać wywołanie zwrotne, w którym mierzona wartość jest przekazywana bezpośrednio jako metryka. Za każdym razem, gdy narzędzie do zbierania danych jest aktualizowane, wywoływana jest funkcja zwrotna, a wartość zwracana przez tę funkcję jest wyświetlana w narzędziu.

  • Histogram (CreateHistogram) – to narzędzie śledzi rozkład pomiarów. Nie istnieje jeden kanoniczny sposób opisywania zestawu pomiarów, ale zaleca się używanie histogramów lub obliczonych percentyli. Załóżmy na przykład, że wywołujący użył Record, aby zarejestrować te pomiary podczas interwału aktualizacji narzędzia zbierającego: 1,5,2,3,10,9,7,4,6,8. Narzędzie do zbierania może zgłosić, że 50., 90. i 95. percentyle tych pomiarów to odpowiednio 5, 9 i 9.

    Notatka

    Aby uzyskać szczegółowe informacje na temat ustawiania zalecanych granic zasobnika przy tworzeniu instrumentu histogramu, zobacz: Wykorzystanie wskazówek do dostosowania instrumentów histogramu.

Najlepsze rozwiązania dotyczące wybierania typu instrumentu

  • W przypadku zliczania elementów lub dowolnej innej wartości, która zwiększa się wyłącznie wraz z upływem czasu, użyj wartości Counter lub ObservableCounter. Wybierz między elementami Counter i ObservableCounter w zależności od tego, co jest łatwiejsze do dodania do istniejącego kodu: wywołanie interfejsu API dla każdej operacji przyrostowej lub wywołanie zwrotne, które odczytuje bieżącą sumę ze zmiennej, którą utrzymuje kod. W bardzo gorących ścieżkach kodu, w których wydajność jest ważna i użycie Add spowodowałoby utworzenie ponad miliona wywołań na sekundę na wątek, użycie narzędzia ObservableCounter może zaoferować większą szansę optymalizacji.

  • Dla pomiaru czasu zwykle preferuje się histogram. Często bardziej przydatne jest zrozumienie ogona tych rozkładów (90., 95., 99. percentylu) zamiast średnich lub łącznych wartości.

  • Inne typowe przypadki, takie jak współczynniki trafień pamięci podręcznej lub rozmiary pamięci podręcznej, kolejek i plików, zwykle dobrze pasują do UpDownCounter lub ObservableUpDownCounter. Wybierz między nimi, w zależności od tego, co jest łatwiejsze do dodania do istniejącego kodu: wywołanie interfejsu API dla każdej operacji przyrostowej i dekrementacji lub wywołanie zwrotne, które odczytuje bieżącą wartość ze zmiennej, którą utrzymuje kod.

Notatka

Jeśli używasz starszej wersji platformy .NET lub pakietu NuGet DiagnosticSource, który nie obsługuje UpDownCounter i ObservableUpDownCounter (przed wersją 7), ObservableGauge jest często dobrym zamiennikiem.

Przykład różnych typów instrumentów

Zatrzymaj wcześniej rozpoczęty przykładowy proces i zastąp przykładowy kod w Program.cs:

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(5, 15)/1000.0);
        }
    }
}

Uruchom nowy proces i użyj liczników dotnet-counters tak jak wcześniej w drugiej powłoce, aby wyświetlić metryki.

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

Name                                                  Current Value
[HatCo.Store]
    hatco.store.coats_sold (Count)                        8,181    
    hatco.store.hats_sold (Count)                           548    
    hatco.store.order_processing_time
        Percentile
        50                                                    0.012    
        95                                                    0.013   
        99                                                    0.013
    hatco.store.orders_pending                                9    

W tym przykładzie użyto kilku losowo wygenerowanych liczb, więc wartości będą się nieco różnić. Dotnet-counters renderuje histogramy jako trzy percentyle: 50., 95., i 99., ale inne narzędzia mogą podsumowywać rozkład inaczej lub oferować więcej opcji konfiguracji.

Najlepsze rozwiązania

  • Histogramy zwykle przechowują dużo więcej danych w pamięci niż inne typy metryk. Jednak dokładne użycie pamięci zależy od używanego narzędzia do zbierania. Jeśli definiujesz dużą liczbę (>100) metryk histogramu, może być konieczne przekazanie użytkownikom wskazówek, aby nie włączali ich wszystkich w tym samym czasie, lub skonfigurować narzędzia, które oszczędzają pamięć przez zmniejszenie dokładności. Niektóre narzędzia do zbierania danych mogą mieć sztywne limity liczby współbieżnych histogramów, które będą monitorować, aby zapobiec nadmiernemu zużyciu pamięci.

  • Wywołania zwrotne dla wszystkich obserwowalnych instrumentów są wywoływane w sekwencji, więc każde wywołanie zwrotne, które trwa długo, może opóźnić lub uniemożliwić zbieranie wszystkich metryk. Preferowanie szybkiego odczytywania buforowanej wartości, niezwracanie żadnych pomiarów lub zgłaszanie wyjątku zamiast wykonywania potencjalnie długotrwałej lub blokującej operacji.

  • Wywołania zwrotne ObservableCounter, ObservableUpDownCounter i ObservableGauge występują w wątku, który zwykle nie jest synchronizowany z kodem aktualizującym wartości. Twoim obowiązkiem jest zsynchronizowanie dostępu do pamięci lub zaakceptowanie niespójnych wartości, które mogą wynikać z korzystania z niezsynchronizowanego dostępu. Typowe podejścia do synchronizowania dostępu to użycie blokady lub wywołania Volatile.Read i Volatile.Write.

  • Funkcje CreateObservableGauge i CreateObservableCounter zwracają obiekt instrument, ale w większości przypadków nie trzeba go zapisywać w zmiennej, ponieważ nie jest potrzebna dalsza interakcja z obiektem. Przypisanie jej do zmiennej statycznej tak jak w przypadku innych instrumentów jest legalne, ale podatne na błędy, ponieważ statyczne inicjowanie języka C# jest leniwe, a zmienna zwykle nigdy nie jest przywoływała. Oto przykład problemu:

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

Opisy i jednostki

Instrumenty mogą określać opcjonalne opisy i jednostki. Te wartości są nieprzezroczyste dla wszystkich obliczeń metryk, ale można je wyświetlić w interfejsie użytkownika narzędzia kolekcji, aby pomóc inżynierom zrozumieć, jak interpretować dane. Zatrzymaj wcześniej rozpoczęty przykładowy proces i zastąp przykładowy kod w Program.cs:

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

Uruchom nowy proces i użyj narzędzia dotnet-counters tak samo jak poprzednio w drugiej powłoce, aby wyświetlić metryki.

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

Name                                                       Current Value
[HatCo.Store]
    hatco.store.hats_sold ({hats})                                40

funkcja dotnet-counters nie używa obecnie tekstu opisu w interfejsie użytkownika, ale wyświetla jednostkę, gdy zostanie podana. W tym przypadku zobaczysz, że "{hats}" zastąpiło ogólny termin "Count", który jest widoczny w poprzednich opisach.

Najlepsze rozwiązania

  • Interfejsy API .NET umożliwiają używanie dowolnego ciągu jako jednostki, ale zalecamy używanie standardu UCUM, międzynarodowego standardu nazw jednostek. Nawiasy klamrowe wokół "{hats}" są częścią standardu UCUM, co oznacza, że jest to adnotacja opisowa, a nie nazwa jednostki o standardowym znaczeniu, takim jak sekundy lub bajty.

  • Jednostka określona w konstruktorze powinna opisywać jednostki właściwe dla indywidualnej miary. Czasami różni się to od jednostek ostatniej zgłoszonej metryki. W tym przykładzie każda wartość jest liczbą kapeluszy, więc "{hats}" jest odpowiednią jednostką do przekazania w konstruktorze. Narzędzie do zbierania mogło obliczyć wskaźnik zmian i samodzielnie ustalić, że odpowiednią jednostką dla wskaźnika obliczeniowego jest {hats}/s.

  • Podczas rejestrowania pomiarów czasu preferuj jednostki sekund rejestrowane jako zmiennoprzecinkowa lub podwójna wartość.

Metryki wielowymiarowe

Miary można również skojarzyć z parami klucz-wartość nazywanymi tagami, które umożliwiają kategoryzowanie danych na potrzeby analizy. Na przykład HatCo może chcieć zarejestrować nie tylko liczbę sprzedanych kapeluszy, ale także rozmiar i kolor, które były. Podczas późniejszego analizowania danych inżynierowie HatCo mogą podzielić sumy według rozmiaru, koloru lub dowolnej kombinacji obu tych elementów.

Tagi licznika i histogramu można określić w przeciążeniach Add i Record, które przyjmują co najmniej jeden argument KeyValuePair. Na przykład:

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

Zastąp kod Program.cs i ponownie uruchom aplikację oraz liczniki dotnet-counters, tak jak wcześniej.

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

Liczniki Dotnet-counters pokazują teraz podstawową kategoryzację:

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

Name                                                  Current Value
[HatCo.Store]
    hatco.store.hats_sold (Count)
        product.color product.size
        blue          19                                     73
        red           12                                    146    

W przypadku funkcji ObservableCounter i ObservableGauge pomiary oznakowane można podać w wywołaniu zwrotnym przekazanym do konstruktora:

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

Po uruchomieniu z licznikami dotnet-counter tak jak poprzednio wynik to:

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

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

Najlepsze rozwiązania

  • Mimo że interfejs API umożliwia używanie dowolnego obiektu jako wartości tagu, typy liczbowe i ciągi są oczekiwane przez narzędzia kolekcji. Inne typy mogą być lub nie być obsługiwane przez dane narzędzie do zbierania danych.

  • Zalecamy stosowanie nazw tagów zgodnie z wytycznymi dotyczącymi nazewnictwa OpenTelemetry, które używają małych liter hierarchicznych z znakami "_", aby oddzielić wiele wyrazów w tym samym elemecie. Jeśli nazwy tagów są ponownie używane w różnych metrykach lub innych rekordach telemetrii, powinny mieć takie samo znaczenie i zestaw wartości prawnych wszędzie, gdzie są używane.

    Przykładowe nazwy tagów:

    • customer.country
    • store.payment_method
    • store.purchase_result
  • Uważaj, że w praktyce są rejestrowane bardzo duże lub niezwiązane kombinacje wartości tagów. Chociaż implementacja API platformy .NET może je obsłużyć, narzędzia do zbierania danych prawdopodobnie przydzielą miejsce na dane metryczne skojarzone z każdą kombinacją tagów, co może osiągnąć znaczne rozmiary. Na przykład, jeśli HatCo ma 10 różnych kolorów kapeluszy i 25 rozmiarów kapeluszy, co daje maksymalnie 10*25=250 sum sprzedaży do śledzenia. Jednak jeśli HatCo doda trzeci tag, który jest identyfikatorem CustomerID dla sprzedaży, i sprzedaje 100 milionom klientów na całym świecie, prawdopodobnie teraz będzie rejestrowana miliardy różnych kombinacji tagów. Większość narzędzi do zbierania metryk będzie odrzucać dane, aby pozostać w granicach technicznych, lub mogą powstać wysokie koszty na pokrycie przechowywania i przetwarzania danych. Implementacja każdego narzędzia do zbierania określa swoje limity, ale prawdopodobnie mniej niż 1000 kombinacji dla jednego instrumentu jest bezpieczna. Wszystkie kombinacje powyżej 1000 będą wymagały, aby narzędzie zbierające zastosowało filtrowanie lub zostało zaprojektowane do działania w dużej skali. Implementacje histogramu zwykle używają znacznie większej ilości pamięci niż inne metryki, więc bezpieczne limity mogą być 10–100 razy niższe. Jeśli przewidujesz dużą liczbę unikatowych kombinacji tagów, dzienniki, transakcyjne bazy danych lub systemy przetwarzania danych big data mogą być bardziej odpowiednie do działania w odpowiedniej skali.

  • W przypadku instrumentów, które będą miały bardzo dużą liczbę kombinacji tagów, należy preferować użycie mniejszego typu pamięci w celu zmniejszenia obciążenia pamięci. Na przykład przechowywanie short dla Counter<short> zajmuje tylko 2 bajty na kombinację tagów, natomiast double dla Counter<double> zajmuje 8 bajtów na kombinację tagów.

  • Zachęcamy do optymalizacji narzędzi do zbierania kodu, który określa ten sam zestaw nazw tagów w tej samej kolejności dla każdego wywołania w celu rejestrowania pomiarów w tym samym instrumentze. W przypadku kodu o wysokiej wydajności, który musi wywoływać Add i Record często, preferuj używanie tej samej sekwencji nazw tagów dla każdego wywołania.

  • Interfejs API platformy .NET jest zoptymalizowany pod kątem braku przydzielania dla wywołań Add i Record z trzema lub mniej tagami określonymi indywidualnie. Aby uniknąć alokacji z większą liczbą tagów, użyj TagList. Ogólnie rzecz biorąc, obciążenie związane z wydajnością tych wywołań zwiększa się w miarę użycia większej liczby tagów.

Notatka

Funkcja OpenTelemetry określa tagi jako "atrybuty". Są to dwie różne nazwy dla tej samej funkcjonalności.

Dostosowywanie instrumentów histogramu przy użyciu wskazówek

Korzystając z histogramów, narzędzie lub biblioteka odpowiedzialna za zbieranie danych musi zdecydować, jak najlepiej przedstawić rozkład zarejestrowanych wartości. Typowa strategia (i tryb domyślny w przypadku używaniaOpenTelemetry) polega na podzieleniu zakresu możliwych wartości na podzakresy nazywane zasobnikami i raportowaniem liczby zarejestrowanych wartości w każdym zasobniku. Na przykład narzędzie może podzielić liczby na trzy zasobniki, te mniejsze niż 1, te z zakresu od 1 do 10 i większe niż 10. Jeśli aplikacja zarejestrowała wartości 0,5, 6, 0,1, 12, wówczas w pierwszym zasobniku znajdują się dwa punkty danych, jeden na drugim i jeden na trzecim.

Narzędzie lub biblioteka zbierająca dane histogramu jest odpowiedzialna za zdefiniowanie zasobników, które będą używane. Domyślna konfiguracja zasobnika podczas korzystania z biblioteki OpenTelemetry to: [ 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 ].

Wartości domyślne mogą nie prowadzić do uzyskania najlepszego stopnia szczegółowości dla każdego histogramu. Na przykład czasy trwania żądania krótsze niż sekunda będą należeć do kategorii 0.

Narzędzie lub biblioteka zbierająca dane histogramu może oferować mechanizmy umożliwiające użytkownikom dostosowywanie konfiguracji zasobnika. Na przykład funkcja OpenTelemetry definiuje interfejs API View. Wymaga to jednak akcji użytkownika końcowego i sprawia, że użytkownik ponosi odpowiedzialność za zrozumienie dystrybucji danych wystarczająco dobrze, aby wybrać odpowiednie zasobniki.

Aby ulepszyć wrażenia, w wersji 9.0.0 pakietu System.Diagnostics.DiagnosticSource wprowadzono interfejs API (InstrumentAdvice<T>).

Interfejs API InstrumentAdvice może być używany przez autorów instrumentacji w celu określenia zestawu zalecanych domyślnych granic zasobników dla danego histogramu. Narzędzie lub biblioteka zbierająca dane histogramu może następnie użyć tych wartości podczas konfigurowania agregacji, co prowadzi do bardziej płynnego procesu wdrażania użytkowników. Jest to obsługiwane w zestawie SDK platformy .NET platformy OpenTelemetry w wersji 1.10.0.

Ważny

Ogólnie rzecz biorąc, więcej kubełków prowadzi do bardziej precyzyjnych danych dla danego histogramu, ale każdy kubełek wymaga pamięci do przechowywania zagregowanych szczegółów i istnieje koszt CPU związany ze znalezieniem odpowiedniego kubełka podczas przetwarzania miary. Ważne jest, aby zrozumieć zależności między dokładnością a użyciem procesora/pamięci podczas wybierania liczby kubełków zalecanych za pośrednictwem interfejsu API InstrumentAdvice.

Poniższy kod przedstawia przykład użycia interfejsu API InstrumentAdvice do ustawiania zalecanych zasobników domyślnych.

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

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Histogram<double> s_orderProcessingTime = s_meter.CreateHistogram<double>(
        name: "hatco.store.order_processing_time",
        unit: "s",
        description: "Order processing duration",
        advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = [0.01, 0.05, 0.1, 0.5, 1, 5] });

    static Random s_rand = new Random();

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while (!Console.KeyAvailable)
        {
            // Pretend our store has one transaction each 100ms
            Thread.Sleep(100);

            // 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(5, 15) / 1000.0);
        }
    }
}

Dodatkowe informacje

Aby uzyskać więcej informacji na temat histogramów z eksplitywnymi przedziałami w usłudze OpenTelemetry, sprawdź:

Testowanie metryk niestandardowych

Możliwe jest przetestowanie wszelkich metryk niestandardowych dodanych przy użyciu MetricCollector<T>. Ten typ ułatwia rejestrowanie pomiarów z określonych instrumentów i potwierdzanie, że wartości były poprawne.

Test z wstrzykiwaniem zależności

Poniższy kod przedstawia przykładowy przypadek testowy dla składników kodu korzystających z wstrzykiwania zależności i IMeterFactory.

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

Każdy obiekt MetricCollector rejestruje wszystkie miary dla jednego instrumentu. Jeśli musisz zweryfikować pomiary z wielu instrumentów, utwórz jeden moduł MetricCollector dla każdego z nich.

Testowanie bez wstrzykiwania zależności

Istnieje również możliwość przetestowania kodu używającego współużytkowanego globalnego obiektu miernika w polu statycznym, ale upewnij się, że takie testy nie są skonfigurowane do równoległego uruchamiania. Ponieważ obiekt Meter jest współużytkowany, funkcja MetricCollector w jednym teście będzie obserwować pomiary utworzone na podstawie innych testów uruchomionych równolegle.

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