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.
Esta entrada forma parte de la serie “Creación de una API REST con Express y TypeScript” de la cual forman los siguientes entregas:
- Creación de una API REST con Express y TypeScript
- Organizar el código del proyecto
- Configurar TypeORM para acceder a la base de datos
- Creación de rutas para consultar y agregar los registros
- Creación de rutas para modificar y borrar los registros
- Agregando logs al API con Winston
- Requerir autenticación mediante JWT
- Registro de usuarios
- Incluir un certificado en Express para servir el API mediante HTTPS
- Ejecutar la aplicación en producción con PM2
Tabla de contenidos
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.
El código creado en toda la serie de publicaciones se puede encontrar en la cuenta de GitHub de Analytics Lane.
Imagen de Tayeb MEZAHDIA en Pixabay
Sebastian Guandique dice
Me impresiona como las APIs permiten que los equipos informáticos manejen el trabajo automatizando la forma en la que se trabaja.