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 System.Diagnostics.Metrics APIs 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.

Nota

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 para corresponder Program.cs :

> 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 System.Diagnostics.Metrics.Meter tipo é 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 costumávamos CreateCounter 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 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 um dos , , , , floatshort, longintdoubleou decimal dependendo do tamanho dos bytedados 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 o 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 Meter construtor deve ser único para distingui-lo de outros medidores. Recomendamos as diretrizes de nomenclatura do OpenTelemetry que usam nomes hierárquicos pontilhados. Nomes de assembly ou nomes de namespace para código que está sendo instrumentado geralmente são uma boa escolha. Se um assembly adiciona instrumentação para código em 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 que usam nomes hierárquicos pontilhados minúsculos 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 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 do 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álise 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 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, use contadores de pontos 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á 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 se registrarão IMeterFactory automaticamente no contêiner de serviço ou você poderá registrar manualmente o tipo em qualquer IServiceCollection um 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 em um tipo projetado para DI, adicione um IMeterFactory parâmetro ao construtor e, em seguida, chame Create. Este exemplo mostra o uso de IMeterFactory em um aplicativo ASP.NET Core.

Defina um tipo para segurar 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 todos os objetos que cria, descartando-os Meter quando o contêiner DI é descartado. É desnecessário adicionar código extra para invocar Dispose() no Meter, e isso não terá qualquer efeito.

Tipos de instrumentos

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

  • Cálculos métricos padrão - As 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 invoca uma vez a cada segundo com valores sucessivos Add() 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 invoca uma vez a cada segundo com valores sucessivos Add() 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 coleta 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 variação no total disponível. Se apenas um puder ser mostrado, recomenda-se a taxa de mudança. 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 é invocado 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 pelo retorno de chamada será mostrado na ferramenta de coleta inalterado como o total.

  • ObservableGauge (CreateObservableGauge) - Este instrumento permite que o chamador forneça um retorno de chamada onde o valor medido é passado diretamente como a métrica. Cada vez que a ferramenta de coleta é atualizada, o retorno de chamada é invocado e qualquer valor retornado pelo retorno de chamada é 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 foi Record chamado 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.

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 é 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 que o código mantém. Em caminhos de código extremamente quentes, onde o desempenho é importante e o uso criaria mais de um milhão de chamadas por segundo por thread, usar o Add ObservableCounter pode oferecer mais oportunidades de otimização.

  • Para coisas de 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.

Nota

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 por Program.cs :

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 contadores de pontos 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

Este exemplo usa alguns números gerados aleatoriamente para que seus valores variem um pouco. Você pode ver que hatco.store.hats_sold (o Contador) e hatco.store.coats_sold (o Contador Observado) aparecem como uma taxa. O ObservableGauge, , hatco.store.orders_pendingaparece como um valor absoluto. Contadores de pontos renderizam instrumentos de histograma como estatísticas de três percentis (50º, 95º e 99º), mas outras ferramentas podem resumir a distribuição de forma 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, não retornando medições ou lançando uma exceção sobre a execução de qualquer operação potencialmente de longa duração ou bloqueio.

  • 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 do acesso não sincronizado. As abordagens comuns para sincronizar o acesso são usar um bloqueio ou chamada Volatile.Read e Volatile.Write.

  • As CreateObservableGauge funções 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 por Program.cs :

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 contadores de pontos 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

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, você verá que "{hats}" substituiu o termo genérico "Contagem" 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 chaves 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. Neste exemplo, cada medida é um número de chapéus, portanto, "{hats}" é a unidade apropriada para passar no construtor. A ferramenta de coleta calculou uma taxa e calculou por si só que a unidade apropriada para a métrica 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 usam um ou mais KeyValuePair argumentos. Por exemplo:

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

Substitua o código e execute novamente o aplicativo e os contadores de Program.cs pontos 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));
        }
    }
}

Contadores de pontos agora mostra 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, 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 contadores de pontos 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

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 tags sigam as diretrizes de nomenclatura do OpenTelemetry que usam nomes hierárquicos pontilhados minúsculos com caracteres '_' para separar várias 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
  • Cuidado para não ter combinações muito grandes ou ilimitadas de valores de tag sendo gravadas 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 for a Counter<short> ocupa apenas 2 bytes por combinação de tag, enquanto um double for 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 API .NET é otimizada para Add ser livre de alocação e Record chamadas com três ou menos tags especificadas individualmente. Para evitar alocações com um número maior de tags, use TagList. Em geral, a sobrecarga de desempenho dessas chamadas aumenta à medida que mais tags são usadas.

Nota

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

Testar métricas personalizadas

É possível testar qualquer métrica personalizada que você adicionar usando MetricCollector<T>o . 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);
    }
}