Создание метрик

Эта статья относится к : ✔️ .NET Core 6 и более поздних✔️ версий платформа .NET Framework версии 4.6.1 и более поздних версий

Приложения .NET можно инструментировать с помощью API-интерфейсов System.Diagnostics.Metrics для отслеживания важных метрик. Некоторые метрики включены в стандартные библиотеки .NET, но может потребоваться добавить новые пользовательские метрики, которые будут актуальны для имеющихся приложений и библиотек. В этом руководстве вы добавите новые метрики и узнаете, какие типы метрик доступны.

Примечание.

.NET содержит несколько старых API-интерфейсов метрик, а именно EventCounters и System.Diagnostics.PerformanceCounter, которые здесь не рассматриваются. Дополнительные сведения об этих альтернативах см. в статье Сравнение API-интерфейсов метрик.

Создание пользовательской метрики

Предварительные требования: пакет SDK для .NET Core 6 или более поздняя версия

Создайте консольное приложение, которое ссылается на пакет NuGet System.Diagnostics.DiagnosticsSource версии 8 или более поздней. Приложения, предназначенные для .NET 8+, включают эту ссылку по умолчанию. Затем обновите код в Program.cs в соответствии с приведенным ниже.

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

Тип System.Diagnostics.Metrics.Meter является точкой входа для библиотеки, с помощью которой создается именованная группа инструментов. Инструменты записывают числовые измерения, необходимые для вычисления метрик. Здесь мы использовались CreateCounter для создания инструмента счетчика с именем "hatco.store.hats_sold". Во время каждой символической транзакции код вызывает Add, чтобы зафиксировать измерение проданных шляпы, в данном случае — 4. Инструмент "hatco.store.hats_sold" неявно определяет некоторые метрики, которые можно вычислить из этих измерений, например общее количество шляп продано или шляпы продано/с. В конечном счете это зависит от средств сбора метрик, чтобы определить, какие метрики следует вычислять и как выполнять эти вычисления, но каждый инструмент имеет некоторые соглашения по умолчанию, которые передают намерение разработчика. Для инструментов-счетчиков это соглашение заключается в том, что средства сбора отображают общее количество и (или) скорость увеличения количества.

Универсальный параметр int для Counter<int> и CreateCounter<int>(...) определяет, что этот счетчик должен иметь возможность хранить значения до максимально возможного — Int32.MaxValue. В зависимости от размера данных, которые необходимо сохранить, а также чтобы указать, требуются ли дробные значения, можно использовать любой из типов — byte, short, int, long, float, double или decimal.

Запустите приложение и оставьте его запущенным. Мы посмотрим метрики чуть позже.

> dotnet run
Press any key to exit

Рекомендации

  • Для кода, который не предназначен для использования в контейнере внедрения зависимостей (DI), создайте счетчик один раз и сохраните его в статической переменной. Для использования в библиотеках с поддержкой DI статические переменные считаются анти-шаблоном, а в приведенном ниже примере di показан более идиоматический подход. Каждая библиотека и каждый из вложенных компонентов библиотеки могут (и часто должны) создавать собственный класс Meter. Рассмотрите возможность создания нового счетчика, а не повторного использования существующего, если вы ожидаете, что разработчики приложений смогут легко включить и отключить группы метрик отдельно.

  • Имя, переданное конструктору Meter , должно быть уникальным, чтобы отличить его от других метров. Рекомендуется использовать рекомендации по именованию OpenTelemetry, которые используют пунктирные иерархические имена. Имена сборок или имена пространств имен для инструментирования кода обычно являются хорошим выбором. Если сборка добавляет инструментирование для кода во вторую, независимую сборку, имя должно основываться на сборке, определяющей счетчик, а не сборку, код которой инструментируется.

  • Платформа .NET не применяет ни одну схему именования для инструментов, но мы рекомендуем использовать правила именования OpenTelemetry, которые используют строчные знаки иерархических имен и подчеркивание ('_') в качестве разделителя между несколькими словами в одном элементе. Не все средства метрик сохраняют имя счетчика как часть окончательного имени метрики, поэтому полезно сделать имя инструмента глобально уникальным.

    Примеры имен инструментов:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • API-интерфейсы для создания инструментов и записи измерений являются потокобезопасными. В библиотеках .NET большинству методов экземпляров требуется синхронизация, если они вызываются в одном и том же объекта из нескольких потоков. Но в данном случае в этом нет необходимости.

  • API инструментирования для записи измерений (Add в этом примере) обычно выполняются в <10 ns, если данные не собираются, или десятки до сотен наносекунд при сборе измерений библиотекой или средством высокой производительности. Благодаря этому такие API-интерфейсы можно широко использовать в большинстве случаев, но следует соблюдать осторожность при работе с кодом, который чрезвычайно чувствителен к производительности.

