DevOps

Introdução ao Monitoramento: Métricas, Logs e Traces Já leu

15 min de leitura

Introdução ao Monitoramento: Métricas, Logs e Traces
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

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.

Comentários

Mais em DevOps

Publicando Imagens no Docker Hub e GitHub Container Registry
Publicando Imagens no Docker Hub e GitHub Container Registry

Uma imagem que existe apenas na máquina do desenvolvedor não serve a nenhum p...

Processos, Serviços e o Comando `systemctl`
Processos, Serviços e o Comando `systemctl`

Cada programa em execução no Linux — seja um servidor web...

Artigo 29 — Ansible: Configurando Servidores de Forma Declarativa
Artigo 29 — Ansible: Configurando Servidores de Forma Declarativa

Artigo 29 — Ansible: Configurando Servidores de Forma Declarativa O Que o Ter...