Passar horas, ou mesmo dias, tentando consertar um bug obscuro é frustrante e improdutivo. Eventualmente, você vai acabar olhando para a tela esperando que um momento eureca aconteça magicamente.
Mas e se, em vez de esperar que a solução chegue até você magicamente, você tivesse o superpoder de rastrear sistematicamente qualquer bug de caso extremo com o qual está lidando?
Você pode-com o registro. Quando usado corretamente, o registro pode fornecer os insights necessários sobre seu aplicativo para que você possa descobrir exatamente o que aconteceu. O registro adequado pode ser a diferença entre um despejo ruim de instruções de depuração e uma ferramenta de depuração poderosa que ajuda a encontrar bugs mais facilmente e a corrigi-los mais rápido.
O que é uma biblioteca de registro e por que você deve usá-la?
No registro, é importante ter uma saída que seja fácil de ler por humanos e analisável por máquinas. Para nós, desenvolvedores, é importante que, quando olhamos e inspecionamos os registros, possamos entendê-los. As máquinas precisam ser capazes de analisar os registros para que possamos executar consultas avançadas e realizar agregações sofisticadas.
JSON é um formato que se encaixa em ambos os critérios, e é por isso que uma biblioteca de registro analisa a saída em um JSON válido e garante que seus registros sejam sempre formatados corretamente.
Você pode obter usando console.log
em projetos de hobby. Em aplicativos Node.js de nível de produção , no entanto, muitas vezes é útil ser capaz de distinguir entre diferentes níveis de registro.
Entre nas bibliotecas de registro, que também permitem que você ligue e desligue o registro em diferentes níveis. Em um ambiente de produção, você normalmente gostaria de ter erros e talvez avisos, mas em um ambiente de teste, registros de depuração/mais detalhados também são úteis, o que, de outra forma, adicionaria muito ruído na produção.
O que é Pino?
Pino é uma biblioteca de registro popular no ecossistema Node.js. É rápido e tem sobrecarga mínima.
Pré-requisitos para usar Pino
- Node.js versão>=12.17 ou>=13.10
- NPM (geralmente incluído com Node.js)
- Um servidor da web Express ao qual deseja adicionar o registro
Usando Pino em um aplicativo Node.js
Para instalar o Pino em seu projeto Node.js, execute:
pino de instalação npm
Crie um arquivo chamado logger.js
. Neste arquivo, vamos importar o Pino, configurá-lo e, em seguida, exportar uma instância de registro para ser usada em todo o projeto:
//logger.js const pino=requer ('pino'); //Crie uma instância de registro const logger=pino ({ nível: process.env.NODE_ENV==='produção'?'info':'debug', }); module.exports.logger=logger;
Usar o Pino é relativamente simples. Você pode registrar mensagens em diferentes níveis de registro (depuração, informação, aviso, erro, etc.) usando métodos com nomes semelhantes. Você também pode passar objetos e/ou erros para contexto adicional:
const {logger}=require ('./logger.js'); //Registra uma mensagem simples no nível"info" logger.info ('Aplicativo iniciado!'); //Outputs: //{"level": 30,"time": 1608568334356,"pid": 67017,"hostname":"Maxims-MacBook-Pro.local","msg":"Application started!"} //Registra um objeto além de uma mensagem para fornecer contexto const user={firstName:'Maxim', lastName:'Orlov'}; logger.info (usuário,'Usuário autenticado com sucesso'); //Outputs: //{"level": 30,"time": 1608568334356,"pid": 67017,"hostname":"Maxims-MacBook-Pro.local","firstName":"Maxim","lastName":"Orlov","msg":"Usuário autenticado com sucesso"} //Registra um erro no nível"erro" erro const=novo erro ('Banco de dados travado!'); logger.error (erro,'Falha ao buscar usuário'); //Outputs: //{"level": 50,"time": 1608568334356,"pid": 67017,"hostname":"Maxims-MacBook-Pro.local","stack":"Erro: banco de dados travado! \ n no objeto.(/Users/maxim/Code/playground/test.js:19:15)\n em Module._compile (node: internal/modules/cjs/loader: 1102: 14) \ n em Object.Module._extensions..js (nó: internal/modules/cjs/loader: 1131: 10) \ n em Module.load (nó: internal/modules/cjs/loader: 967: 32) \ n em Function.Module._load (nó: internal/modules/cjs/loader: 807: 14) \ n em Function.executeUserEntryPoint [como runMain] (nó: internal/modules/run_main: 76: 12) \ n no nó: internal/main/run_main_module: 17: 47","tipo":"Erro","msg":"Falha ao buscar usuário"}
Para integrar o Pino a um aplicativo Node.js existente, basta importar a instância do logger e usá-la em todo o projeto. Se o seu projeto estiver usando console.log
, você pode fazer uma busca ampla do projeto e substituir por logger.info
. Apenas certifique-se de importar a instância do logger na parte superior do arquivo.
Além disso, você pode passar algum tempo categorizando seus registros em diferentes níveis de registro. Isso permitirá que você diferencie as mensagens de log em diferentes níveis de gravidade, e você pode silenciar os logs abaixo de um determinado nível de log para ambientes específicos, como logs de depuração em produção.
No mínimo, eu uso os níveis de registro de “informações” e “erros” em meus projetos para poder distinguir facilmente os registros de erros dos registros de operação normal.
Aqui está um exemplo de aplicativo Node.js executando um servidor da web Express com um único endpoint para buscar um usuário:
//server.js const express=require ('express'); const {logger}=require ('./logger.js'); const db=require ('./db.js'); const PORT=process.env.PORT || 3000; const app=express (); app.get ('/users/: id', assíncrono (req, res)=> { deixe userId=req.params.id; if (isNaN (userId)) { logger.warn ({userId},'ID de usuário inválido'); return res.status (400).send ('ID de usuário inválido'); } outro { userId=Número (userId); } tentar { logger.info ({userId},'Buscando usuário do banco de dados'); const user=await db.getUser ({userId}); if (! usuário) { logger.warn ({userId},'Usuário não encontrado'); return res.status (404).send ('Usuário não encontrado'); } logger.debug ({usuário},'Usuário encontrado, enviando ao cliente'); retornar res.status (200).json (usuário); } catch (erro) { logger.error (erro,'Falha ao buscar usuário do banco de dados'); return res.status (500).send ('Ocorreu um erro ao buscar usuário'); } }); app.listen (PORTA, ()=> { logger.info (`Servidor ouvindo em http://localhost: $ {PORT}`); });
Observe os diferentes níveis de log (depuração, informação, aviso e erro) usados neste exemplo. Eu tendo a registrar com gravidade de “aviso” quando algo ocorreu que não é uma operação normal nem um erro, então algo intermediário.
Os logs de depuração são desabilitados por padrão no Pino, então eu tendo a usá-los para logs que adicionariam muito ruído na produção, mas podem ser úteis na preparação ou no desenvolvimento local durante a depuração.
O que é AsyncLocalStorage e como funciona?
Node.js é uma linguagem de thread único e, portanto, usa o loop de eventos para lidar com tarefas assíncronas simultâneas. Embora isso torne o Node.js muito rápido no atendimento de solicitações da Web, a desvantagem é que a pilha de funções e o contexto são perdidos no processo.
classe AsyncLocalStorage faz parte da async_hooks
módulo. É uma API Node.js relativamente nova que permite armazenar dados em funções de retorno de chamada e operações assíncronas.
Para usá-lo, você cria uma nova instância de classe e chama o método run
passando dois argumentos: o armazenamento e uma função de retorno de chamada.
O armazenamento pode ser qualquer coisa, desde um inteiro ou string simples até um objeto ou mapa complexo. A função de retorno de chamada, passada como o segundo argumento, será executada no contexto da loja. Para acessar a loja, chame o método getStore
na instância.
Vamos ver como isso funciona:
const {AsyncLocalStorage}=require ('async_hooks'); //Crie um novo contexto const context=new AsyncLocalStorage (); function doSomethingAsync () { //Use setImmediate para imitar a execução assíncrona setImmediate (()=> { const store=context.getStore (); store.get ('nome');//Maxim }); } function main () { loja const=novo Map (); store.set ('nome','Maxim'); //Execute a função de retorno de chamada (segundo argumento) dentro de um contexto com `armazenar` como dados context.run (store, ()=> { doSomethingAsync (); }); //Isso está fora do contexto context.getStore ();//Indefinido } a Principal();
Lembre-se de que a loja só pode ser acessada por meio da função de retorno de chamada e de qualquer um de seus filhos. Em qualquer lugar fora, o método getStore
retornará undefined
.
Este exemplo usa apenas um nível de aninhamento e ambas as funções estão no mesmo arquivo, então o benefício pode não parecer muito óbvio. No entanto, em um projeto maior, onde a pilha de funções tem várias camadas de profundidade e está espalhada por arquivos diferentes, você pode imaginar a utilidade de ter acesso a um armazenamento local de thread de qualquer lugar.
Associando logs a uma solicitação específica
O armazenamento e a recuperação de dados no nível da pilha têm vários casos de uso. Um caso de uso é associar logs a solicitações da web.
Quando confrontado com um log de erro em um aplicativo de produção, é extremamente útil ser capaz de ver todos os outros logs que fizeram parte do mesmo ciclo de solicitação/resposta. Isso permite que você rastreie a solicitação conforme ela percorre seu aplicativo, para que possa reunir as condições e variáveis que levaram a um bug específico.
Para fazer isso, precisamos atribuir algum tipo de identificador único a cada solicitação. Um Universally Unique ID (UUID) é o que estamos procurando e o uuid
biblioteca nos dá exatamente isso. Mais especificamente, geraremos UUIDs da versão 4 para cada solicitação.
Vamos expandir nosso aplicativo de exemplo Node.js acima. Primeiro, criaremos um módulo que exporta uma instância AsyncLocalStorage:
//async-context.js const {AsyncLocalStorage}=require ('async_hooks'); const context=new AsyncLocalStorage (); module.exports=context;
A seguir, expandiremos o arquivo logger.js
com uma função de middleware Express que cria um registrador filho com um ID de solicitação exclusivo e o adiciona ao armazenamento de contexto:
//logger.js const pino=requer ('pino'); const uuid=requer ('uuid'); const context=require ('./async-context.js'); //Crie uma instância de registro const logger=pino ({ nível: process.env.NODE_ENV==='produção'?'info':'debug', }); //Proxify a instância do logger para usar o logger filho do contexto, se existir module.exports.logger=new Proxy (logger, { get (target, property, receiver) { target=context.getStore () ?. get ('logger') || alvo; retornar Reflect.get (alvo, propriedade, receptor); }, }); //Gere um ID único para cada solicitação de entrada e armazena um registrador filho no contexto //para sempre registrar o ID do pedido module.exports.contextMiddleware=(req, res, next)=> { const child=logger.child ({requestId: uuid.v4 ()}); loja const=novo Map (); store.set ('logger', filho); retornar context.run (armazenar, próximo); };
Os registradores filhos são uma forma de adicionar estado a um registrador. As propriedades com as quais você cria o criador de logs filho são geradas em cada linha de log quando esse criador de logs filho é usado. Os registradores filhos são um excelente caso de uso para adicionar um ID de solicitação aos registros.
Além disso, também usamos um Proxy para modificar a instância de registro para registrar usando a instância do criador de logs filho, se houver.
Finalmente, vamos usar a função de middleware em server.js
:
//server.js const express=require ('express'); const {logger, contextMiddleware}=require ('./logger.js'); const db=require ('./db.js'); const PORT=process.env.PORT || 3000; const app=express (); //Anexe um ID de solicitação exclusivo a cada linha de registro app.use (contextMiddleware); app.get ('/users/: id', assíncrono (req, res)=> { deixe userId=req.params.id; if (isNaN (userId)) { logger.warn ({userId},'ID de usuário inválido'); return res.status (400).send ('ID de usuário inválido'); } outro { userId=Número (userId); } tentar { logger.info ({userId},'Buscando usuário do banco de dados'); const user=await db.getUser ({userId}); if (! usuário) { logger.warn ({userId},'Usuário não encontrado'); return res.status (404).send ('Usuário não encontrado'); } logger.debug ({usuário},'Usuário encontrado, enviando ao cliente'); retornar res.status (200).json (usuário); } catch (erro) { logger.error (erro,'Falha ao buscar o usuário do banco de dados'); return res.status (500).send ('Ocorreu um erro ao buscar usuário'); } }); app.listen (PORTA, ()=> { logger.info (`Servidor ouvindo em http://localhost: $ {PORT}`); });
É isso! Agora temos um ID de solicitação exclusivo anexado a cada linha de registro.
{...,"requestId":"da672623-818b-4b18-89ca-7eb073accbfe","userId": 1,"msg":"Buscando usuário do banco de dados"} {...,"requestId":"da672623-818b-4b18-89ca-7eb073accbfe","user": {...},"msg":"Usuário encontrado, enviando para o cliente"} {...,"requestId":"01107c17-d3c8-4e20-b1ed-165e279a9f75","userId": 2,"msg":"Buscando usuário do banco de dados"} {...,"requestId":"01107c17-d3c8-4e20-b1ed-165e279a9f75","usuário": {...},"msg":"Usuário encontrado, enviando ao cliente"}
Conclusão
Você pode encontrar um exemplo funcional completo neste repositório no Github .
Além da ID da solicitação, você também pode achar útil incluir outros dados em cada registro, como ID de usuário e/ou e-mail. Um bom lugar para fazer isso seria no middleware de autenticação logo após a autenticação de uma solicitação.
Agora você tem tudo no lugar para seguir a localização atual de cada bug obscuro que encontrar.
A postagem Registrando com Pino e AsyncLocalStorage no Node.js apareceu primeiro no LogRocket Blog .