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

[299] Introdução ao Monitoramento: Métricas, Logs e Traces

Os três pilares da observabilidade e a stack que os sustenta: métricas com Prometheus e a linguagem PromQL, logs agregados com Loki, a instrumentação de logs estruturados na própria aplicação, e as regras de alerta que transformam coleta de dados em aviso acionável.
DevOps

21 min de leitura

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

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

Exercícios

Exercício 1

Quais são os três pilares da observabilidade, que pergunta cada um responde, e qual o trade-off entre cardinalidade e custo de armazenamento em cada caso?

Ver resposta

✓ Resposta: Métricas respondem o quê e quanto — valores numéricos ao longo do tempo, como requisições por segundo ou uso de CPU. Logs respondem o que exatamente aconteceu — eventos discretos com timestamp. Traces respondem onde no sistema o problema ocorreu — o caminho de uma requisição pelos serviços, com o tempo gasto em cada um.

O trade-off: métricas têm baixa cardinalidade e por isso são baratas de armazenar e rápidas de consultar — mas descrevem o sistema em agregado, sem permitir chegar a um evento individual. Logs têm alta cardinalidade e trazem o detalhe de cada ocorrência, ao custo de armazenamento e indexação caros em volume. Traces são os mais custosos por requisição, e por isso costumam ser amostrados, mas são os únicos que reconstroem o percurso completo — indispensáveis em microsserviços, onde uma requisição atravessa dezenas de serviços.

São complementares, não substitutos: a métrica avisa que a latência subiu às 14h37, o trace mostra qual serviço da cadeia é o responsável, e o log diz o que exatamente falhou ali.

Exercício 2

O código instrumenta três tipos de métrica: Counter, Gauge e Histogram. Explique a diferença entre eles usando os exemplos do artigo e responda: por que consultas sobre http_requests_total quase sempre aparecem dentro de rate()?

Ver resposta

✓ Resposta: O Counter só cresce — http_requests_total acumula requisições desde que o processo subiu, e apenas volta a zero quando ele reinicia. O Gauge sobe e desce livremente, representando um valor instantâneo: active_connections faz inc() ao começar a requisição e dec() ao terminar; process_resident_memory_bytes é da mesma natureza. O Histogram distribui observações em faixas cumulativas (buckets) e mantém também soma e contagem — é o caso de http_request_duration_seconds, que permite calcular percentis de latência.

O rate() é necessário porque o valor bruto de um counter é acumulado, não uma taxa. Ver 1547 num gráfico diz apenas quantas requisições ocorreram desde o boot — um número que sempre sobe e cuja inclinação é o que realmente interessa. rate(http_requests_total[5m]) converte isso em requisições por segundo, calculadas sobre a janela de 5 minutos.

Há um segundo motivo, menos óbvio: o rate() trata corretamente o reinício do processo. Quando o counter zera, uma subtração ingênua daria um valor negativo enorme; o rate() detecta a reinicialização e a compensa.

Exercício 3

Use os valores do endpoint /metrics mostrado no artigo. (a) Calcule o percentual de requisições com erro 5xx. (b) Em qual intervalo de buckets cai o percentil 95 da latência, e qual valor aproximado o histogram_quantile retornaria?

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

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
Ver resposta

✓ Resposta: (a) O total de requisições é 1547 + 12 + 234 = 1793, das quais 12 são 5xx. O percentual é 12 / 1793 × 100 ≈ 0,67%.

(b) O total de observações do histograma é 1547 (o bucket +Inf, igual ao _count). O percentil 95 corresponde à observação 0,95 × 1547 ≈ 1469,65. Percorrendo os buckets cumulativos: 1203 observações estão em até 0,01 s e 1489 em até 0,025 s — logo o valor procurado cai entre 0,01 e 0,025 segundo.

O histogram_quantile interpola linearmente dentro do bucket: 0,01 + (0,025 − 0,01) × (1469,65 − 1203) / (1489 − 1203) ≈ 0,01 + 0,015 × 0,932 ≈ 0,024 s, ou cerca de 24 ms.

