Partilhar via


Criação de métricas

Este artigo aplica-se 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 controlar métricas importantes. Algumas métricas estão incluídas em bibliotecas .NET padrão, mas talvez você queira adicionar novas métricas personalizadas que sejam 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, consulte Comparar APIs de métricas.

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 destinados ao .NET 8+ incluem essa referência por padrão. Em seguida, atualize o código no 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 para uma biblioteca criar um grupo nomeado de instrumentos. Os instrumentos registram as medidas numéricas necessárias para calcular as métricas. Aqui nós usamos CreateCounter para criar um instrumento de contador chamado "hatco.store.hats_sold". Durante cada transação simulada, o código chama Add para registrar a medição dos chapéus que foram vendidos, 4 neste caso. O instrumento "hatco.store.hats_sold" define implicitamente algumas métricas que podem ser calculadas a partir dessas medições, como o número total de chapéus vendidos ou chapéus vendidos/seg. Em última análise, cabe às ferramentas de coleta de métricas determinar quais métricas computar e como executar esses cálculos, mas cada instrumento tem algumas convenções padrão que transmitem a intenção do desenvolvedor. Para instrumentos de contador, a convenção é que as ferramentas de coleta mostrem a contagem total e/ou a taxa na qual a contagem está aumentando.

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

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

> dotnet run
Press any key to exit

Melhores práticas

  • Para código que não foi projetado para uso 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 de 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 muitas vezes deve) criar seu próprio Meter. Considere criar um novo medidor em vez de reutilizar um existente se você antecipar 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 único para distingui-lo de outros Medidores. Recomendamos diretrizes de nomenclatura do OpenTelemetry, que usam nomes hierárquicos pontilhados. Os nomes de assembly ou os nomes de namespace para código a ser instrumentado geralmente são uma boa escolha. Se um assembly adiciona instrumentação para o código de um segundo assembly independente, o nome deve ser baseado no assembly que define o Medidor, 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 do OpenTelemetry (e), que utilizam nomes hierárquicos em minúsculas separados por pontos e um sublinhado ('_') como separador entre múltiplas palavras no mesmo elemento. Nem todas as ferramentas métricas preservam o nome do medidor como parte do nome da métrica final, por isso é benéfico tornar o nome do instrumento globalmente único por si só.

    Exemplos de nomes de instrumentos:

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

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

Ver a nova métrica

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

> 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 a aplicação de exemplo ainda estiver em execução, use o dotnet-counters para monitorizar 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á constantemente vendendo 4 chapéus por segundo.

Obter um medidor através de injeção de dependência

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

Para obter um Meter num tipo projetado para DI, adicione um parâmetro IMeterFactory ao construtor e, em seguida, chame Create. Este exemplo mostra o uso de 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 DI em Program.cs.

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

Injete o tipo de métrica e registre valores onde necessário. Como o tipo de métrica é registrado em DI, ele pode ser usado com controladores MVC, APIs mínimas ou qualquer outro tipo criado por DI:

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

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

Melhores práticas

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

Tipos de instrumentos

Até agora só demonstramos um instrumento Counter<T>, mas há mais tipos de instrumentos disponíveis. Os instrumentos diferem de duas formas:

  • Cálculos métricos padrão - Ferramentas que coletam e analisam as medições do instrumento calcularão diferentes métricas padrão dependendo do instrumento.
  • Armazenamento de dados agregados - As métricas mais úteis precisam que os dados sejam agregados a partir de muitas medições. Uma opção é o chamador fornecer medições individuais em momentos arbitrários e a ferramenta de coleta gerencia a agregação. Como alternativa, o chamador pode gerenciar as medições agregadas e fornecê-las sob demanda em um retorno de chamada.