Просмотр новой метрики

Существует множество вариантов для хранения и просмотра метрик. В этом руководстве используется программа dotnet-counters, которая хорошо подходит для выполнения специального анализа. Сведения о других альтернативах можно найти в руководстве по сбору метрик. Если программа dotnet-counters еще не установлена, установите ее с помощью пакета SDK:

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

Хотя пример приложения по-прежнему работает, используйте счетчики dotnet-counters для мониторинга нового счетчика:

> 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

Как и ожидалось, вы видите, что магазин HatCo стабильно продает 4 шляпы каждую секунду.

Получение измерения с помощью внедрения зависимостей

В предыдущем примере Метр был получен путем создания new и назначения ему статического поля. Использование статических способов не является хорошим подходом при использовании внедрения зависимостей (DI). В коде, использующем DI, например ASP.NET Core или приложения с универсальным узлом, создайте объект Meter с помощью IMeterFactory. Начиная с .NET 8 узлы автоматически регистрируются IMeterFactory в контейнере службы или можно вручную зарегистрировать тип в любом IServiceCollection из вызовов AddMetrics. Фабрика счетчиков интегрирует метрики с DI, сохраняя Счетчики в разных коллекциях служб, изолированных друг от друга, даже если они используют идентичное имя. Это особенно полезно для тестирования, чтобы несколько тестов, выполняющихся параллельно, наблюдали только измерения, созданные в одном тестовом случае.

Чтобы получить счетчик в типе, предназначенном для DI, добавьте IMeterFactory параметр в конструктор, а затем вызовите Create. В этом примере показано использование IMeterFactory в приложении ASP.NET Core.

Определите тип для хранения инструментов:

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

Зарегистрируйте тип в контейнере Program.csDI.

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

Введите тип метрик и значения записей, если это необходимо. Так как тип метрик зарегистрирован в DI, его можно использовать с контроллерами MVC, минимальными API или любым другим типом, созданным di:

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

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

Рекомендации

  • System.Diagnostics.Metrics.Meter реализует IDisposable, но IMeterFactory автоматически управляет временем существования всех Meter объектов, которые он создает, и удаляет их при удалении контейнера DI. Не требуется добавить дополнительный код для вызова Dispose() в нее Meter, и он не будет иметь никакого эффекта.

Типы инструментов

До сих пор мы только продемонстрировали Counter<T> инструмент, но существуют более доступные типы инструментов. Инструменты различаются по двум признакам.

  • Вычисления метрик по умолчанию. Средства, собирающие и анализирующие измерения инструментов, будут вычислять различные метрики по умолчанию в зависимости от инструмента.
  • Хранение агрегированных данных. Наиболее полезные метрики требуют агрегирования данных из множества измерений. Вот один из вариантов: вызывающая сторона предоставляет отдельные измерения в произвольные моменты времени, а средство сбора управляет агрегированием. Или вызывающая сторона может управлять агрегатными измерениями и предоставлять их по запросу в обратном вызове.