Repare na limitação que isso expõe: o resultado é uma estimativa, cuja precisão depende inteiramente de haver buckets bem posicionados na região de interesse. Se o percentil caísse no último intervalo, entre 0,1 e +Inf, não haveria limite superior para interpolar e o valor retornado seria inútil — daí a importância de escolher os buckets em função da latência esperada do serviço.

Exercício 4

O Loki não indexa o conteúdo dos logs, apenas os labels. Qual a consequência prática disso para as consultas? No pipeline do Promtail, por que level e service viram labels e msg não? E o que o estágio drop resolve?

Ver resposta

✓ Resposta: A consequência é uma troca deliberada: o Loki fica muito mais barato em armazenamento e processamento que o Elasticsearch, porque não mantém índice invertido do texto — mas buscas de texto completo ficam mais lentas, já que ele precisa varrer os streams selecionados. Por isso a consulta eficiente começa sempre estreitando por labels ({service="minha-api"}) e só então filtra o conteúdo (|= "error"): os labels reduzem o volume que será efetivamente lido.

level e service viram labels porque têm poucos valores distintosinfo, warn, error; um punhado de serviços — e são justamente os critérios pelos quais se costuma recortar a busca. O msg fica de fora porque é praticamente único a cada linha: promovê-lo a label criaria um stream separado por mensagem, uma explosão de cardinalidade que degrada o Loki severamente. A regra é que label serve para selecionar, não para guardar conteúdo.

O estágio drop com expression: '.*GET /health.*' descarta os logs de health check. O load balancer bate nesse endpoint a cada poucos segundos, indefinidamente — é o maior volume de log da aplicação e o de menor valor diagnóstico. O drop_counter_reason: health_check_noise ainda registra quantas linhas foram descartadas e por quê, para que a filtragem não vire um buraco invisível.

Exercício 5

Descreva o caminho completo do requestId: onde nasce, como chega a todos os logs daquela requisição, por que volta no cabeçalho da resposta e como ele é usado depois no LogQL. Por que o nível do log é escolhido pela faixa do res.statusCode?

Ver resposta

✓ Resposta: Ele nasce no middleware: se a requisição já chega com x-request-id — vinda de outro serviço ou do proxy —, esse valor é reaproveitado; caso contrário, um randomUUID() é gerado. Reaproveitar é o que mantém o mesmo identificador ao longo de uma cadeia de serviços.

Ele chega a todos os logs através de logger.child({ requestId }), atribuído a req.log. O child logger carrega esse campo automaticamente em cada linha emitida durante aquela requisição — não é preciso repeti-lo em nenhuma chamada.

Volta na resposta via res.setHeader('x-request-id', requestId) para que ele seja observável de fora: um cliente que recebeu erro pode informar esse identificador no chamado de suporte, e a equipe salta direto para os logs daquela requisição específica com {service="minha-api"} | json | request_id="abc-123-def" — reconstruindo toda a linha do tempo sem precisar adivinhar por horário.

O nível derivado do status (>= 500 vira error, >= 400 vira warn, o resto info) separa o que é falha do servidor do que é erro do cliente. Um 404 ou um 422 são respostas legítimas a requisições malformadas e não indicam problema no sistema; tratá-los como error encheria os alertas de ruído. Como level é label no Loki, essa classificação é o que torna prático consultar e alertar apenas sobre erros reais.

Comentários

Mais em DevOps

Seus Primeiros Recursos na AWS com Terraform
Seus Primeiros Recursos na AWS com Terraform

Uma infraestrutura AWS completa provisionada inteiramente por código: a…

Gitea: Self-Hosted Leve para Times Menores
Gitea: Self-Hosted Leve para Times Menores

Servidor Git self-hosted em um único binário Go: instalação por systemd ou…

O Terminal Não é o Inimigo
O Terminal Não é o Inimigo

Por que um curso de DevOps começa por um mês inteiro de terminal: o que é o…