创建指标

本文适用于:✔️ .NET Core 6 及更高版本 ✔️ .NET Framework 4.6.1 及更高版本

可以使用 System.Diagnostics.Metrics API 检测 .NET 应用程序以跟踪重要指标。 某些指标包含在标准 .NET 库中,但你可能想要添加新的自定义指标,这些指标与应用程序和库相关。 在本教程中,你将添加新指标并了解可用的指标类型。

注意

.NET 具有一些较旧的指标 API,即 EventCountersSystem.Diagnostics.PerformanceCounter,此处未介绍这些 API。 若要详细了解这些替代方法,请参阅 比较指标 API

创建自定义指标

先决条件.NET Core 6 SDK 或更高版本

创建引用 System.Diagnostics.DiagnosticSource NuGet 包版本 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”工具隐式定义了一些指标,这些指标可以从这些度量中计算得出,例如销售的帽子总数或每秒销售的帽子数量。归根结底,由指标收集工具来确定要计算哪些指标以及如何执行这些计算,但每个检测工具都有一些默认约定来传达开发人员的意图。 对于 Counter 检测,约定是集合工具显示总计数和/或计数增加的速率。

Counter<int>CreateCounter<int>(...) 上的泛型参数 int 定义此计数器必须能够存储最多 Int32.MaxValue的值。 可以使用任何 byteshortintlongfloatdoubledecimal,具体取决于存储的数据大小以及是否需要小数值。

运行应用并使其暂时保持运行状态。 接下来,我们将查看指标。

> dotnet run
Press any key to exit

最佳做法

  • 对于未设计用于依赖注入(DI)容器的代码,应该只创建一次度量器,并将其存储在一个静态变量中。 对于在 DI 感知库中的使用,静态变量被视为反模式,下面的 DI 示例展示了一种更惯用的方法。 每个库或库子组件都可以(并且通常应该)创建自己的 Meter。 如果您预计应用开发人员会欣赏能够单独轻松启用和禁用不同指标组的功能,请考虑创建一个新的监测器,而不是重用现有的监测器。

  • 传递给 Meter 构造函数的名称应该是唯一的,以便将其与其他计量器区分开来。 我们推荐使用虚线分层名称的 OpenTelemetry 命名准则。 要检测的代码的程序集名称或命名空间名称通常是一个不错的选择。 如果程序集在第二个独立程序集中添加代码检测,则名称应基于定义计量的程序集,而不是要检测其代码的程序集。

  • .NET 不强制使用 Instruments 的任何命名方案,但我们建议遵循 OpenTelemetry 命名准则,这些准则使用小写的点点分层名称和下划线('_')作为同一元素中多个单词之间的分隔符。 并非所有度量工具都保留计量表名称作为最终度量名称的一部分,因此,使仪器名称本身具有全球唯一性是有益的。

    示例仪器名称:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • 用于创建仪器和记录度量的 API 是线程安全的。 在 .NET 库中,大多数实例方法在从多个线程在同一对象上调用时需要同步,但在这种情况下不需要同步。

  • 在没有收集数据时,用于记录度量值的检测 API(在本例中为 Add)通常会运行不到 10 纳秒的时间,而在高性能集合库或工具收集度量值时,其运行时间为数十到数百纳秒。 这允许在大多数情况下自由使用这些 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 或应用)中使用 IMeterFactory创建 Meter 对象。 从 .NET 8 开始,主机应用程序将自动在服务容器中注册 IMeterFactory,也可以通过调用 AddMetrics在任何 IServiceCollection 中手动注册此类型。 计量工厂将指标与 DI 集成,即使它们使用相同的名称,也会保持不同服务集合中的计量器相互隔离。 这对于测试特别有用,因此,多个并行运行的测试只观察同一测试用例中生成的度量值。

若要在为 DI 设计的类型中获取计量器,请将 IMeterFactory 参数添加到构造函数中,然后调用 Create。 此示例演示如何在 ASP.NET Core 应用中使用 IMeterFactory。

定义用于保存检测的类型:

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.cs 中向 DI 容器注册类型。

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 容器时处置它们。 无需添加额外的代码来调用 Meter上的 Dispose(),并且不会有任何影响。

仪器类型

到目前为止,我们只演示了一个 Counter<T> 仪器,但有更多的仪器类型可用。 仪器在两个方面有所不同:

  • 默认指标计算 - 收集和分析仪器测量数据的这些工具将根据仪器计算不同的默认指标。
  • 聚合数据的存储 - 最有用的指标需要从许多度量值聚合数据。 一个选项是调用方在任意时间提供单个度量值,集合工具管理聚合。 或者,调用方可以管理这些聚合度量,并在需要时通过回调提供它们。

