带有强类型标记的源生成指标

新式 .NET 应用程序可以使用 API 捕获指标 System.Diagnostics.Metrics 。 这些指标通常以称为 标记 的键值对的形式(有时称为遥测系统中 的维度 )的形式包含其他上下文。 本文介绍如何使用编译时源生成器来定义 强类型指标标记 (TagNames)和指标记录类型和方法。 通过使用强类型标记,可以消除重复的样板代码,并确保相关指标共享具有编译时安全性的相同标记名称集。 此方法的主要好处是提高开发人员的工作效率和类型安全性。

注释

在指标的上下文中,标记有时也称为“维度”。本文使用“标记”来明确和一致性与 .NET 指标术语。

立即开始

若要开始,请安装 📦 Microsoft.Extensions.Telemetry.Abstractions NuGet 包:

dotnet add package Microsoft.Extensions.Telemetry.Abstractions

有关详细信息,请参阅 dotnet add package管理 .NET 应用程序中的包依赖项

标记名称默认值和自定义

默认情况下,源生成器从标记类的字段和属性名称派生指标标记名称。 换句话说,强类型标记对象中的每个公共字段或属性默认变为标记名称。 可以通过在字段或属性中使用 TagNameAttribute 来指定自定义标记名称,从而覆盖此值。 在下面的示例中,你将看到这两种方法都在作中。

示例 1:具有单个标签的基本指标

以下示例演示了一个带有一个标记的简单计数器指标。 在此方案中,我们希望对已处理的请求数进行计数,并按 Region 标记对其进行分类:

public struct RequestTags
{
    public string Region { get; set; }
}

public static partial class MyMetrics
{
    [Counter<int>(typeof(RequestTags))]
    public static partial RequestCount CreateRequestCount(Meter meter);
}

在前面的代码中, RequestTags 是具有单个属性 Region的强类型标记结构。 CreateRequestCount 方法标记为 CounterAttribute<T>,其中 Tint,表示它生成一个跟踪 Counter 值的 int 仪器。 属性引用 typeof(RequestTags),这意味着计数器使用记录指标时定义的 RequestTags 标记。 源生成器生成一个名为RequestCount的强类型仪器类,并具有一个Add方法,该方法接受整数值和RequestTags对象。

若要使用生成的指标,请创建Meter并记录度量值,如下所示:

Meter meter = new("MyCompany.MyApp", "1.0");
RequestCount requestCountMetric = MyMetrics.CreateRequestCount(meter);

// Create a tag object with the relevant tag value
var tags = new RequestTags { Region = "NorthAmerica" };

// Record a metric value with the associated tag
requestCountMetric.Add(1, tags);

在此用法示例中,调用 MyMetrics.CreateRequestCount(meter) 将创建计数器检测(通过 Meter)并返回 RequestCount 指标对象。 调用 requestCountMetric.Add(1, tags) 时,指标系统记录与标记 Region="NorthAmerica" 关联的计数为 1。 可以重复使用对象 RequestTags 或创建新对象来记录不同区域的计数,标记名称 Region 将一致地应用于每个度量值。

示例 2:包含嵌套标记对象的指标

对于更复杂的方案,可以定义包含多个标记、嵌套对象甚至继承属性的标记类。 这允许一组相关指标有效地共享一组常见的标记。 在下一个示例中,定义一组标记类并将其用于三个不同的指标:

using Microsoft.Extensions.Diagnostics.Metrics;

namespace MetricsGen;

public class MetricTags : MetricParentTags
{
    [TagName("Dim1DimensionName")]
    public string? Dim1;                      // custom tag name via attribute
    public Operations Operation { get; set; } // tag name defaults to "Operation"
    public MetricChildTags? ChildTagsObject { get; set; }
}

public enum Operations
{
    Unknown = 0,
    Operation1 = 1,
}

public class MetricParentTags
{
    [TagName("DimensionNameOfParentOperation")]
    public string? ParentOperationName { get; set; }  // custom tag name via attribute
    public MetricTagsStruct ChildTagsStruct { get; set; }
}

public class MetricChildTags
{
    public string? Dim2 { get; set; }  // tag name defaults to "Dim2"
}

public struct MetricTagsStruct
{
    public string Dim3 { get; set; }   // tag name defaults to "Dim3"
}

前面的代码定义指标继承和对象形状。 以下代码演示如何将这些形状与生成器一起使用,如类中 Metric 所示:

using System.Diagnostics.Metrics;
using Microsoft.Extensions.Diagnostics.Metrics;

public static partial class Metric
{
    [Histogram<long>(typeof(MetricTags))]
    public static partial Latency CreateLatency(Meter meter);

