Como Usar o Winston para Registrar Aplicações Node.js no Ubuntu 20.04

Introdução

Uma solução de registro eficaz é crucial para o sucesso de qualquer aplicativo. Winston é uma biblioteca de registro versátil e uma solução de registro popular disponível para aplicativos Node.js. As características do Winston incluem suporte para várias opções de armazenamento, níveis de registro, consultas de registro e um profiler integrado.

Neste tutorial, você usará o Winston para registrar um aplicativo Node/Express que você criará como parte desse processo. Você também verá como combinar o Winston com o Morgan, outro middleware de registro de solicitações HTTP popular para Node.js, para consolidar os registros de dados de solicitações HTTP com outras informações. Após a conclusão deste tutorial, seu servidor Ubuntu estará executando um pequeno aplicativo Node/Express, e o Winston será implementado para registrar erros e mensagens em um arquivo e no console.

Pré-requisitos

Para seguir este tutorial, você precisará de:

Passo 1 — Criando um Aplicativo Básico Node/Express

Winston é frequentemente utilizado para registrar eventos de aplicações web construídas com Node.js. Neste passo, você criará um simples aplicativo web Node.js usando o framework Express. Você usará express-generator, uma ferramenta de linha de comando, para iniciar rapidamente um aplicativo web Node/Express.

Como você instalou o Gerenciador de Pacotes Node durante os pré-requisitos, você pode usar o comando npm para instalar o express-generator:

  1. sudo npm install express-generator -g

A flag -g instala o pacote globalmente, o que significa que ele pode ser usado como uma ferramenta de linha de comando fora de um projeto/módulo Node existente.

Com o express-generator instalado, você pode criar seu aplicativo usando o comando express, seguido pelo nome do diretório que você deseja usar para o projeto:

  1. express myApp

Para este tutorial, o projeto será chamado myApp.

Nota: Também é possível executar a ferramenta express-generator diretamente sem instalá-la globalmente como um comando de sistema primeiro. Para fazer isso, execute este comando:

  1. npx express-generator myApp

O comando npx é um executor de comandos enviado com o Node Package Manager que facilita a execução de ferramentas de linha de comando do registro npm.

Durante a primeira execução, ele perguntará se você concorda em baixar o pacote:

Output
Need to install the following packages:
  express-generator
Ok to proceed? (y)

Responda y e pressione ENTER. Agora você pode usar npx express-generator no lugar de express.

Em seguida, instale Nodemon, que irá recarregar automaticamente o aplicativo sempre que você fizer alterações. Um aplicativo Node.js precisa ser reiniciado sempre que são feitas alterações no código fonte para que essas alterações tenham efeito, então o Nodemon irá automaticamente observar as alterações e reiniciar o aplicativo. Como você deseja poder usar o nodemon como uma ferramenta de linha de comando, instale-o com a flag -g:

  1. sudo npm install nodemon -g

Para finalizar a configuração do aplicativo, vá para o diretório do aplicativo e instale as dependências da seguinte forma:

  1. cd myApp
  2. npm install

Por padrão, as aplicações criadas com express-generator rodam na porta 3000, então você precisa garantir que o firewall não bloqueie a porta.

Para abrir a porta 3000, execute o seguinte comando:

  1. sudo ufw allow 3000

Agora você tem tudo o que precisa para iniciar sua aplicação web. Para fazer isso, execute o seguinte comando:

  1. nodemon bin/www

Este comando inicia a aplicação na porta 3000. Você pode testar se está funcionando apontando seu navegador para http://seu_ip_do_servidor:3000. Você deverá ver algo assim:

Neste ponto, você pode iniciar uma segunda sessão SSH para o seu servidor para o restante deste tutorial, deixando a aplicação web que você acabou de iniciar rodando na sessão original. Para o resto deste artigo, a sessão SSH inicial que está atualmente rodando a aplicação será chamada de Sessão A. Todos os comandos na Sessão A aparecerão em um fundo azul escuro como este:

  1. nodemon bin/www

Você usará a nova sessão SSH para executar comandos e editar arquivos. Esta sessão será chamada de Sessão B. Todos os comandos na Sessão B aparecerão em um fundo azul claro como este:

  1. cd ~/myApp