当前可用的仪器类型:

  • 计数器CreateCounter) - 此仪器跟踪随时间推移增加的值,调用方使用 Add报告增量。 大多数工具将计算总数和总数的变化率。 对于仅显示一项内容的工具,建议显示变化率。 例如,假设调用方每秒调用 Add() 一次,连续值 1、2、4、5、4、3。 如果收集工具每三秒更新一次,则三秒后的总计为 1+2+4=7,6 秒后的总计为 1+2+4+5+4+3=19。 变化率是(当前总量 - 之前总量),因此在三秒时,工具报告 7-0=7,六秒后,它报告 19-7=12。

  • UpDownCounterCreateUpDownCounter) - 此仪器跟踪一个值,该值可能会随着时间增加或减少。 调用方使用 Add 来报告增量和减量。 例如,假设调用方每秒调用一次 Add(),连续值 1、5、-2、3、-1、-3。 如果收集工具每三秒更新一次,则三秒后的总计为 1+5-2=4,6 秒后的总数为 1+5-2+3-1-3=3。

  • ObservableCounterCreateObservableCounter) - 此工具类似于 Counter,只是调用方现在负责维护聚合总数。 调用方在创建 ObservableCounter 时提供回调委托,每当工具需要观察当前总数时,就会调用回调。 例如,如果集合工具每三秒更新一次,则回调函数也将每隔三秒调用一次。 大多数工具都提供总计数以及总计数中的变化率。 如果只能显示一个,则建议显示变化率。 如果回调在初始调用时返回 0,在三秒后再次调用时返回 7,在 6 秒后再次调用回调时返回 19,则该工具将报告这些值不变为总计。 对于变化率,该工具将在三秒后显示 7-0=7,六秒后显示 19-7=12。

  • ObservableUpDownCounterCreateObservableUpDownCounter) - 此工具与 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 可能会提供更多优化机会。

  • 对于涉及计时的情况,通常首选的是 Histogram。 通常,了解这些分布的尾部(第 90、95、99 百分位数)而不是平均值或总计很有用。

  • 其他常见情况(如缓存命中率或缓存大小、队列和文件)通常非常适合 UpDownCounterObservableUpDownCounter。 根据哪个更容易添加到现有代码中,在以下两者中进行选择:一是针对每个递增和递减操作进行 API 调用,二是使用回调函数从代码维护的变量中读取当前值。

注意

如果使用旧版 .NET 或不支持 UpDownCounterObservableUpDownCounter 的 DiagnosticSource NuGet 包(版本 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);
        }
    }
}

运行新进程,并在第二个 shell 中使用 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-counter 将直方图工具呈现为三个百分位统计信息(第 50 位、95 位和第 99 位),但其他工具可能以不同的方式汇总分布或提供更多配置选项。

最佳做法

  • 直方图往往将比其他指标类型更多的数据存储在内存中。 但是,确切的内存使用量由正在使用的收集工具确定。 如果您正在定义大量(>100)直方图指标,可能需要指导用户不要一次性同时启用所有指标,或者建议用户通过降低精度来配置他们的工具以节省内存。 某些收集工具可能对它们要监视的并发直方图数量有硬性限制,以防止内存过多使用。

  • 所有可观测仪器的回调都按顺序调用,因此任何花费很长时间的回调都可能会延迟或阻止收集所有指标。 优先选择快速读取缓存值、不返回度量值或者在执行任何可能长时间运行或阻止操作的回调时引发异常。

  • ObservableCounter、ObservableUpDownCounter 和 ObservableGauge 回调发生在通常与更新值的代码不同步的线程上。 你有责任同步内存访问,或者接受使用非同步访问导致的不一致值。 同步访问的常见方法是使用锁或调用 Volatile.ReadVolatile.Write

  • CreateObservableGaugeCreateObservableCounter 函数确实返回检测对象,但在大多数情况下,无需将其保存在变量中,因为不需要与对象进一步交互。 像我们针对其他工具所做的那样将其赋给静态变量是合法的,但容易出错,因为 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();
        }
    }
    

说明和单位

检测可以指定可选说明和单位。 这些值对所有指标计算不透明,但可以在集合工具 UI 中显示,以帮助工程师了解如何解释数据。 停止之前启动的示例进程,并将 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);
        }
    }
}

运行新进程,并在第二个 shell 中使用 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 当前不使用 UI 中的说明文本,但在提供说明文本时,它确实会显示单元。 在本例中,可以看到“{Hats}”替换了在之前的说明中可见的一般术语“Count”。

