收集指标

本文适用于:✔️ .NET 6.0 及更高版本 ✔️ .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

如果应用运行的 .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 中的指标

概述

OpenTelemetry

本教程演示了使用 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 配置为:
    • 在端口上公开 Prometheus 的指标终结点 9184
    • 使用 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 状态

  3. 在 Prometheus Web 门户的 Graph 页上,在表达式文本框中输入 hats 并选择 hats_sold_Hats“帽子 ”选项卡,Prometheus 显示示例应用发出的“hats-sold”计数器的值增加。 Prometheus hats sold graph

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

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

在 Grafana 仪表板上显示指标

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

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

    Hats 销售 Grafana 仪表板编辑器

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

    Hats 销售 Grafana 仪表板 ]

使用 .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度量值。 可以使用 、、byteshortintlongfloatdouble类型创建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。