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

Compute, Storage e Redes no Azure
Compute, Storage e Redes no Azure

O artigo anterior estabeleceu a visão geral: como o Azure se organiza, como a...

O Que é CI/CD e Por Que Sua Empresa Precisa Disso
O Que é CI/CD e Por Que Sua Empresa Precisa Disso

Para compreender o valor do CI/CD, é necessário primeiro entender o que exist...

Gerando e Revisando Pipelines com IA
Gerando e Revisando Pipelines com IA

O GitHub Copilot foi lançado em 2021. O ChatGPT em novembro de 2022. O Cursor...