创建指标

本文适用范围:✔️ .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 不会强制实施任何命名方案来进行检测,但我们建议遵循 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 创建计量对象。 从 .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> 检测,但可用的检测类型还有很多。 可从两个方面区分这些检测:

  • 默认指标计算 - 收集和分析检测度量值的工具会根据不同的检测计算不同的默认指标。
  • 存储聚合数据 - 最有用的度量值需要通过多个度量值聚合数据。 一种选择是调用方在任意时间提供单独的度量值,再由集合工具管理聚合。 或者,调用方可以管理聚合度量值,并在回调中按需提供它们。

当前可用的检测类型:

  • Counter (CreateCounter) - 此检测跟踪随时间增加的值,并且调用方使用 Add 来报告增量。 大多数工具将计算总计数和总计数中的变化率。 对于仅显示一项内容的工具,建议显示变化率。 例如,假定调用方每秒调用一次 Add(),使用的值依次为 1、2、4、5、4、3。 如果集合工具每三秒钟更新一次,则三秒后的总计数为 1 + 2 + 4 = 7,六秒后的总计数为 1 + 2 + 4 + 5 + 4 + 3 = 19。 变化率是 (current_total - previous_total),因此在三秒后,该工具报告 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%)的尾部值比了解平均值或总计数更有用。

  • 其他常见的情况(例如缓存命中率或缓存大小、队列和文件)则一般适合 UpDownCounterObservableUpDownCounter。 要在它们之间进行选择,具体要考虑其中哪一个更容易添加到现有代码中:是对每个增量和减量操作的 API 调用,还是从代码维护的变量中读取当前值的回调。

注意

如果使用的是不支持 UpDownCounterObservableUpDownCounter 的旧版 .NET 或 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(0.005, 0.015));
        }
    }
}

运行新进程,并在第二个 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

[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 指标,则可能需要指导用户不要同时启用所有指标,或者将其工具配置为通过降低精准率来节省内存。 部分集合工具可能对它们将监视的并发 Histogram 数量有硬性限制,目的是防止过度使用内存。

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

  • 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

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

dotnet-counters 当前不在 UI 中使用说明文本,但它在提供时会显示单位。 在本例中,可以看到“{Hats}”替换了在之前的说明中可见的一般术语“Count”。

最佳实践

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

  • 构造函数中指定的单位应描述适用于各个度量值的单位。 这有时与最终指标中的单位不同。 在此示例中,每个度量值表示一定数量的帽子,因此“{hats}”是要在构造函数中传递的适当单位。 集合工具计算了速率,并自行派生出计算指标的适当单位为 {hats}/sec。

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

多维指标

度量值还可以与被称为标记的键值对相关联,从而能对数据进行分类以进行分析。 例如,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

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

  • 对于将具有大量标记组合的检测,建议使用较小的存储类型来帮助降低内存开销。 例如,为 Counter<short> 存储 short每个标记组合只占用 2 个字节,而为 Counter<double> 存储 double,每个标记组合占用 8 个字节。

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

  • .NET API 经过优化,对于单独指定三个或更少标记的 AddRecord 调用,可以实现无分配。 若要避免带有大量标记的分配,请使用 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。

在没有依赖注入的情况下进行测试

还可以测试在静态字段中使用共享全局计量对象的代码,但请确保此类测试未配置为并行运行。 由于计量对象正在共享,因此一个测试中的 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);
    }
}