Udostępnij za pośrednictwem


Tworzenie metryk

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

Aplikacje platformy .NET można instrumentować przy użyciu System.Diagnostics.Metrics interfejsów API 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.

Uwaga

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 nową metrykę niestandardową

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

Utwórz nową aplikację konsolową, która odwołuje 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 pliku, aby był zgodny z następującym kodem:

> 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 jest punktem wejścia biblioteki, aby utworzyć nazwaną grupę instrumentów. Instrumenty rejestrują pomiary liczbowe potrzebne do obliczenia metryk. W tym miejscu utworzyliśmy CreateCounter instrument counter o nazwie "hatco.store.hats_sold". Podczas każdej transakcji udawania kod wywołuje Add , aby zarejestrować pomiar kapeluszy, które zostały sprzedane, 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 instrumentów counter konwencja polega na tym, że narzędzia do zbierania pokazują łączną liczbę i/lub szybkość, z jaką liczba rośnie.

Ogólny parametr int w systemie Counter<int> i CreateCounter<int>(...) definiuje, że ten licznik musi mieć możliwość przechowywania wartości do Int32.MaxValue. Możesz użyć dowolnej z bytewartości , short, , longintfloat, , doublelub decimal w zależności od rozmiaru danych potrzebnych do przechowywania i tego, 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 wstrzykiwania zależności (DI), utwórz miernik raz i zapisz go w zmiennej statycznej. W przypadku użycia w bibliotekach obsługujących di-aware zmienne statyczne są uważane za anty-wzorzec, a poniższy przykład di pokazuje bardziej idiotyczne 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ć unikatowa, aby odróżnić ją od innych mierników. Zalecamy wytyczne dotyczące nazewnictwa 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 OpenTelemetry, które używają małych liter hierarchicznych i podkreślenia ('_') jako separatora między wieloma wyrazami w tym samym elemecie. Nie wszystkie narzędzia metryk zachowują nazwę miernika jako część końcowej nazwy metryki, dlatego korzystne jest, aby nazwa instrumentu była globalnie unikatowa na własną rękę.

    Przykładowe nazwy instrumentów:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • Interfejsy API do tworzenia instrumentów i pomiarów rekordó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 API instrumentowania do rejestrowania pomiarów (Add w tym przykładzie) zwykle działają w 10 ns, <gdy nie są zbierane żadne dane, lub dziesiątki do setek nanosekund, gdy pomiary są zbierane przez bibliotekę lub narzędzie kolekcji 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 zapoznać się z innymi alternatywami. Jeśli narzędzie dotnet-counters nie jest jeszcze zainstalowane, użyj zestawu SDK, aby go 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 skonstruowanie go new i przypisanie go do pola statycznego. Użycie statycznych w ten sposób nie jest dobrym podejściem podczas korzystania z wstrzykiwania zależności (DI). W kodzie używającym di, takim jak ASP.NET Core lub aplikacje z hostem ogólnym, utwórz obiekt Miernik przy użyciu polecenia IMeterFactory. Począwszy od platformy .NET 8, hosty będą automatycznie rejestrować się IMeterFactory w kontenerze usługi lub ręcznie zarejestrować typ w dowolnym IServiceCollection za pomocą wywołania metody AddMetrics. Fabryka mierników integruje metryki z di, utrzymując mierniki w różnych kolekcjach usług odizolowanych 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 do konstruktora, a następnie wywołaj metodę IMeterFactoryCreate. 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 pliku Program.cs.

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

W razie potrzeby należy wstrzyknąć typ metryk i wartości rekordów. 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 okresem istnienia tworzonych Meter obiektów, dysponując je po usunięciu kontenera DI. Nie trzeba dodawać dodatkowego kodu do wywołania Dispose() na Meterobiekcie i nie będzie mieć żadnego wpływu.

Typy instrumentów

Do tej pory przedstawiliśmy Counter<T> tylko instrument, 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 funkcja wywołująca udostępnia poszczególne pomiary w dowolnym czasie, a narzędzie do zbierania 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 zwiększa się wraz z upływem czasu, a obiekt wywołujący zgłasza przyrosty przy użyciu metody 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 Add() wywołuje raz na sekundę kolejne wartości 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 (current_total - previous_total), więc po trzech sekundach narzędzie zgłasza 7-0=7, a po sześciu sekundach raportuje 19-7=12.

  • UpDownCounter (CreateUpDownCounter) — ten instrument śledzi wartość, która może wzrosnąć lub zmniejszyć w czasie. Obiekt wywołujący zgłasza przyrosty i dekrementy przy użyciu polecenia Add. Załóżmy na przykład, że obiekt wywołujący Add() wywołuje raz na sekundę kolejne wartości 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) — ten instrument jest podobny do licznika, 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 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. 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) — ten instrument jest podobny 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 wywołanie zwrotne będzie wyświetlana w narzędziu kolekcji bez zmian jako suma.

  • ObservableGauge (CreateObservableGauge) — ten instrument umożliwia wywołaniu zwrotnemu, w którym mierzona wartość jest przekazywana bezpośrednio jako metryka. Za każdym razem, gdy narzędzie do zbierania aktualizacji, wywołanie zwrotne jest wywoływane, a dowolna wartość zwracana przez wywołanie zwrotne jest wyświetlana w narzędziu.

  • Histogram (CreateHistogram) — ten instrument ś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 wywołany Record w celu zarejestrowania tych pomiarów w interwale aktualizacji narzędzia kolekcji: 1,5,2,3,10,9,7,4,6,8. Narzędzie do zbierania może zgłosić, że odpowiednio 50, 90 i 95. percentylów tych pomiarów to odpowiednio 5, 9 i 9.

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 funkcji ObservableCounter może zaoferować większą szansę na optymalizację.

  • W przypadku chronometrażu histogram jest zwykle preferowany. Często warto zrozumieć ogon tych rozkładów (90., 95., 99. percentyl) zamiast średnich lub sum.

  • Inne typowe przypadki, takie jak współczynniki trafień pamięci podręcznej lub rozmiary pamięci podręcznej, kolejek i plików, są zwykle odpowiednie dla 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.

