Atualizado em 03/07/2024!
Eu estou desde 2006 no mercado de TI e durante todo esse período passei mais tempo dando manutenção e evoluindo aplicações já existentes do que criando elas do zero. Costumo sempre citar aos meus alunos o caso da RedeHost. Trabalhei por 5 anos lá, sendo que em pouco mais de um ano ajudei a desenvolver o sistema mais importante da empresa, o Painel de Controle de Hospedagem e Cloud, e nos outros quase 4 anos fiquei dando manutenção e evoluindo ele.
E este não foi um caso isolado.
E enquanto evoluir um produto ou sistema é uma tarefa bacana, dar manutenção e, principalmente, resolver problemas não é tanto assim. Alguns problemas você consegue reproduzir, outros, tem que caçar logs, relatos e qualquer pista possível para entender o que está acontecendo e poder resolver e é aí que os logs entram em cena.
Logging é o ato de registrar, em algum lugar, o que acontece na sua aplicação, para que possa tirar insights mais tarde mas, principalmente (na minha humilde opinião), identificar e corrigir anomalias. Geralmente isto é feito em arquivos de texto ou em mecanismos de persistência NoSQL como MongoDB e ELK. Esses registros não devem conter informações sensíveis dos clientes mas devem conter informações úteis para entender o que se passa na aplicação, principalmente erros inesperados.
Mas ao contrário da RedeHost e de outras pequenas empresas em que trabalhei e/ou fundei, onde eu tinha acesso fácil e rápido a logs de aplicação, trabalhei em algumas grandes corporações (que não posso citar nomes) onde tinha de pedir amém a meia dúzia de coordenadores, gerentes e analistas até conseguir que alguém acessasse um servidor para pegar um log de texto cru para mim, para então iniciar uma longa análise de algumas horas, não raro envolvendo mais de um técnico. Às vezes para resolver uma transação de cliente que custava poucos reais gastava-se centenas de reais, tudo por causa da ineficiência no logging da aplicação ou, muitas vezes, das aplicações.
Aí que entra a ideia do tutorial de hoje. Ao invés de apenas fazer o que 90% dos programadores fazem que é despejar os logs em arquivos de texto que depois dá uma trabalheira de guardar e analisar, que tal usar um serviço em nuvem que te permite não apenas salvar os logs mas consultá-los de maneira mais tranquila?
Lhes apresento o CloudWatch Logs da AWS.
O que é o CloudWatch?
O CloudWatch é um das centenas de serviços disponíveis na nuvem pública da AWS e que, por padrão, oferece serviços para monitoramento da sua infraestrutura na própria AWS. Ou seja, para registrar logs e criar alertas do que acontece nas suas Lambdas serverless, nos seus servidores EC2, nas suas filas do SQS, e por aí vai.
No entanto, o CloudWatch pode ser perfeitamente utilizado para registro e consulta de logs de qualquer natureza, como logs das suas aplicações, por exemplo. E como tudo na Amazon, você paga pelo consumo que tiver do serviço, sendo os principais custos para logging a coleta e o armazenamento (U$0,50 e U$0,03 por GB, respectivamente) na data que escrevo este tutorial.
Sim, possivelmente você possui custos mais baixos escrevendo em TXT no disco do seu servidor, mas a menos que suba uma stack ELK ou crie algum painel para gestão de um MongoDB dedicado para logs, a trabalheira que dá para obter e analisar logs de TXT se pagam rapidamente com uma solução pronta e na nuvem como o CloudWatch.
Sendo assim e, se você já usa outros serviços da AWS, o CloudWatch é uma excelente opção e você conta com os primeiros 5GB de coleta e mais 5GB de armazenamento gratuitos, sem prazo de expiração.
Vou partir do pressuposto aqui que você já tem uma conta criada na AWS e que está autenticado no painel de administração deles. Procure por CloudWatch e depois por Logs, para acessar a tela abaixo, onde você criará o grupo de logs para a sua aplicação.
Um grupo de logs é uma organização administrativa. Nele você poderá definir uma série de regras de retenção e armazenamento, mas ele não é diretamente ligado a uma única aplicação, ok?
Vou dar o nome de test-group e vou mandar criar, enquanto que na tela seguinte você pode especificar a validade dos seus logs o que é uma boa você definir um valor longo o bastante que lhe permita buscar acontecimentos recentes que precisam ser investigados, mas curto o bastante que evite que tenha altos custos com armazenamento. Sugiro começar com no mínimo 7 dias e no máximo 30 dias.
Ao terminar esta rápida configuração, você será direcionado para uma tela de listagem dos seus grupos de logs. Ao clicar no nome do seu grupo, você irá para uma tela de detalhes onde você tem acesso às streams de logs, aos filtros de métricas, assinaturas e insights.
Um grupo de logs pode ter várias streams, sendo que cada stream refere-se à uma fonte de logs, o que você pode entender como sendo uma aplicação da sua empresa. Mas não se preocupe em criar nada manualmente aqui ou de entender tudo agora, ficará mais claro quando mexermos no código.
Eu espero, haha.
Mas antes disso, vamos criar uma stream de logs no painel da Amazon, clicando no botão “Criar Stream de logs” dentro da tela do Log Group. Vou dar o nome de test-stream pra minha.
Como usar CloudWatch com Node.js?
Parto do pressuposto aqui que você possui uma aplicação Node.js ou que saiba criar uma, não é escopo deste tutorial ensinar a fazê-lo e, portanto, usarei o meu fork do projeto express-generator para criar uma para a gente rapidamente.
1 2 3 4 5 6 |
npm install -g https://github.com/luiztools/express-generator.git express --git nodejs-cloudwatch cd nodejs-cloudwatch npm i |
Com isto, você tem uma aplicação pronta para uso e é nela que vamos fazer os nossos testes do AWS CloudWatch com Node.js.
O primeiro passo para usarmos qualquer serviço da AWS com Node.js é instalarmos o AWS-SDK.
1 2 3 |
npm i aws-sdk dotenv-safe |
Aproveitei para instalar também o dotenv-safe, que é um pacote útil para gerenciarmos algumas variáveis de ambiente que vamos precisar.
Crie na raiz do seu projeto dois arquivos, um chamado .env e outro chamado .env.example.
O .env.example é nosso template de configurações, coloque nele o seguinte conteúdo.
1 2 3 4 5 6 7 8 9 |
#.env.example - commit to repo PORT= AWS_CW_REGION= AWS_CW_GROUP= AWS_CW_STREAM= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= |
Já o .env é o arquivo de configuração em si, coloque nele as mesmas variáveis do arquivo anterior, mas com os seus valores personalizados. O AWS_CW_REGION é a região do seu CloudWatch (geralmente a mesma região da sua aplicação, eu uso us-east-1). Esta informação você obtém no canto superior direito, clicando no nome da região.
Já as variáveis AWS_ACCESS_KEY e AWS_SECRET_ACCESS_KEY você obtém criando credenciais no menu My Security Credentials da imagem acima. Certifique-se de dar permissões de gestão no produto CloudWatch para estas credenciais que criar.
O seu arquivo .env completo deve se parecer como abaixo (não adianta copiar, coloque os SEUS valores). Os que você pode copiar são o AWS_CW_GROUP e o AWS_CW_STREAM, se você criou ambas com os mesmos nomes que eu.
1 2 3 4 5 6 7 8 9 |
#.env - don't commit to repo PORT=3000 AWS_CW_REGION=us-east-1 AWS_CW_GROUP=test-group AWS_CW_STREAM=test-stream AWS_ACCESS_KEY_ID=XXX AWS_SECRET_ACCESS_KEY=XXX |
Para que esse arquivo de configuração seja carregado na inicialização da aplicação, adicione a seguinte linha de código no topo do seu arquivo app.js (primeira linha, antes de tudo).
1 2 3 |
require('dotenv-safe').config(); |
O próximo passo é identificar na nossa aplicação, algum ponto comum para capturarmos e enviarmos erros para o CloudWatch. No caso de uma aplicação Express como essa que geramos, é muito comum termos um middleware para tratamento de erros, o que no nosso template encontra-se ao final do arquivo app.js, como abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// error handler app.use(function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); }); module.exports = app; |
O que esse código faz hoje é definir algumas variáveis no HTTP Response (variável res) e renderizar a view de erro para o usuário. Vamos ajustar este código para que ele, além de fazer o que já faz, registre um log deste erro.
Embora você possa querer logar todos os acessos à aplicação, lembre-se que o custo aqui é por GB de coleta e de armazenamento, então vamos focar no mais crítico que são os erros. Se tivermos os erros bem logados e de fácil acesso pelo time técnico, fazer o troubleshooting diário de aplicações em produção se torna muito mais simples.
Estamos com tudo pronto para começar!
Criando um módulo de logging
Antes de sair mexendo na aplicação, vamos criar um módulo de logging, que vai encapsular a complexidade dessa atividade para uso em diferentes pontos da nossa aplicação. Isso é uma boa prática para reuso de código, facilidade na manutenção e até para que, no futuro, você possa sair mais facilmente da AWS se assim o desejar.
Crie um arquivo logger.js na raiz da sua aplicação e coloque o seguinte conteúdo dentro dele.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
const AWS = require('aws-sdk'); async function describeLogStreams() { const cloudwatchlogs = new AWS.CloudWatchLogs({ region: process.env.AWS_CW_REGION }); const params = { logGroupName: process.env.AWS_CW_GROUP, }; return cloudwatchlogs.describeLogStreams(params).promise(); } let nextSequenceToken = null; async function createLog(message) { if (!nextSequenceToken) { const res = await describeLogStreams(); nextSequenceToken = res.logStreams[0].uploadSequenceToken; } const cloudwatchlogs = new AWS.CloudWatchLogs({ region: process.env.AWS_CW_REGION }); const params = { logEvents: [{message, timestamp: (new Date()).getTime()}], logGroupName: process.env.AWS_CW_GROUP, logStreamName: process.env.AWS_CW_STREAM, sequenceToken: nextSequenceToken }; const response = await cloudwatchlogs.putLogEvents(params).promise(); nextSequenceToken = response.nextSequenceToken; return response; } module.exports = { createLog } |
Aqui temos duas funções, uma interna que só vai ser usada no próprio módulo, e outra externa, que vai ser chamada pela aplicação.
A função describeLogStreams faz uma consulta na sua conta da AWS e acessa seus logs, para trazer as informações das Log Streams que você possui. Precisamos disso pois quando a gente for inserir um log pela primeira vez após subir nossa aplicação, precisamos de um sequence token que a AWS gera pra gente e que alimentará uma variável homônima.
Já a segunda função, createLog, espera uma mensagem e acessar o CloudWatch Logs para salvar esta mensagem com o timestamp atual e uma série de configurações que em sua maioria temos em nossas variáveis de ambiente, além do sequence token que se já tivermos localmente, ótimo, caso contrário, ele irá até a AWS buscar.
Agora, para usarmos este módulo de logging, vá até o middleware de erro no seu app.js e adicione nele uma chamada, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// error handler const {createLog} = require('./logger'); app.use(async (err, req, res, next) => { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; await createLog(err.message); // render the error page res.status(err.status || 500); res.render('error'); }); |
E por fim, para conseguirmos testar esta configuração, crie uma rota que intencionalmente irá disparar um erro.
1 2 3 4 5 |
app.use('/erro', (req, res, next) => { throw new Error('Just a test error'); }); |
Agora rode a sua aplicação com “npm start”, acesse a rota de erro e verifique que no painel da AWS, se tudo foi feito corretamente, teremos uma mensagem de erro dentro da Log Stream que configuramos para a aplicação.
E voilá, agora seus logs estão sendo guardados na nuvem!
Depois, você pode criar contas na AWS com permissão somente de visualização dos logs para todos os que precisam acessar com frequência logs de aplicação e a fim de solucionar problemas em produção.
Opcionalmente, você pode combinar esta técnica com log files em Winston e/ou usar o Winston para se comunicar com o CloudWatch, para que tenha mais detalhes ainda em logs locais, inclusive tem um pacote que une os dois mundos, o winston-cloudwatch. Uma vez com todo o setup configurado na AWS e as variáveis de ambiente setadas na sua aplicação, usá-lo é bem simples, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import winston from 'winston'; import WinstonCloudWatch from 'winston-cloudwatch'; const AWS_LOG_GROUP = process.env.AWS_CW_GROUP; const AWS_LOG_STREAM = process.env.AWS_CW_STREAM; const AWS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; const AWS_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; const AWS_LOG_REGION = process.env.AWS_CW_REGION; const logger = winston.createLogger({ format: winston.format.combine( winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new WinstonCloudWatch({ level: 'error', logGroupName: AWS_LOG_GROUP, logStreamName: AWS_LOG_STREAM, awsAccessKeyId: AWS_KEY_ID, awsSecretKey: AWS_ACCESS_KEY, awsRegion: AWS_LOG_REGION }) ] }) |
Para mais detalhes de como funciona o Winston, confira este tutorial.
Caso tenha gostado deste tutorial, considere conhecer minha formação em Web FullStack JS, clicando no banner abaixo!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.