DEV Community

Cover image for Métricas customizadas - counters, gauges e histograms com uma mini app PHP
Rafael Dutra for apsis-cc

Posted on

Métricas customizadas - counters, gauges e histograms com uma mini app PHP

1. Retomando: de uma métrica a um conjunto completo

No artigo anterior, a mini aplicação em Python expunha uma única métrica — uma contagem de requisições. Na prática, uma aplicação real quer responder perguntas de negócio mais específicas: quantos pedidos foram criados? Quanto tempo cada requisição levou? Quantas conexões estão abertas agora? Cada uma dessas perguntas pede um tipo diferente de métrica. Este artigo cobre os três tipos que cobrem praticamente todo caso de uso — counter, gauge e histogram — com uma segunda mini aplicação, desta vez em PHP puro, expondo métricas de negócio.

2. Counter: só sobe (até o processo reiniciar)

Um counter é um valor que só aumenta (ou zera, se o processo reinicia). É o tipo certo para "quantas vezes algo aconteceu": número de requisições, número de erros, número de pedidos criados. A mini app do artigo anterior já usava um counter (app_requests_total).

O sufixo _total no nome é uma convenção do Prometheus para deixar explícito que a métrica é um counter cumulativo, não um valor instantâneo — vale seguir essa convenção mesmo implementando o texto à mão.

# HELP orders_created_total Total de pedidos criados
# TYPE orders_created_total counter
orders_created_total 187
Enter fullscreen mode Exit fullscreen mode

3. Gauge: sobe e desce livremente

Um gauge representa um valor que pode aumentar ou diminuir a qualquer momento — é o tipo certo para "quanto existe agora": número de conexões abertas, itens numa fila, memória em uso, temperatura. Diferente do counter, não existe uma direção esperada.

# HELP orders_queue_size Numero de pedidos aguardando processamento
# TYPE orders_queue_size gauge
orders_queue_size 4
Enter fullscreen mode Exit fullscreen mode

4. Histogram: distribuição de valores em buckets

Um histogram captura a distribuição de uma medida contínua — o caso mais comum é tempo de resposta. Em vez de guardar cada valor individual (inviável em alta escala) ou só uma média (que esconde outliers), um histogram conta quantas observações caíram dentro de faixas pré-definidas, chamadas buckets.

# HELP orders_duration_seconds Tempo de processamento de um pedido
# TYPE orders_duration_seconds histogram
orders_duration_seconds_bucket{le="0.1"} 120
orders_duration_seconds_bucket{le="0.5"} 180
orders_duration_seconds_bucket{le="1"} 195
orders_duration_seconds_bucket{le="+Inf"} 200
orders_duration_seconds_sum 42.7
orders_duration_seconds_count 200
Enter fullscreen mode Exit fullscreen mode

Cada linha _bucket{le="X"} é cumulativa: "quantas observações foram menores ou iguais a X segundos". _sum é a soma de todos os valores observados, e _count é o total de observações (igual ao último bucket, +Inf). Com essas peças, o Prometheus consegue calcular percentis aproximados (p50, p95, p99) em tempo de consulta, via histogram_quantile — sem que a aplicação precise calcular percentil nenhum sozinha.

5. Mini app em PHP puro com métricas de negócio

Sem nenhum framework, usando apenas o servidor embutido do PHP (php -S), uma aplicação que simula processamento de pedidos e expõe as três métricas vistas acima:

<?php
// app.php

$stateFile = __DIR__ . '/metrics_state.json';

function readState($file) {
    if (!file_exists($file)) {
        return ['requests_total' => 0, 'queue_size' => 0, 'durations' => []];
    }
    return json_decode(file_get_contents($file), true);
}

function writeState($file, $state) {
    file_put_contents($file, json_encode($state));
}

$state = readState($stateFile);