Uwaga

Jeśli używasz starszej wersji platformy .NET lub pakietu NuGet DiagnosticSource, który nie obsługuje UpDownCounter i ObservableUpDownCounter (przed wersją 7), ObservableGauge często jest 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 pliku:

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

Uruchom nowy proces i użyj liczników dotnet-counters tak jak poprzednio 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

[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

W tym przykładzie użyto kilku losowo wygenerowanych liczb, więc wartości będą się nieco różnić. Zobaczysz, że hatco.store.hats_sold (Licznik) i hatco.store.coats_sold (ObserwowalnyCounter) są wyświetlane jako stawka. Funkcja ObservableGauge hatco.store.orders_pending, , jest wyświetlana jako wartość bezwzględna. Liczniki Dotnet renderuje instrumenty histogramu jako trzy statystyki percentylu (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 do zapisywania pamięci przez zmniejszenie dokładności. Niektóre narzędzia kolekcji mogą mieć twarde limity liczby współbieżnych histogramów, które będą monitorować, aby zapobiec nadmiernemu uż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. Faworyzowanie szybkiego odczytywania buforowanej wartości, zwracania żadnych pomiarów lub zgłaszania wyjątku w przypadku wykonywania dowolnej potencjalnie długotrwałej lub blokującej operacji.

  • Wywołania zwrotne ObservableCounter, ObservableUpDownCounter i ObservableGauge występują w wątku, który nie jest zwykle synchronizowany z kodem, który aktualizuje 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 przykładowy proces, który został wcześniej uruchomiony, i zastąp przykładowy kod w Program.cs pliku:

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 liczników dotnet-counters tak jak poprzednio w drugiej powłoce, aby wyświetlić metryki:

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

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

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

Najlepsze rozwiązania

  • Interfejsy API platformy .NET umożliwiają używanie dowolnego ciągu jako jednostki, ale zalecamy używanie interfejsu UCUM, standardu międzynarodowego dla 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 metryki końcowej. W tym przykładzie każda miara jest liczbą kapeluszy, więc "{hats}" jest odpowiednią jednostką do przekazania konstruktora. Narzędzie do zbierania obliczyło szybkość i wygenerowało na własną rękę, że odpowiednia jednostka dla metryki obliczeniowej to {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 elementu i Record które przyjmują co najmniej jeden KeyValuePair argument. 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 uruchom ponownie aplikację oraz liczniki dotnet-jak poprzednio:

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ą kategoryzacja:

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

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

[HatCo.Store]
    hatco.store.orders_pending
        customer.country=Italy                                             6
        customer.country=Mexico                                            1
        customer.country=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ć 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 z znakami hierarchicznymi 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 interfejsu API platformy .NET może go obsłużyć, narzędzia kolekcji prawdopodobnie przydzielą magazyn dla danych metryk skojarzonych z każdą kombinacją tagów i może to stać się bardzo duże. Na przykład, jeśli HatCo ma 10 różnych kolorów kapelusza i 25 rozmiarów kapeluszy dla maksymalnie 10*25=250 sum sprzedaży do śledzenia. Jeśli jednak hatCo dodał trzeci tag, który jest identyfikatorem CustomerID na sprzedaż i sprzedaje 100 milionów klientów na całym świecie, teraz istnieje prawdopodobieństwo, że miliardy różnych kombinacji tagów są rejestrowane. Większość narzędzi do zbierania metryk upuszcza dane, aby pozostać w granicach technicznych, lub może istnieć duże koszty pieniężne w celu pokrycia magazynu 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 elementy powyżej 1000 kombinacji będą wymagały, aby narzędzie kolekcji zastosowało filtrowanie lub było zaprojektowane tak, aby działało na dużą skalę. 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, preferuj użycie mniejszego typu magazynu, aby zmniejszyć obciążenie pamięci. Na przykład przechowywanie short elementu dla elementu Counter<short> zajmuje tylko 2 bajty na kombinację tagów, natomiast double wartość dla Counter<double> parametru 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 być wywoływany 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 bez alokacji dla Add wywołań i Record z trzema lub mniejszą liczbą tagów określonych indywidualnie. Aby uniknąć alokacji z większą liczbą tagów, użyj polecenia 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.

Uwaga

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

Testowanie metryk niestandardowych

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

Testowanie za pomocą wstrzykiwania 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);
    }
}