Cómo utilizar Winston para registrar aplicaciones Node.js en Ubuntu 20.04

Introducción

Una solución de registro efectiva es crucial para el éxito de cualquier aplicación. Winston es una biblioteca de registro versátil y una solución de registro popular disponible para aplicaciones de Node.js. Las características de Winston incluyen soporte para múltiples opciones de almacenamiento, niveles de registro, consultas de registro y un perfilador incorporado.

En este tutorial, utilizarás Winston para registrar una aplicación Node/Express que crearás como parte de este proceso. También verás cómo combinar Winston con Morgan, otro middleware de registro de solicitud HTTP popular para Node.js, para consolidar los registros de datos de solicitudes HTTP con otra información. Después de completar este tutorial, tu servidor Ubuntu ejecutará una pequeña aplicación Node/Express, y Winston se implementará para registrar errores y mensajes en un archivo y en la consola.

Prerrequisitos

Para seguir este tutorial, necesitarás:

Paso 1 — Creación de una Aplicación Básica de Node/Express

Winston se usa a menudo para registrar eventos de aplicaciones web construidas con Node.js. En este paso, crearás una aplicación web Node.js simple usando el framework Express. Usarás express-generator, una herramienta de línea de comandos, para poner en funcionamiento rápidamente una aplicación web Node/Express.

Como instalaste el Gestor de Paquetes de Node durante los requisitos previos, puedes usar el comando npm para instalar express-generator:

  1. sudo npm install express-generator -g

La bandera -g instala el paquete de forma global, lo que significa que se puede usar como una herramienta de línea de comandos fuera de un proyecto/módulo Node existente.

Con express-generator instalado, puedes crear tu aplicación usando el comando express, seguido del nombre del directorio que deseas usar para el proyecto:

  1. express myApp

Para este tutorial, el proyecto se llamará myApp.

Nota: También es posible ejecutar la herramienta express-generator directamente sin instalarla globalmente como un comando de sistema primero. Para hacerlo, ejecuta este comando:

  1. npx express-generator myApp

El comando npx es un ejecutador de comandos incluido con el Administrador de Paquetes de Node que facilita la ejecución de herramientas de línea de comandos desde el registro de npm.

Durante la primera ejecución, te preguntará si estás de acuerdo en descargar el paquete:

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

Responde con y y presiona ENTER. Ahora puedes usar npx express-generator en lugar de express.

A continuación, instala Nodemon, que recargará automáticamente la aplicación cada vez que realices cambios. Una aplicación Node.js necesita reiniciarse cada vez que se realizan cambios en el código fuente para que estos tengan efecto, por lo que Nodemon buscará automáticamente cambios y reiniciará la aplicación. Dado que deseas poder usar nodemon como una herramienta de línea de comandos, instálala con la bandera -g:

  1. sudo npm install nodemon -g

Para finalizar la configuración de la aplicación, muévete al directorio de la aplicación e instala las dependencias de la siguiente manera:

  1. cd myApp
  2. npm install

Por defecto, las aplicaciones creadas con express-generator se ejecutan en el puerto 3000, así que debes asegurarte de que el firewall no bloquee ese puerto.

Para abrir el puerto 3000, ejecuta el siguiente comando:

  1. sudo ufw allow 3000

Ahora tienes todo lo necesario para iniciar tu aplicación web. Para hacerlo, ejecuta el siguiente comando:

  1. nodemon bin/www

Este comando inicia la aplicación en el puerto 3000. Puedes probar si funciona apuntando tu navegador a http://la_dirección_ip_de_tu_servidor:3000. Deberías ver algo como esto:

En este punto, puedes iniciar una segunda sesión SSH en tu servidor para el resto de este tutorial, dejando la aplicación web que acabas de iniciar en la sesión original. Para el resto de este artículo, la sesión SSH inicial que está ejecutando la aplicación se llamará Sesión A. Cualquier comando en la Sesión A aparecerá en un fondo azul marino oscuro como este:

  1. nodemon bin/www

Utilizarás la nueva sesión SSH para ejecutar comandos y editar archivos. Esta sesión se llamará Sesión B. Cualquier comando en la Sesión B aparecerá en un fondo azul claro como este:

  1. cd ~/myApp

A menos que se indique lo contrario, ejecutarás todos los comandos restantes en la Sesión B.

En este paso, creaste la aplicación básica. A continuación, la personalizarás.

