Skip to main content

Percetils | Observabilidade | CSharp

  • Percetils | Observabilidade | CSharp

DOC: Percentis (P50/P90/P95/P99) com Prometheus no .NET (Histograms)

1- O que é P50/P95/P99?

P50/P95/P99 são tempos de latência (duração).

Se você vê:

  • P50 = 120ms

Significa:

50% das requisições terminaram em 120ms ou menos ✅ e 50% terminaram acima disso

Ou seja: percentil = um tempo, não “uma porcentagem de tempo”.


2- De onde saem os “tempos”?

Os tempos vêm de medições reais feitas pela sua aplicação.

Exemplo: você mede quanto tempo um trecho levou para executar:

  • request levou 35ms
  • request levou 140ms
  • request levou 900ms
  • request levou 2.4s

✅ Esses valores são coletados automaticamente quando você usa um timer.


3- O que você define vs o que é automático

✅ Automático (runtime mede)

  • A latência real de cada operação (ex: request HTTP)

✅ Você define

  • O histogram
  • Os buckets (faixas de tempo)
  • As labels (route/method/status)

📌 Prometheus calcula os percentis por aproximação com base nos buckets.


4- O jeito CERTO no Prometheus: HISTOGRAM

Para ter P50/P95/P99 no Prometheus você precisa:

Histogram + histogram_quantile() (PromQL)

Prometheus trabalha assim:

  1. Sua aplicação grava durações em um histogram
  2. O Prometheus coleta do endpoint /metrics
  3. Você consulta P50/P95/P99 via PromQL

5- .NET: setup mínimo com /metrics

Instalar pacote

dotnet add package prometheus-net.AspNetCore

Program.cs

using Prometheus;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "OK");

// Expor endpoint para Prometheus
app.MapMetrics(); // /metrics

app.Run();

✅ Pronto: http://localhost:xxxx/metrics


6- Criar métrica de latência (Histogram)

Crie um arquivo AppMetrics.cs:

using Prometheus;

public static class AppMetrics
{
// Histogram mede em segundos por padrão (melhor prática)
public static readonly Histogram HttpDuration = Metrics.CreateHistogram(
"app_http_request_duration_seconds",
"Duração das requests HTTP (segundos)",
new HistogramConfiguration
{
LabelNames = new[] { "route", "method", "status" },

// BUCKETS = faixas de tempo (segundos)
// Você define isso (não existe “um único padrão”)
Buckets = new[] { 0.01, 0.03, 0.05, 0.1, 0.2, 0.5, 1, 2, 5 }
});
}

✅ O que esses buckets significam?

Eles são limites “até X”:

  • 0.01 = até 10ms
  • 0.1 = até 100ms
  • 0.5 = até 500ms
  • 1 = até 1s
  • 5 = até 5s
  • +Inf = tudo

7- Medir a latência no endpoint (de onde sai o tempo medido)

Exemplo de endpoint medindo duração real:

app.MapGet("/orders/{id}", async (string id) =>
{
// Aqui começa a medir
using var timer = AppMetrics.HttpDuration
.WithLabels("/orders/{id}", "GET", "200")
.NewTimer();

// Seu código real (simula latência)
await Task.Delay(Random.Shared.Next(10, 800));

// Aqui o timer registra automaticamente o tempo medido
return Results.Ok(new { id });
});

✅ O tempo registrado vai ser o tempo real entre Start e Dispose.


8- O que aparece no /metrics (histogram gera 3 coisas)

Quando você usa histogram, aparecem essas métricas:

  • app_http_request_duration_seconds_bucket{le="0.1"...}
  • app_http_request_duration_seconds_sum{...}
  • app_http_request_duration_seconds_count{...}

Como entender “bucket”?

Exemplo:

...bucket{le="0.1"} significa:

✅ “quantas requests terminaram em 100ms ou menos


9- PromQL: como tirar P50/P90/P95/P99

✅ Regra padrão (use sempre)

Percentis no Prometheus vêm de:

histogram_quantile(
QUANTIL,
sum(rate(SEU_HISTOGRAM_bucket[5m])) by (le)
)

📌 rate(...[5m]) = pega o comportamento nos últimos 5 minutos 📌 by (le) = mantém a estrutura dos buckets


P50 (mediana)

histogram_quantile(
0.50,
sum(rate(app_http_request_duration_seconds_bucket[5m])) by (le)
)

P90

histogram_quantile(
0.90,
sum(rate(app_http_request_duration_seconds_bucket[5m])) by (le)
)

P95

histogram_quantile(
0.95,
sum(rate(app_http_request_duration_seconds_bucket[5m])) by (le)
)

P99

histogram_quantile(
0.99,
sum(rate(app_http_request_duration_seconds_bucket[5m])) by (le)
)

