Boas práticas de logging estruturado em aplicações

1. Fundamentos do logging estruturado

O logging estruturado é uma abordagem moderna para registro de eventos em aplicações que substitui o texto livre tradicional por dados formatados e padronizados. Enquanto um log tradicional pode ser:

2024-01-15 10:30:45 - Usuário 123 fez login com sucesso

Um log estruturado no formato JSON seria:

{"timestamp":"2024-01-15T10:30:45.123Z","level":"INFO","message":"Login realizado","user_id":123,"action":"login","duration_ms":245}

Os benefícios são significativos: busca eficiente em ferramentas como Elasticsearch, análise automatizada com métricas agregadas e correlação entre sistemas distribuídos. Os formatos mais comuns incluem JSON (amplamente suportado), Logfmt (legível e compacto) e MessagePack (binário e performático).

2. Definindo um esquema de campos obrigatórios

Todo log estruturado deve seguir um esquema consistente. Campos essenciais incluem:

{
  "timestamp": "2024-01-15T10:30:45.123Z",
  "level": "INFO",
  "message": "Requisição processada",
  "service_name": "api-pedidos",
  "version": "2.3.1",
  "environment": "production",
  "trace_id": "abc123def456",
  "span_id": "span789ghi",
  "request_id": "req-001",
  "duration_ms": 342
}

Campos de identificação como service_name, version e environment são cruciais para filtrar logs em ambientes com múltiplos serviços. Nunca inclua dados sensíveis como senhas, tokens ou informações pessoais identificáveis (PII). Se necessário, registre apenas hashes ou identificadores anonimizados.

3. Níveis de severidade e quando utilizá-los

A hierarquia padrão de níveis segue a especificação Syslog e SLF4J:

DEBUG: Informações detalhadas para depuração (ativado apenas em desenvolvimento)
INFO: Eventos normais e esperados (inicializações, conclusões de processos)
WARN: Situações anormais que não impedem o funcionamento (retry, degradação)
ERROR: Falhas que exigem ação humana (exceções não tratadas, falhas em operações críticas)
FATAL: Erros catastróficos que encerram a aplicação

Regras práticas importantes:
- INFO para eventos normais como criação de usuários ou processamento de pedidos
- ERROR apenas para falhas que exigem intervenção imediata
- Evite DEBUG em produção (exceto temporariamente para diagnósticos)
- Nunca use WARN como substituto genérico para ERROR sem contexto claro

4. Estruturação de mensagens e contexto

Mensagens descritivas devem vir acompanhadas de contexto estruturado:

// Ruim: sem contexto estruturado
{"level":"ERROR","message":"Falha ao criar usuário"}

// Bom: com campos estruturados
{
  "level": "ERROR",
  "message": "Falha ao criar usuário - email duplicado",
  "action": "create_user",
  "user_email_hash": "a1b2c3d4",
  "error_code": "EMAIL_DUPLICATE",
  "duration_ms": 150,
  "stack_trace": "com.app.UserService.createUser(UserService.java:45)\ncom.app.UserController.create(UserController.java:23)"
}

Para exceções, armazene o stack trace em campo separado e inclua a causa raiz como objeto aninhado:

{
  "level": "ERROR",
  "exception": {
    "type": "NullPointerException",
    "message": "Campo 'email' é nulo",
    "cause": "ValidationUtils.validateEmail",
    "stack_trace": "..."
  }
}

5. Estratégias de amostragem e filtragem

Em sistemas de alta frequência, a amostragem adaptativa é essencial:

// Amostragem por probabilidade (10% das requisições)
{"level":"INFO","sampling_rate":0.1,"request_id":"req-001","endpoint":"/api/users"}

// Filtragem por endpoint (ignorar health checks)
if (endpoint != "/health" && endpoint != "/metrics") {
    logger.info("Requisição processada", context);
}

// Rate limiting: máximo de 100 logs/minuto por serviço
if (rateLimiter.tryAcquire()) {
    logger.error("Falha no banco de dados", exception);
}

Amostragem adaptativa reduz custos de armazenamento sem perder visibilidade crítica. Para endpoints de health check, considere registrar apenas quando houver falha.

6. Integração com sistemas de observabilidade

Logs estruturados são enviados para agregadores centralizados:

// Envio para Loki via Promtail
{"level":"ERROR","service":"api-pedidos","trace_id":"abc123"}

// Correlação com tracing via OpenTelemetry
{"trace_id":"abc123","span_id":"def456","parent_span_id":"ghi789"}

// Política de retenção: 
// - ERROR e FATAL: 90 dias
// - INFO e WARN: 30 dias
// - DEBUG: 7 dias

A correlação entre logs, métricas e traces é feita através de IDs compartilhados (trace_id, span_id). Ferramentas como Grafana permitem navegar de um log para o trace correspondente, acelerando diagnósticos.

7. Testes e validação de logs

Testes unitários devem verificar a emissão correta de logs em cenários críticos:

// Teste unitário com JUnit + WireMock
@Test
void testCreateUserLogsErrorOnDuplicate() {
    // Simula falha
    when(userRepository.findByEmail("test@test.com")).thenReturn(Optional.of(existingUser));

    // Executa ação
    assertThrows(DuplicateEmailException.class, () -> userService.createUser("test@test.com"));

    // Verifica log emitido
    verify(logger).error(eq("Falha ao criar usuário - email duplicado"), any());
}

Ferramentas de linting como loglint validam o esquema dos logs:

// Validação de esquema com JSON Schema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": ["timestamp", "level", "message", "service_name"],
  "properties": {
    "level": {"enum": ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"]}
  }
}

8. Evolução contínua e governança

A governança de logs deve ser um processo contínuo:

// Documentação viva: exemplos esperados
Serviço: api-pedidos
Logs esperados:
  - Criação de pedido: INFO com campos {order_id, user_id, total}
  - Falha de pagamento: ERROR com campos {order_id, payment_gateway, error_code}

// Revisão trimestral do esquema
Data: 2024-03-15
Mudanças: Adicionado campo "region" para logs de infraestrutura

Templates e bibliotecas compartilhadas padronizam a implementação entre times:

// Biblioteca compartilhada (pseudo-código)
class StructuredLogger {
    log(level, message, context) {
        const logEntry = {
            timestamp: new Date().toISOString(),
            level,
            message,
            service_name: process.env.SERVICE_NAME,
            environment: process.env.NODE_ENV,
            ...context
        };
        console.log(JSON.stringify(logEntry));
    }
}

Revisões periódicas, documentação viva e automação garantem que as boas práticas evoluam junto com a aplicação, mantendo logs úteis e acionáveis.

Referências