Tipos de instrumentos atualmente disponíveis:

  • Contador (CreateCounter) - Este instrumento rastreia 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 variação do total. Para ferramentas que mostram apenas uma coisa, a taxa de mudança é 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 coleta for atualizada a cada três segundos, o total após três segundos é 1+2+4=7 e o total após seis segundos é 1+2+4+5+4+3=19. A taxa de mudança é a (current_total - previous_total), portanto, em três segundos a ferramenta relata 7-0=7, e após seis segundos, relata 19-7=12.

  • UpDownCounter (CreateUpDownCounter) - Este instrumento rastreia um valor que pode aumentar ou diminuir ao longo do tempo. O chamador relata os incrementos e decréscimos 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 coleta for atualizada a cada três segundos, o total após três segundos é 1+5-2=4 e o total após seis segundos é 1+5-2+3-1-3=3.

  • ObservableCounter (CreateObservableCounter) - Este instrumento é semelhante ao Contador, exceto que o chamador agora é responsável por manter o total agregado. O chamador fornece um delegado 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 recolha for atualizada a cada três segundos, a função de callback também será invocada a cada três segundos. A maioria das ferramentas terá tanto o total como a taxa de variação desse total disponíveis. Se apenas um puder ser mostrado, recomenda-se a taxa de variação. 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 mudança, a ferramenta mostrará 7-0=7 após três segundos e 19-7=12 após seis segundos.

  • ObservableUpDownCounter (CreateObservableUpDownCounter) - Este instrumento é semelhante ao UpDownCounter, exceto que o chamador agora é responsável por manter o total agregado. O chamador fornece um delegado de retorno de chamada quando o ObservableUpDownCounter é criado, e o retorno de chamada é acionado sempre que as ferramentas precisam observar o total atual. Por exemplo, se uma ferramenta de coleta 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 pela função de retorno será mostrado inalterado na ferramenta de recolha como o total.

  • Gauge (CreateGauge) - Este instrumento permite que o chamador defina o valor atual da métrica usando o método Record. O valor pode ser atualizado a qualquer momento invocando o método novamente e uma ferramenta de coleta de métricas exibirá qualquer valor que tenha sido definido mais recentemente.

  • ObservableGauge (CreateObservableGauge) - Este instrumento permite que o chamador forneça um retorno de chamada onde o valor observado é passado diretamente como a métrica. Cada vez que a ferramenta de coleta é atualizada, a função de retorno é chamada, e qualquer valor retornado por ela é exibido na ferramenta.

  • Histograma (CreateHistogram) - Este instrumento acompanha a distribuição das medidas. Não há uma única maneira canônica de descrever um conjunto de medições, mas recomenda-se que as ferramentas usem histogramas ou percentis computados. Por exemplo, suponha que o chamador invocou Record para registrar essas medições durante o intervalo de atualização da ferramenta de coleta: 1,5,2,3,10,9,7,4,6,8. Uma ferramenta de coleta pode relatar que os percentis 50, 90 e 95 dessas medições são 5, 9 e 9, respectivamente.

    Observação

    Para obter detalhes sobre como definir os limites de bucket recomendados ao criar um instrumento de histograma, consulte: Orientações para personalizar instrumentos de histograma.

Práticas recomendadas ao selecionar um tipo de instrumento

  • Para contar coisas, ou qualquer outro valor que aumente apenas com o tempo, use Counter ou ObservableCounter. Escolha entre Counter e ObservableCounter dependendo do que for mais fácil de adicionar ao código existente: fazer uma chamada de API para cada operação de incremento ou um retorno de chamada que lê o total atual de uma variável mantida pelo código. Em caminhos de código extremamente quentes, onde o desempenho é importante e o uso de Add criaria mais de um milhão de chamadas por segundo por thread, usar o ObservableCounter pode oferecer mais oportunidades de otimização.

  • Para medir o tempo, o histograma é geralmente preferido. Muitas vezes é útil entender a cauda dessas distribuições (percentil 90, 95, 99) em vez de médias ou totais.

  • Outros casos comuns, como taxas de acerto de 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 de API para cada operação de incremento e decréscimo ou um retorno de chamada que lerá o valor atual de uma variável que o código mantém.

Observação

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

Exemplo de diferentes tipos de instrumentos

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(5, 15)/1000.0);
        }
    }
}

Execute o novo processo e utilize o dotnet-counters como antes num segundo shell para visualizar 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

Name                                                  Current Value
[HatCo.Store]
    hatco.store.coats_sold (Count)                        8,181    
    hatco.store.hats_sold (Count)                           548    
    hatco.store.order_processing_time
        Percentile
        50                                                    0.012    
        95                                                    0.013   
        99                                                    0.013
    hatco.store.orders_pending                                9    

Este exemplo usa alguns números gerados aleatoriamente para que seus valores variem um pouco. Dotnet-counters apresenta os instrumentos de histograma como três estatísticas de percentis (50.º, 95.º e 99.º), mas outras ferramentas podem resumir a distribuição de maneira diferente ou oferecer mais opções de configuração.

