本文适用于:✔️ .NET 6.0 及更高版本 ✔️ .NET Framework 4.6.1 及更高版本
检测代码可以记录数值度量,但通常需要聚合、传输和存储度量值,以便创建有用的监视指标。 聚合、传输和存储数据的过程称为收集。 本教程演示了收集指标的几个示例:
- 使用 OpenTelemetry 和 Prometheus 填充 Grafana 中的指标。
- 使用
实时查看指标 - 使用基础 .NET MeterListener API 创建自定义收集工具。
有关自定义指标检测和选项的详细信息,请参阅 比较指标 API。
先决条件
- .NET 6.0 SDK 或更高版本
创建示例应用
在收集指标之前,必须生成度量值。 本教程创建一个具有基本指标检测的应用。 .NET 运行时还 内置了各种指标。 有关使用 System.Diagnostics.Metrics.Meter API 创建新指标的详细信息,请参阅 检测教程。
dotnet new console -o metric-instr
cd metric-instr
dotnet add package System.Diagnostics.DiagnosticSource
将 Program.cs 以下内容的内容替换为:
using System.Diagnostics.Metrics;
class Program
{
static Meter s_meter = new("HatCo.HatStore", "1.0.0");
static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hats-sold");
static void Main(string[] args)
{
var rand = Random.Shared;
Console.WriteLine("Press any key to exit");
while (!Console.KeyAvailable)
{
//// Simulate hat selling transactions.
Thread.Sleep(rand.Next(100, 2500));
s_hatsSold.Add(rand.Next(0, 1000));
}
}
}
前面的代码模拟随机间隔和随机时间销售帽子。
使用 dotnet-counters 查看指标
dotnet-counters 是一种命令行工具,可按需查看 .NET Core 应用的实时指标。 它不需要设置,因此可用于临时调查或验证指标检测是否正常工作。 它适用于 System.Diagnostics.Metrics 基于的 API 和 EventCounters。
如果未安装 dotnet-counters 工具,请运行以下命令:
dotnet tool update -g dotnet-counters
如果应用运行的 .NET 版本低于 .NET 9,则 dotnet-counters 的输出 UI 将略有不同:有关详细信息,请参阅 dotnet-counters 。
在示例应用运行时,启动 dotnet-counters。 以下命令显示了监视计量中所有指标dotnet-counters的示例HatCo.HatStore。 计量名称区分大小写。 我们的示例应用 metric-instr.exe,请将它替换为示例应用的名称。
dotnet-counters monitor -n metric-instr HatCo.HatStore
将显示类似于以下内容的输出:
Press p to pause, r to resume, q to quit.
Status: Running
[HatCo.HatStore]
hats-sold (Count / 1 sec) 4
dotnet-counters 可以使用一组不同的指标运行,以便从 .NET 运行时查看一些内置检测:
dotnet-counters monitor -n metric-instr
将显示类似于以下内容的输出:
System.Runtime
Press p to pause, r to resume, q to quit.
Status: Running
Name Current Value
[System.Runtime]
dotnet.assembly.count ({assembly}) 11
dotnet.gc.collections ({collection})
gc.heap.generation
------------------
gen0 0
gen1 0
gen2 0
dotnet.gc.heap.total_allocated (By) 1,376,024
dotnet.gc.last_collection.heap.fragmentation.size (By)
gc.heap.generation
------------------
gen0 0
gen1 0
gen2 0
loh 0
poh 0
dotnet.gc.last_collection.heap.size (By)
gc.heap.generation
------------------
gen0 0
gen1 0
gen2 0
loh 0
poh 0
dotnet.gc.last_collection.memory.committed_size (By) 0
dotnet.gc.pause.time (s) 0
dotnet.jit.compilation.time (s) 0.253
dotnet.jit.compiled_il.size (By) 79,536
dotnet.jit.compiled_methods ({method}) 743
dotnet.monitor.lock_contentions ({contention}) 0
dotnet.process.cpu.count ({cpu}) 22
dotnet.process.cpu.time (s)
cpu.mode
--------
system 0.125
user 46.453
dotnet.process.memory.working_set (By) 34,447,360
dotnet.thread_pool.queue.length ({work_item}) 0
dotnet.thread_pool.thread.count ({thread}) 0
dotnet.thread_pool.work_item.count ({work_item}) 0
dotnet.timer.count ({timer}) 0
有关详细信息,请参阅 dotnet-counters。 若要详细了解 .NET 中的指标,请参阅 内置指标。
使用 OpenTelemetry 和 Prometheus 查看 Grafana 中的指标
概述
- 是 Cloud Native Computing Foundation 支持的供应商中立开源项目。
- 标准化生成和收集云原生软件的遥测数据。
- 使用 .NET 指标 API 处理 .NET。
- 由 Azure Monitor 和许多 APM 供应商认可。
本教程演示了使用 OSS Prometheus 和 Grafana 项目可用于 OpenTelemetry 指标的集成之一。 指标数据流:
.NET 指标 API 记录示例应用的度量值。
在应用中运行的 OpenTelemetry 库聚合度量值。
Prometheus 导出库通过 HTTP 指标终结点提供聚合数据。 “导出程序”是 OpenTelemetry 调用的库,用于将遥测数据传输到供应商特定的后端。
Prometheus 服务器:
- 轮询指标终结点
- 读取数据
- 将数据存储在数据库中,以便长期保留。 Prometheus 是指读取和存储数据作为 擦除 终结点。
- 可以在其他计算机上运行
Grafana 服务器:
- 查询 Prometheus 中存储的数据,并将其显示在基于 Web 的监视仪表板上。
- 可以在其他计算机上运行。
将示例应用配置为使用 OpenTelemetry 的 Prometheus 导出程序
将对 OpenTelemetry Prometheus 导出程序引用添加到示例应用:
dotnet add package OpenTelemetry.Exporter.Prometheus.HttpListener --prerelease
注释
本教程使用在撰写时提供的 OpenTelemetry Prometheus 支持的预发行版本。
使用 OpenTelemetry 配置进行更新 Program.cs :
using OpenTelemetry;
using OpenTelemetry.Metrics;
using System.Diagnostics.Metrics;
class Program
{
static Meter s_meter = new("HatCo.HatStore", "1.0.0");
static Counter<int> s_hatsSold = s_meter.CreateCounter<int>(
name: "hats-sold",
unit: "Hats",
description: "The number of hats sold in our store");
static void Main(string[] args)
{
using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("HatCo.HatStore")
.AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { "http://localhost:9184/" })
.Build();
var rand = Random.Shared;
Console.WriteLine("Press any key to exit");
while (!Console.KeyAvailable)
{
//// Simulate hat selling transactions.
Thread.Sleep(rand.Next(100, 2500));
s_hatsSold.Add(rand.Next(0,1000));
}
}
}
在上述代码中:
-
AddMeter("HatCo.HatStore")配置 OpenTelemetry 以传输应用中定义的计量收集的所有指标。 -
AddPrometheusHttpListener将 OpenTelemetry 配置为:- 在端口上公开 Prometheus 的指标终结点
9184 - 使用 HttpListener。
- 在端口上公开 Prometheus 的指标终结点
有关 OpenTelemetry 配置选项的详细信息,请参阅 OpenTelemetry 文档 。 OpenTelemetry 文档显示 ASP.NET 应用的托管选项。
运行应用并使其保持运行状态,以便收集度量值:
dotnet run
设置和配置 Prometheus
按照 Prometheus 的第一个步骤设置 Prometheus 服务器,并确认它是否正常工作。
修改 prometheus.yml 配置文件,以便 Prometheus 擦除示例应用公开的指标终结点。 在 scrape_configs 节中添加以下突出显示的文本:
# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: "prometheus"
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
static_configs:
- targets: ["localhost:9090"]
- job_name: 'OpenTelemetryTest'
scrape_interval: 1s # poll very quickly for a more responsive demo
static_configs:
- targets: ['localhost:9184']
启动 Prometheus
重新加载配置或重启 Prometheus 服务器。
确认 OpenTelemetryTest 在 Prometheus Web 门户的“状态>”页中处于 UP 状态。
在 Prometheus Web 门户的 Graph 页上,在表达式文本框中输入
hats并选择hats_sold_Hats
”选项卡,Prometheus 显示示例应用发出的“hats-sold”计数器的值增加。
在上图中,图形时间设置为 5 米,即 5 分钟。
如果 Prometheus 服务器长时间未抓取示例应用,则可能需要等待数据累积。
在 Grafana 仪表板上显示指标
按照 标准说明 安装 Grafana 并将其连接到 Prometheus 数据源。
单击 + Grafana Web 门户中左侧工具栏上的图标,然后选择 “仪表板”,创建 Grafana 仪表板。 在显示的仪表板编辑器中,在 PromQL 表达式字段中输入 Title 输入框和 rate(hats_sold[500 万])中输入 Hats Sold/Sec:
单击“ 应用” 保存并查看新仪表板。
]
使用 .NET MeterListener API 创建自定义收集工具
.NET MeterListener API 允许创建自定义进程内逻辑来观察所记录 System.Diagnostics.Metrics.Meter的度量值。 有关创建自定义逻辑与旧 EventCounters 检测兼容的指南,请参阅 EventCounters。
修改要使用的Program.cs代码MeterListener:
using System.Diagnostics.Metrics;
class Program
{
static Meter s_meter = new("HatCo.HatStore", "1.0.0");
static Counter<int> s_hatsSold = s_meter.CreateCounter<int>(
name: "hats-sold",
unit: "Hats",
description: "The number of hats sold in our store");
static void Main(string[] args)
{
using MeterListener meterListener = new();
meterListener.InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name is "HatCo.HatStore")
{
listener.EnableMeasurementEvents(instrument);
}
};
meterListener.SetMeasurementEventCallback<int>(OnMeasurementRecorded);
// Start the meterListener, enabling InstrumentPublished callbacks.
meterListener.Start();
var rand = Random.Shared;
Console.WriteLine("Press any key to exit");
while (!Console.KeyAvailable)
{
//// Simulate hat selling transactions.
Thread.Sleep(rand.Next(100, 2500));
s_hatsSold.Add(rand.Next(0, 1000));
}
}
static void OnMeasurementRecorded<T>(
Instrument instrument,
T measurement,
ReadOnlySpan<KeyValuePair<string, object?>> tags,
object? state)
{
Console.WriteLine($"{instrument.Name} recorded measurement {measurement}");
}
}
以下输出显示了每个度量值上具有自定义回调的应用输出:
> dotnet run
Press any key to exit
hats-sold recorded measurement 978
hats-sold recorded measurement 775
hats-sold recorded measurement 666
hats-sold recorded measurement 66
hats-sold recorded measurement 914
hats-sold recorded measurement 912
...
示例代码说明
本节中的代码片段来自前面的示例。
在以下突出显示的代码中,将创建一个 MeterListener 实例来接收度量值。 当超出范围时using,关键字Dispose会导致meterListener调用。
using MeterListener meterListener = new();
meterListener.InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name is "HatCo.HatStore")
{
listener.EnableMeasurementEvents(instrument);
}
};
以下突出显示的代码配置侦听器从中接收度量的仪器。 InstrumentPublished 是在应用中创建新检测时调用的委托。
using MeterListener meterListener = new();
meterListener.InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name is "HatCo.HatStore")
{
listener.EnableMeasurementEvents(instrument);
}
};
委托可以检查检测,以确定是否订阅。 例如,委托可以检查名称、计量或任何其他公共属性。 EnableMeasurementEvents 启用从指定仪器接收度量值。 通过另一种方法获取对检测的引用的代码:
- 通常不完成。
- 随时可以使用引用调用
EnableMeasurementEvents()。
通过调用 SetMeasurementEventCallback来配置从仪器接收度量值时调用的委托:
meterListener.SetMeasurementEventCallback<int>(OnMeasurementRecorded);
// Start the meterListener, enabling InstrumentPublished callbacks.
meterListener.Start();
var rand = Random.Shared;
Console.WriteLine("Press any key to exit");
while (!Console.KeyAvailable)
{
//// Simulate hat selling transactions.
Thread.Sleep(rand.Next(100, 2500));
s_hatsSold.Add(rand.Next(0, 1000));
}
}
static void OnMeasurementRecorded<T>(
Instrument instrument,
T measurement,
ReadOnlySpan<KeyValuePair<string, object?>> tags,
object? state)
{
Console.WriteLine($"{instrument.Name} recorded measurement {measurement}");
}
泛型参数控制回调接收的度量数据类型。 例如,生成Counter<int>int度量值,Counter<double>生成double度量值。 可以使用 、、byte、shortint、 longfloat和double类型创建decimal工具。 建议为每个数据类型注册回调,除非你具有特定于方案的知识,即不需要所有数据类型。 对具有不同泛型参数的重复调用 SetMeasurementEventCallback 似乎有点不同寻常。 API 设计为允许 MeterListener 接收性能开销低的度量值,通常只需几纳秒。
调用时 MeterListener.EnableMeasurementEvents ,可以将对象 state 作为参数之一提供。 对象 state 是任意的。 如果在该调用中提供状态对象,则它将随该检测一起存储,并在回调中作为 state 参数返回给你。 这既是为了方便又作为性能优化。 侦听器通常需要:
- 为每个存储内存中度量值的工具创建一个对象。
- 让代码对这些度量执行计算。
或者,创建一个 Dictionary 从仪器映射到存储对象,并在每次测量时查找它。 使用 a Dictionary 比从中 state访问它要慢得多。
meterListener.Start();
前面的代码将启动 MeterListener 启用回调。 为 InstrumentPublished 进程中每个预先存在的检测调用委托。 新创建的 Instruments 对象也触发 InstrumentPublished 要调用。
using MeterListener meterListener = new MeterListener();
当应用完成侦听时,释放侦听器将停止回调流,并释放对侦听器对象的任何内部引用。
using声明meterListener原因Dispose时使用的关键字在变量超出范围时调用。 请注意, Dispose 它只会承诺不会启动新的回调。 由于回调发生在不同的线程上,因此调用 Dispose 返回后可能仍有回调正在进行。
若要保证回调中的某个代码区域当前未执行,并且将来不会执行,必须添加线程同步。
Dispose 默认情况下不包括同步,因为:
- 同步会在每个度量回调中添加性能开销。
-
MeterListener设计为具有高性能意识的 API。