if ($_SERVER['REQUEST_URI'] === '/metrics') {
    $buckets = [0.1, 0.5, 1.0];
    $durations = $state['durations'];
    $sum = array_sum($durations);
    $count = count($durations);

    header('Content-Type: text/plain; version=0.0.4');

    echo "# HELP orders_created_total Total de pedidos criados\n";
    echo "# TYPE orders_created_total counter\n";
    echo "orders_created_total {$state['requests_total']}\n";

    echo "# HELP orders_queue_size Numero de pedidos aguardando processamento\n";
    echo "# TYPE orders_queue_size gauge\n";
    echo "orders_queue_size {$state['queue_size']}\n";

    echo "# HELP orders_duration_seconds Tempo de processamento de um pedido\n";
    echo "# TYPE orders_duration_seconds histogram\n";
    $cumulative = 0;
    foreach ($buckets as $le) {
        $cumulative = count(array_filter($durations, fn($d) => $d <= $le));
        echo "orders_duration_seconds_bucket{le=\"{$le}\"} {$cumulative}\n";
    }
    echo "orders_duration_seconds_bucket{le=\"+Inf\"} {$count}\n";
    echo "orders_duration_seconds_sum {$sum}\n";
    echo "orders_duration_seconds_count {$count}\n";
    exit;
}

// Simula o processamento de um novo pedido
$start = microtime(true);
$state['queue_size']++;
usleep(random_int(20_000, 400_000)); // simula trabalho real, 20-400ms
$state['queue_size']--;
$state['requests_total']++;
$state['durations'][] = round(microtime(true) - $start, 3);

writeState($stateFile, $state);

header('Content-Type: text/plain');
echo "pedido processado\n";
Enter fullscreen mode Exit fullscreen mode

Subindo com o servidor embutido do PHP:

php -S 0.0.0.0:8001 app.php
Enter fullscreen mode Exit fullscreen mode

Gerando alguns pedidos e conferindo as métricas:

for i in $(seq 1 10); do curl -s http://localhost:8001/ > /dev/null; done
curl http://localhost:8001/metrics
Enter fullscreen mode Exit fullscreen mode

O estado é persistido em um arquivo JSON local só para manter o exemplo simples e sem dependências — em uma aplicação real, esses contadores normalmente vivem em memória do próprio processo (e cada worker/processo exporia os seus, deixando a agregação entre réplicas por conta do Prometheus).

6. Adicionando a app PHP à stack

# docker-compose.yml (trecho adicionado)
services:
  php-app:
    image: php:8.3-cli
    working_dir: /app
    volumes:
      - ./app.php:/app/app.php
    command: ["php", "-S", "0.0.0.0:8001", "app.php"]
    ports:
      - "8001:8001"
Enter fullscreen mode Exit fullscreen mode
# prometheus.yml (job adicionado)
  - job_name: "php-app"
    static_configs:
      - targets: ["php-app:8001"]
Enter fullscreen mode Exit fullscreen mode

7. Consultando um histogram com histogram_quantile

Com dados fluindo, a consulta mais útil sobre um histogram no PromQL é o percentil de latência:

# p95 de tempo de processamento nos ultimos 5 minutos
histogram_quantile(0.95, rate(orders_duration_seconds_bucket[5m]))

# tamanho atual da fila
orders_queue_size

# taxa de pedidos criados por segundo
rate(orders_created_total[1m])
Enter fullscreen mode Exit fullscreen mode

histogram_quantile reconstrói o percentil aproximado a partir da distribuição por buckets — quanto mais granulares os buckets ao redor do percentil de interesse, mais precisa a estimativa. É uma aproximação, não um cálculo exato sobre os valores individuais (que o Prometheus nunca armazena um a um), mas suficiente para a grande maioria dos casos de uso de observação de latência em produção.

8. Conclusão e próximos passos

Cobrimos os três tipos fundamentais de métrica — counter para contagens cumulativas, gauge para valores que sobem e descem, histogram para distribuições como latência — e construímos uma segunda mini aplicação, agora em PHP puro, expondo métricas de negócio reais. Com duas aplicações diferentes já enviando métricas para o Prometheus, o próximo artigo muda de pilar: como enviar logs dessas mesmas aplicações para o Loki, e por que o modelo de indexação dele é tão diferente do Prometheus.


Imagem de capa: Logo oficial do Prometheus — repositório prometheus/prometheus, licença Apache 2.0 (convertido para PNG via wsrv.nl)

Referências:

  1. Prometheus — Metric Types
  2. Prometheus — Histograms and Summaries
  3. PromQL — histogram_quantile
  4. PHP Manual — Built-in web server

Top comments (0)