Criando métricas

Este artigo se aplica a: ✔️ .NET Core 6 e versões posteriores ✔️ .NET Framework 4.6.1 e versões posteriores

Os aplicativos .NET podem ser instrumentados usando as APIs System.Diagnostics.Metrics para acompanhar métricas importantes. Algumas métricas estão incluídas em bibliotecas padrão do .NET, mas talvez você queira adicionar novas métricas personalizadas relevantes para seus aplicativos e bibliotecas. Neste tutorial, você adicionará novas métricas e entenderá quais tipos de métricas estão disponíveis.

Observação

O .NET tem algumas APIs de métricas mais antigas, ou seja, EventCounters e System.Diagnostics.PerformanceCounter, que não são abordadas aqui. Para saber mais sobre essas alternativas, confira Comparar APIs de métrica.

Criar uma métrica personalizada

Pré-requisitos: SDK do .NET Core 6 ou uma versão posterior

Crie um novo aplicativo de console que faça referência ao pacote NuGet System.Diagnostics.DiagnosticSource versão 8 ou superior. Os aplicativos com destino ao .NET 8+ incluem essa referência por padrão. Em seguida, atualize o código Program.cs para corresponder:

> dotnet new console
> dotnet add package System.Diagnostics.DiagnosticSource
using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each second that sells 4 hats
            Thread.Sleep(1000);
            s_hatsSold.Add(4);
        }
    }
}

O tipo System.Diagnostics.Metrics.Meter é o ponto de entrada de uma biblioteca para criar um grupo nomeado de instrumentos. Os instrumentos registram as medidas numéricas necessárias para calcular as métricas. Aqui utilizamos CreateCounter para criar um instrumento do Contador denominado "hatco.store.hats_sold". Durante cada simulação de transação, o código chama Add para registrar a medida de chapéus que foram vendidos, quatro nesse caso. O instrumento "hatco.store.hats_sold" define implicitamente algumas métricas que poderão ser computadas a partir dessas medidas, como o número total de chapéus vendidos ou chapéus vendidos/segundo. Em última análise, cabe às ferramentas de coleção de métricas determinar quais métricas devem ser computadas e como fazer para computá-las, mas cada instrumento tem algumas convenções padrão que transmitem a intenção do desenvolvedor. Para instrumentos Counter, a convenção é que as ferramentas de coleção mostram a contagem total e/ou a taxa na qual a contagem está aumentando.

O parâmetro int genérico em Counter<int> e CreateCounter<int>(...) define que esse contador deve ser capaz de armazenar valores até Int32.MaxValue. Você pode usar qualquer dentre byteshort, int, long, float, double ou decimal dependendo do tamanho dos dados necessários para armazenar e se valores fracionários são necessários.

Execute o aplicativo e deixe-o em execução por enquanto. Exibiremos as métricas a seguir.

> dotnet run
Press any key to exit

Práticas recomendadas

  • Para códigos que não foram projetados para serem utilizados em um contêiner de Injeção de Dependência (DI), crie o medidor uma vez e armazene-o em uma variável estática. Para uso em bibliotecas com reconhecimento da DI, as variáveis estáticas são consideradas um antipadrão e o exemplo de DI abaixo mostra uma abordagem mais idiomática. Cada biblioteca ou subcomponente de biblioteca pode (e geralmente deve) criar sua própria Meter. Considere a possibilidade de criar um novo Medidor em vez de reutilizar um existente se você prevê que os desenvolvedores de aplicativos gostariam de poder habilitar e desabilitar facilmente os grupos de métricas separadamente.

  • O nome passado para o construtor Meter deve ser exclusivo para distingui-lo de outros Medidores. Recomendamos as diretrizes de nomenclatura da OpenTelemetry, que utilizam nomes hierárquicos pontilhados. Nomes de assemblies ou namespaces para o código que está sendo instrumentado geralmente são uma boa escolha. Se um assembly adicionar instrumentação para o código em um segundo assembly independente, o nome deverá ser baseado no assembly que define o Medidor, e não no assembly cujo código está sendo instrumentado.

  • O .NET não impõe nenhum esquema de nomenclatura para instrumentos, mas recomendamos seguir as diretrizes de nomenclatura OpenTelemetry, que utilizam nomes hierárquicos pontilhados em letras minúsculas e um sublinhado ('_') como separador entre várias palavras no mesmo elemento. Nem todas as ferramentas métricas preservam o nome do Medidor como parte do nome final da métrica, portanto, é vantajoso fazer com que o nome do instrumento seja globalmente exclusivo por si só.

    Exemplo de nomenclatura de instrumentos:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • As APIs para criar instrumentos e registrar medidas de são thread-safe. Nas bibliotecas do .NET, a maioria dos métodos de instância exige sincronização quando invocados no mesmo objeto de vários threads, mas isso não é necessário nesse caso.

  • As APIs de Instrumento para registrar medidas (Add neste exemplo) normalmente são executadas em <10 ns quando nenhum dado está sendo coletado, ou em dezenas a centenas de nanossegundos quando as medidas estão sendo coletadas por uma biblioteca ou ferramenta de coleção de alto desempenho. Isso permite que essas APIs sejam usadas livremente na maioria dos casos, mas tome cuidado com o código, que é extremamente sensível ao desempenho.

