Partilhar via


Métricas de origem geradas com tags fortemente tipadas

Os aplicativos .NET modernos podem capturar métricas usando a API System.Diagnostics.Metrics. Essas métricas geralmente incluem contexto adicional na forma de pares chave-valor chamados tags (às vezes referidas como dimensões em sistemas de telemetria). Este artigo mostra como usar um gerador de código-fonte em tempo de compilação para definir etiquetas métricas fortemente tipificadas (TagNames) e tipos e métodos de registo de métricas. Usando tags fortemente tipadas, você elimina o código clichê repetitivo e garante que as métricas relacionadas compartilhem o mesmo conjunto de nomes de tags com segurança em tempo de compilação. O principal benefício dessa abordagem é melhorar a produtividade do desenvolvedor e a segurança do tipo.

Observação

No contexto das métricas, uma tag às vezes também é chamada de "dimensão". Este artigo usa "tag" para clareza e consistência com a terminologia de métricas do .NET.

Introdução

Para começar, instale o pacote NuGet Microsoft.Extensions.Telemetry.Abstractions📦:

dotnet add package Microsoft.Extensions.Telemetry.Abstractions

Para obter mais informações, consulte dotnet add package ou Manage package dependencies in .NET applications.

Predefinições e personalização do nome da tag

Por padrão, o gerador de origem deriva os nomes das etiquetas métricas a partir dos nomes de campo e propriedade da sua classe de etiqueta. Em outras palavras, cada campo ou propriedade pública no objeto de etiquetas fortemente tipado transforma-se, por padrão, num nome de etiqueta. Pode alterar isto utilizando o TagNameAttribute num campo ou propriedade para especificar um nome de etiqueta personalizado. Nos exemplos abaixo, você verá ambas as abordagens em ação.

Exemplo 1: Métrica básica com uma única tag

O exemplo a seguir demonstra uma métrica de contador simples com uma tag. Nesse cenário, queremos contar o número de solicitações processadas e categorizá-las por uma tag 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);
}

No código anterior, RequestTags é uma struct de tag com tipagem forte com uma única propriedade Region. O CreateRequestCount método é marcado com CounterAttribute<T> onde T é um int, indicando que gera um Counter instrumento que rastreia int valores. O atributo faz referência typeof(RequestTags), ou seja, o contador usa as tags definidas em RequestTags ao gravar métricas. O gerador de origem produz uma classe de instrumento com forte tipagem (chamada RequestCount) com um método Add que aceita um valor inteiro e um objeto RequestTags.

Para usar a métrica gerada, crie um Meter e registre as medições, conforme mostrado abaixo:

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);

Neste exemplo de uso, chamar MyMetrics.CreateRequestCount(meter) cria um instrumento de contador (por meio do Meter) e retorna um objeto de métrica RequestCount. Quando você chama requestCountMetric.Add(1, tags), o sistema métrico registra uma contagem de 1 associada à tag Region="NorthAmerica". Você pode reutilizar o objeto RequestTags ou criar novos para registrar contagens para diferentes regiões, e o nome da tag Region será aplicado consistentemente a cada medição.

Exemplo 2: Métrica com objetos de tag aninhados

Para cenários mais complexos, você pode definir classes de tag que incluem várias tags, objetos aninhados ou até mesmo propriedades herdadas. Isso permite que um grupo de métricas relacionadas compartilhe efetivamente um conjunto comum de tags. No próximo exemplo, você define um conjunto de classes de tag e as usa para três métricas diferentes:

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"
}

O código anterior define a herança métrica e as formas do objeto. O código a seguir demonstra como usar essas formas com o gerador, conforme mostrado na Metric classe:

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);
}

