Existe uma diferença fundamental entre um sistema que funciona e um sistema que se sabe que funciona. O primeiro pode estar falhando silenciosamente, acumulando erros que só serão descobertos quando um cliente reclama ou quando um processo crítico falha de forma catastrófica. O segundo tem instrumentação suficiente para que a equipe responsável saiba, a qualquer momento, qual é o estado real do sistema.
Observabilidade é a propriedade de um sistema que permite inferir seu estado interno a partir de suas saídas externas. Um sistema é observável quando é possível responder perguntas como: por que a latência aumentou às 14h37? Qual porcentagem das requisições ao endpoint /checkout está falhando? A memória do servidor de banco de dados está crescendo ao longo do tempo?
A observabilidade é construída sobre três pilares complementares — frequentemente chamados de os três pilares da observabilidade:
Métricas — valores numéricos coletados ao longo do tempo. Representam o comportamento quantitativo do sistema: quantas requisições por segundo, qual o percentual de CPU em uso, quanto tempo uma operação leva em média. Métricas são eficientes de armazenar e consultar, mas têm baixa cardinalidade — descrevem o sistema como um todo, não eventos individuais.
Logs — registros de eventos discretos com timestamp. Cada entrada de log descreve algo que aconteceu: uma requisição foi recebida, um erro ocorreu, uma transação foi completada. Logs têm alta cardinalidade e são detalhados, mas são caros de armazenar e indexar em volume.
Traces — registros do caminho percorrido por uma requisição através dos serviços de um sistema distribuído. Um trace captura quanto tempo cada serviço levou, quais dependências foram chamadas e onde os erros ocorreram. Traces são essenciais em arquiteturas de microsserviços, onde uma única requisição pode passar por dezenas de serviços.
Cada pilar responde a um tipo diferente de pergunta. Métricas respondem o quê e quanto. Logs respondem o quê exatamente aconteceu. Traces respondem onde no sistema o problema ocorreu.
A Stack de Observabilidade Moderna
Diversas combinações de ferramentas são usadas na prática. Duas stacks dominam o mercado:
Stack Prometheus + Grafana — a stack open source mais popular. O Prometheus coleta e armazena métricas, o Grafana visualiza dashboards e envia alertas. Frequentemente complementada com Loki para logs e Tempo para traces, formando a stack PLG — Prometheus, Loki, Grafana.
Stack Elastic (ELK/EFK) — Elasticsearch para armazenamento e indexação, Logstash ou Fluentd para coleta e transformação, Kibana para visualização. Historicamente mais focada em logs, mas evoluiu para cobrir métricas e traces.
Para serviços gerenciados na nuvem, alternativas como Datadog, New Relic e AWS CloudWatch oferecem as três capacidades em uma única plataforma, reduzindo a complexidade operacional ao custo de maior dependência de fornecedor.
Este módulo foca na stack Prometheus + Grafana + Loki por ser a mais relevante para times que gerenciam sua própria infraestrutura e pela profundidade do conhecimento transferível para outras ferramentas.
Métricas com Prometheus
O Prometheus opera em um modelo de pull — em vez de os serviços enviarem métricas para um servidor central, o Prometheus periodicamente consulta os endpoints de métricas de cada serviço. Cada serviço expõe suas métricas em um endpoint HTTP — convencionalmente /metrics — em um formato de texto simples que o Prometheus entende.
Um endpoint de métricas típico exposto por uma aplicação Node.js:
# HELP http_requests_total Total de requisições HTTP recebidas
# TYPE http_requests_total counter
http_requests_total{method="GET",route="/api/users",status="200"} 1547
http_requests_total{method="GET",route="/api/users",status="500"} 12
http_requests_total{method="POST",route="/api/users",status="201"} 234
# HELP http_request_duration_seconds Duração das requisições HTTP em segundos
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.005"} 892
http_request_duration_seconds_bucket{le="0.01"} 1203
http_request_duration_seconds_bucket{le="0.025"} 1489
http_request_duration_seconds_bucket{le="0.05"} 1521
http_request_duration_seconds_bucket{le="0.1"} 1541
http_request_duration_seconds_bucket{le="+Inf"} 1547
http_request_duration_seconds_sum 38.4
http_request_duration_seconds_count 1547
# HELP process_resident_memory_bytes Memória residente do processo em bytes
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 52428800
Instrumentando uma aplicação Node.js com prom-client:
// src/metrics.js
const client = require('prom-client');
// Coleta métricas padrão do Node.js automaticamente
// (uso de CPU, memória, event loop lag, etc.)
const collectDefaultMetrics = client.collectDefaultMetrics;
collectDefaultMetrics({ prefix: 'minha_api_' });
// Contador de requisições HTTP
const httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total de requisições HTTP recebidas',
labelNames: ['method', 'route', 'status'],
});
// Histograma de duração das requisições
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duração das requisições HTTP em segundos',
labelNames: ['method', 'route', 'status'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
});
// Gauge para conexões ativas
const activeConnections = new client.Gauge({
name: 'active_connections',
help: 'Número de conexões ativas no momento',
});
// Middleware Express que instrumenta todas as requisições
function metricsMiddleware(req, res, next) {
const start = Date.now();
activeConnections.inc();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const route = req.route?.path || req.path;
const labels = {
method: req.method,
route,
status: res.statusCode.toString(),
};
httpRequestsTotal.inc(labels);
httpRequestDuration.observe(labels, duration);
activeConnections.dec();
});
next();
}
// Endpoint /metrics que o Prometheus vai consultar
async function metricsHandler(req, res) {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
}
module.exports = { metricsMiddleware, metricsHandler };
// src/app.js
const express = require('express');
const { metricsMiddleware, metricsHandler } = require('./metrics');
const app = express();
// Instrumentação antes de qualquer rota
app.use(metricsMiddleware);
// Endpoint de métricas — acessível pelo Prometheus
app.get('/metrics', metricsHandler);
// Endpoint de saúde — acessível pelo load balancer
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Rotas da aplicação
app.get('/api/users', async (req, res) => {
// ... lógica da rota
});
module.exports = app;
Instalando e Configurando o Prometheus
Com Docker Compose, o Prometheus pode ser levantado rapidamente para desenvolvimento local:
# docker-compose.observabilidade.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus:v2.49.0
container_name: prometheus
volumes:
- ./observabilidade/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- ./observabilidade/prometheus/rules/:/etc/prometheus/rules/
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--web.enable-lifecycle'
ports:
- "9090:9090"
restart: unless-stopped
grafana:
image: grafana/grafana:10.3.0
container_name: grafana
volumes:
- grafana_data:/var/lib/grafana
- ./observabilidade/grafana/provisioning/:/etc/grafana/provisioning/
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}
GF_USERS_ALLOW_SIGN_UP: "false"
ports:
- "3001:3000"
depends_on:
- prometheus
restart: unless-stopped
loki:
image: grafana/loki:2.9.4
container_name: loki
volumes:
- ./observabilidade/loki/loki.yml:/etc/loki/local-config.yaml
- loki_data:/loki
ports:
- "3100:3100"
restart: unless-stopped
promtail:
image: grafana/promtail:2.9.4
container_name: promtail
volumes:
- ./observabilidade/promtail/promtail.yml:/etc/promtail/config.yml
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
depends_on:
- loki
restart: unless-stopped
node-exporter:
image: prom/node-exporter:v1.7.0
container_name: node-exporter
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.rootfs=/rootfs'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
ports:
- "9100:9100"
restart: unless-stopped
volumes:
prometheus_data:
grafana_data:
loki_data:
Arquivo de configuração do Prometheus:
# observabilidade/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: 'minha-api'
environment: 'staging'
# Regras de alerta
rule_files:
- "rules/*.yml"
# Configuração do Alertmanager
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
# Targets de scraping
scrape_configs:
# O próprio Prometheus
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Métricas do sistema operacional via Node Exporter
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
relabel_configs:
- source_labels: [__address__]
target_label: instance
regex: '([^:]+).*'
replacement: '$1'
# A aplicação — descobre serviços via Docker labels
- job_name: 'minha-api'
static_configs:
- targets: ['minha-api:3000']
metrics_path: /metrics
scrape_interval: 10s
# Descobre targets automaticamente via Docker
- job_name: 'docker-containers'
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 30s
relabel_configs:
# Mantém apenas containers com a label prometheus.scrape=true
- source_labels: [__meta_docker_container_label_prometheus_scrape]
regex: 'true'
action: keep
# Define o path de métricas a partir da label
- source_labels: [__meta_docker_container_label_prometheus_path]
target_label: __metrics_path__
regex: '(.+)'
# Define a porta a partir da label
- source_labels: [__address__, __meta_docker_container_label_prometheus_port]
target_label: __address__
regex: '([^:]+)(?::\d+)?;(\d+)'
replacement: '$1:$2'
PromQL: A Linguagem de Consulta do Prometheus
O Prometheus tem sua própria linguagem de consulta — PromQL — que permite extrair e transformar métricas com expressividade considerável. Dominar PromQL é essencial para construir dashboards e alertas significativos.
Consultas fundamentais:
# Taxa de requisições por segundo (média dos últimos 5 minutos)
rate(http_requests_total[5m])
# Taxa de erros (5xx) por segundo
rate(http_requests_total{status=~"5.."}[5m])
# Percentual de erros em relação ao total
rate(http_requests_total{status=~"5.."}[5m])
/
rate(http_requests_total[5m])
* 100
# Latência no percentil 95
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
# Latência no percentil 99
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# Uso de memória do processo em MB
process_resident_memory_bytes / 1024 / 1024
# Uso de CPU do sistema em percentual
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# Espaço em disco disponível em GB
node_filesystem_avail_bytes{mountpoint="/"} / 1024 / 1024 / 1024
# Número de requisições por rota nos últimos 10 minutos
increase(http_requests_total[10m])
Logs com Loki
O Loki é o sistema de agregação de logs do ecossistema Grafana. Diferente do Elasticsearch, o Loki não indexa o conteúdo dos logs — apenas os labels associados a cada stream de log. Isso o torna muito mais econômico em termos de armazenamento e processamento, ao custo de buscas de texto completo mais lentas.
Configuração do Loki:
# observabilidade/loki/loki.yml
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v12
index:
prefix: index_
period: 24h
limits_config:
retention_period: 744h # 31 dias
Configuração do Promtail — agente que coleta logs e os envia ao Loki:
# observabilidade/promtail/promtail.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# Coleta logs dos containers Docker
- job_name: docker-containers
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container'
- source_labels: ['__meta_docker_container_log_stream']
target_label: 'stream'
- source_labels: ['__meta_docker_container_label_com_docker_compose_service']
target_label: 'service'
pipeline_stages:
# Parseia logs JSON da aplicação
- json:
expressions:
level: level
msg: msg
timestamp: time
request_id: requestId
duration: duration
- labels:
level:
service:
# Descarta logs de health check para reduzir volume
- drop:
expression: '.*GET /health.*'
drop_counter_reason: health_check_noise
# Coleta logs do sistema
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*.log
Consultando logs com LogQL no Grafana:
# Todos os logs de erro da aplicação
{service="minha-api"} |= "error"
# Logs de erro com detalhes parseados
{service="minha-api"} | json | level="error"
# Taxa de erros por minuto
rate({service="minha-api"} | json | level="error" [1m])
# Logs de uma requisição específica (rastreamento por request_id)
{service="minha-api"} | json | request_id="abc-123-def"
# Latência média das requisições (logs estruturados com campo duration)
avg_over_time(
{service="minha-api"} | json | unwrap duration [5m]
)
Configurando Logs Estruturados na Aplicação
Para que o Loki e o LogQL funcionem bem, a aplicação precisa emitir logs estruturados em JSON em vez de texto puro. A biblioteca pino é a escolha mais eficiente para Node.js:
// src/logger.js
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// Em desenvolvimento, usa o pretty-print; em produção, JSON puro
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
// Campos incluídos em todos os logs
base: {
service: 'minha-api',
version: process.env.APP_VERSION || 'unknown',
environment: process.env.NODE_ENV || 'development',
},
// Renomeia o campo de timestamp para 'time'
timestamp: pino.stdTimeFunctions.isoTime,
// Serializa objetos de erro corretamente
serializers: {
err: pino.stdSerializers.err,
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
},
});
module.exports = logger;
// src/middleware/logging.js
const logger = require('../logger');
const { randomUUID } = require('crypto');
function loggingMiddleware(req, res, next) {
const requestId = req.headers['x-request-id'] || randomUUID();
const start = Date.now();
// Adiciona o request_id ao contexto do log desta requisição
req.log = logger.child({ requestId });
// Propaga o request_id na resposta para rastreamento
res.setHeader('x-request-id', requestId);
req.log.info({
msg: 'Requisição recebida',
method: req.method,
url: req.url,
userAgent: req.headers['user-agent'],
ip: req.ip,
});
res.on('finish', () => {
const duration = Date.now() - start;
const level = res.statusCode >= 500 ? 'error'
: res.statusCode >= 400 ? 'warn'
: 'info';
req.log[level]({
msg: 'Requisição concluída',
method: req.method,
url: req.url,
status: res.statusCode,
duration,
});
});
next();
}
module.exports = loggingMiddleware;
Regras de Alerta no Prometheus
Métricas sem alertas são apenas dados históricos. As regras de alerta transformam métricas em ações — notificações enviadas quando condições problemáticas são detectadas:
# observabilidade/prometheus/rules/alertas-aplicacao.yml
groups:
- name: aplicacao
interval: 30s
rules:
# Alerta quando a taxa de erros ultrapassa 5%
- alert: AltaTaxaDeErros
expr: |
rate(http_requests_total{status=~"5.."}[5m])
/
rate(http_requests_total[5m])
> 0.05
for: 2m
labels:
severity: critical
team: backend
annotations:
summary: "Alta taxa de erros em {{ $labels.instance }}"
description: |
A taxa de erros está em {{ $value | humanizePercentage }}
no endpoint {{ $labels.route }}.
Threshold: 5% por mais de 2 minutos.
runbook_url: "https://wiki.empresa.com/runbooks/alta-taxa-erros"
# Alerta quando a latência P95 ultrapassa 500ms
- alert: AltaLatencia
expr: |
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m])
) > 0.5
for: 5m
labels:
severity: warning
team: backend
annotations:
summary: "Latência P95 alta em {{ $labels.instance }}"
description: |
Latência P95 está em {{ $value | humanizeDuration }}
(threshold: 500ms por mais de 5 minutos).
- name: infraestrutura
rules:
# Alerta quando o disco está acima de 85%
- alert: DiscoQuasecheio
expr: |
(
node_filesystem_size_bytes{mountpoint="/"}
- node_filesystem_avail_bytes{mountpoint="/"}
)
/ node_filesystem_size_bytes{mountpoint="/"}
> 0.85
for: 10m
labels:
severity: warning
annotations:
summary: "Disco quase cheio em {{ $labels.instance }}"
description: |
O disco está {{ $value | humanizePercentage }} ocupado
em {{ $labels.instance }}.
# Alerta quando a memória está acima de 90%
- alert: AltaMemoria
expr: |
(1 - (
node_memory_MemAvailable_bytes
/ node_memory_MemTotal_bytes
)) > 0.90
for: 5m
labels:
severity: critical
annotations:
summary: "Memória crítica em {{ $labels.instance }}"
description: |
Uso de memória em {{ $value | humanizePercentage }}
em {{ $labels.instance }}.
O Que Vem a Seguir
O próximo artigo aprofunda os dashboards no Grafana — como construir visualizações que realmente comunicam o estado do sistema, como organizar painéis para diferentes audiências e como configurar o Alertmanager para roteamento inteligente de alertas.
Referências para Aprofundamento
Documentação oficial - Prometheus Documentation — prometheus.io — Documentação completa do Prometheus, incluindo referência da PromQL, guias de configuração e boas práticas de instrumentação. - Grafana Loki Documentation — grafana.com — Documentação completa do Loki, cobrindo instalação, configuração do Promtail e referência da linguagem LogQL.
Instrumentação de aplicações - prom-client — GitHub — Biblioteca oficial do cliente Prometheus para Node.js, com exemplos de contadores, gauges, histogramas e summaries. - pino — GitHub — Biblioteca de logging estruturado para Node.js com foco em performance, amplamente usada em produção.
Observabilidade como disciplina - Observability Engineering — O'Reilly — Livro de referência sobre observabilidade escrito pelos criadores do Honeycomb, cobrindo os três pilares com profundidade teórica e prática. - Google SRE Book — Monitoring Distributed Systems — Capítulo do livro de SRE do Google sobre monitoramento, disponível gratuitamente online. Define os quatro sinais de ouro que todo sistema deve monitorar.