Сейчас доступны следующие типы инструментов:

  • Счетчик (CreateCounter) — этот инструмент отслеживает значение, которое увеличивается с течением времени, и вызывающий сообщает о добавочных значениях с помощью Add. Большинство средств вычисляют итоговое значение и скорость изменения в итоговом значении. Для средств, которые показывают только одно, рекомендуется использовать скорость изменений. Например, предположим, что вызывающая сторона вызывает Add() каждую секунду с последовательными значениями 1, 2, 4, 5, 4, 3. Если средство сбора обновляется каждые три секунды, итоговое значение по прошествии трех секунд равно 1+2+4=7, а итоговое значение по прошествии шести секунд равно 1+2+4+5+4+3=19. Скорость изменения вычисляется как (текущее_итоговое_значение-предыдущее_итоговое_значение), поэтому через три секунды средство сообщает, что 7-0=7, а через шесть секунд сообщает, что 19-7=12.

  • UpDownCounter (CreateUpDownCounter) — этот инструмент отслеживает значение, которое может увеличиваться или уменьшаться с течением времени. Вызывающий сообщает о увеличениях и уменьшениях использования Add. Например, предположим, что вызывающий объект вызывает Add() один раз в секунду с последовательными значениями 1, 5, -2, 3, -1, -3. Если средство сбора обновляется каждые три секунды, то общая сумма после трех секунд составляет 1+5-2=4, а общая сумма после шести секунд составляет 1+5-2+3-1-3=3.

  • ObservableCounter (CreateObservableCounter) — этот инструмент аналогичен инструменту Counter, за исключением того, что вызывающая сторона теперь отвечает за поддержание агрегированного итогового значения. При создании ObservableCounter вызывающая сторона предоставляет делегат обратного вызова, и этот обратный вызов вызывается всякий раз, когда средства должны наблюдать за текущим итоговым значением. Например, если средство сбора обновляется каждые три секунды, функция обратного вызова также будет вызываться каждые три секунды. В большинстве средств доступны как итоговое значение, так и скорость изменения в итоговом значении. Если можно отобразить только одни данные, рекомендуется выбрать скорость изменения. Если обратный вызов возвращает значение 0 при первоначальном вызове, 7 при повторном вызове через три секунды и 19 при вызове после шести секунд, средство сообщает эти значения без изменений в качестве итогов. В качестве значения скорости изменения средство будет показывать 7-0=7 через три секунды и 19-7=12 через шесть секунд.

  • ObservableUpDownCounter (CreateObservableUpDownCounter) — этот инструмент похож на UpDownCounter, за исключением того, что вызывающий объект теперь отвечает за поддержание совокупного общего объема. Вызывающий объект предоставляет делегат обратного вызова при создании ObservableUpDownCounter, и обратный вызов вызывается всякий раз, когда средства должны наблюдать за текущим итогом. Например, если средство сбора обновляется каждые три секунды, функция обратного вызова также будет вызываться каждые три секунды. Любое значение, возвращаемое обратным вызовом, будет отображаться в средстве сбора без изменений в качестве общего значения.

  • ObservableGauge (CreateObservableGauge) — этот инструмент позволяет вызывающей стороне выполнять обратный вызов, где измеряемое значение передается напрямую в качестве метрики. При каждом обновлении средства сбора вызывается обратный вызов, и в средстве отображается любое значение, возвращаемое обратным вызовом.

  • Histogram (CreateHistogram) — этот инструмент отслеживает распределение измерений. Единого канонического способа описания набора измерений не существует, в средствах рекомендуется использовать гистограммы или вычисленные процентили. Например, предположим, что вызывающая сторона вызвала метод Record для записи этих измерений во время интервала обновления средства сбора: 1, 5, 2, 3, 10, 9, 7, 4, 6, 8. Средство сбора данных может сообщить о том, что 50-м, 90-м и 95-м процентилями этих измерений являются 5, 9 и 9, соответственно.

Рекомендации по выбору типа инструмента

  • Для подсчета объектов или любых других значений, которые только увеличиваются с течением времени, следует использовать Counter или ObservableCounter. Выбор инструмента — Counter или ObservableCounter — зависит от того, что проще всего добавить в существующий код: вызов API для каждой операции приращения или обратный вызов, который будет считывать текущее итоговое значение из переменной, которую поддерживает код. В чрезвычайно критических путях к коду, где важна производительность, и использование Add создаст более миллиона вызовов в секунду на поток, использование ObservableCounter может предоставить больше возможностей для оптимизации.

  • Для работы со значениями времени обычно предпочтителен инструмент Histogram. Часто полезно понимать заключительные фрагменты этих распределений (90-й, 95-й, 99-й процентили), а не средние или итоговые значения.

  • Другие распространенные случаи, такие как скорость попадания кэша или размеры кэша, очередей и файлов, обычно хорошо подходят для UpDownCounter или ObservableUpDownCounter. Выберите между ними в зависимости от того, что проще добавить в существующий код: вызов API для каждой операции добавок и уменьшения или обратный вызов, который будет считывать текущее значение из переменной, которая поддерживает код.

Примечание.

Если вы используете более раннюю версию .NET или пакет NuGet DiagnosticSource, который не поддерживается UpDownCounter и ObservableUpDownCounter (до версии 7), ObservableGauge часто является хорошим заменой.

Пример различных типов инструментов

Остановите пример процесса, запущенный ранее, и замените пример кода в 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(0.005, 0.015));
        }
    }
}