A menos que seja observado o contrário, você executará todos os comandos restantes na Sessão B.

Neste passo, você criou o aplicativo básico. Em seguida, você irá personalizá-lo.

Passo 2 — Personalizando as Variáveis de Log

Embora a aplicação padrão criada pelo express-generator seja um bom ponto de partida, você precisa personalizar a aplicação para que ela chame o logger correto quando necessário.

O express-generator inclui o middleware de logging HTTP Morgan que você irá usar para registrar dados sobre todas as requisições HTTP. Como o Morgan suporta fluxos de saída, ele se combina bem com o suporte a fluxos incorporado no Winston, permitindo que você consolide logs de dados de requisições HTTP com qualquer outra coisa que você escolher para logar com o Winston.

O boilerplate do express-generator usa a variável logger ao referenciar o pacote morgan. Como você usará tanto o morgan quanto o winston, que são ambos pacotes de logging, pode ser confuso chamar qualquer um deles de logger. Para especificar qual variável você deseja, você pode alterar as declarações de variável editando o arquivo app.js.

Para abrir o app.js para edição, use o nano ou seu editor de texto favorito:

  1. nano ~/myApp/app.js

Encontre a seguinte linha perto do topo do arquivo:

~/myApp/app.js
...
var logger = require('morgan');
...

Altere o nome da variável de logger para morgan:

~/myApp/app.js
...
var morgan = require('morgan');
...

Esta atualização especifica que a variável declarada morgan chamará o método require() vinculado ao logger de requisição Morgan.

Você precisa encontrar onde mais a variável logger foi referenciada no arquivo e mudá-la para morgan. Você também precisará alterar o formato de log usado pelo pacote morgan para combined, que é o formato de log padrão do Apache e incluirá informações úteis nos logs, como o endereço IP remoto e o cabeçalho de solicitação HTTP do usuário-agente.

Para fazer isso, encontre a seguinte linha:

~/myApp/app.js
...
app.use(logger('dev'));
...

Atualize para o seguinte:

~/myApp/app.js
...
app.use(morgan('combined'));
...

Essas alterações ajudarão você a entender qual pacote de log está sendo referenciado a qualquer momento após a integração da configuração do Winston.

Ao terminar, salve e feche o arquivo.

Agora que seu aplicativo está configurado, você pode começar a trabalhar com o Winston.

Passo 3 — Instalando e Configurando o Winston

Neste passo, você instalará e configurará o Winston. Você também explorará as opções de configuração disponíveis como parte do pacote winston e criará um logger para registrar informações em um arquivo e no console.

Instale o winston com o seguinte comando:

  1. cd ~/myApp
  2. npm install winston

É útil manter quaisquer arquivos de configuração de suporte ou utilitários para suas aplicações em um diretório especial. Crie uma pasta config que conterá a configuração do winston:

  1. mkdir ~/myApp/config

Em seguida, crie uma pasta que conterá seus arquivos de log:

  1. mkdir ~/myApp/logs

Finalmente, instale app-root-path:

  1. npm install app-root-path --save

O pacote app-root-path é útil ao especificar caminhos no Node.js. Embora este pacote não esteja diretamente relacionado ao Winston, é útil ao determinar caminhos para arquivos no Node.js. Você o usará para especificar a localização dos arquivos de log do Winston a partir da raiz do projeto e evitar uma sintaxe de caminho relativo feia.

Agora que a configuração para manipulação de logs está no lugar, você pode definir suas configurações. Crie e abra ~/myApp/config/winston.js para edição:

  1. nano ~/myApp/config/winston.js

O arquivo winston.js conterá sua configuração do winston.

Em seguida, adicione o seguinte código para requerer os pacotes app-root-path e winston:

~/myApp/config/winston.js
const appRoot = require('app-root-path');
const winston = require('winston');

Com essas variáveis no lugar, você pode definir as configurações de configuração para seus transportes. Transportes são um conceito introduzido pelo Winston que se refere aos mecanismos de armazenamento/saída usados para os logs. O Winston vem com quatro transportes principais integrados: Console, File, HTTP e Stream.

