Criando e consumindo métricas com System.Diagnostics.Metrics no .NET
O System.Diagnostics.Metrics é a API do .NET para criar e consumir métricas de forma padronizada, leve e extensível. Com ela, a aplicação registra indicadores como contadores, latências e taxas de erro sem depender de nenhuma ferramenta de monitoramento específica.
A observabilidade se tornou um pilar essencial no desenvolvimento de aplicações modernas. Afinal, em sistemas distribuídos e altamente escaláveis, entender o comportamento da aplicação em tempo de execução é fundamental para garantir desempenho, confiabilidade e capacidade de diagnóstico.
Neste artigo, vamos ver como a API separa quem produz e quem consome métricas. Em seguida, vamos criar instrumentos, adicionar tags, consumir medições com o MeterListener e fechar com as boas práticas recomendadas pela Microsoft.
Por que métricas importam na observabilidade
A observabilidade costuma se apoiar em três sinais: logs, traces e métricas. Enquanto logs contam o que aconteceu em um evento específico, as métricas mostram tendências numéricas ao longo do tempo.
Por isso, métricas desempenham um papel central no monitoramento. Elas permitem acompanhar quantas requisições chegam por segundo, quanto tempo cada operação leva e quantos erros acontecem em determinado intervalo.
Além disso, métricas são baratas de coletar quando comparadas a logs detalhados. Em vez de guardar cada evento, as ferramentas agregam os valores e armazenam apenas totais, taxas e distribuições.
Se você já trabalha com logs estruturados, vale conhecer também o uso do Serilog no ASP.NET. Logs e métricas se complementam muito bem.
Visão geral do System.Diagnostics.Metrics
O namespace System.Diagnostics.Metrics foi projetado para oferecer uma abstração moderna, alinhada aos conceitos das ferramentas de observabilidade mais adotadas, como o OpenTelemetry. De acordo com a documentação oficial da Microsoft, a API está disponível a partir do .NET 6 e também no .NET Framework 4.6.1 ou superior, por meio do pacote System.Diagnostics.DiagnosticSource.
Nas aplicações que usam .NET 8 ou superior, a referência a esse pacote já vem incluída por padrão. Portanto, na maioria dos projetos atuais, basta adicionar o using e começar a instrumentar.
O .NET também possui APIs de métricas mais antigas, como EventCounters e PerformanceCounter. No entanto, a recomendação atual para código novo é usar o System.Diagnostics.Metrics.
Produtores e consumidores de métricas
A API separa claramente dois papéis principais:
- Produtores de métricas: criam e registram valores métricos dentro da aplicação.
- Consumidores de métricas: observam, coletam e exportam esses valores para sistemas externos.
Essa separação mantém o código da aplicação desacoplado das ferramentas de monitoramento. Assim, você pode trocar a solução de observabilidade sem mexer na lógica de negócio.
O papel do Meter
O ponto central do System.Diagnostics.Metrics é a classe Meter. Ela representa uma fonte lógica de métricas, normalmente associada a um componente, biblioteca ou aplicação específica.
Cada Meter tem um nome e, opcionalmente, uma versão. A partir dele, criamos os instrumentos que de fato registram os valores.
Criando um Meter estático
Em código que não usa injeção de dependência, a recomendação é criar o Meter uma única vez e guardá-lo em uma variável estática. Dessa forma, ele representa uma única fonte de métricas durante todo o ciclo de vida da aplicação.
using System.Diagnostics.Metrics;
public static class StoreMetrics
{
// um único Meter para todo o componente
private static readonly Meter Meter = new("Balta.Store", "1.0.0");
// contador de pedidos concluídos
public static readonly Counter<int> OrdersCompleted =
Meter.CreateCounter<int>(
name: "balta.store.orders_completed",
unit: "{order}",
description: "Number of completed orders");
}
O nome do Meter deve ser único para não se confundir com outras fontes. Por isso, a Microsoft sugere usar o nome do assembly ou do namespace, seguindo o padrão hierárquico com pontos do OpenTelemetry.
Criando um Meter com IMeterFactory
Em aplicações com injeção de dependência, como o ASP.NET Core, campos estáticos são considerados um antipadrão. Nesse caso, o caminho é usar a interface IMeterFactory, que o host registra automaticamente a partir do .NET 8.
using System.Diagnostics.Metrics;
public class OrderMetrics
{
private readonly Counter<int> _ordersCompleted;
private readonly Histogram<double> _orderDuration;
public OrderMetrics(IMeterFactory meterFactory)
{
// o factory controla o ciclo de vida do Meter
var meter = meterFactory.Create("Balta.Store");
_ordersCompleted = meter.CreateCounter<int>("balta.store.orders_completed");
_orderDuration = meter.CreateHistogram<double>(
name: "balta.store.order_duration",
unit: "s");
}
public void OrderCompleted(double elapsedSeconds)
{
_ordersCompleted.Add(1);
_orderDuration.Record(elapsedSeconds);
}
}
Depois, basta registrar a classe no container e injetá-la onde for necessário. Se você quiser revisar esse mecanismo, o artigo sobre injeção de dependência no ASP.NET Core explica os tempos de vida dos serviços.
var builder = WebApplication.CreateBuilder(args);
// registra a classe de métricas como singleton
builder.Services.AddSingleton<OrderMetrics>();
var app = builder.Build();
app.MapPost("/orders", (OrderMetrics metrics) =>
{
// ... lógica de negócio para salvar o pedido ...
metrics.OrderCompleted(0.25);
return Results.Created();
});
app.Run();
O IMeterFactory também isola os Meter de cada container, mesmo quando eles têm o mesmo nome. Além disso, ele descarta os objetos automaticamente quando o container é descartado, então não precisamos chamar Dispose() manualmente.
Instrumentos do System.Diagnostics.Metrics
Os instrumentos são os objetos que registram os valores métricos. A API oferece diferentes tipos, e cada um é adequado a um tipo específico de medição.
No System.Diagnostics.Metrics, os instrumentos diferem em dois pontos. Primeiro, nas métricas que as ferramentas calculam por padrão, e depois em quem guarda os dados agregados, se é a ferramenta ou o próprio código.
O parâmetro genérico define o tipo numérico armazenado. Por exemplo, podemos usar byte, short, int, long, float, double ou decimal, conforme o tamanho e a precisão necessários.
Counter
O Counter<T> registra valores cumulativos que apenas aumentam ao longo do tempo. Ele é apropriado para o total de requisições, a quantidade de operações executadas ou os eventos processados.
// incrementa o total de requisições recebidas
requestsCounter.Add(1);
Os valores registrados em um contador são sempre incrementais. Na prática, as ferramentas costumam exibir o total e a taxa de crescimento, como requisições por segundo.
UpDownCounter
O UpDownCounter<T> permite tanto incrementos quanto decrementos. Por isso, ele é indicado para quantidades variáveis, como conexões ativas ou itens em uma fila.
// entrou uma conexão
activeConnections.Add(1);
// saiu uma conexão
activeConnections.Add(-1);
Diferentemente do Counter, esse instrumento não impõe restrições quanto à direção da variação. Ou seja, o valor pode subir e descer livremente.
Histogram
O Histogram<T> registra distribuições de valores. Ele é especialmente útil para medir latências, tamanhos de payloads e qualquer métrica que se beneficie de análise estatística, como médias, percentis e máximos.
var stopwatch = Stopwatch.StartNew();
await ProcessOrderAsync(order);
// registra a duração em segundos
orderDuration.Record(stopwatch.Elapsed.TotalSeconds);
Em vez de armazenar cada valor, o histograma agrupa os dados em faixas, chamadas de buckets. Assim, as ferramentas conseguem calcular percentis como p50, p95 e p99 com eficiência.
Para medir tempo, a Microsoft recomenda usar segundos em ponto flutuante. Além disso, a partir da versão 9.0 do pacote System.Diagnostics.DiagnosticSource, a classe InstrumentAdvice<T> permite sugerir os limites dos buckets para as ferramentas de coleta.
Gauge e instrumentos observáveis
Além dos três instrumentos principais, a API oferece o Gauge<T>, que define o valor atual de uma métrica com o método Record. Ele chegou com o .NET 9 e serve para valores não aditivos, como nível de ruído ou temperatura.
Existem também as versões observáveis ObservableCounter, ObservableUpDownCounter e ObservableGauge. Nelas, o próprio código mantém o valor e fornece um callback, que a ferramenta chama sempre que precisa ler a métrica.
// o callback é chamado a cada ciclo de coleta
meter.CreateObservableGauge(
name: "balta.store.pending_orders",
observeValue: () => orderQueue.Count);
Como os callbacks rodam em sequência, um callback lento atrasa a coleta de todas as métricas. Por isso, o ideal é apenas ler um valor em cache, sem operações bloqueantes.
Nomes, unidades e descrições
No System.Diagnostics.Metrics, todo instrumento pode receber, além do nome, uma unidade e uma descrição opcionais. Esses valores não interferem nos cálculos, mas aparecem nas ferramentas de coleta e ajudam a interpretar os dados.
// nome, unidade e descrição deixam a métrica autoexplicativa
var ordersCompleted = meter.CreateCounter<int>(
name: "balta.store.orders_completed",
unit: "{order}",
description: "Number of completed orders");
O .NET não impõe nenhum padrão de nomes para instrumentos. Mesmo assim, a recomendação é seguir as convenções do OpenTelemetry, com nomes em minúsculas, hierarquia separada por pontos e sublinhado entre palavras do mesmo nível.
Para as unidades, a sugestão é usar o padrão internacional UCUM. Nesse padrão, as chaves em {order} indicam uma anotação descritiva, e não uma unidade com significado fixo, como segundos ou bytes.
Também vale lembrar que nem toda ferramenta preserva o nome do Meter no nome final da métrica. Por isso, o próprio nome do instrumento deve ser único globalmente.
Consumindo o System.Diagnostics.Metrics com MeterListener
Além de criar métricas, o System.Diagnostics.Metrics fornece mecanismos para consumi-las. O consumo acontece por meio de listeners, que se inscrevem nos instrumentos criados pelos Meter.
A classe MeterListener observa instrumentos e recebe callbacks sempre que um valor é registrado. Assim, o listener pode filtrar quais Meter e quais instrumentos deseja acompanhar.
using System.Diagnostics.Metrics;
using MeterListener listener = new();
// decide quais instrumentos acompanhar
listener.InstrumentPublished = (instrument, meterListener) =>
{
if (instrument.Meter.Name is "Balta.Store")
{
meterListener.EnableMeasurementEvents(instrument);
}
};
// recebe cada medição do tipo int
listener.SetMeasurementEventCallback<int>(OnMeasurementRecorded);
// inicia o listener e dispara os eventos de publicação
listener.Start();
static void OnMeasurementRecorded<T>(
Instrument instrument,
T measurement,
ReadOnlySpan<KeyValuePair<string, object?>> tags,
object? state)
{
Console.WriteLine($"{instrument.Name} recorded {measurement}");
}
Durante a configuração, definimos o que acontece quando um instrumento é publicado e quando uma medição ocorre. Dessa forma, o consumidor pode coletar, processar ou exportar os dados conforme necessário.
Repare que o callback é registrado por tipo numérico. Portanto, se a aplicação usa Counter<int> e Histogram<double>, precisamos de um callback para int e outro para double.
Para os instrumentos observáveis, o listener precisa chamar RecordObservableInstruments() periodicamente. É esse método que dispara os callbacks e entrega os valores atuais.
Ferramentas para o System.Diagnostics.Metrics
Na maioria dos projetos, você não precisa escrever um MeterListener próprio. Isso porque as ferramentas de mercado já consomem o System.Diagnostics.Metrics por você.
Para análises rápidas durante o desenvolvimento, a ferramenta dotnet-counters mostra as métricas em tempo real no terminal. Ela exibe o total dos contadores e, para histogramas, os percentis 50, 95 e 99.
dotnet tool update -g dotnet-counters
dotnet-counters monitor -n MyApp --counters Balta.Store
Para produção, o caminho mais comum é o OpenTelemetry. O SDK do OpenTelemetry para .NET consome os mesmos instrumentos e exporta os dados para Prometheus, Grafana, Azure Monitor e outros destinos.
Essa integração só funciona bem porque o System.Diagnostics.Metrics segue as convenções do OpenTelemetry. Inclusive, o que o .NET chama de tags, o OpenTelemetry chama de atributos.
Testando métricas com MetricCollector
Métricas também merecem testes, inclusive as criadas com System.Diagnostics.Metrics. Para isso, a Microsoft oferece a classe MetricCollector<T>, que registra as medições de um instrumento e permite verificar os valores.
public class OrderMetricsTests
{
[Fact]
public void OrderCompleted_IncrementsCounter()
{
// Arrange
var services = new ServiceCollection();
services.AddMetrics();
services.AddSingleton<OrderMetrics>();
var provider = services.BuildServiceProvider();
var metrics = provider.GetRequiredService<OrderMetrics>();
var factory = provider.GetRequiredService<IMeterFactory>();
var collector = new MetricCollector<int>(
factory, "Balta.Store", "balta.store.orders_completed");
// Act
metrics.OrderCompleted(0.1);
// Assert
var measurements = collector.GetMeasurementSnapshot();
Assert.Single(measurements);
Assert.Equal(1, measurements[0].Value);
}
}
Como o IMeterFactory isola cada container, testes executados em paralelo observam apenas as próprias medições. Já com um Meter estático, os testes precisam rodar em sequência.
Se você está começando com testes, o artigo sobre fundamentos dos testes de unidade é um bom ponto de partida.
Separação entre instrumentação e exportação
Um dos principais benefícios do System.Diagnostics.Metrics é a separação clara entre a instrumentação e a exportação. A aplicação apenas registra valores, enquanto os consumidores cuidam de coletar e enviar os dados para sistemas externos.
Essa abordagem reduz o acoplamento e melhora a testabilidade. Além disso, bibliotecas instrumentadas podem ser reaproveitadas em diferentes contextos, independentemente da solução de observabilidade adotada.
Outro ponto interessante é o custo. Segundo a documentação, chamar Add leva menos de 10 nanossegundos quando ninguém está coletando, então instrumentar o código não pesa no desempenho.
Essa mesma lógica aparece em outros recursos de monitoramento, como o health check no ASP.NET. Em ambos os casos, a aplicação expõe informações e uma ferramenta externa decide o que fazer com elas.
Boas práticas com System.Diagnostics.Metrics
Ao instrumentar uma aplicação com System.Diagnostics.Metrics, algumas boas práticas fazem diferença:
- Crie um
Meterpor biblioteca ou componente lógico. - Escolha o tipo de instrumento adequado para cada métrica.
- Use nomes claros e consistentes, em minúsculas, separados por pontos e com sublinhado entre palavras.
- Informe a unidade de cada instrumento, preferindo o padrão UCUM, como
spara segundos e{order}para contagens descritivas. - Aplique tags apenas quando elas agregarem valor analítico.
- Evite métricas redundantes ou de alto custo computacional.
- Prefira
IMeterFactoryem código que usa injeção de dependência.
Seguir essas diretrizes contribui para um sistema de observabilidade mais eficiente e fácil de manter. Além disso, nomes padronizados facilitam a criação de dashboards e alertas.
Conclusão sobre System.Diagnostics.Metrics
O System.Diagnostics.Metrics representa um avanço significativo na forma como o ecossistema .NET trata métricas. Ao oferecer uma abstração padronizada, flexível e desacoplada, ele permite instrumentar aplicações de forma consistente com as práticas modernas de observabilidade.
Com a separação entre produção e consumo, você foca na lógica de negócio, enquanto ferramentas especializadas cuidam da coleta e da análise. O resultado é uma aplicação mais transparente, monitorável e preparada para ambientes distribuídos.
FAQ
A partir de qual versão do .NET o System.Diagnostics.Metrics está disponível?
A API está disponível a partir do .NET 6 e no .NET Framework 4.6.1 ou superior, via pacote System.Diagnostics.DiagnosticSource. No .NET 8 em diante, a referência já vem incluída.
Qual a diferença entre Counter e UpDownCounter?
O Counter só aumenta e serve para totais acumulados. Já o UpDownCounter aceita valores negativos e serve para quantidades que sobem e descem.
Quando devo usar um Histogram?
Use um Histogram para medir durações e tamanhos. Ele permite analisar percentis como p95 e p99, que mostram o comportamento das piores requisições.
Preciso do OpenTelemetry para usar a API?
Não. A API funciona sozinha e pode ser consumida pelo MeterListener ou pelo dotnet-counters, mas o OpenTelemetry é o caminho mais comum para exportar métricas em produção.
O que é alta cardinalidade em métricas?
É quando as tags geram um número enorme de combinações, como um identificador por cliente. Isso aumenta muito o consumo de memória e o custo das ferramentas de monitoramento.