Melhores práticas

  • Os histogramas tendem a armazenar muito mais dados na memória do que outros tipos de métricas. No entanto, o uso exato da memória é determinado pela ferramenta de coleta que está sendo usada. Se você estiver definindo um grande número (>100) de métricas de histograma, talvez seja necessário fornecer orientação aos usuários para não habilitá-los todos ao mesmo tempo ou configurar suas ferramentas para economizar memória reduzindo a precisão. Algumas ferramentas de coleta podem ter limites rígidos no número de histogramas simultâneos que 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 demore 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, retornar sem medições ou lançar uma exceção em vez de executar qualquer operação que potencialmente seja de longa duração ou bloqueio.

  • Os callbacks ObservableCounter, ObservableUpDownCounter e ObservableGauge ocorrem em uma thread que normalmente não está sincronizada 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 do acesso não sincronizado. As abordagens comuns para sincronizar o acesso são usar um bloqueio ou chamadas a 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. Atribuí-lo a uma variável estática como fizemos para os outros instrumentos é legal, mas propenso a erros, porque a inicialização estática em C# é preguiçosa 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 métricos, mas podem ser mostrados na interface do usuário da ferramenta de coleta 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 num segundo terminal para ver as métricas.

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

Name                                                       Current Value
[HatCo.Store]
    hatco.store.hats_sold ({hats})                                40

dotnet-counters atualmente não usa o texto de descrição na interface do usuário, mas mostra a unidade quando ela é fornecida. Nesse caso, vê que "{hats}" substituiu o termo genérico "Count" que é visível nas descrições anteriores.

Melhores práticas

  • As APIs do .NET permitem que qualquer cadeia de caracteres seja usada como unidade, mas recomendamos o uso do UCUM, um padrão internacional para nomes de unidades. As chavetas em torno de "{hats}" fazem parte do padrão UCUM, indicando que é uma anotação descritiva em vez 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 medição individual. Isso às vezes será diferente das unidades na métrica final relatada. Neste exemplo, cada medida é uma quantidade de chapéus, portanto, "{hats}" é a unidade apropriada para utilizar no construtor. A ferramenta de coleta poderia ter calculado a taxa de mudança e derivado por conta própria que a unidade apropriada para a métrica de taxa calculada é {hats}/seg.

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

Métricas multidimensionais

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

As tags contador e histograma podem ser especificadas em sobrecargas do Add e Record que aceitam um ou vários 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 do Program.cs e volte a executar a aplicação e o 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));
        }
    }
}

Dotnet-counters agora mostra uma categorização básica.

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

Name                                                  Current Value
[HatCo.Store]
    hatco.store.hats_sold (Count)
        product.color product.size
        blue          19                                     73
        red           12                                    146    

Para ObservableCounter e ObservableGauge, medições 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 o dotnet-counters como anteriormente, o resultado é:

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

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

Melhores práticas

  • Embora a API permita que qualquer objeto seja usado como o valor da tag, tipos numéricos e cadeias de caracteres são antecipados pelas ferramentas de coleta. Outros tipos podem ou não ser suportados por uma determinada ferramenta de recolha.

  • Recomendamos que os nomes de etiquetas sigam as diretrizes de nomenclatura do OpenTelemetry, que usam nomes hierárquicos com pontuação em minúsculas e caracteres '_' para separar múltiplas palavras no mesmo elemento. Se os nomes das tags forem reutilizados em métricas diferentes ou em outros registros de telemetria, eles devem ter o mesmo significado e conjunto de valores legais em todos os lugares em que forem usados.

    Exemplos de nomes de tags:

    • customer.country
    • store.payment_method
    • store.purchase_result
  • Tenha cuidado para não ter combinações muito grandes ou sem restrições de valores de tags a serem registradas na prática. Embora a implementação da API .NET possa lidar com isso, as ferramentas de coleta provavelmente alocarão armazenamento para dados métricos associados a cada combinação de tags 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 para rastrear. No entanto, se a HatCo adicionou uma terceira tag que é um CustomerID para a venda e eles vendem para 100 milhões de clientes em todo o mundo, agora é provável que haja bilhões de combinações de tags diferentes sendo gravadas. A maioria das ferramentas de coleta de métricas descarta os dados para ficar 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 recolha determinará os seus limites, mas provavelmente menos de 1000 combinações para um instrumento é segura. Qualquer combinação acima de 1000 exigirá que a ferramenta de coleta aplique filtragem ou seja projetada para operar em alta escala. As implementações de histograma tendem a usar muito mais memória do que outras métricas, portanto, os limites de segurança podem ser de 10 a 100 vezes menores. Se você prevê um grande número de combinações exclusivas de tags, logs, bancos de dados transacionais ou sistemas de processamento de big data podem ser soluções mais apropriadas para operar na escala necessária.

  • Para instrumentos que terão um número muito grande de combinações de tags, 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 tag, enquanto um double para Counter<double> ocupa 8 bytes por combinação de tag.

  • As ferramentas de coleta são incentivadas a otimizar o código que especifica o mesmo conjunto de nomes de tags na mesma ordem para cada chamada para registrar medições no mesmo instrumento. Para código de alto desempenho que precisa chamar Add e Record com frequência, prefira usar a mesma sequência de nomes de tags para cada chamada.

  • A .NET API foi otimizada para ser sem alocação para chamadas Add e Record com três ou menos tags especificadas individualmente. Para evitar alocações com números maiores de etiquetas, use TagList. Em geral, a sobrecarga de desempenho dessas chamadas aumenta à medida que mais tags são usadas.

