Construindo uma API com NestJS, PostgreSQL e Docker —Parte 4: Adicionando logs à nossa aplicação
Recentemente comecei a estudar Node pensei em desenvolver uma API com funcionalidades básicas, que praticamente todo projeto precisa, como forma de me familiarizar com a linguagem. Para me auxiliar nessa tarefa utilizei o framework NestJS. Nesta série de tutoriais vou compartilhar o que aprendi.
Série completa:
Irei atualizando essa lista com links para as devidas partes conforme eu for postando!
- Parte 1: Criando nosso primeiro endpoint
- Parte 2: Adicionando Autenticação e Autorização
- Parte 3: Finalizando nosso CRUD de usuários
- Parte 4: Adicionando logs à nossa aplicação
- Parte 5: Enviando emails de confirmação e recuperação de senha
- Parte 6: Escrevendo testes
- Parte 7: Deploy!
Requisitos
O código da etapa anterior se encontra em um repositório no meu GitHub:
TL;DR
O código desta etapa também se encontra em um repositório no meu GitHub:
Logs
Logs são uma parte essencial de qualquer aplicação. É através deles que conseguimos manter um registro de tudo o que acontece no nosso servidor e conseguimos nos resguardar contra possíveis imprevistos.
Com a Lei Geral de Proteção de Dados (LGPD) batendo na porta, é importante utilizar de todos os meios disponíveis para garantir que sua aplicação mantenha um constante relatório de como ela está sendo usada, o que pode ajudar a identificar e conter possíveis ameaças.
Adicionar logs a uma API construída com NestJS é bem simples, e vou mostrar como fazer isso a seguir.
Winston
Para gerar um log de uso de nossa API vamos utilizar o Winston.
Winston é uma biblioteca de logs universal, com suporte a diferentes meios de saída, como saída em console ou saída em arquivo. No nosso caso, utilizaremos principalmente o recurso de saída em arquivo.
Aqui é possível encontrar uma lista das saídas suportadas pelo Winston (repare que a saída é referenciada como “transports”. Um transporte nada mais é do que um meio onde os logs podem ser armazenados. Um logger pode possuir vários transportes distintos, e às vezes até transportes repetidos, mas com configurações diferentes (ex.: manter um arquivo de logs de erro e um arquivo de logs geral, ambos utilizaram o transporte de arquivos, porém com configurações diferentes).
Reparem como o Winston é popular dentro da comunidade: há quatro tipos de transportes padrões do Winston, 3 transportes mantidos pelos mesmos membros que contribuem com o Winston e mais de 30 transportes mantidos pela comunidade.
Existem transportes que permitem gravar logs em bancos de dados (MongoDB, Postgres, SQLite), transporte para enviar logs via email (pode ser útil para notificar quando ocorre um erro crítico na aplicação), dentre vários outros. Vale a pena conferir as possibilidades!
NestJS + Winston
Vamos voltar ao foco desse tutorial: adicionar logs à nossa API construída com NestJS. Para isso, vamos primeiros instalar dois novos pacotes:
$ npm i --save winston nest-winston
O pacote nest-winston nada mais é do que o Winston encapsulado em um módulo do NestJS e precisa do pacote winston para funcionar, por motivos óbvios.
Agora temos um problema: seria interessante adicionar logs a todos os endpoints da nossa API. Entretanto, não parece uma saída interessante adicionar cada log individualmente, a cada endpoint nos nossos controllers.
E não é mesmo. Se você já trabalho com Express, ou Node em geral, pode pensar em criar uma middleware para resolver esse problema. E realmente um middleware global funcionaria em diversos cenários, e o NestJS aceita a criação de middleware da forma como você estaria acostumado.
Entretanto, ao consultar a documentação do NestJS podemos encontrar uma descrição do ciclo de vida de uma requisição dentro do NestJS. Em resumo, uma requisição passa pelas seguintes etapas:
Podemos perceber que uma middleware global seria executada assim que a requisição é recebida. No nosso caso, seria interessante se identificássemos o usuário que está realizando a requisição, no caso de endpoints que exigem autenticação. Isso não seria possível pois as guards só são executadas após as middlewares, então nosso usuário só poderia ser identificado após a execução das guards.
Mas é aí então que nos deparamos com um novo conceito interessante:
Interceptors!
Interceptors são classes injetáveis que contêm um método transform que recebe como argumentos o contexto de execução da aplicação e um segundo argumento do tipo CallHandler que contém uma função a ser executada para dar seguimento ao ciclo de vida da requisição após a execução do interceptor. Você pode ler a documentação completa de interceptors aqui.
Agora, mão na massa para criação do nosso interceptor de logs.
Primeiro, vamos criar um arquivo com as configurações desejadas do nosso logger, dentro da pasta configs, criada anteriormente:
Informações mais detalhadas sobre os parâmetros de configuração podem ser encontradas na documentação do Winston. No nosso caso, vale observar que criamos dois transports: um deles para exibição do log no console e outro para salvar o log em um arquivo.
Agora, vamos incluir o logger na nossa aplicação. Para que ele se torne o logger padrão da nossa aplicação podemos adicioná-lo aos parâmetros de inicialização do NestJS, em main.ts:
Vamos agora criar uma pasta chamada interceptors e dentro dela vamos criar o logger.interceptor.ts.
Alguns detalhes desse logger:
- Nós armazenamos 6 coisas da requisição: A data e hora que ela foi realizada, o método ou verbo HTTP utilizado, o endpoint que foi acessado, o conteúdo da requisição, o endereço de IP de origem da requisição e o email do usuário que realizou a requisição;
- Repare que nós eliminamos do corpo da requisição os parâmetros com nome password e passwordConfirmation que são os parâmetros com as senhas do usuário. Isso garante que nós não salvamos nos nossos logs informações tão sensíveis. Repare também que nós criamos uma cópia do corpo da requisição antes de remover esses dados, pois se fossem removidos direto do corpo da requisição os mesmos não seriam acessíveis posteriormente quando estivéssemos tratando nossa requisição.
Por último, só falta adicionarmos este interceptor de forma global à nossa aplicação. Para isso, vamos alterar nosso app.module.ts:
Pronto! Agora podemos testar nosso logger. Vamos inicializar a aplicação e enviar algumas requisições no Postman:
Vamos conferir a saída do nosso console:
Reparem que como a requisição feita não necessita de autenticação, não temos um email de usuário para mostrar. Reparem também que não salvamos a senha que o usuário usou para realizar a requisição. O IP não está sendo mostrado pois nossa aplicação está rodando em um ambiente local. Caso esteja usando o Docker para rodar a aplicação na sua máquina você já poderá ver no campo “from” o IP de onde partiu a requisição. Caso não esteja usando o Docker poderá ver este IP assim que subirmos nossa apliação para núvem, na última parte dessa série.
Vamos agora conferir o arquivo de logs que está sendo gerado, em logs/application.log.
No final do nosso arquivo podemos ver o registro da requisição que acabamos de fazer, que corresponde aos dados que vimos no console. Repare que o arquivo de logs gerado pelo Winston utiliza o formato JSON para armazenar os dados. Isso irá facilitar MUITO a analize desses arquivos posteriormente, podendo facilmente ser feito um script que leia as mensagens de log como JSON e filtre os resultados.
Agora vamos fazer uma requisição autenticado:
Conferindo nossos logs:
Reparem como agora, com o usuário autenticado, temos o email para identificar o usuário que realizou a requisição.
Com isso encerramos a parte de logs da nossa série. Na próxima parte vamos tratar do envio de emails!