Paso 2 — Personalización de las Variables de Registro

Si bien la aplicación predeterminada creada por express-generator es un buen comienzo, necesitas personalizar la aplicación para que llame al registro correcto cuando sea necesario.

express-generator incluye el middleware de registro HTTP Morgan que utilizarás para registrar datos sobre todas las solicitudes HTTP. Dado que Morgan admite flujos de salida, se combina muy bien con el soporte de flujo integrado en Winston, lo que te permite consolidar los registros de datos de solicitudes HTTP con cualquier otra cosa que elijas registrar con Winston.

El esquema base de express-generator utiliza la variable logger al hacer referencia al paquete morgan. Dado que usarás morgan y winston, que son paquetes de registro, puede ser confuso llamar a cualquiera de ellos logger. Para especificar qué variable deseas, puedes cambiar las declaraciones de variables editando el archivo app.js.

Para abrir app.js para editarlo, utiliza nano o tu editor de texto favorito:

  1. nano ~/myApp/app.js

Encuentra la siguiente línea cerca del inicio del archivo:

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

Cambia el nombre de la variable de logger a morgan:

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

Esta actualización especifica que la variable declarada morgan llamará al método require() vinculado al registro de solicitudes Morgan.

Necesitas encontrar dónde más se hace referencia a la variable logger en el archivo y cambiarlo a morgan. También deberás cambiar el formato de registro utilizado por el paquete morgan a combined, que es el formato estándar de registro de Apache y que incluirá información útil en los registros, como la dirección IP remota y la cabecera de solicitud HTTP del agente de usuario.

Para hacerlo, encuentra la siguiente línea:

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

Actualízala a lo siguiente:

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

Estos cambios te ayudarán a entender qué paquete de registro se referencia en cualquier momento después de integrar la configuración de Winston.

Cuando hayas terminado, guarda y cierra el archivo.

Ahora que tu aplicación está configurada, puedes empezar a trabajar con Winston.

Paso 3 — Instalación y configuración de Winston

En este paso, instalarás y configurarás Winston. También explorarás las opciones de configuración disponibles como parte del paquete winston y crearás un registro para registrar información en un archivo y en la consola.

Instala winston con el siguiente comando:

  1. cd ~/myApp
  2. npm install winston

Es útil mantener cualquier archivo de configuración de soporte o utilidad para tus aplicaciones en un directorio especial. Crea una carpeta config que contendrá la configuración de winston:

  1. mkdir ~/myApp/config

A continuación, crea una carpeta que contendrá tus archivos de registro:

  1. mkdir ~/myApp/logs

Finalmente, instala app-root-path:

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

El paquete app-root-path es útil al especificar rutas en Node.js. Aunque este paquete no está directamente relacionado con Winston, es útil al determinar las rutas a los archivos en Node.js. Lo utilizarás para especificar la ubicación de los archivos de registro de Winston desde la raíz del proyecto y para evitar una sintaxis de ruta relativa poco elegante.

Ahora que la configuración para manejar el registro está en su lugar, puedes definir tus ajustes. Crea y abre ~/miApp/config/winston.js para editarlo:

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

El archivo winston.js contendrá tu configuración de winston.

A continuación, agrega el siguiente código para requerir los paquetes app-root-path y winston:

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

Con estas variables en su lugar, puedes definir los ajustes de configuración para tus transportes. Los transportes son un concepto introducido por Winston que se refiere a los mecanismos de almacenamiento/salida utilizados para los registros. Winston viene con cuatro transportes principales incorporados: Consola, Archivo, HTTP y Stream.

Te enfocarás en los transportes de consola y archivo para este tutorial. El transporte de consola registrará información en la consola, y el transporte de archivo registrará información en un archivo especificado. Cada definición de transporte puede contener ajustes de configuración, como el tamaño del archivo, los niveles de registro y el formato de registro.

Aquí tienes un resumen rápido de los ajustes que usarás para cada transporte:

  • nivel: nivel de mensajes a registrar.
  • nombre_archivo: el archivo que se utilizará para escribir los datos de registro.
  • manejarExcepciones: capturar y registrar excepciones no controladas.
  • tamañoMaximo: tamaño máximo del archivo de registro, en bytes, antes de que se cree un nuevo archivo.
  • maxArchivos: límite del número de archivos creados cuando se excede el tamaño del archivo de registro.
  • formato: cómo se formateará la salida de registro.

