本文内容创建自定义指标通过依赖注入获取计量检测类型说明和单位显示另外 2 个
本文适用范围:✔️ .NET Core 6 及更高版本 ✔️ .NET Framework 4.6.1 及更高版本
可以使用 System.Diagnostics.Metrics API 来检测 .NET 应用程序以跟踪重要指标。 一些指标包括在标准 .NET 库中,但可能需要添加与应用程序和库相关的新的自定义指标。 在本教程中,你将添加新的指标并了解可用的指标类型。
备注
.NET 有一些较旧的指标 API,即 EventCounters 和 System.Diagnostics.PerformanceCounter,此处不会介绍这些 API。 若要详细了解这些选项,请参阅比较指标 API。
先决条件:.NET Core 6 SDK 或更高版本
创建引用 System.Diagnostics.DiagnosticSource NuGet 包版本 8 或更高版本的新控制台应用程序。 默认情况下,面向 .NET 8 及更高版本的应用程序包括此引用。 然后,更新 Program.cs 中的代码以匹配:
.NET CLI
> dotnet new console
> dotnet add package System.Diagnostics.DiagnosticSource
C#
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 的值。 可以使用 byte、short、int、long、float、double 或decimal 中的任何一个,具体取决于需要存储的数据大小以及是否需要小数值。
运行应用并使其保持运行状态。 接下来,我们将查看指标。
.NET CLI
> dotnet run
Press any key to exit
contoso.ticket_queue.durationcontoso.reserved_ticketscontoso.purchased_tickets有很多选项可用于存储和查看指标。 本教程使用 dotnet-counters 工具,此工具适用于即席分析。 还可以查看指标集合教程,了解其他替代方法。 如果尚未安装 dotnet-counters 工具,请使用 SDK 进行安装:
.NET CLI
> 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 监视新计数器:
.NET CLI
> 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。
定义用于保存检测的类型:
C#
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 容器注册类型。
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<HatCoMetrics>();
根据需要注入指标类型和记录值。 由于指标类型已在 DI 中注册,因此它可以与 MVC 控制器、最小 API 或 DI 创建的任何其他类型一起使用:
C#
app.MapPost("/complete-sale", ([FromBody] SaleModel model, HatCoMetrics metrics) =>
{
// ... business logic such as saving the sale to a database ...
metrics.HatsSold(model.QuantitySold);
});
Meter 对象的生存期,从而在释放 DI 容器时释放它们。 调用 Meter 上的 Dispose() 时,无需添加额外的代码,并且不会产生任何效果。到目前为止,我们只演示了一个 Counter 检测,但可用的检测类型还有很多。 可从两个方面区分这些检测:
当前可用的检测类型:
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。Add(),使用的值依次为 1、5、-2、3、-1、-3。 如果集合工具每三秒钟更新一次,则三秒后的总计数为 1+5-2=4,六秒后的总计数为 1+5-2+3-1-3=3。UpDownCounter 或 ObservableUpDownCounter。 要在它们之间进行选择,具体要考虑其中哪一个更容易添加到现有代码中:是对每个增量和减量操作的 API 调用,还是从代码维护的变量中读取当前值的回调。备注
如果使用的是不支持 UpDownCounter 和 ObservableUpDownCounter 的旧版 .NET 或 DiagnosticSource NuGet 包(低于版本 7),则通常建议使用 ObservableGauge 代替。
停止前面启动的示例进程,并将 Program.cs 中的示例代码替换为:
C#
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 以查看指标:
.NET CLI
> 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%),但是其他工具可能会以不同的方式汇总分布情况,或提供更多配置选项。
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 中的示例代码替换为:
C#
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 以查看指标:
.NET CLI
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”。
度量值还可以与被称为标记的键值对相关联,从而能对数据进行分类以进行分析。 例如,HatCo 不仅想要记录售出的帽子数量,还想要记录它们的大小和颜色。 在稍后分析数据时,HatCo 工程师可以按大小、颜色或两者的任意组合来对总计数进行分解。
Counter 和 Histogram 标记可以在采用一个或多个 KeyValuePair 参数的 Add 和 Record 的重载中指定。 例如:
C#
s_hatsSold.Add(2,
new KeyValuePair<string, object>("product.color", "red"),
new KeyValuePair<string, object>("product.size", 12));
替换 Program.cs 的代码,并像以前一样重新运行应用和 dotnet-counters:
C#
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 现在显示基本分类:
.NET CLI
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,可以在传递给构造函数的回调中提供带标记的度量值:
C#
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 运行时,结果为:
.NET CLI
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
customer.countrystore.payment_methodstore.purchase_resultCounter<short> 存储 short每个标记组合只占用 2 个字节,而为 Counter<double> 存储 double,每个标记组合占用 8 个字节。备注
OpenTelemetry 将标记引用为“特性”。 它们是同一功能的两个不同名称。
可以使用 MetricCollector 测试你添加的任何自定义指标。 使用此类型,可以轻松地记录来自特定检测的度量值,并断言值是正确的。
以下代码演示了使用依赖注入和 IMeterFactory 的代码组件的示例测试用例。
C#
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 将观察到根据并行运行的任何其他测试创建的度量值。
C#
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);
}
}