Exibir a nova métrica

Há muitas opções para armazenar e exibir métricas. Este tutorial usa a ferramenta dotnet-counters, que é útil para análise ad hoc. Você também pode ver o tutorial da coleção de métricas para outras alternativas. Se a ferramenta dotnet-counters ainda não estiver instalada, use o SDK para instalá-la:

> dotnet tool update -g dotnet-counters
You can invoke the tool using the following command: dotnet-counters
Tool 'dotnet-counters' (version '7.0.430602') was successfully installed.

Enquanto o aplicativo de exemplo ainda estiver em execução, utilize contadores de dotnet para monitorar o novo contador:

> dotnet-counters monitor -n metric-demo.exe --counters HatCo.Store
Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold (Count / 1 sec)                          4

Como esperado, você pode ver que a loja HatCo está vendendo constantemente quatro chapéus a cada segundo.

Obtenha um Medidor por meio da injeção de dependência

No exemplo previamente apresentado, o Medidor foi obtido construindo-o com new e atribuindo-o a um campo estático. Usar a estática dessa forma não é uma boa abordagem quando se utiliza a injeção de dependência (DI). No código que utiliza o DI, como ASP.NET Core ou aplicativos com Host Genérico, crie o objeto Medidor usando IMeterFactory. A partir do .NET 8, os hosts registrarão automaticamente IMeterFactory no serviço de contêiner ou você poderá registrar manualmente o tipo em qualquer IServiceCollection chamando AddMetrics. O alocador de medidores integra métricas com DI, mantendo os medidores em diferentes coleções de serviços isolados uns dos outros, mesmo que utilizem um nome idêntico. Isso é especialmente útil para testes, de modo que vários testes executados em paralelo observem apenas as medidas produzidas dentro do mesmo caso de teste.

Para obter um Medidor em um tipo projetado para DI, adicione um parâmetro IMeterFactory ao construtor e, em seguida, chame Create. Este exemplo mostra o uso do IMeterFactory em um aplicativo ASP.NET Core.

Defina um tipo para armazenar os instrumentos:

public class HatCoMetrics
{
    private readonly Counter<int> _hatsSold;

    public HatCoMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("HatCo.Store");
        _hatsSold = meter.CreateCounter<int>("hatco.store.hats_sold");
    }

    public void HatsSold(int quantity)
    {
        _hatsSold.Add(quantity);
    }
}

Registre o tipo com o contêiner de DI em Program.cs.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<HatCoMetrics>();

Insira o tipo de métrica e os registre os valores quando necessário. Como o tipo de métrica é registrado no DI, ele pode ser utilizado com controladores MVC, APIs mínimas ou qualquer outro tipo que tenha sido criado pelo DI:

app.MapPost("/complete-sale", ([FromBody] SaleModel model, HatCoMetrics metrics) =>
{
    // ... business logic such as saving the sale to a database ...

    metrics.HatsSold(model.QuantitySold);
});

Práticas recomendadas

  • System.Diagnostics.Metrics.Meter implementa o IDisposable, mas o IMeterFactory gerencia automaticamente o tempo de vida dos objetos Meter que ele cria, descartando-os quando o contêiner de DI é descartado. Não é necessário adicionar um código extra para invocar Dispose() no Meter, e isso não terá nenhum efeito.