Observação

OpenTelemetry refere-se a tags como 'atributos'. Estes são dois nomes diferentes para a mesma funcionalidade.

Usando o Advice para personalizar instrumentos de histograma

Ao utilizar Histogramas, é da responsabilidade da ferramenta ou biblioteca que recolhe os dados decidir qual a melhor forma de representar a distribuição dos valores que foram registados. Uma estratégia comum (e o modo padrão ao usar o OpenTelemetry) é dividir o intervalo de valores possíveis em subintervalos chamados buckets e relatar quantos valores registrados estavam em cada bucket. Por exemplo, uma ferramenta pode dividir os números em três grupos, aqueles menores que 1, aqueles entre 1-10 e aqueles maiores que 10. Se seu aplicativo registrasse os valores 0,5, 6, 0,1, 12, haveria dois pontos de dados no primeiro bucket, um no segundo e um no 3º.

A ferramenta ou biblioteca que coleta os dados do Histograma é responsável por definir os buckets que usará. A configuração de bucket padrão ao usar OpenTelemetry é: [ 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 ].

Os valores padrão podem não levar à melhor granularidade para cada histograma. Por exemplo, todas as durações de requisições inferiores a um segundo seriam classificadas no balde 0.

A ferramenta ou biblioteca que coleta os dados do histograma pode oferecer mecanismo(s) para permitir que os usuários personalizem a configuração do bucket. Por exemplo, OpenTelemetry define uma API de exibição de . No entanto, isso requer ação do usuário final e torna responsabilidade do usuário entender a distribuição de dados bem o suficiente para escolher buckets corretos.

Para melhorar a experiência, a versão 9.0.0 do pacote System.Diagnostics.DiagnosticSource introduziu a API (InstrumentAdvice<T>).

A API InstrumentAdvice pode ser usada por autores de instrumentação para especificar o conjunto de limites de bucket padrão recomendados para um determinado histograma. A ferramenta ou biblioteca que coleta os dados do Histograma pode optar por usar esses valores ao configurar a agregação, levando a uma experiência de integração mais perfeita para os usuários. Isso é suportado no SDK do OpenTelemetry .NET a partir da versão 1.10.0.

Importante

Em geral, mais buckets levarão a dados mais precisos para um determinado histograma, mas cada bucket requer memória para armazenar os detalhes agregados e há custo de CPU para encontrar o bucket correto ao processar uma medição. É importante entender as compensações entre precisão e consumo de CPU/memória ao escolher o número de buckets a recomendar por meio da API InstrumentAdvice.

O código a seguir mostra um exemplo usando a API InstrumentAdvice para definir buckets padrão recomendados.

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

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Histogram<double> s_orderProcessingTime = s_meter.CreateHistogram<double>(
        name: "hatco.store.order_processing_time",
        unit: "s",
        description: "Order processing duration",
        advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = [0.01, 0.05, 0.1, 0.5, 1, 5] });

    static Random s_rand = new Random();

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while (!Console.KeyAvailable)
        {
            // Pretend our store has one transaction each 100ms
            Thread.Sleep(100);

            // 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(5, 15) / 1000.0);
        }
    }
}

Informações adicionais

Para obter mais detalhes sobre histogramas de bucket explícito no OpenTelemetry consulte:

Testar métricas personalizadas

É possível testar qualquer métrica personalizada que você adicionar usando MetricCollector<T>. Este tipo facilita o registo das medições a partir de instrumentos específicos e a afirmação dos valores corretos.

Teste com injeção de dependência

O código a seguir mostra um exemplo de caso de teste para componentes de código que usam injeção de dependência e 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 medições de vários instrumentos, crie um MetricCollector para cada um.

Teste sem injeção de dependência

Também é possível testar o código que usa um objeto Meter global compartilhado em um campo estático, mas certifique-se de que esses testes estejam configurados para não serem executados em paralelo. Como o objeto Meter está sendo compartilhado, o MetricCollector em um teste observará as medidas criadas a partir de quaisquer outros testes executados 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);
    }
}