Neste exemplo, MetricTags é uma classe de tag que herda de MetricParentTags e também contém um objeto de tag aninhado (MetricChildTags) e uma struct aninhada (MetricTagsStruct). As propriedades da tag demonstram nomes de tag padrão e personalizados:

  • O campo Dim1 no MetricTags tem um atributo [TagName("Dim1DimensionName")], portanto, seu nome de tag será "Dim1DimensionName".
  • A propriedade Operation não tem nenhum atributo, portanto, seu nome de tag assume como padrão "Operation".
  • No MetricParentTags, a propriedade ParentOperationName é substituída por um nome de etiqueta personalizado "DimensionNameOfParentOperation".
  • A classe MetricChildTags aninhada define uma propriedade Dim2 (sem nenhum atributo, nome da etiqueta "Dim2").
  • A estrutura MetricTagsStruct define um campo Dim3 (nome da etiqueta "Dim3").

Todas as três definições métricas CreateLatency, CreateTotalCounte CreateTotalFailures usam MetricTags como seu tipo de objeto de marca. Isso significa que os tipos de métricas gerados (Latency, TotalCounte TotalFailures) esperam uma instância MetricTags quando registam dados. Cada uma dessas métricas terá o mesmo conjunto de nomes de tag:Dim1DimensionName, Operation, Dim2, Dim3e DimensionNameOfParentOperation.

O código a seguir mostra como criar e usar essas métricas em uma classe:

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);
        }
    }
}

No método MyClass.DoWork anterior, um objeto MetricTags é preenchido com valores para cada tag. Este único objeto tags é então passado para os três instrumentos ao gravar dados. A métrica Latency (um histograma) registra o tempo decorrido, e ambos os contadores (TotalCount e TotalFailures) registram contagens de ocorrências. Como todas as métricas compartilham o mesmo tipo de objeto de tag, as tags (Dim1DimensionName, Operation, Dim2, Dim3, DimensionNameOfParentOperation) estão presentes em todas as medições.

Especificação de unidades

A partir do .NET 10.2, podes opcionalmente especificar uma unidade de medida para as tuas métricas usando o Unit parâmetro. Isto ajuda a fornecer contexto sobre o que a métrica mede (por exemplo, "segundos", "bytes" e "pedidos"). A unidade é passada para a subjacente Meter ao criar o instrumento.

O código seguinte demonstra como usar o gerador com tipos primitivos com unidades especificadas:

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

    [Counter<long>(typeof(MetricTags), Unit = "requests")]
    public static partial TotalCount CreateTotalCount(Meter meter);

    [Counter<int>(typeof(MetricTags), Unit = "failures")]
    public static partial TotalFailures CreateTotalFailures(Meter meter);
}

Considerações sobre desempenho

O uso de tags fortemente tipadas por meio da geração de código-fonte não adiciona nenhuma sobrecarga em comparação com o uso direto de métricas. Se você precisar minimizar ainda mais as alocações para métricas de frequência muito alta, considere definir seu objeto de tag como um struct (tipo de valor) em vez de um class. Usar um struct para o objeto de etiqueta pode evitar alocações de heap ao gravar métricas, uma vez que as etiquetas seriam passadas por valor.

Requisitos do método métrico gerado

Ao definir métodos métricos de fábrica (os métodos parciais decorados com [Counter], [Histogram], etc.), o gerador de fonte impõe alguns requisitos:

  • Cada método deve ser public static partial (para que o gerador de código-fonte forneça a implementação).
  • O tipo de retorno de cada método parcial deve ser exclusivo (para que o gerador possa criar um tipo nomeado exclusivamente para a métrica).
  • O nome do método não deve começar com um sublinhado (_), e os nomes dos parâmetros não devem começar com um sublinhado.
  • O primeiro parâmetro deve ser um Meter (esta é a instância de medidor usada para criar o instrumento subjacente).
  • Os métodos não podem ser genéricos e não podem ter parâmetros genéricos.
  • As propriedades da tag na classe tag só podem ser do tipo string ou enum. Para outros tipos (por exemplo, tipos bool ou numéricos), converta o valor em uma cadeia de caracteres antes de atribuí-lo ao objeto de marca.

A adesão a esses requisitos garante que o gerador de origem possa produzir com sucesso os tipos e métodos métricos.

Ver também