Tipos de instrumentos

Até o momento, demonstramos apenas um instrumento Counter<T>, mas existem mais tipos de instrumentos disponíveis. Os instrumentos diferem de duas maneiras:

  • Cálculos de métrica padrão – Ferramentas que coletam e analisam as medidas de instrumento calcularão métricas padrão diferentes, dependendo do instrumento.
  • Armazenamento de dados agregados – As métricas mais úteis precisam que os dados sejam agregados a partir de muitas medidas. Uma opção é que o chamador fornece medidas individuais em horários aleatórios e a ferramenta de coleção gerencia a agregação. Como alternativa, o chamador pode gerenciar as medidas de agregação e fornecê-las sob demanda em um retorno de chamada.

Tipos de instrumentos disponíveis atualmente:

  • Counter (CreateCounter) – Esse instrumento controla um valor que aumenta ao longo do tempo e o chamador relata os incrementos usando Add. A maioria das ferramentas calculará o total e a taxa de alteração no total. Para ferramentas que mostram apenas uma coisa, a taxa de alteração é recomendada. Por exemplo, suponha que o chamador invoque Add() uma vez a cada segundo com valores sucessivos, 1, 2, 4, 5, 4, 3. Se a ferramenta de coleção for atualizada a cada três segundos, o total após três segundos será 1+2+4=7 e o total após seis segundos será de 1+2+4+5+4+3=19. A taxa de alteração é (total_atual - total_anterior), portanto, em três segundos a ferramenta relata 7-0=7 e, após seis segundos, ela relata 19-7=12.

  • UpDownCounter (CreateUpDownCounter) – Esse instrumento rastreia um valor que pode aumentar ou diminuir ao longo do tempo. O chamador relata os incrementos e decrementos usando Add. Por exemplo, suponha que o chamador invoque Add() uma vez a cada segundo com valores sucessivos 1, 5, -2, 3, -1, -3. Se a ferramenta de coleção for atualizada a cada três segundos, o total após três segundos será 1+5-2=4 e o total após seis segundos será de 1+5-2+3-1-3=3.

  • ObservableCounter (CreateObservableCounter) – Esse instrumento é semelhante ao Counter, exceto que o chamador agora é responsável por manter o total agregado. O chamador fornece um representante de retorno de chamada quando o ObservableCounter é criado, e o retorno de chamada é invocado sempre que as ferramentas precisam observar o total atual. Por exemplo, se uma ferramenta de coleção for atualizada a cada três segundos, a função de retorno de chamada também será invocada a cada três segundos. A maioria das ferramentas terá o total e a taxa de alteração no total disponível. Se apenas um puder ser mostrado, a taxa de alteração será recomendada. Se o retorno de chamada retornar 0 na chamada inicial, 7 quando for chamado novamente após três segundos e 19 quando chamado após seis segundos, a ferramenta relatará esses valores inalterados como os totais. Para a taxa de alteração, a ferramenta mostrará 7-0=7 após três segundos e 19-7=12 após seis segundos.

  • ObservableUpDownCounter (CreateObservableUpDownCounter) – Esse instrumento é semelhante ao UpDownCounter, exceto que o chamador agora é responsável por manter o total agregado. O chamador fornece um representante de retorno de chamada quando o ObservableUpDownCounter é criado, e o retorno de chamada é invocado sempre que as ferramentas precisam observar o total atual. Por exemplo, se uma ferramenta de coleção for atualizada a cada três segundos, a função de retorno de chamada também será invocada a cada três segundos. Qualquer valor retornado pelo retorno de chamada será mostrado na ferramenta de coleção inalterada como o total.

  • ObservableGauge (CreateObservableGauge) – Esse instrumento permite que o chamador forneça um retorno de chamada em que o valor medido é passado diretamente como a métrica. Sempre que a ferramenta de coleção é atualizada, o retorno de chamada é invocado e qualquer valor retornado pelo retorno de chamada é exibido na ferramenta.

  • Histogram (CreateHistogram) – Esse instrumento rastreia a distribuição de medidas. Não há uma única maneira canônica de descrever um conjunto de medidas, mas recomendamos que as ferramentas usem histogramas ou percentis computados. Por exemplo, vamos suporte que o chamador invocou Record para registrar essas medidas durante o intervalo de atualização da ferramenta de coleção: 1,5,2,3,10,9,7,4,6,8. Uma ferramenta de coleção pode relatar que os percentis 50, 90 e 95 dessas medidas são 5, 9 e 9, respectivamente.