最佳做法

  • .NET API 允许将任何字符串用作单元,但我们建议使用 UCUM(单元名称的国际标准)。 “{hats}”周围的大括号是 UCUM 标准的一部分,用于指示它是描述性注释,而不是带有标准化含义(如秒或字节)的单位名称。

  • 构造函数中指定的单位应描述适用于各个度量值的单位。 这有时与最终报告指标上的单位不同。 在此示例中,每个度量值表示一定数量的帽子,因此“{hats}”是要在构造函数中传递的适当单位。 收集工具能够计算变更率,并自行推导出计算出的速率指标的相应单位为 {hats}/秒。

  • 在记录时间度量时,首选以浮点或双精度值形式记录的秒单位。

多维指标

度量还可以与称为标记的键值对相关联,这些标记允许对数据进行分类进行分析。 例如,HatCo 可能希望不仅记录已售出的帽子数量,还记录它们的大小和颜色。 稍后分析数据时,HatCo 工程师可以按大小、颜色或两者的任何组合来细分总计。

Counter 和 Histogram 标记可以在采用一个或多个 KeyValuePair 参数的 AddRecord 的重载中指定。 例如:

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
  • 请注意在实际操作中记录的标记值的组合非常大或不受限的情况。 尽管 .NET API 实现可以处理它,但收集工具可能会为与每个标记组合关联的指标数据分配存储,这可能会变得非常大。 例如,假设 HatCo 有 10 种不同的帽子颜色和 25 种帽子的尺寸,也就是要跟踪的销售总计数是 10*25=250 个,这很正常。但是,如果 HatCo 添加了第三个标记,该标记是销售的 CustomerID,并且向全球 1 亿客户销售产品,就可能会记录数十亿个不同的标记组合。 大多数指标收集工具会删除数据,以保持在技术限制范围内,或者可能会产生大量的货币成本来涵盖数据存储和处理。 每个收集工具的实现将确定其限制,而对于一个仪器来说,大概少于 1000 个组合是安全可靠的。 超过 1000 个组合的任何内容将会需要集合工具应用筛选,或者设计为以大规模运行。 直方图实现往往使用比其他指标更多的内存,因此安全限制可以降低 10-100 倍。 如果您预计会有大量独特的标记组合,则日志、事务数据库或大数据处理系统可能是更合适的解决方案,以便在所需的规模上运行。

  • 对于可能包含非常多标记组合的仪器,应优先考虑使用较小的存储类型来帮助减少内存开销。 例如,存储 shortCounter<short> 仅占用每个标记组合的 2 个字节,而 doubleCounter<double> 占用每个标记组合的 8 个字节。

  • 推荐集合工具优化代码,为每个调用指定顺序相同的相同标记名称集来记录同一检测的度量值。 对于需要频繁调用 AddRecord 的高性能代码,首选对每个调用使用相同的标记名称序列。

  • .NET API 经过优化,对于单独指定三个或更少标记的 AddRecord 调用,可以实现无分配。 若要避免带有大量标记的分配,请使用 TagList。 一般情况下,这些调用的性能开销会随着使用更多标记而增加。

注意

OpenTelemetry 将标记称为“attributes”。 对于同一功能,这些名称是两个不同的名称。

使用“建议”自定义直方图检测

使用直方图时,该工具或库负责收集数据,以确定如何最好地表示记录的值的分布。 使用 OpenTelemetry时,常见的策略(以及 默认模式)是将可能值的范围划分为称为存储桶的子范围,并报告每个存储桶中记录的值数。 例如,这个工具可以将数字划分为三个类别,即小于1的、介于1至10之间的以及大于10的。 如果应用记录了值 0.5、6、0.1、12,则第一个存储桶将有两个数据点,一个在第二个存储桶中,第三个桶中有一个数据点。

收集直方图数据的工具或库负责定义将使用的存储桶。 使用 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 包引入了(InstrumentAdvice<T>) API。

检测编写者可以使用 InstrumentAdvice API 来指定给定直方图的建议默认存储桶边界集。 然后,收集直方图数据的工具或库可以在配置聚合时选择使用这些值,从而为用户提供更无缝的载入体验。 从版本 1.10.0开始,openTelemetry .NET SDK 支持此功能。

重要

通常,更多的存储桶会为给定的直方图提供更精确的数据结果,但每个存储桶都需要内存来存储汇总的详细信息,并且在处理测量值时需要 CPU 成本来找到正确的存储桶。 在选择要通过 InstrumentAdvice API 推荐的存储桶数时,请务必了解精度与 CPU/内存消耗之间的权衡。

以下代码演示了使用 InstrumentAdvice API 设置建议的默认存储桶的示例。

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