Leodario.com

Leodario.com – Tudo sobre Tecnologia

Aula 38 – API JavaScript, Node.js e Express – Logging Middleware – morgan, winston

Imagem destacada da aula de API

Introdução (3 min)

Estimados alunos e futuras mentes brilhantes do desenvolvimento de APIs! Sejam muito bem-vindos à nossa trigésima oitava aula. Preparem-se para mergulhar em um dos pilares da robustez e confiabilidade de qualquer aplicação moderna: o registro de eventos, ou como chamamos no jargão técnico, logging.

Imagine que sua API é como um enorme centro de comando e controle de tráfego aéreo. Cada avião (requisição) que decola ou aterrissa, cada comunicação com a torre (servidor), cada falha de motor (erro na aplicação) – tudo precisa ser meticulosamente anotado em um diário de bordo. Se algo der errado, ou se quisermos entender os padrões de voo, esse diário é a nossa única fonte da verdade. No mundo das APIs, esse diário é o nosso sistema de logging.

É vital para APIs contemporâneas ter um sistema de logging eficaz. Ele não apenas nos auxilia a identificar e resolver problemas de forma ágil, mas também fornece visibilidade sobre o comportamento dos usuários, padrões de uso e até mesmo tentativas de acesso não autorizado. É a sua primeira linha de defesa e a sua ferramenta mais potente para monitoramento.

Nesta sessão, vamos praticar a implementação de um sistema de logging profissional usando duas ferramentas extremamente populares no ecossistema Node.js/Express: o morgan para registrar requisições HTTP de maneira elegante e o winston para um gerenciamento de logs mais sofisticado e personalizável. Você vai desenvolver código funcional que poderá ser empregado imediatamente em seus projetos.

Dentro do universo Node.js e Express, o conceito de middleware é central. Ele consiste em funções que interceptam as requisições antes que elas cheguem às suas rotas finais. O morgan é um exemplo clássico de middleware de logging, enquanto o winston pode ser integrado através de um middleware customizado para oferecer um controle granular sobre seus registros.

Conceito Fundamental (7 min)

O logging é, em sua essência, o ato de registrar informações sobre a execução de um programa. São como as caixas-pretas de um avião: gravam todos os eventos pertinentes que acontecem enquanto o sistema está operacional. Isso abrange desde uma simples requisição recebida até um erro crítico de banco de dados.

A terminologia correta da indústria é fundamental para uma comunicação clara. Entendamos alguns termos:

    • Log: É o registro individual de um evento. Cada linha em um arquivo de log é um log.
    • Logger: É a ferramenta ou biblioteca responsável por coletar, formatar e enviar os logs para seus destinos.
    • Nível de Log (Log Level): Indica a gravidade ou importância de um log. Os níveis comuns são debug, info, warn, error e fatal. O debug é para informações detalhadas de depuração, enquanto error e fatal são para falhas críticas.
    • Transport: É o destino onde os logs são armazenados ou exibidos. Pode ser o console, um arquivo, um banco de dados, um serviço de nuvem, etc.
    • Middleware: No Express.js, é uma função que tem acesso aos objetos de requisição (req), resposta (res) e à próxima função middleware no ciclo de requisição-resposta. Ela pode modificar esses objetos, executar código arbitrário, finalizar o ciclo de requisição-resposta ou invocar o próximo middleware.

Os casos de uso reais em produção para logging são vastos e demonstram a sua indispensabilidade:

    • Depuração e Diagnóstico: Quando um erro ocorre, os logs são a primeira fonte para entender o que aconteceu e onde.
    • Monitoramento de Performance: Registrar o tempo de resposta de requisições ajuda a identificar gargalos e otimizar o desempenho.
    • Auditoria e Segurança: Rastrear acessos, autenticações e modificações importantes é valioso para auditorias e para detectar atividades suspeitas.
    • Análise de Uso: Compreender como os usuários interagem com sua API pode subsidiar decisões de negócios e de desenvolvimento.
    • Conformidade Regulatória: Muitas regulamentações exigem que as empresas mantenham registros detalhados de certas operações.