Melhores práticas ao selecionar um tipo de instrumento

  • Para contar coisas ou qualquer outro valor que só aumenta ao longo do tempo, use Counter ou ObservableCounter. Escolha entre Counter e ObservableCounter, dependendo do que é mais fácil de adicionar ao código existente: uma chamada de API para cada operação de incremento ou um retorno de chamada que lerá o total atual de uma variável mantida pelo código. Em caminhos de código de acesso extremamente frequente em que o desempenho é importante, e usar Add criaria mais de um milhão de chamadas por segundo por thread, o uso do ObservableCounter pode oferecer mais oportunidade de otimização.

  • Para questões de tempo, Histogram costuma ser preferido. Muitas vezes, é útil entender a parte final dessas distribuições (90º, 95º, 99º percentil) em vez de médias ou totais.

  • Outros casos comuns, como taxas de ocorrência no cache ou tamanhos de caches, filas e arquivos, geralmente são adequados para UpDownCounter ou ObservableUpDownCounter. Escolha entre eles dependendo do que é mais fácil de adicionar ao código existente: uma chamada à API para cada operação de incremento e decremento ou um retorno de chamada que lerá o valor atual de uma variável mantida pelo código.

Observação

Se você estiver usando uma versão mais antiga do .NET ou um pacote NuGet DiagnosticSource que não dá suporte a UpDownCounter e ObservableUpDownCounter (antes da versão 7), ObservableGauge geralmente será um bom substituto.

Exemplo de diferentes tipos de instrumento

Pare o processo de exemplo iniciado anteriormente e substitua o código de exemplo em Program.cs por:

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");
    static Histogram<double> s_orderProcessingTime = s_meter.CreateHistogram<double>("hatco.store.order_processing_time");
    static int s_coatsSold;
    static int s_ordersPending;

    static Random s_rand = new Random();

    static void Main(string[] args)
    {
        s_meter.CreateObservableCounter<int>("hatco.store.coats_sold", () => s_coatsSold);
        s_meter.CreateObservableGauge<int>("hatco.store.orders_pending", () => s_ordersPending);

        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has one transaction each 100ms that each sell 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);

            // Pretend we also sold 3 coats. For an ObservableCounter we track the value in our variable and report it
            // on demand in the callback
            s_coatsSold += 3;

            // Pretend we have some queue of orders that varies over time. The callback for the orders_pending gauge will report
            // this value on-demand.
            s_ordersPending = s_rand.Next(0, 20);

            // Last we pretend that we measured how long it took to do the transaction (for example we could time it with Stopwatch)
            s_orderProcessingTime.Record(s_rand.Next(0.005, 0.015));
        }
    }
}

Execute o novo processo e use dotnet-counters como antes em um segundo shell para exibir as métricas:

> dotnet-counters monitor -n metric-demo.exe --counters HatCo.Store
Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.coats_sold (Count / 1 sec)                                27
    hatco.store.hats_sold (Count / 1 sec)                                 36
    hatco.store.order_processing_time
        Percentile=50                                                      0.012
        Percentile=95                                                      0.014
        Percentile=99                                                      0.014
    hatco.store.orders_pending                                             5

Esse exemplo usa alguns números gerados aleatoriamente para que seus valores variem um pouco. Você pode ver que hatco.store.hats_sold (o Counter) e hatco.store.coats_sold (o ObservableCounter) aparecem como uma taxa. O ObservableGauge, hatco.store.orders_pending, aparece como um valor absoluto. Dotnet-counters renderizam os instrumentos de Histogram como três estatísticas de percentil (50º, 95º e 99º), mas outras ferramentas podem resumir a distribuição de forma diferente ou oferecer mais opções de configuração.

