Поделиться через


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

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

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

Заметка

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

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

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

Создайте консольное приложение, которое ссылается на пакет NuGet system.Diagnostics.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), создайте Meter один раз и сохраните его в статической переменной. Для использования в библиотеках с поддержкой 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);
    }
}

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

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) — этот инструмент аналогичен счетчику, за исключением того, что пользователь теперь отвечает за поддержание агрегированного итогового значения. Вызывающий объект предоставляет делегат обратного вызова при создании ObservableCounter, и эта функция обратного вызова выполняется всякий раз, когда инструменты должны наблюдать за текущим итогом. Например, если средство сбора обновляется каждые три секунды, функция обратного вызова также будет вызываться каждые три секунды. Большинство инструментов будут показывать как общее количество, так и скорость изменения доступного количества. Если можно показать только один из них, рекомендуется использовать частоту изменений. Если обратный вызов возвращает значение 0 при первоначальном вызове, 7 при повторном вызове через три секунды и 19 при вызове после шести секунд, средство сообщает эти значения без изменений в качестве итогов. Для скорости изменения средство будет отображать 7-0=7 после трех секунд и 19-7=12 после шести секунд.

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

  • датчик (CreateGauge) — этот инструмент позволяет пользователю задать текущее значение показателя с помощью метода Record. Значение можно обновить в любое время, вызвав метод еще раз, и средство сбора метрик будет отображать любое значение, которое было задано в последнее время.

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

  • гистограмма (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 может предложить больше возможностей для оптимизации.

  • Для измерения времени Гистограмма обычно предпочтительна. Часто полезно понять хвост этих распределений (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(5, 15)/1000.0);
        }
    }
}

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

> 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    

В этом примере используются некоторые случайные числа, поэтому значения будут отличаться немного. Dotnet-counters отображает инструменты гистограмм как три процентиля (50-й, 95-й и 99-й), но другие инструменты могут по-разному обобщать распределение или предоставлять больше вариантов конфигурации.

Лучшие практики

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

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

  • Обратные вызовы 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

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

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

Лучшие практики

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

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

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

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

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

Теги счетчиков и гистограмм можно указать в перегруженных версиях 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

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

Для 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

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

Лучшие практики

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

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

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

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

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

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

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

Заметка

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

Использование советов для настройки инструментов гистограммы

При использовании гистограмм на средство или библиотеку, занимающиеся сбором данных, возлагается ответственность за принятие решения о том, как лучше всего представить распределение записанных значений. Общая стратегия (и режим по умолчанию при использовании OpenTelemetry) заключается в том, чтобы разделить диапазон возможных значений на поддиапазоны, называемые корзинами, и сообщить, сколько записанных значений было в каждой корзине. Например, средство может разделить числа на три категории: те, которые меньше 1, от 1 до 10, и те, которые больше 10. Если ваше приложение записало значения 0,5, 6, 0.1, 12, то в первом контейнере будет две точки данных, один во втором и один в 3-м.

Средство или библиотека, собирающая данные гистограммы, отвечает за определение корзин, которые будет использовать. Конфигурация контейнера по умолчанию при использовании OpenTelemetry: [ 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 ].

Значения по умолчанию могут не привести к лучшей детализации для каждой гистограммы. Например, продолжительность запроса менее одной секунды попадёт в бакет 0.

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

Чтобы улучшить впечатление, в версии 9.0.0 пакета System.Diagnostics.DiagnosticSource был введён API (InstrumentAdvice<T>).

API InstrumentAdvice может использоваться авторами инструментирования для указания набора рекомендуемых границ контейнеров по умолчанию для заданной гистограммы. Средство или библиотека, собирающая данные гистограммы, может использовать эти значения при настройке агрегирования, что приводит к более простому интерфейсу подключения для пользователей. Это поддерживается в пакете SDK OpenTelemetry .NET с версии 1.10.0.

Важный

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

В следующем коде показан пример использования API InstrumentAdvice для задания рекомендуемых контейнеров по умолчанию.

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

Дополнительные сведения

Дополнительные сведения о явных гистограммах контейнеров в 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);
    }
}