A integração desses conceitos com outras tecnologias é orgânica. O morgan se integra diretamente como um middleware Express, observando cada requisição HTTP. O winston, sendo uma biblioteca de logging mais abrangente, pode ser invocado em qualquer ponto da sua aplicação (rotas, controladores, serviços) para registrar eventos específicos, e também ser encapsulado em um middleware para logs mais genéricos.

As vantagens de um sistema de logging bem arquitetado são claras: melhora significativa na capacidade de depuração, monitoramento proativo da saúde da aplicação, aumento da segurança e conformidade, e uma base sólida para análises operacionais. No entanto, existem desvantagens que merecem atenção: o overhead de I/O para escrever logs, o volume de armazenamento necessário para manter esses logs e a complexidade inerente ao gerenciamento e análise de grandes volumes de dados de log, especialmente em sistemas distribuídos.

Implementação Prática (10 min)

Agora é o momento de colocar a mão na massa e construir nosso sistema de logging. Vamos desenvolver um servidor Express simples e integrar morgan e winston de forma inteligente e eficaz.

Passo 1: Configuração Inicial do Projeto

Primeiro, crie um novo diretório para o projeto e inicialize-o com npm:

mkdir api-logging-aula
cd api-logging-aula
npm init -y

Em seguida, instale as dependências essenciais: express para o servidor, morgan para os logs de requisição HTTP e winston para um logging mais robusto.

npm install express morgan winston

Passo 2: Configuração do Winston Logger (logger.js)

Crie um arquivo chamado logger.js para centralizar a configuração do winston. Isso é uma melhor prática enterprise, pois viabiliza a reutilização e facilita a manutenção.

// logger.js
const winston = require('winston');
const path = require('path'); // Módulo nativo do Node.js para trabalhar com caminhos de arquivos

// Definindo o formato dos logs // O format.combine permite combinar múltiplos formatos. // timestamp adiciona a data e hora ao log. // printf formata a mensagem final do log. const logFormat = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), // Formato da data e hora winston.format.printf(info => { // Exemplo: 2023-10-27 10:30:00 [info]: Minha mensagem de log. return ${info.timestamp} [${info.level.toUpperCase()}]: ${info.message}; }) );

// Configurando os transports (destinos dos logs) const logger = winston.createLogger({ level: 'info', // Nível de log padrão: 'info' e acima (warn, error, fatal) serão registrados format: logFormat, // Aplicando o formato definido acima transports: [ // Transport para console: logs serão exibidos no terminal new winston.transports.Console({ level: 'debug', // No console, podemos querer ver logs de debug também format: winston.format.combine( winston.format.colorize(), // Adiciona cores aos logs no console logFormat // Reutiliza o formato base ) }), // Transport para arquivo de logs de nível 'info' e acima // path.join garante que o caminho funcione corretamente em diferentes sistemas operacionais. new winston.transports.File({ filename: path.join(__dirname, 'logs', 'app.log'), // Caminho do arquivo de log geral level: 'info' // Apenas logs de 'info' e superiores serão gravados aqui }), // Transport para arquivo de logs de erro (nível 'error' e acima) // É uma boa prática ter um arquivo separado para erros para facilitar a monitoria. new winston.transports.File({ filename: path.join(__dirname, 'logs', 'error.log'), // Caminho do arquivo de log de erros level: 'error' // Apenas logs de 'error' e superiores serão gravados aqui }) ] });

module.exports = logger; // Exporta a instância do logger para ser usada em outros arquivos

Para a compatibilidade com HostGator Plano M, é crucial garantir que o diretório logs exista e tenha permissões de escrita. Você pode criar o diretório manualmente ou adicionar um script simples no package.json para criá-lo ao iniciar o servidor, ou usar o fs.mkdirSync com recursive: true no logger.js (embora isso não seja ideal em produção para cada inicialização, para um exemplo prático serve).

mkdir logs

Passo 3: Servidor Express com Morgan e Winston (server.js)

Agora, vamos criar o arquivo principal do nosso servidor, server.js, e integrar tudo.