Запустите новый процесс и используйте программу dotnet-counters как раньше во второй оболочке для просмотра метрик.

> 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

В этом примере используются случайно сгенерированные числа, поэтому ваши значения будут немного отличаться. Видно, что значения hatco.store.hats_sold (Counter) и hatco.store.coats_sold (ObservableCounter) отображаются как скорость. ObservableGauge, hatco.store.orders_pending, отображается как абсолютное значение. Программа dotnet-counters отображает инструменты Histogram в виде трех статистик процентиля (50-й, 95-й и 99-й), но другие средства могут суммировать распределение по-другому или предложить больше вариантов конфигурации.

Рекомендации

  • Гистограммы, как правило, хранят гораздо больше данных в памяти, чем другие типы метрик. Однако точное использование памяти определяется используемым средством сбора. Если вы определяете большое количество (>100) метрик гистограммы, вам может потребоваться предоставить пользователям рекомендации, чтобы не включить их все одновременно или настроить их средства для экономии памяти путем снижения точности. Некоторые средства сбора данных могут иметь жесткие ограничения на количество одновременно отслеживаемых инструментов Histogram, чтобы избежать чрезмерного использования памяти.

  • Обратные вызовы для всех наблюдаемых инструментов вызываются последовательно, поэтому любой обратный вызов, выполнение которого занимает много времени, может задержать или заблокировать сбор всех метрик. Вместо выполнения любой потенциально длительной или блокирующей операции, сделайте выбор в пользу быстрого считывания кэшированного значение, возврата результата без измерений или возникновения исключения.

  • Обратные вызовы ObservableCounter, ObservableUpDownCounter и ObservableGauge происходят в потоке, который обычно не синхронизирован с кодом, обновляющим значения. Вы несете ответственность за синхронизацию доступа к памяти или принятие несогласованных значений, которые могут привести к использованию несинхронизованного доступа. Распространенные подходы к синхронизации доступа — использовать блокировку или вызов Volatile.Read и Volatile.Write.

  • Функции CreateObservableGauge и CreateObservableCounter возвращают объект инструмента, но в большинстве случаев нет необходимости сохранять его в переменной, так как дальнейшее взаимодействие с объектом не требуется. Можно назначить его статической переменной (как это было сделано для других инструментов), но это чревато ошибками, поскольку статическая инициализация в C# является отложенной, и ссылки на переменную обычно отсутствуют. Ниже приведен пример проблемы:

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

Описания и единицы измерения

Инструменты могут задавать необязательные описания и единицы измерения. Эти значения непрозрачны для всех вычислений метрик, но могут быть показаны в пользовательском интерфейсе средства сбора, чтобы помочь инженерам понять, как интерпретировать данные. Остановите пример процесса, запущенный ранее, и замените пример кода в 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);
        }
    }
}

Запустите новый процесс и используйте программу dotnet-counters как раньше во второй оболочке для просмотра метрик.

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

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

Сейчас программа dotnet-counters не использует текст описания в пользовательском интерфейсе, но отображает единицу, если она указана. В этом случае вы увидите, что "{hats}" заменил универсальный термин Count, который отображается в предыдущих описаниях.

Рекомендации

  • API .NET позволяют использовать любую строку в качестве единицы, но мы рекомендуем использовать UCUM, международный стандарт для имен единиц. Фигурные скобки вокруг "{hats}" являются частью стандарта UCUM, указывая, что это описательная заметка, а не имя единицы со стандартным значением, например секунды или байты.

  • Единица, указанная в конструкторе, должна описывать единицы, подходящие для отдельного измерения. Иногда они отличаются от единиц в окончательной метрике. В этом примере каждое измерение представляет собой ряд шляп, поэтому "{hats}" — это соответствующая единица для передачи в конструктор. Средство сбора вычисляет скорость и получается самостоятельно, что соответствующая единица для вычисляемой метрики — {hats}/с.

  • При записи измерений времени предпочитайте единицы секунд, записанные как плавающую точку или двойное значение.

Многомерные метрики

Измерения также могут быть связаны с парами "ключ-значение", называемыми тегами, которые позволяют классифицировать данные для анализа. Например, магазину HatCo может потребоваться записывать не только количество проданных шляп, но и их размер и цвет. При последующем анализе данных инженеры HatCo могут разделить итоговые данные по размеру, цвету или любому сочетанию этих категорий.

Теги Counter и Histogram можно указывать в перегрузках методов Add и Record, принимающих один или несколько аргументов KeyValuePair. Например:

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

