收集指标

本文适用范围:✔️ .NET Core 3.1 及更高版本 ✔️ .NET Framework 4.6.1 及更高版本

检测的代码可以记录数值度量值,但通常需要聚合、传输和存储度量值,以创建用于监视的有用指标。 聚合、传输和存储数据的过程称为集合。 本教程演示收集指标的几个示例:

有关自定义指标检测和选项的详细信息,请参阅比较指标 API

先决条件

创建示例应用

必须先生成度量值,然后才能收集指标。 本教程创建具有基本指标检测的应用。 .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

运行示例应用时,启动 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

显示了类似下面的输出:

Press p to pause, r to resume, q to quit.
    Status: Running

[System.Runtime]
    % Time in GC since last GC (%)                                 0
    Allocation Rate (B / 1 sec)                                8,168
    CPU Usage (%)                                                  0
    Exception Count (Count / 1 sec)                                0
    GC Heap Size (MB)                                              2
    Gen 0 GC Count (Count / 1 sec)                                 0
    Gen 0 Size (B)                                         2,216,256
    Gen 1 GC Count (Count / 1 sec)                                 0
    Gen 1 Size (B)                                           423,392
    Gen 2 GC Count (Count / 1 sec)                                 0
    Gen 2 Size (B)                                           203,248
    LOH Size (B)                                             933,216
    Monitor Lock Contention Count (Count / 1 sec)                  0
    Number of Active Timers                                        1
    Number of Assemblies Loaded                                   39
    ThreadPool Completed Work Item Count (Count / 1 sec)           0
    ThreadPool Queue Length                                        0
    ThreadPool Thread Count                                        3
    Working Set (MB)                                              30

有关详细信息,请参阅 dotnet-counters。 要详细了解 .NET 中的指标,请参阅内置指标

使用 OpenTelemetry 和 Prometheus 查看 Grafana 中的指标

概述

OpenTelemetry

  • 是一个由云原生计算基金会支持的供应商中立开源项目。
  • 标准化云原生软件的遥测数据生成和收集。
  • 使用 .NET 指标 API 与 .NET 配合使用。
  • 得到 Azure Monitor 和许多 APM 供应商的认可。

本教程使用 OSS PrometheusGrafana 项目展示了可用于 OpenTelemetry 指标的集成之一。 指标数据流:

  1. .NET 指标 API 记录示例应用中的度量值。

  2. 在应用中运行的 OpenTelemetry 库将聚合这些度量值。

  3. Prometheus 导出程序库通过 HTTP 指标终结点提供聚合数据。 “导出程序”指的是 OpenTelemetry 调用库来将遥测数据传输到供应商特定的后端。

  4. Prometheus 服务器:

    • 轮询指标终结点
    • 读取数据
    • 将数据存储在数据库中以实现长期持久存储。 Prometheus 将读取和存储数据称为抓取终结点。
    • 可以在其他计算机上运行
  5. 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 配置为:
    • 在端口 9184 上公开 Prometheus 的指标终结点
    • 使用 HttpListener。

有关 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

  1. 重新加载配置或重启 Prometheus 服务器。

  2. 确认 OpenTelemetryTest 在 Prometheus Web 门户的“状态”>“目标”页中处于 UP 状态。 Prometheus status

  3. 在 Prometheus 门户网站“图表”页面,在表达式文本框中输入 hats 并在图表选项卡中选择 hats_sold_Hatshat,Prometheus 将显示示例应用程序发出的“hats-sold”计数器的值不断增加。 Prometheus hats sold graph

在上图中,图形时间设置为 5m,即 5 分钟。

如果 Prometheus 服务器长时间未抓取示例应用,可能需要等待数据累积。

在 Grafana 仪表板上显示指标

  1. 按照标准说明安装 Grafana,并将其连接到 Prometheus 数据源。

  2. 创建 Grafana 仪表板:单击 Grafana Web 门户左侧工具栏上的 + 图标,然后选择“仪表板”。 在出现的仪表板编辑器中,在标题输入框中输入 Hats Sold/Sec,并在 PromQL 表达式字段中输入 rate(hats_sold[5m])

    Hats sold Grafana dashboard editor

  3. 单击“应用”保存并查看新仪表板。

    Hats sold Grafana dashboard]

使用 .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 实例来接收度量值。 当 meterListener 超出范围时,using 关键字会导致调用 Dispose

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 值度量。 可以使用 byteshortintlongfloatdoubledecimal 类型创建检测。 建议为每种数据类型注册回调,除非你了解特定于场景的知识,知道并非所有数据类型都是必需的。 使用不同的泛型参数重复调用 SetMeasurementEventCallback 可能看起来有点异常。 API 的设计方式使 MeterListener 能以较低的性能开销接收度量值,通常只有几纳秒。

调用 MeterListener.EnableMeasurementEvents 时,可以将 state 对象作为其中一个参数提供。 对象 state 是任意的。 如果在该调用中提供状态对象,则它将随该检测一起存储,并作为回调中的 state 参数返回。 这是为了方便起见,也是为了优化性能。 侦听器通常需要:

  • 为每个在内存中存储测量值的检测创建一个对象。
  • 有代码对这些度量值执行计算。

或者,创建一个从检测映射到存储对象的 Dictionary,并在每次测量时查找它。 使用 Dictionary 比从 state 访问它要慢得多。

meterListener.Start();

前面的代码启动了启用回调的 MeterListener。 流程中每个预先存在的检测都会调用 InstrumentPublished 委托。 新创建的检测对象也会触发 InstrumentPublished 调用。

using MeterListener meterListener = new MeterListener();

应用完成侦听后,释放侦听器会停止回调流,并释放对侦听器对象的任何内部引用。 声明 meterListener 时使用的 using 关键字会导致在变量超出范围时调用 Dispose。 请注意,Dispose 只保证它不会发起新的回调。 因为回调发生在不同的线程上,因此在对 Dispose 的调用返回后,可能仍有正在进行中的回调。

要保证回调中的某个代码区域当前未执行且将来也不会执行,必须添加线程同步。 Dispose 默认情况下不包括同步,因为:

  • 同步会增加每个测量回调的性能开销。
  • MeterListener 设计为具有高性能意识的 API。