Los niveles de registro indican la prioridad del mensaje y están denotados por un entero. Winston utiliza los niveles de registro de npm que están priorizados del 0 al 6 (de más alto a más bajo):

  • 0: error
  • 1: advertencia
  • 2: información
  • 3: http
  • 4: detallado
  • 5: depuración
  • 6: tonto

Al especificar un nivel de registro para un transporte particular, se registrarán todos los mensajes en ese nivel o superior. Por ejemplo, al establecer un nivel de información, se registrarán todos los mensajes en el nivel error, advertencia o información.

Los niveles de registro se especifican al llamar al registrador, lo que significa que puede ejecutar el siguiente comando para registrar un error: registrador.error('mensaje de error de prueba').

Todavía en el archivo de configuración, agregue el siguiente código para definir la configuración de los transportes archivo y consola en la configuración de winston:

~/myApp/config/winston.js
...
// Define la configuración personalizada para cada transporte (archivo, consola)
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()
    ),
  },
};

A continuación, agrega el siguiente código para instanciar un nuevo registrador winston con transportes de archivo y consola utilizando las propiedades definidas en la variable options:

~/myApp/config/winston.js
...
// Instancia un nuevo registrador Winston con la configuración definida anteriormente
const logger = winston.createLogger({
  transports: [
    new winston.transports.File(options.file),
    new winston.transports.Console(options.console),
  ],
  exitOnError: false, // No salir en excepciones manejadas
});

Por defecto, morgan solo imprime en la consola, así que definirás una función de flujo que pueda llevar la salida generada por morgan a los archivos de registro de winston. Usarás el nivel de registro info para recoger la salida por ambos transportes (archivo y consola). Agrega el siguiente código al archivo de configuración:

~/myApp/config/winston.js
...
// Crea un objeto de flujo con una función 'write' que será utilizada por `morgan`
logger.stream = {
  write: function(message, encoding) {
    // Usa el nivel de registro 'info' para que la salida sea recogida por ambos
    // transportes (archivo y consola)
    logger.info(message);
  },
};

Finalmente, agrega el siguiente código para exportar el registrador para que pueda ser utilizado en otras partes de la aplicación:

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

El archivo de configuración de winston completado se verá así:

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

// define las configuraciones personalizadas para cada transporte (archivo, consola)
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()
    ),
  },
};

// instanciar un nuevo registro de Winston con las configuraciones definidas arriba
const logger = winston.createLogger({
  transports: [
    new winston.transports.File(options.file),
    new winston.transports.Console(options.console),
  ],
  exitOnError: false, // no salir en excepciones manejadas
});

// crear un objeto de flujo con una función 'write' que será usada por `morgan`
logger.stream = {
  write: function (message, encoding) {
    // usar el nivel de registro 'info' para que la salida sea recogida por ambos
    // transportes (archivo y consola)
    logger.info(message);
  },
};

module.exports = logger;

Guarde y cierre el archivo.

Ahora tiene configurado el registrador, pero su aplicación aún no es consciente de ello, ni de cómo usarlo, así que necesita integrar el registrador con la aplicación.

Paso 4 — Integrar Winston con la Aplicación

Para que su registrador funcione con la aplicación, necesita hacer que express sea consciente de ello. Vio en el Paso 2 que su configuración de express está ubicada en app.js, así que puede importar su registrador en este archivo.

Abra el archivo para editarlo:

  1. nano ~/myApp/app.js

Agregue una declaración de variable winston cerca de la parte superior del archivo con las otras declaraciones de require:

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

El primer lugar donde utilizarás winston es con morgan. Todavía en app.js, encuentra la siguiente línea:

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

Actualízala para incluir la opción stream:

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

Aquí, estableces la opción stream en la interfaz de flujo que creaste como parte de la configuración de winston.

Guarda y cierra el archivo.

En este paso, configuraste tu aplicación Express para trabajar con Winston. A continuación, revisarás los datos de registro.

Paso 5 — Acceso a los Datos de Registro y Registro de Mensajes de Registro Personalizados

Ahora que la aplicación ha sido configurada, estás listo para ver algunos datos de registro. En este paso, revisarás las entradas de registro y actualizarás tus configuraciones con un mensaje de registro personalizado de ejemplo.

Si recargas la página en el navegador web, deberías ver algo similar a la siguiente salida en la consola de la Sesión 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"