Замените код Program.cs и повторно запустите приложение и программу dotnet-counters, как раньше:

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 показывает базовую классификацию:

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

Для инструментов ObservableCounter и ObservableGauge измерения с тегами можно указать в обратном вызове, передаваемом конструктору:

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

При запуске с помощью программы dotnet-counters, как и раньше, результат будет следующим:

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

Рекомендации

  • Хотя API позволяет использовать в качестве значения тега любой объект, в средствах сбора рекомендуется применять числовые типы и строки. Конкретное средство сбора может поддерживать или не поддерживать другие типы.

  • Мы рекомендуем использовать имена тегов в соответствии с рекомендациями по именованию OpenTelemetry, которые используют строчные знаки с символами "_" для разделения нескольких слов в одном элементе. Если имена тегов повторно используются в разных метриках или других записях телеметрии, они должны иметь одинаковый смысл и набор юридических значений везде, где они используются.

    Примеры имен тегов:

    • customer.country
    • store.payment_method
    • store.purchase_result
  • Следует избегать ситуаций, когда записываются очень большие или неограниченные комбинации значений тегов. Несмотря на то, что реализация API .NET может справиться с таким объемом, средства сбора данных, скорее всего, будут выделять хранилище для данных метрик, связанных с каждой комбинацией тегов, и его размер может стать очень большим. Например, это нормально, если у магазина HatCo есть 10 различных цветов шляп и 25 размеров шляп для отслеживания максимум 10*25=250 итоговых продаж. Однако если HatCo добавит третий тег, который является идентификатором клиента для продажи, и магазин продаст товар 100 миллионам клиентов по всему миру, то после этого, вероятно, будут записываться миллиарды различных комбинаций тегов. Большинство средств сбора метрик либо удаляют данные, чтобы оставаться в пределах технических ограничений, либо могут потребоваться большие денежные затраты на хранение и обработку данных. Реализация каждого средства сбора определяет свои ограничения, но, скорее всего, для одного инструмента будет достаточно менее 1000 комбинаций. Что-либо выше 1000 сочетаний потребуется средство сбора для применения фильтрации или инженера для работы в большом масштабе. Реализации Histogram обычно потребляют гораздо больше памяти, чем другие метрики, поэтому приемлемые значения ограничений должны быть в 10–100 раз ниже. Если вы ожидаете большое количество сочетаний уникальных тегов, то журналы, транзакционные базы данных или системы обработки больших данных могут быть более подходящими решениями для работы в нужном масштабе.

  • Для инструментов, в которых будет огромное количество комбинаций тегов, рекомендуется использовать хранилище небольшого объема, чтобы сократить нагрузку на память. Например, хранение short для Counter<short> занимает всего 2 байта на комбинацию тегов, в то время как double для Counter<double> занимает 8 байт для каждой комбинации тегов.

  • Рекомендуется оптимизировать средства сбора для кода, который задает одинаковый набор имен тегов в одном и том же порядке для каждого вызова для записи измерений с одного и того же инструмента. Для высокопроизводительного кода, который должен часто вызывать Add и Record, следует использовать ту же последовательность имен тегов для каждого вызова.

  • API .NET оптимизирован для работы без выделений с вызовами Add и Record с тремя или менее тегами, указанными по отдельности. Чтобы избежать выделений с большим количеством тегов, используйте TagList. Как правило, по мере использования большего количества тегов происходит снижение производительности.

Примечание.

В OpenTelemetry теги называются атрибутами. Это два разных имени для одной и той же функциональности.

Тестирование пользовательских метрик

Его можно протестировать любые пользовательские метрики, которые вы добавляете с помощью MetricCollector<T>. Этот тип позволяет легко записывать измерения из определенных инструментов и утверждать, что значения были правильными.

Тестирование с внедрением зависимостей

В следующем коде показан пример тестового случая для компонентов кода, использующих внедрение зависимостей и 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();
    }
}

Каждый объект MetricCollector записывает все измерения для одного инструмента. Если необходимо проверить измерения из нескольких инструментов, создайте один MetricCollector для каждого из них.

Тестирование без внедрения зависимостей

Также можно протестировать код, использующий общий глобальный объект Meter в статическом поле, но убедитесь, что такие тесты настроены не параллельно. Так как объект Meter предоставляется совместно, MetricCollector в одном тесте будет наблюдать измерения, созданные из любых других тестов, выполняемых параллельно.

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