✅ O resultado vem em segundos

  • 0.12 = 120ms
  • 1.3 = 1.3s

10- Percentil por rota (muito usado)

Se você quer separar por rota:

P95 por rota

histogram_quantile(
0.95,
sum(rate(app_http_request_duration_seconds_bucket[5m])) by (le, route)
)

P99 por rota

histogram_quantile(
0.99,
sum(rate(app_http_request_duration_seconds_bucket[5m])) by (le, route)
)

11- “Eu defino os tempos ou tem padrão?”

✅ Os tempos medidos = vêm da aplicação (runtime) ✅ O que você “define” = buckets (faixas)

Existe padrão oficial único?

❌ Não existe 1 padrão perfeito universal.

Mas existe padrão recomendado?

✅ Sim, esse conjunto é uma base muito boa pra APIs:

10ms, 30ms, 50ms, 100ms, 200ms, 500ms, 1s, 2s, 5s

Buckets = new[] { 0.01, 0.03, 0.05, 0.1, 0.2, 0.5, 1, 2, 5 };

12- Como escolher buckets bons (sem complicar)

✅ Pegue o seu sistema e pense:

  • “normal” = 30ms ~ 200ms
  • “ruim” = 500ms ~ 2s
  • “horrível” = > 2s

Então você precisa buckets cobrindo essas faixas.

📌 Se você escolher buckets ruins:

  • P99 fica impreciso
  • percentil “vai pular”
  • você perde resolução

13- Boas práticas (curtas e importantes)

✅ Use labels estáveis:

  • route (template tipo /orders/{id})
  • method
  • status

❌ NÃO use:

  • user_id
  • order_id
  • URL completa com querystring

Porque explode cardinalidade e derruba Prometheus/Grafana.


14- Extras úteis (consultas comuns)

RPS (requests por segundo)

sum(rate(app_http_request_duration_seconds_count[5m]))

Latência média (não substitui percentil)

sum(rate(app_http_request_duration_seconds_sum[5m]))
/
sum(rate(app_http_request_duration_seconds_count[5m]))

Resumo final (curtão)

✅ O “tempo” do P50/P95/P99 é a latência real medida das requisições. ✅ Você não inventa os tempos — você mede. ✅ Você define os buckets (faixas) do histogram. ✅ Prometheus calcula percentis com:

histogram_quantile(0.99, sum(rate(METRIC_bucket[5m])) by (le))

Desenhando cenário

1- De onde sai o “tempo” (latência)

Request começa                         Request termina
|---------------------------------------|
tempo medido (latência)
ex: 120ms = 0.12s

✅ Esse tempo é medido pelo seu código (timer), exemplo:

using var t = histogram.NewTimer();
// ... request roda ...
t.Dispose(); // registra o tempo medido

2- Histogram = “baldes de tempo” (buckets)

Você define buckets assim (em segundos):

0.01   0.03   0.05   0.1   0.2   0.5    1     2     5     +Inf
10ms 30ms 50ms 100ms 200ms 500ms 1s 2s 5s tudo

Cada request “cai” em um bucket dependendo da latência.

Exemplo de 10 requests medidas:

12ms, 18ms, 40ms, 55ms, 70ms, 110ms, 140ms, 400ms, 900ms, 1.8s

3- Como o Prometheus conta (bucket acumulado)

Bucket é acumulado (até aquele tempo):

le="0.01" (<=10ms)   -> 0
le="0.03" (<=30ms) -> 2 (12ms, 18ms)
le="0.05" (<=50ms) -> 3 (+40ms)
le="0.1" (<=100ms) -> 5 (+55ms, 70ms)
le="0.2" (<=200ms) -> 7 (+110ms, 140ms)
le="0.5" (<=500ms) -> 8 (+400ms)
le="1" (<=1s) -> 9 (+900ms)
le="2" (<=2s) -> 10 (+1.8s)
le="+Inf" (todas) -> 10

4- Onde ficam P50 / P95 / P99 nesse desenho?

Se você tem 10 requests:

  • P50 = posição 50% → 5ª request
  • P95 = posição 95% → 10ª request (quase no fim)
  • P99 = posição 99% → 10ª request (praticamente o pior caso)

Visual:

[----50%----|----------------------95%---------------------|----99%----]
^P50 ^P95/^P99

No nosso exemplo de 10 requests:

  • P50 cai perto do bucket <=100ms
  • P95 e P99 caem perto do bucket <=2s

✅ O Prometheus estima isso usando:

histogram_quantile(...)

5- Fluxo completo (fim a fim)

.NET mede tempo real (timer)
|
v
Histogram junta em buckets
|
v
/metrics expõe *_bucket/_sum/_count
|
v
Prometheus coleta (scrape)
|
v
PromQL calcula:
P50/P95/P99 = histogram_quantile(...)