Agregando logs al API con Winston (6ª parte de creación de una API REST con Express y TypeScript)

Publicado el 16 noviembre 2022 por Daniel Rodríguez @analyticslane

En las publicaciones anteriores se ha visto como configurar Express para la creación de una API y TypeORM para la conexión con una base de datos. Aunque con esto ya es suficiente para publicar un servicio, aún faltan algunos puntos clave, como la posibilidad de guardar logs. Algo que será clave a la hora de depurar y auditar el servicio. Aunque hasta ahora todos los mensajes se han sacado por pantalla con console.log(), es mejor usar una librería de logging como Winston para ello. Veamos los pasos necesarios para agregar logs al API con Winston. Además, también veremos como se puede usar dotenv para guardar en un sitio las opciones.

Instalación de Winston

Winston es una librería universal de logging para Node. Con ella se pueden crear uno o más logs en los que guardar registros de las operaciones realizadas. Como en el resto de las ocasiones es necesario instalar este paquete a través de npm, escribiendo para ello en la terminal

npm install winston winston-daily-rotate-file

En este caso, además de Winston, también se instala la librería winston-daily-rotate-file con la se simplifica la tarea de rotar los archivos de log.

Configuración de Winston

Para la configuración de Winston se creará un archivo llamado logger.ts con el siguiente código.

import winston from 'winston';
import 'winston-daily-rotate-file';

const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4,
};

const colors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'white',
};

winston.addColors(colors);

const format = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
  winston.format.colorize({ all: true }),
  winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`)
);

const transports = [
  new winston.transports.Console(),
  new winston.transports.DailyRotateFile({
    filename: './logs/log-%DATE%.log',
    datePattern: 'YYYY-MM-DD',
    level: process.env.LOGGING_FILE_LEVEL || 'info',
    maxFiles: process.env.LOGGING_RETENTION || '30d',
  }),
];

const logger = winston.createLogger({
  level: process.env.LOGGING_LEVEL || 'info',
  levels,
  format,
  transports,
});

export default logger;

En este lo primero que se hace después de cargar las librerías es definir los diferentes niveles para los logs y sus colores asociados. Asignando estos mediante la propiedad addColors(). Usando cinco niveles, los que se corresponden los errores (error) en rojo, las advertencias (warn) en amarillo, otra información (info) en verde, información de las conexiones http en magenta y mensajes de depuración (debug) en blanco.

Posteriormente se define el formato con el que los logs se mostrarán por pantalla. Lo que se hace con el método winston.format.combine(). En este se indica un formato para las fechas, el uso de colores y el formato del mensaje.

Tras el formato se definirá donde se mostrarán los logs. Lo normal es mostrarlo en la consola y guardado en un archivo de logs para su posterior análisis. Los archivos se guardan con winston-daily-rotate-file en la carpeta logs, con un nivel de detalle que se puede determinar mediante la propiedad de entorno LOGGING_FILE_LEVEL y persistimos un hasta el tiempo indicado en la variable LOGGING_RETENTION. Si las variables no existen se usará por defecto el nivel 'info' y 30 días respectivamente.

Finalmente se crea el logger en sí con las opciones anteriores y se exporta.

Uso del logger

Una vez creado el logger para enviar un mensaje solamente se tiene que usar este con los métodos definidos en level. Esto es, en cualquier parte del código donde se importe el logger, simplemente se llama a este seguido del nivel y el mensaje como parámetro. Así para generar un error se debería usar:

logger.error('Mensaje de error');

Lo que mostrará en el archivo de log y por pantalla el un mensaje en rojo del estilo:

2022-11-01 00:00:00:00 error: Mensaje de error

Mientras que para un mensaje de depuración

debug('Mensaje de depuración');

Lo que, en caso de que el nivel de log sea debug mostrará un mensaje en blanco similar al siguiente

2022-11-01 00:00:00:00 debug: Mensaje de depuración

Ahora, en el código actual, se puede buscar todos apariciones de console.log() y reemplazar por logger.info().

Uso de Winston con Morgan

Como middlewares para sacar por pantalla información de las peticiones se usaba Morgan. Ahora en lugar de que el mensaje salga por la consola, se puede hacer que use este logger para guardar los mensajes en el archivo de logs cuando es necesario. Para esto debemos ir al archivo middlewares/index.ts, importar el logger y reemplazar la entrada actual de Morgan por esta

morgan('tiny', { stream: { write: (m) => logger.http(m.split('\n')[0]) } }),

Lo que hace que la información salga como un mensaje HTTP en el log.

Función para respuestas del API

Cuando se produce un error en el API se envía un mensaje con la información, este mensaje se puede enviar también al log para su depuración. Para ello se puede crear una función que permita simplificar esta operación. Por ejemplo, se puede agregar y exportar la siguiente función en logger.ts.

export function responseAndLogger(res: Response, message: string, status = 500): void {
  if (status >= 500) {
    logger.error(`${message} (${status})`);
  } else {
    logger.warn(`${message} (${status})`);
  }

  res.status(status).send({ message });
}

Cuando se envía un mensaje con un estado 5xx se guarda un mensaje de error, en caso contrario el mensaje se guarda como advertencia. Ahora simplemente se puede llamar a este método cuando sea necesario enviar un estado de error.

Archivo de configuración con dotenv

En el programa se están usado muchas variables de entorno para modificar el comportamiento. Valores que se pueden asignar a la hora de ejecutar Node de diferentes maneras, una de las formas más sencillas es usar el paquete dotenv para guardar en un archivo .env y leerlas nada al indicar el programa.

Para esto lo primero que hay que hacer es instalar dotenv ejecutando la siguiente línea en la terminal

npm install dotenv

Una vez hecho esto, en la primera línea del archivo index.ts donde se inicia el servidor se escribirá

import 'dotenv/config';

Es importante que sea la primera línea que se importa, ya que en el caso de que se importa antes de datasource.ts o logger.ts no se habrán cargado las variables de entorno. Usando por lo tanto los valores por defecto.

Ahora solo hay que crear un archivo de configuración en la raíz del proyecto con las opciones que se deseen modificar. Archivo que no debe agregarse al repositorio ya que puede contener información clave, como la que se agrega en la próxima entrada. Un ejemplo de este archivo puede ser el siguiente.

Conclusiones

En esta entrada se han visto los pasos necesarios para añadir opciones de logs al API con Winston. Una librería que nos da mucha flexibilidad a la hora de trabajar. Además se ha visto cómo usar dotenv para mover las opciones a un archivo de configuración. La semana que viene veremos cómo se puede crear un middeware para controlar el acceso a los datos, evitando que cualquiera pueda consultar cualquier petición.

Imagen de Tayeb MEZAHDIA en Pixabay