Práticas recomendadas

  • Os histogramas tendem a armazenar muito mais dados na memória do que outros tipos de métrica. No entanto, o uso exato da memória é determinado pela ferramenta de coleção que está sendo usada. Se você estiver definindo um grande número (>100) de métricas de Histogram, talvez seja necessário fornecer orientação aos usuários para não habilitá-los todos ao mesmo tempo ou configurar suas ferramentas para salvar a memória reduzindo a precisão. Algumas ferramentas de coleção podem ter limites rígidos no número de Histogramas simultâneos que eles monitorarão para evitar o uso excessivo de memória.

  • Os retornos de chamada para todos os instrumentos observáveis são invocados em sequência, portanto, qualquer retorno de chamada que leva muito tempo pode atrasar ou impedir que todas as métricas sejam coletadas. Favoreça a leitura rápida de um valor armazenado em cache, não retornando nenhuma medida ou lançando uma exceção sobre a execução de qualquer operação de bloqueio ou execução potencialmente longa.

  • Os retornos de chamada ObservableCounter, ObservableUpDownCounter e ObservableGauge ocorrem em um thread que geralmente não é sincronizado com o código que atualiza os valores. É sua responsabilidade sincronizar o acesso à memória ou aceitar os valores inconsistentes que podem resultar do uso de acesso não sincronizado. Abordagens comuns para sincronizar o acesso são usar um bloqueio ou chamar Volatile.Read e Volatile.Write.

  • As funções CreateObservableGauge e CreateObservableCounter retornam um objeto de instrumento, mas na maioria dos casos você não precisa salvá-lo em uma variável porque nenhuma interação adicional com o objeto é necessária. A atribuição a uma variável estática, como fizemos para os outros instrumentos, é legal, mas propensa a erros, porque a inicialização estática de C# é lenta e a variável geralmente nunca é referenciada. Aqui está um exemplo do problema:

    using System;
    using System.Diagnostics.Metrics;
    
    class Program
    {
        // BEWARE! Static initializers only run when code in a running method refers to a static variable.
        // These statics will never be initialized because none of them were referenced in Main().
        //
        static Meter s_meter = new Meter("HatCo.Store");
        static ObservableCounter<int> s_coatsSold = s_meter.CreateObservableCounter<int>("hatco.store.coats_sold", () => s_rand.Next(1,10));
        static Random s_rand = new Random();
    
        static void Main(string[] args)
        {
            Console.ReadLine();
        }
    }
    

Descrições e unidades

Os instrumentos podem especificar descrições e unidades opcionais. Esses valores são opacos para todos os cálculos de métrica, mas podem ser mostrados na interface do usuário da ferramenta de coleção para ajudar os engenheiros a entender como interpretar os dados. Pare o processo de exemplo iniciado anteriormente e substitua o código de exemplo em Program.cs por:

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>(name: "hatco.store.hats_sold",
                                                                unit: "{hats}",
                                                                description: "The number of hats sold in our store");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each 100ms that sells 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);
        }
    }
}

Execute o novo processo e use dotnet-counters como antes em um segundo shell para exibir as métricas:

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

[HatCo.Store]
    hatco.store.hats_sold ({hats} / 1 sec)                                40

No momento, os dotnet-counters não usam o texto de descrição na interface do usuário, mas mostram a unidade quando ela é fornecida. Nesse caso, você verá que "{hats}" substituiu o termo genérico "Contar", visível nas descrições previamente.

Práticas recomendadas

  • As APIs do .NET permitem que qualquer cadeia de caracteres seja utilizada como unidade, mas recomendamos o uso de UCUM, um padrão internacional para nomes de unidades. As chaves em torno de "{hats}" fazem parte do padrão UCUM, indicando que se trata de uma anotação descritiva ao invés de um nome de unidade com um significado padronizado, como segundos ou bytes.

  • A unidade especificada no construtor deve descrever as unidades apropriadas para uma medida individual. Às vezes, isso será diferente das unidades na métrica final. Neste exemplo, cada medida é um número de chapéus, de modo que "{hats}" é a unidade apropriada a ser passada no construtor. A ferramenta de coleção calculou uma taxa e derivou por conta própria que a unidade apropriada para a métrica calculada é {hats}/s.

  • Ao registrar medidas de tempo, prefira unidades de segundos registradas como ponto flutuante ou valor duplo.

Métricas multidimensionais

