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 de 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.
Nota
O .NET tem algumas APIs de métrica 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 direcionados ao .NET 8+ incluem essa referência por padrão. Em seguida, atualize o código em 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". Em cada transação simulada, o código chama Add para registrar a quantidade de chapéus vendidos, neste caso, 4. O instrumento "hatco.store.hats_sold" define implicitamente algumas métricas que podem ser computadas a partir dessas medidas, como o número total de chapéus vendidos ou chapéus vendidos/s. Por fim, cabe às ferramentas de coleta de métricas determinar quais métricas calcular e como executar esses cálculos, 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 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
, 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 em seguida.
> 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 de DI, 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ê prever que os desenvolvedores de aplicativos gostariam de ser capazes de 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ó.
Nomes de instrumentos de exemplo:
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 requer sincronização quando invocadas 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 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 liberalmente 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, use contadores 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 4 chapéus a cada segundo.
Obtenha um Medidor por meio da 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 forma não é uma boa abordagem ao usar a DI (injeção de dependência). 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 serviço de contêiner ou você poderá registrar manualmente o tipo em qualquer IServiceCollection chamando AddMetrics.
A fábrica de medidores integra as métricas ao DI, mantendo os Medidores em diferentes coleções de serviços isoladas umas das outras, mesmo que usem um nome idêntico. Isso é especialmente útil para testes para que vários testes em execução 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 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 de DI em Program.cs
.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<HatCoMetrics>();
Insira o tipo de métrica e os valores de registro quando 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);
});
Práticas recomendadas
- System.Diagnostics.Metrics.Meter implementa IDisposable, mas o IMeterFactory gerencia automaticamente o tempo de vida de qualquer objeto
Meter
que ele cria, descartando-os quando o contêiner de DI é descartado. É desnecessário adicionar código extra para invocarDispose()
noMeter
e não terá nenhum efeito.
Tipos de instrumentos
Até agora, apenas demonstramos um instrumento de Counter<T>, mas há mais tipos de instrumentos disponíveis. Os instrumentos diferem de duas maneiras:
- cálculos de métrica padrão – as ferramentas que coletam e analisam as medidas de instrumento calcularão diferentes métricas padrão, dependendo do instrumento.
- Armazenamento de dados agregados – a maioria das métricas úteis precisa que os dados sejam agregados de muitas medidas. Uma opção é que o chamador fornece medidas individuais em horários arbitrários e a ferramenta de coleção gerencia a agregação. Como alternativa, o chamador pode gerenciar as medições agregadas e fornecê-las sob demanda em uma chamada de retorno.
Tipos de instrumentos disponíveis no momento:
Counter (CreateCounter) – Esse 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 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á 1+2+4+5+4+3=19. A taxa de alteração é a (current_total - previous_total), 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á 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 coleta 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á 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 coleta for atualizada a cada três segundos, a função de retorno de chamada também será ativada a cada três segundos. Qualquer valor retornado pelo callback será exibido inalterado como o total na ferramenta de coleta.
Gauge (CreateGauge) – Esse 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 coleção de métricas exibirá qualquer valor definido mais recentemente.
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.
Histograma (CreateHistogram) – Este instrumento acompanha a distribuição de medições. Não há uma única maneira canônica de descrever um conjunto de medidas, mas é recomendável usar histogramas ou percentis computados. Por exemplo, suponha que o chamador invocou Record para registrar essas medidas durante o intervalo de atualização da ferramenta de coleta: 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.
Nota
Para obter detalhes sobre como definir os limites recomendados do bucket ao criar um instrumento Histograma, consulte: Usando Aviso para personalizar os instrumentos de Histograma.
Práticas recomendadas ao selecionar um tipo de instrumento
Para contar itens ou qualquer outro valor que só aumente 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 extremamente frequentes em que o desempenho é importante e usar Add criaria mais de um milhão de chamadas por segundo por thread, o uso de ObservableCounter pode oferecer mais oportunidades de otimização.
Para questões de tempo, Histogram costuma ser preferido. Geralmente, é ú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
ouObservableUpDownCounter
. 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 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 dá suporte a UpDownCounter
e ObservableUpDownCounter
(antes da versão 7), ObservableGauge
geralmente é um bom substituto.
Exemplo de diferentes tipos de instrumento
Interrompa 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 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
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 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 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 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 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 fazer uma chamada para 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. Atribuir a uma variável estática como fizemos para os outros instrumentos é legal, mas propenso a erros, pois 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 coleta para ajudar os engenheiros a entender como interpretar os dados. Interrompa 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
Name Current Value
[HatCo.Store]
hatco.store.hats_sold ({hats}) 40
No momento, o dotnet-counters não usa o texto descritivo na interface do usuário, mas mostra 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 usada como a unidade, mas recomendamos usar UCUM, um padrão internacional para nomes de unidade. 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 difere das unidades na métrica final relatada. 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 coleta poderia ter calculado a taxa de alteração e ter concluído sozinha que a unidade apropriada para a métrica de taxa calculada é {hats}/seg.
Ao registrar medidas de tempo, prefira unidades de segundos registradas como um ponto flutuante ou valor duplo.
Métricas multidimensionais
As medidas também podem ser associadas a pares chave-valor chamados marcas que permitem que os dados sejam categorizados para análise. Por exemplo, o HatCo pode querer gravar 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 do HatCo podem 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 reinicie o aplicativo 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));
}
}
}
Os dotnet-counters agora mostram 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, 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 como antes com dotnet-counters, 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
Práticas recomendadas
Embora a API permita que qualquer objeto seja usado como o valor da etiqueta, tipos numéricos e cadeias de caracteres são comumente utilizados por ferramentas de coleta. Outros tipos podem ou não ter suporte de uma determinada ferramenta de coleção.
Recomendamos que os nomes de marca sigam as diretrizes de nomenclatura OpenTelemetry, que usam nomes hierárquicos pontilhados em letras minúsculas com caracteres '_' para separar várias palavras no mesmo elemento. Se os nomes de marca forem reutilizados em métricas diferentes ou outros registros de telemetria, eles deverão ter o mesmo significado e conjunto de valores legais em todos os lugares em que forem usados.
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 o HatCo tiver 10 cores de chapéu diferentes e 25 tamanhos de chapéu para até 10*25=250 totais de vendas a serem rastreados. No entanto, se a HatCo adicionou uma terceira marca que é uma 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 marcas diferentes sendo registradas. A maioria das ferramentas de coleta de métricas removerá 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 menos de 1000 combinações para um instrumento são seguras. Qualquer coisa acima de 1000 combinações exigirá que a ferramenta de coleção 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 seguros podem ser 10 a 100 vezes menores. Se você antecipar um grande número de combinações de tags únicas, 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 marcas, prefira usar um tipo de armazenamento menor para ajudar a reduzir a sobrecarga de memória. Por exemplo, armazenar o
short
para umCounter<short>
ocupa apenas 2 bytes por combinação de marca, enquanto umdouble
paraCounter<double>
ocupa 8 bytes por combinação de marca.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 medidas 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 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 tags são usadas.
Nota
OpenTelemetry refere-se a marcas como "atributos". Esses são dois nomes diferentes para a mesma funcionalidade.
Usando Aviso para personalizar os instrumentos de Histograma
Ao usar Histogramas, é responsabilidade da ferramenta ou biblioteca coletar os dados para decidir a melhor maneira de representar a distribuição de valores que foram registrados. Uma estratégia comum (e o modo padrão ao usar o OpenTelemetry) é dividir o intervalo de valores possíveis em sub-intervalos chamados buckets e relatar quantos valores registrados estavam em cada bucket. Por exemplo, uma ferramenta pode dividir números em três buckets, aqueles menores que 1, aqueles entre 1 e 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 outro no terceiro.
A ferramenta ou biblioteca que coleta os dados do Histograma é responsável por definir os buckets que ele usará. A configuração de bucket padrão ao usar o 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, as durações de solicitação de sub-segundos cairiam no bucket 0
.
A ferramenta ou biblioteca que coleta os dados do Histograma pode oferecer mecanismos para permitir que os usuários personalizem a configuração do bucket. Por exemplo, o OpenTelemetry define uma API de Exibição. No entanto, isso requer a ação do usuário final e torna responsabilidade do usuário entender bem a distribuição de dados 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 desenvolvedores de instrumentos para especificar o conjunto de limites de bucket padrão recomendados para um determinado Histograma. A ferramenta ou biblioteca que coleta os dados do Histogram 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 tem suporte no SDK do .NET do OpenTelemetry 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á um custo de CPU para encontrar o bucket correto ao processar uma medida. É importante entender as compensações entre a precisão e o consumo de CPU/memória ao escolher o número de buckets a serem recomendados 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ícitos no OpenTelemetry, consulte:
Testar métricas personalizadas
É possível testar as métricas personalizadas que você adicionar usando MetricCollector<T>. Esse tipo facilita o registro das medidas de instrumentos específicos e afirma que os valores estavam 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 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 medidas de um instrumento. Se você precisar verificar as medidas 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 usa um objeto medidor global compartilhado em um campo estático, mas verifique se esses testes estão configurados para não serem executados em paralelo. Como o objeto Meter está sendo compartilhado, MetricCollector em um teste observará as medidas criadas de qualquer outro teste em execução 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);
}
}