Você se concentrará nos transportes de console e de arquivo para este tutorial. O transporte de console registrará informações no console, e o transporte de arquivo registrará informações em um arquivo especificado. Cada definição de transporte pode conter configurações de configuração, como tamanho do arquivo, níveis de log e formato de log.

Aqui está um resumo rápido das configurações que você usará para cada transporte:

  • nível: nível das mensagens a serem registradas.
  • nome_do_arquivo: o arquivo a ser usado para escrever os dados de log.
  • lidarComExceções: capturar e registrar exceções não tratadas.
  • tamanhoMáximo: tamanho máximo do arquivo de log, em bytes, antes que um novo arquivo seja criado.
  • máximoArquivos: limite do número de arquivos criados quando o tamanho do arquivo de log é excedido.
  • formato: como a saída de log será formatada.

Níveis de registro indicam a prioridade das mensagens e são denotados por um número inteiro. O Winston usa os níveis de registro do npm que são priorizados de 0 a 6 (maior para menor):

  • 0: erro
  • 1: aviso
  • 2: informação
  • 3: http
  • 4: detalhado
  • 5: depuração
  • 6: bobo

Ao especificar um nível de registro para um transporte específico, qualquer coisa nesse nível ou mais alto será registrada. Por exemplo, ao definir um nível de informação, qualquer coisa no nível erro, aviso ou informação será registrada.

Os níveis de log são especificados ao chamar o logger, o que significa que você pode executar o seguinte comando para registrar um erro: logger.error('mensagem de erro de teste').

Ainda no arquivo de configuração, adicione o seguinte código para definir as configurações de transporte de arquivo e console na configuração do winston:

~/myApp/config/winston.js
...
// definir as configurações personalizadas para cada transporte (arquivo, console)
const options = {
  file: {
    level: "info",
    filename: `${appRoot}/logs/app.log`,
    handleExceptions: true,
    maxsize: 5242880, // 5MB
    maxFiles: 5,
    format: winston.format.combine(
      winston.format.timestamp(),
      winston.format.json()
    ),
  },
  console: {
    level: "debug",
    handleExceptions: true,
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    ),
  },
};

Em seguida, adicione o seguinte código para instanciar um novo logger winston com os transportes de arquivo e console usando as propriedades definidas na variável options:

~/myApp/config/winston.js
...
// instanciar um novo Winston Logger com as configurações definidas acima
const logger = winston.createLogger({
  transports: [
    new winston.transports.File(options.file),
    new winston.transports.Console(options.console),
  ],
  exitOnError: false, // não sair em exceções tratadas
});

Por padrão, morgan só gera saída para o console, então você irá definir uma função de fluxo que será capaz de pegar a saída gerada pelo morgan nos arquivos de log do winston. Você usará o nível info para pegar a saída por ambos os transportes (arquivo e console). Adicione o seguinte código ao arquivo de configuração:

~/myApp/config/winston.js
...
// criar um objeto de fluxo com uma função 'write' que será usada pelo `morgan`
logger.stream = {
  write: function(message, encoding) {
    // usar o nível de log 'info' para que a saída seja capturada por ambos
    // transportes (arquivo e console)
    logger.info(message);
  },
};

Finalmente, adicione o código abaixo para exportar o logger para que possa ser usado em outras partes da aplicação:

~/myApp/config/winston.js
...
module.exports = logger;

O arquivo de configuração winston completo agora terá essa aparência:

~/myApp/config/winston.js
const appRoot = require("app-root-path");
const winston = require("winston");

// defina as configurações personalizadas para cada transporte (arquivo, console)
const options = {
  file: {
    level: "info",
    filename: `${appRoot}/logs/app.log`,
    handleExceptions: true,
    maxsize: 5242880, // 5MB
    maxFiles: 5,
    format: winston.format.combine(
      winston.format.timestamp(),
      winston.format.json()
    ),
  },
  console: {
    level: "debug",
    handleExceptions: true,
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    ),
  },
};