As medidas também podem ser associadas a pares chave-valor chamados de marcas, que permitem que os dados sejam categorizados para análise. Por exemplo, a HatCo pode querer gravar não apenas o número de chapéus que foram vendidos, mas também qual o tamanho e a cor deles. Ao analisar os dados posteriormente, os engenheiros da HatCo poderão dividir os totais por tamanho, cor ou qualquer combinação de ambos.

Marcas de Counter e Histogram podem ser especificadas em sobrecargas de Add e Record que recebem um ou mais argumentos KeyValuePair. Por exemplo:

s_hatsSold.Add(2,
               new KeyValuePair<string, object>("product.color", "red"),
               new KeyValuePair<string, object>("product.size", 12));

Substitua o código de Program.cs e execute novamente o aplicativo e os dotnet-counters como antes:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction, every 100ms, that sells two size 12 red hats, and one size 19 blue hat.
            Thread.Sleep(100);
            s_hatsSold.Add(2,
                           new KeyValuePair<string,object>("product.color", "red"),
                           new KeyValuePair<string,object>("product.size", 12));
            s_hatsSold.Add(1,
                           new KeyValuePair<string,object>("product.color", "blue"),
                           new KeyValuePair<string,object>("product.size", 19));
        }
    }
}

Os dotnet-counters agora mostram uma categorização básica:

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

[HatCo.Store]
    hatco.store.hats_sold (Count / 1 sec)
        product.color=blue,product.size=19                                 9
        product.color=red,product.size=12                                 18

Para ObservableCounter e ObservableGauge, as medidas marcadas podem ser fornecidas no retorno de chamada passado para o construtor:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");

    static void Main(string[] args)
    {
        s_meter.CreateObservableGauge<int>("hatco.store.orders_pending", GetOrdersPending);
        Console.WriteLine("Press any key to exit");
        Console.ReadLine();
    }

    static IEnumerable<Measurement<int>> GetOrdersPending()
    {
        return new Measurement<int>[]
        {
            // pretend these measurements were read from a real queue somewhere
            new Measurement<int>(6, new KeyValuePair<string,object>("customer.country", "Italy")),
            new Measurement<int>(3, new KeyValuePair<string,object>("customer.country", "Spain")),
            new Measurement<int>(1, new KeyValuePair<string,object>("customer.country", "Mexico")),
        };
    }
}

Quando executado com dotnet-counters como antes, o resultado é:

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

[HatCo.Store]
    hatco.store.orders_pending
        customer.country=Italy                                             6
        customer.country=Mexico                                            1
        customer.country=Spain                                             3

Práticas recomendadas

  • Embora a API permita que qualquer objeto seja usado como o valor da marca, tipos numéricos e cadeias de caracteres são antecipados pelas ferramentas de coleção. Outros tipos podem ou não ter suporte de uma determinada ferramenta de coleção.

  • Recomendamos que os nomes das marcas sigam as diretrizes de nomenclatura da OpenTelemetry, que utilizam nomes hierárquicos pontilhados em minúsculas com caracteres '_' para separar várias palavras no mesmo elemento. Se as marcas forem reutilizadas em diferentes métricas ou outros registros de telemetria, elas deverão ter o mesmo significado e conjunto de valores legais em todos os lugares em que forem utilizadas.

    Exemplo de nomes de marcas:

    • customer.country
    • store.payment_method
    • store.purchase_result
  • Cuidado com combinações muito grandes ou não associadas de valores de marca sendo registrados na prática. Embora a implementação da API do .NET possa lidar com isso, as ferramentas de coleção provavelmente alocarão armazenamento para dados de métrica associados a cada combinação de marcas, e isso pode se tornar muito grande. Por exemplo, tudo bem se a HatCo tiver 10 cores de chapéu diferentes e 25 tamanhos de chapéu para até 10*25=250 totais de vendas a serem rastreadas. No entanto, se a HatCo adicionar uma terceira marca, CustomerID, para a venda e venderem para 100 milhões de clientes em todo o mundo, provavelmente haverá bilhões de combinações de marcas diferentes sendo registradas. A maioria das ferramentas de coleção de métricas descartará dados para permanecer dentro dos limites técnicos ou pode haver grandes custos monetários para cobrir o armazenamento e o processamento de dados. A implementação de cada ferramenta de coleção determinará seus limites, mas, provavelmente, uma quantidade inferior a 1000 combinações para um instrumento é segura. Qualquer coisa acima de 1.000 combinações exigirá que a ferramenta de coleção aplique a filtragem ou seja projetada para operar em alta escala. As implementações de histograma costumam usar muito mais memória do que outras métricas, portanto, os limites seguros podem ser 10-100 vezes menores. Se você prevê um grande número de combinações de marcas exclusivas, então os logs, os bancos de dados transacionais ou os sistemas de processamento de Big Data poderão ser as soluções mais adequadas para operar na escala necessária.

  • Para instrumentos que terão um número muito grande de combinações de marcas, prefira usar um tipo de armazenamento menor para ajudar a reduzir a sobrecarga de memória. Por exemplo, armazenar o short para um Counter<short> ocupa apenas 2 bytes por combinação de marca, enquanto um double para Counter<double> ocupa 8 bytes por combinação de marca.

  • As ferramentas de coleção são incentivadas a otimizar o código que especifica o mesmo conjunto de nomes de marcas na mesma ordem para cada chamada registrar medidas no mesmo instrumento. Para código de alto desempenho que precisa chamar Add e Record frequentemente, prefira usar a mesma sequência de nomes de marca para cada chamada.

  • A API do .NET é otimizada para ser livre de alocação para chamadas Add e Record com três ou menos marcas especificadas individualmente. Para evitar alocações com um número maior de marcas, use TagList. Em geral, a sobrecarga de desempenho dessas chamadas aumenta à medida que mais marcas são usadas.