// server.js
const express = require('express');
const morgan = require('morgan');
const logger = require('./logger'); // Importa o logger que configuramos

const app = express(); const PORT = process.env.PORT || 3000;

// Configuração do Morgan para logs de requisição HTTP // O formato 'combined' é um padrão que inclui muitas informações úteis. // A opção 'stream' permite redirecionar os logs do morgan para o winston. app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } }));

// Middleware para parsing de JSON no corpo das requisições app.use(express.json());

// Exemplo de rota: GET / app.get('/', (req, res) => { logger.info('Requisição GET recebida na rota principal.'); // Log com winston res.status(200).send('Bem-vindo à API de Logging!'); });

// Exemplo de rota: POST /data app.post('/data', (req, res) => { const { item } = req.body; // Pega o item do corpo da requisição

// Validação de entrada robusta (melhores práticas enterprise) if (!item) { logger.warn('Tentativa de POST /data sem o campo "item".'); // Log de advertência return res.status(400).json({ error: 'O campo "item" é obrigatório.' }); }

logger.info(Dado recebido: ${item}); // Log de informação // Simula algum processamento res.status(201).json({ message: Item "${item}" processado com sucesso! }); });

// Exemplo de rota com erro simulado app.get('/error', (req, res, next) => { try { logger.debug('Tentando acessar rota /error. Gerando um erro intencionalmente.'); throw new Error('Este é um erro simulado na aplicação!'); // Gera um erro } catch (error) { // O error handling impressionante! // Envia o erro para o próximo middleware de tratamento de erros next(error); } });

// Middleware de tratamento de erros global (melhor prática) // Este middleware captura qualquer erro que seja passado para next(error) app.use((err, req, res, next) => { // Registra o erro com o winston, utilizando o nível 'error' logger.error(Ocorreu um erro: ${err.message}, err); // Inclui a mensagem e o objeto do erro completo // Responde ao cliente com uma mensagem de erro genérica (segurança: não exponha detalhes internos) res.status(500).json({ error: 'Ops! Ocorreu um erro interno no servidor.' }); });

// Inicia o servidor app.listen(PORT, () => { logger.info(Servidor Express operando na porta ${PORT}); logger.debug('Modo de depuração ativo.'); // Log de debug só aparece no console });

Variações e Alternativas:

    • Morgan Formats: Experimente outros formatos pré-definidos do morgan como 'dev' (mais conciso, colorido), 'tiny' ou 'short'. Você também pode definir formatos customizados.
    • Winston Transports: O winston é extremamente flexível. Você pode adicionar transports para enviar logs para serviços de nuvem como AWS CloudWatch, Google Cloud Logging, ou ferramentas de monitoramento como Sentry e Loggly, utilizando pacotes adicionais. Para HostGator Plano M, mantenha-se nos logs de arquivo.
    • Níveis de Log: Altere o logger.level no logger.js para debug ou warn para ver como o volume de logs muda. Em produção, info ou warn é o mais comum.

Configurações Específicas para HostGator Plano M:

Seus arquivos de log (app.log, error.log) serão criados dentro da pasta logs no mesmo diretório do seu server.js. Certifique-se de que a conta de usuário do servidor da HostGator que executa seu Node.js tenha permissões de escrita para a pasta logs e para os arquivos dentro dela. Geralmente, chmod 755 logs e chmod 644 logs/*.log são permissões seguras para iniciar.

O HostGator Plano M tipicamente não oferece ferramentas avançadas de gerenciamento de logs (como rotação automática de logs). Você pode precisar implementar sua própria lógica de rotação (por exemplo, usando o pacote winston-daily-rotate-file) para evitar que os arquivos de log cresçam indefinidamente e consumam todo o espaço em disco. Mantenha os logs em info para minimizar o volume.

Testes Básicos Incluídos:

Para testar, salve os arquivos (logger.js, server.js) e execute o servidor:

node server.js

Você verá os logs iniciais no seu terminal. Agora, abra outro terminal ou use ferramentas como curl ou Postman para enviar requisições:

    • Requisição GET simples:
      curl http://localhost:3000/
              

    • Requisição POST com dados (válida):
      curl -X POST -H "Content-Type: application/json" -d '{"item": "produto-x"}' http://localhost:3000/data
              

    • Requisição POST sem dados (inválida):
      curl -X POST -H "Content-Type: application/json" -d '{}' http://localhost:3000/data
              

    • Requisição que gera erro:
      curl http://localhost:3000/error
              

Após cada requisição, observe o terminal onde o servidor está rodando (logs do console) e verifique o conteúdo dos arquivos logs/app.log e logs/error.log.

Exercício Hands-On (5 min)

Para solidificar seu aprendizado, proponho um desafio prático: Modifique o logger para incluir um ID único de requisição em cada log. Isso é uma prática valiosa em cenários complexos, pois permite rastrear todos os logs pertencentes a uma única requisição através de diferentes sistemas ou serviços.

Desafio: Adicionar um ID de Requisição Único aos Logs

Crie um middleware Express que gera um ID único para cada requisição recebida e o anexa ao objeto req. Em seguida, modifique o formato do winston para que este ID seja incluído em todas as mensagens de log associadas a essa requisição.

Solução Detalhada Passo a Passo:

    • Instalar um Gerador de ID Único:

      Para gerar IDs realmente únicos, usaremos a biblioteca uuid. Instale-a:

      npm install uuid
              

    • Criar o Middleware de ID de Requisição:

      Crie um novo middleware em server.js (ou em um arquivo separado como requestIdMiddleware.js para melhor organização).

      // Adicione esta linha no topo do server.js, junto com os outros 'requires'
      const { v4: uuidv4 } = require('uuid'); // Importa a função v4 do pacote uuid

      // ...

      // Middleware para gerar um ID único para cada requisição (adicione antes de morgan e outras rotas) app.use((req, res, next) => { req.id = uuidv4(); // Gera um UUID (Universally Unique Identifier) e o anexa a req.id logger.debug(Requisição ID: ${req.id} - Início da requisição para ${req.method} ${req.originalUrl}); next(); // Continua para o próximo middleware ou rota });

      // ...

    • Modificar o Formato do Winston Logger:

      Precisamos que o logger tenha acesso ao req.id. Isso é um pouco mais complexo, pois o winston por padrão não está ciente do contexto da requisição. Uma solução elegante é adicionar o req.id ao objeto info que o winston processa. Vamos adaptar o logger.js e o server.js.

      Primeiro, modifique logger.js para aceitar um defaultMeta que pode incluir o requestId:

      // logger.js
      const winston = require('winston');
      const path = require('path');

      const logFormat = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.printf(info => { // Se houver um requestId, inclua-o no log const requestId = info.requestId ? [${info.requestId}] : ''; return ${info.timestamp} ${requestId}[${info.level.toUpperCase()}]: ${info.message}; }) );

      const logger = winston.createLogger({ level: 'info', format: logFormat, // Adicione um campo defaultMeta, que pode ser sobrescrito em cada log defaultMeta: { service: 'api-logging' }, transports: [ new winston.transports.Console({ level: 'debug', format: winston.format.combine( winston.format.colorize(), logFormat ) }), new winston.transports.File({ filename: path.join(__dirname, 'logs', 'app.log'), level: 'info' }), new winston.transports.File({ filename: path.join(__dirname, 'logs', 'error.log'), level: 'error' }) ] });

      // Exporta o logger para ser usado em outros arquivos module.exports = logger;

      Em seguida, em server.js, cada vez que você usar o logger, passe o requestId como um metadado adicional:

      // server.js
      // ... (imports e setup do Express)

      const { v4: uuidv4 } = require('uuid'); const logger = require('./logger'); // Certifique-se de que o logger.js foi modificado

      // ... (morgan setup)

      // Middleware para gerar um ID único para cada requisição (coloque antes das rotas) app.use((req, res, next) => { req.id = uuidv4(); // Gera um UUID e o anexa a req.id // logger.debug(Requisição ID: ${req.id} - Início da requisição para ${req.method} ${req.originalUrl}); next(); });

      // Middleware para anexar o request ID ao logger para esta requisição // Isso é um pattern common para winston chamado "child logger" ou "contextual logging" app.use((req, res, next) => { req.logger = logger.child({ requestId: req.id }); // Cria um "logger filho" com o requestId req.logger.info(Requisição recebida: ${req.method} ${req.originalUrl}); next(); });

      app.get('/', (req, res) => { req.logger.info('Requisição GET recebida na rota principal.'); // Agora usa req.logger res.status(200).send('Bem-vindo à API de Logging!'); });

      app.post('/data', (req, res) => { const { item } = req.body; if (!item) { req.logger.warn('Tentativa de POST /data sem o campo "item".', { payload: req.body }); // Pode incluir payload return res.status(400).json({ error: 'O campo "item" é obrigatório.' }); } req.logger.info(Dado recebido: ${item}); res.status(201).json({ message: Item "${item}" processado com sucesso! }); });

      app.get('/error', (req, res, next) => { try { req.logger.debug('Tentando acessar rota /error. Gerando um erro intencionalmente.'); throw new Error('Este é um erro simulado na aplicação!'); } catch (error) { next(error); } });

      app.use((err, req, res, next) => { // Usamos o req.logger aqui também para garantir que o ID da requisição seja registrado req.logger.error(Ocorreu um erro: ${err.message}, { stack: err.stack, requestId: req.id }); res.status(500).json({ error: 'Ops! Ocorreu um erro interno no servidor.' }); });

      app.listen(PORT, () => { logger.info(Servidor Express operando na porta ${PORT}); // Este log não terá requestId pois está fora do contexto da requisição logger.debug('Modo de depuração ativo.'); });

Como Testar e Validar o Resultado:

Reinicie seu servidor (node server.js) e faça as requisições de teste novamente. Observe os logs no terminal e nos arquivos logs/app.log e logs/error.log. Você deverá ver um ID único (um UUID) no início de cada linha de log gerada a partir de uma requisição.

# Exemplo de saída esperada no log:
2023-10-27 11:45:30 [a1b2c3d4-e5f6-7890-1234-567890abcdef] [INFO]: Requisição GET recebida na rota principal.

Troubleshooting dos Erros Mais Comuns:

    • ID de Requisição Ausente: Verifique se o middleware app.use((req, res, next) => { req.id = uuidv4(); ... }) está ANTES de qualquer rota ou middleware que utilize o req.logger.
    • Logs Não Aparecem: Confirme os níveis de log no logger.js (level: 'info') e no transport do console (level: 'debug'). Verifique se os arquivos de log estão sendo criados e se o diretório logs tem permissões de escrita.
    • Erro uuidv4 is not a function: Certifique-se de que a importação do uuid está correta: const { v4: uuidv4 } = require('uuid');.

Próximos Passos Sugeridos:

Excelente trabalho! Você já tem uma base sólida. Para ir além, considere:

    • Rotação de Logs: Implemente um sistema de rotação de logs (como winston-daily-rotate-file) para gerenciar o tamanho dos arquivos de log, o que é fundamental em produção e em ambientes com espaço de disco limitado como o HostGator.
    • Logging Estruturado/Semântico: Em vez de apenas mensagens de texto, logue objetos JSON. Isso facilita a análise por ferramentas de agregação de logs (ELK Stack, Grafana Loki). O winston.format.json() pode ajudar com isso.
    • Integração com Serviços Externos: Explore como enviar seus logs para serviços de monitoramento e agregação na nuvem.
    • Contexto de Usuário: Adicione informações sobre o usuário autenticado aos logs, para uma auditoria ainda mais detalhada.

Parabéns por dominar mais um conceito fundamental para se tornar um desenvolvedor de APIs de excelência! O logging é um superpoder que o capacitará a construir sistemas mais confiáveis e observáveis. Sigamos em frente, com paixão pelo conhecimento!

🚀 Pronto para a próxima aula?

Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!

📚 Ver todas as aulas