// instancie um novo Logger Winston com as configurações definidas acima
const logger = winston.createLogger({
  transports: [
    new winston.transports.File(options.file),
    new winston.transports.Console(options.console),
  ],
  exitOnError: false, // não saia em exceções tratadas
});

// crie um objeto de fluxo com uma função 'write' que será usada pelo `morgan`
logger.stream = {
  write: function (message, encoding) {
    // use o nível de log 'info' para que a saída seja capturada por ambos
    // os transportes (arquivo e console)
    logger.info(message);
  },
};

module.exports = logger;

Salve e feche o arquivo.

Agora você tem o logger configurado, mas sua aplicação ainda não está ciente disso, ou como usá-lo, então você precisa integrar o logger com a aplicação.

Passo 4 — Integrando o Winston com a Aplicação

Para fazer o logger funcionar com a aplicação, você precisa fazer o express ficar ciente disso. Você viu no Passo 2 que sua configuração express está localizada em app.js, então você pode importar seu logger para este arquivo.

Abra o arquivo para edição:

  1. nano ~/myApp/app.js

Adicione uma declaração de variável winston perto do topo do arquivo com os outros require declarações:

~/myApp/app.js
...
var winston = require('./config/winston');
...

O primeiro lugar onde você usará winston é com morgan. Ainda no app.js, encontre a seguinte linha:

~/myApp/app.js
...
app.use(morgan('combined'));
...

Atualize-a para incluir a opção stream:

~/myApp/app.js
...
app.use(morgan('combined', { stream: winston.stream }));
...

Aqui, você define a opção stream para a interface de fluxo que você criou como parte da configuração do winston.

Salve e feche o arquivo.

Neste passo, você configurou sua aplicação Express para trabalhar com Winston. Em seguida, você irá revisar os dados de log.

Passo 5 — Acessando Dados de Log e Registrando Mensagens de Log Personalizadas

Agora que a aplicação foi configurada, você está pronto para ver alguns dados de log. Neste passo, você irá revisar as entradas de log e atualizar suas configurações com uma mensagem de log personalizada de exemplo.

Se você recarregar a página no navegador da web, deverá ver algo semelhante à seguinte saída no console da Sessão SSH A:

Output
[nodemon] restarting due to changes... [nodemon] starting `node bin/www` info: ::1 - - [25/Apr/2022:18:10:55 +0000] "GET / HTTP/1.1" 200 170 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36" info: ::1 - - [25/Apr/2022:18:10:55 +0000] "GET /stylesheets/style.css HTTP/1.1" 304 - "http://localhost:3000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"

Aqui estão duas entradas de log: a primeira para a solicitação à página HTML; a segunda para a folha de estilos associada. Como cada transporte está configurado para lidar com dados de log de nível info, você também deverá ver informações semelhantes no transporte de arquivo localizado em ~/myApp/logs/app.log.

Para visualizar o conteúdo do arquivo de log, execute o seguinte comando:

  1. tail ~/myApp/logs/app.log

tail irá exibir as últimas partes do arquivo no seu terminal.

Você deverá ver algo similar ao seguinte:

{"level":"info","message":"::1 - - [25/Apr/2022:18:10:55 +0000] \"GET / HTTP/1.1\" 304 - \"-\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36\"\n","timestamp":"2022-04-25T18:10:55.573Z"}
{"level":"info","message":"::1 - - [25/Apr/2022:18:10:55 +0000] \"GET /stylesheets/style.css HTTP/1.1\" 304 - \"http://localhost:3000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36\"\n","timestamp":"2022-04-25T18:10:55.588Z"}

A saída no arquivo transport será escrita como um objeto JSON, já que você utilizou winston.format.json() na opção format para a configuração do transporte de arquivo. Você pode aprender mais sobre JSON em Uma Introdução ao JSON.

Até agora, o seu registrador está apenas gravando requisições HTTP e dados relacionados. Essas informações são essenciais para ter nos seus logs.

No futuro, você pode querer gravar mensagens de log personalizadas, como para registrar erros ou perfilar o desempenho de consultas de banco de dados. Como exemplo, você irá chamar o registrador a partir da rota de manipulador de erro. Por padrão, o pacote express-generator já inclui uma rota de manipulador de erro 404 e 500, então você irá trabalhar com isso.