Observação

OpenTelemetry refere-se a marcas como "atributos". Esses são dois nomes diferentes para a mesma funcionalidade.

Testar as métricas personalizadas

É possível testar qualquer métrica personalizada que você adicionar utilizando MetricCollector<T>. Esse tipo facilita o registro das medições de instrumentos específicos e a declaração de que os valores estão corretos.

Testar com a injeção de dependência

O código a seguir mostra um exemplo de caso de teste para componentes de código que utilizam a injeção de dependência e o IMeterFactory.

public class MetricTests
{
    [Fact]
    public void SaleIncrementsHatsSoldCounter()
    {
        // Arrange
        var services = CreateServiceProvider();
        var metrics = services.GetRequiredService<HatCoMetrics>();
        var meterFactory = services.GetRequiredService<IMeterFactory>();
        var collector = new MetricCollector<int>(meterFactory, "HatCo.Store", "hatco.store.hats_sold");

        // Act
        metrics.HatsSold(15);

        // Assert
        var measurements = collector.GetMeasurementSnapshot();
        Assert.Equal(1, measurements.Count);
        Assert.Equal(15, measurements[0].Value);
    }

    // Setup a new service provider. This example creates the collection explicitly but you might leverage
    // a host or some other application setup code to do this as well.
    private static IServiceProvider CreateServiceProvider()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddMetrics();
        serviceCollection.AddSingleton<HatCoMetrics>();
        return serviceCollection.BuildServiceProvider();
    }
}

Cada objeto MetricCollector registra todas as medições de um instrumento. Se você precisar verificar as medidas a partir de vários instrumentos, crie um MetricCollector para cada um deles.

Testar sem a injeção de dependência

Também é possível testar o código que utiliza um objeto global compartilhado Medidor em um campo estático, mas verifique se esses testes estão configurados para não serem executados em paralelo. Como o objeto Medidor está sendo compartilhado, o MetricCollector em um teste observará as medições criadas a partir de qualquer outro teste executado em paralelo.

class HatCoMetricsWithGlobalMeter
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    public void HatsSold(int quantity)
    {
        s_hatsSold.Add(quantity);
    }
}

public class MetricTests
{
    [Fact]
    public void SaleIncrementsHatsSoldCounter()
    {
        // Arrange
        var metrics = new HatCoMetricsWithGlobalMeter();
        // Be careful specifying scope=null. This binds the collector to a global Meter and tests
        // that use global state should not be configured to run in parallel.
        var collector = new MetricCollector<int>(null, "HatCo.Store", "hatco.store.hats_sold");

        // Act
        metrics.HatsSold(15);

        // Assert
        var measurements = collector.GetMeasurementSnapshot();
        Assert.Equal(1, measurements.Count);
        Assert.Equal(15, measurements[0].Value);
    }
}