    [Counter<long>(typeof(MetricTags))]
    public static partial TotalCount CreateTotalCount(Meter meter);

    [Counter<int>(typeof(MetricTags))]
    public static partial TotalFailures CreateTotalFailures(Meter meter);
}

在此示例中,MetricTags 是一个标记类,它继承自 MetricParentTags,并包含一个嵌套标记对象(MetricChildTags)和一个嵌套结构(MetricTagsStruct)。 标记属性演示默认标记名称和自定义标记名称:

  • 中的Dim1MetricTags字段具有属性[TagName("Dim1DimensionName")],因此其标记名称将为 "Dim1DimensionName"
  • Operation 属性没有属性,因此其标记名称默认为 "Operation"
  • MetricParentTags中,使用自定义标记名称ParentOperationName覆盖"DimensionNameOfParentOperation"属性。
  • 嵌套 MetricChildTags 类定义属性 Dim2 (无属性、标记名称 "Dim2")。
  • 结构 MetricTagsStruct 定义字段 Dim3 (标记名称 "Dim3")。

三个指标定义 CreateLatencyCreateTotalCountCreateTotalFailures 的标记对象类型均为 MetricTags。 这意味着生成的指标类型(LatencyTotalCountTotalFailures)在记录数据时都期望有一个MetricTags实例。 其中每个指标将具有相同的标记名称集:Dim1DimensionName、、OperationDim2Dim3DimensionNameOfParentOperation

以下代码演示如何在类中创建和使用这些指标:

internal class MyClass
{
    private readonly Latency _latencyMetric;
    private readonly TotalCount _totalCountMetric;
    private readonly TotalFailures _totalFailuresMetric;

    public MyClass(Meter meter)
    {
        // Create metric instances using the source-generated factory methods
        _latencyMetric = Metric.CreateLatency(meter);
        _totalCountMetric = Metric.CreateTotalCount(meter);
        _totalFailuresMetric = Metric.CreateTotalFailures(meter);
    }

    public void DoWork()
    {
        var startingTimestamp = Stopwatch.GetTimestamp();
        bool requestSuccessful = true;
        // Perform some operation to measure
        var elapsedTime = Stopwatch.GetElapsedTime(startingTimestamp);

        // Create a tag object with values for all tags
        var tags = new MetricTags
        {
            Dim1 = "Dim1Value",
            Operation = Operations.Operation1,
            ParentOperationName = "ParentOpValue",
            ChildTagsObject = new MetricChildTags
            {
                Dim2 = "Dim2Value",
            },
            ChildTagsStruct = new MetricTagsStruct
            {
                Dim3 = "Dim3Value"
            }
        };

        // Record the metric values with the associated tags
        _latencyMetric.Record(elapsedTime.ElapsedMilliseconds, tags);
        _totalCountMetric.Add(1, tags);
        if (!requestSuccessful)
        {
            _totalFailuresMetric.Add(1, tags);
        }
    }
}

在前面的 MyClass.DoWork 方法中,MetricTags 对象被填充上每个标记的值。 然后,在记录数据时,此单个 tags 对象将传递到所有三个仪器。 Latency 指标(直方图)记录经过时间,而计数器(TotalCountTotalFailures)记录发生次数。 由于所有指标共享相同的标记对象类型,因此每个度量值都存在标记(Dim1DimensionNameOperationDim2Dim3DimensionNameOfParentOperation

性能注意事项

与直接使用指标相比,通过源生成使用强类型标记不会增加开销。 如果需要进一步将高频率指标的分配降到最低,请考虑将标记对象定义为 struct (值类型)而不是 class值类型。 struct使用标记对象可以避免在记录指标时进行堆分配,因为标记将通过值传递。

生成的指标方法要求

定义指标工厂方法(用 [Counter][Histogram] 等装饰的部分方法)时,源生成器提出了一些要求:

  • 每个方法都必须由 public static partial 来实现(由源生成器提供实现)。
  • 每个分部方法的返回类型必须是唯一的(以便生成器可以为指标创建唯一命名的类型)。
  • 方法名称不应以下划线开头(_),参数名称不应以下划线开头。
  • 第一个参数必须是一个 Meter (这是用于创建基础仪器的计量实例)。
  • 方法不能是泛型方法,不能具有泛型参数。
  • 标记类中的标记属性只能是类型 stringenum。 对于其他类型的(例如, bool 数值类型),在将值分配给标记对象之前,请将该值转换为字符串。

遵循这些要求可确保源生成器能够成功生成指标类型和方法。

另请参阅