Abra o arquivo ~/myApp/app.js:

  1. nano ~/myApp/app.js

Encontre o bloco de código no final do arquivo que se parece com isso:

~/myApp/app.js
...
// manipulador de erro
app.use(function(err, req, res, next) {
  // definir locais, fornecendo apenas erro no desenvolvimento
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // renderizar a página de erro
  res.status(err.status || 500);
  res.render('error');
});
...

Esta seção é a rota final de manipulação de erro que, em última análise, enviará uma resposta de erro de volta ao cliente. Como todos os erros do lado do servidor passarão por esta rota, é um bom lugar para incluir o registrador winston.

Porque você está lidando com erros agora, você quer usar o nível de log error. Ambos os transportes estão configurados para registrar mensagens de nível error, então você deverá ver a saída nos logs do console e do arquivo.

Você pode incluir qualquer coisa que desejar no log, incluindo informações como:

  • err.status: O código de status de erro HTTP. Se não houver um presente, o padrão é 500.
  • err.message: Detalhes do erro.
  • req.originalUrl: O URL que foi solicitado.
  • req.path: A parte do caminho do URL da solicitação.
  • req.method: Método HTTP da solicitação (GET, POST, PUT, etc.).
  • req.ip: Endereço IP remoto da solicitação.

Atualize a rota de manipulação de erros para incluir o registro winston:

~/myApp/app.js
...
// manipulador de erros
app.use(function(err, req, res, next) {
  // definir locais, fornecendo apenas erro no desenvolvimento
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // incluir registro winston
  winston.error(
    `${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`
  );

  // renderizar a página de erro
  res.status(err.status || 500);
  res.render('error');
});
...

Salve e feche o arquivo.

Para testar esse processo, tente acessar uma página inexistente em seu projeto. Acessar uma página inexistente causará um erro 404. Em seu navegador da web, tente carregar o seguinte URL: http://seu_ip_do_servidor:3000/foo. Graças ao modelo básico criado pelo express-generator, a aplicação está configurada para responder a esse tipo de erro.

Seu navegador exibirá uma mensagem de erro como esta:

Ao olhar para o console na Sessão SSH A, deve haver uma entrada de log para o erro. Graças ao formato colorize aplicado, deve ser fácil identificar:

Output
[nodemon] starting `node bin/www` error: 404 - Not Found - /foo - GET - ::1 info: ::1 - - [25/Apr/2022:18:08:33 +0000] "GET /foo HTTP/1.1" 404 982 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36" info: ::1 - - [25/Apr/2022:18:08:33 +0000] "GET /stylesheets/style.css HTTP/1.1" 304 - "http://localhost:3000/foo" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"

Quanto ao registro de arquivos, executar o comando tail novamente deve mostrar os novos registros de log:

  1. tail ~/myApp/logs/app.log

Você verá uma mensagem como a seguinte:

{"level":"error","message":"404 - Not Found - /foo - GET - ::1","timestamp":"2022-04-25T18:08:33.508Z"}

A mensagem de erro inclui todos os dados que você especificamente instruiu o winston a registrar como parte do manipulador de erro. Essas informações incluirão o status do erro (404 – Não encontrado), a URL solicitada (localhost/foo), o método de solicitação (GET), o endereço IP que fez a solicitação e o timestamp para quando a solicitação foi feita.

Conclusão

Neste tutorial, você construiu uma aplicação web simples Node.js e integrou uma solução de registro Winston que funcionará como uma ferramenta eficaz para fornecer insights sobre o desempenho da aplicação.

Você pode fazer muito mais para construir soluções de registro robustas para suas aplicações, especialmente à medida que suas necessidades se tornam mais complexas. Para saber mais sobre os transportes do Winston, consulte a Documentação dos Transportes do Winston. Para criar seus próprios transportes, consulte Adicionando Transportes Personalizados. Para criar um endpoint HTTP para uso com o transporte HTTP principal, consulte winstond. Para usar o Winston como uma ferramenta de perfilamento, consulte Perfilamento.

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-winston-to-log-node-js-applications-on-ubuntu-20-04