Aquí hay dos entradas de registro: la primera para la solicitud a la página HTML; la segunda para la hoja de estilo asociada. Dado que cada transporte está configurado para manejar datos de registro de nivel info, también deberías ver información similar en el transporte de archivo ubicado en ~/myApp/logs/app.log.

Para ver el contenido del archivo de registro, ejecuta el siguiente comando:

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

tail mostrará las últimas partes del archivo en tu terminal.

Deberías ver algo similar a lo siguiente:

{"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"}

La salida en el archivo de transporte se escribirá como un objeto JSON ya que usaste winston.format.json() en la opción format para la configuración del transporte de archivo. Puedes aprender más sobre JSON en Una Introducción a JSON.

Hasta ahora, tu registrador solo está grabando solicitudes HTTP y datos relacionados. Esta información es esencial tener en tus registros.

En el futuro, es posible que desees grabar mensajes de registro personalizados, como para registrar errores o el rendimiento de consultas de base de datos. Como ejemplo, llamarás al registrador desde la ruta de manejo de errores. Por defecto, el paquete express-generator ya incluye una ruta de manejo de errores 404 y 500, así que trabajarás con eso.

Abre el archivo ~/miAplicación/app.js:

  1. nano ~/myApp/app.js

Encuentra el bloque de código al final del archivo que se ve así:

~/myApp/app.js
...
// manejador de errores
app.use(function(err, req, res, next) {
  // establecer variables locales, solo proporcionando error en desarrollo
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

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

Esta sección es la ruta de manejo de errores final que finalmente enviará una respuesta de error al cliente. Dado que todos los errores del servidor pasarán por esta ruta, es un buen lugar para incluir el registrador winston.

Porque ahora estás tratando con errores, deseas utilizar el nivel de registro error. Ambos transportes están configurados para registrar mensajes de nivel error, por lo que deberías ver la salida en los registros de la consola y del archivo.

Puedes incluir cualquier cosa que desees en el registro, incluyendo información como:

  • err.status: El código de estado de error HTTP. Si no está presente, se establecerá por defecto en 500.
  • err.message: Detalles del error.
  • req.originalUrl: La URL que fue solicitada.
  • req.path: La parte de la ruta de la URL solicitada.
  • req.method: Método HTTP de la solicitud (GET, POST, PUT, etc.).
  • req.ip: Dirección IP remota de la solicitud.

Actualiza la ruta del manejador de errores para incluir el registro con winston:

~/myApp/app.js
...
// manejador de errores
app.use(function(err, req, res, next) {
  // establecer locales, proporcionando solo el error en desarrollo
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

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

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

Guarda y cierra el archivo.

Para probar este proceso, intenta acceder a una página inexistente en tu proyecto. Acceder a una página inexistente generará un error 404. En tu navegador web, intenta cargar la siguiente URL: http://tu_dirección_ip_del_servidor:3000/foo. Gracias a la plantilla creada por express-generator, la aplicación está configurada para responder a tal error.

Tu navegador mostrará un mensaje de error como este:

Cuando observes la consola en la Sesión SSH A, debería haber una entrada de registro para el error. Gracias al formato colorize aplicado, debería ser fácil de 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"

En cuanto al registrador de archivos, al ejecutar nuevamente el comando tail, deberías ver los nuevos registros de registro:

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

Verás un mensaje como el siguiente:

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

El mensaje de error incluye todos los datos que específicamente instruiste a winston que registrara como parte del manejador de errores. Esta información incluirá el estado de error (404 – No Encontrado), la URL solicitada (localhost/foo), el método de solicitud (GET), la dirección IP que realizó la solicitud y la marca de tiempo de cuando se realizó la solicitud.

Conclusión

En este tutorial, construiste una aplicación web Node.js simple e integraste una solución de registro Winston que funcionará como una herramienta efectiva para proporcionar información sobre el rendimiento de la aplicación.

Puedes hacer mucho más para construir soluciones de registro robustas para tus aplicaciones, especialmente cuando tus necesidades se vuelven más complejas. Para obtener más información sobre los transportes de Winston, consulta la Documentación de Transportes de Winston. Para crear tus propios transportes, consulta Agregando Transportes Personalizados. Para crear un punto de conexión HTTP para usar con el transporte central HTTP, consulta winstond. Para utilizar Winston como una herramienta de perfilado, consulta Perfilado.

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