Front & Back

Blog sobre el Desarrollo de aplicaciones web

Back end LogMonitor: Programando un sistema de alertas para logs con Node.js

A través del módulo “child_process” Node.js nos permite lanzar un proceso hijo, y mantener un “stream” de datos por cada entrada o salida estándar del mismo (stdin, stdout y stderror), de forma no bloqueante. Por su parte el método “spawn” de este módulo, nos permite la creación de un proceso hijo mediante argumentos en línea de comandos. Aprovechando esta particularidad he programado un pequeño sistema para el control de logs; LogMonitor, que prácticamente acabo de subir a GitHub. La idea es acompañarlo en versiones posteriores de un “GUI”, para monitorizar los datos recogidos en tiempo real, aunque de momento su funcionamiento se limita a la escucha de logs y la realización de acciones según unas reglas dadas, tal como muestra el gráfico siguiente:


LogMonitor – Pequeño sistema para el control de logs con Node.js

A través de un archivo de configuración se especifican los logs sobre los que necesitamos una escucha permanente, relacionando cada uno de ellos con un “handler” donde se almacenan las condiciones y acciones que queremos realizar con eventos concretos en cada log.

Cómo instalar LogMonitor

En primer lugar descargaremos el código de GitHub:
 $ git clone https://github.com/jvcalderon/LogMonitor.git

Ahora tendremos que instalar los módulos necesarios para su funcionamiento; para ello usaremos “npm”; el gestor de paquetes de Node.JS:
$ npm install emailjs #Para esta primera versión sólo necesitamos envío de emails

El siguiente paso sería renombrar el archivo config.json_dist como config.json e introducir los datos de configuración de correo electrónico y logs a observar.

Editando config.json para observar “n” logs

Es interesante que nos detengamos en el parámetro “logsToTail” de config.json; a través de este array podemos crear tantos procesos “tail -f” como necesitemos; su aspecto es el siguiente:
"logsToTail": [
{
"name":"_HERE_YOUR_LOG_NAME_",
"logFile":"_HERE_YOUR_LOG_FILE_PATH_",
"dataHandler" : "_HERE_YOUR_HANDLER_FILE_NAME_WITHOUT_JS_EXTENSION_"
}]

_HERE_YOUR_LOG_NAME_ Es el identificador único para el log que vamos a observar.

_HERE_YOUR_LOG_FILE_PATH_ Es la ruta al log.

_HERE_YOUR_HANDLER_FILE_NAME_WITHOUT_JS_EXTENSION_ Es el nombre del handler que va a manejar la salida del proceso, realizando las acciones pertinentes.

Si por ejemplo quisiéramos observar los logs “dev” y “prod” de Symfony2, nuestro “logsToTail” podría tener el siguiente aspecto:
"logsToTail": [
{
"name":"xxx_dev",
"logFile":"/var/www/xxx/app/logs/dev.log",
"dataHandler" : "symfony2_example"
},
{
"name":"xxx_prod",
"logFile":"/var/www/xxx/app/logs/prod.log",
"dataHandler" : "symfony2_example"
}]

Cómo funcionan los handlers

Como hemos visto en el ejemplo anterior, los dos procesos resultantes de nuestra configuración en “logsToTail” apuntan a un mismo handler “symfony2_example” cuyo código comento a continuación:
//Incializo la variable “symfony2” y le paso los parámetros al constructor
var symfony2 = function(name, logger, config) {
this.__construct(name, logger, config);
};

//__construct recibirá los parámetros enviados en la creación del objeto y los “bindeará”
symfony2.prototype.__construct = function(name, logger, config) {

this.name = name;
this.logger = logger;

this.emailjs = require('../../email.js');
this.emailjs = new this.emailjs(this.logger, config);

};

//handle es el método que se encargará de recibir los datos del proceso “tail -f” y realizar las acciones que se especifiquen
symfony2.prototype.handle = function(data) {

//En este caso una condición para las salidas que contengan la palabra “CRITICAL”
if(data.match("CRITICAL"))
{
this.logger.error('['+this.name+'] '+data); //Logueamos el error en el log de la aplicación
this.emailjs.send(data); //Enviamos un email
}

return null;

};

module.exports=symfony2; //Imprescindible para usar la “clase” como módulo de Node.js

Como podemos ver, podríamos crear todo tipo de handlers; para monitorizar el log de PHP, de Apache, etc, y crear distintas reglas o acciones para cada uno de ellos; bastaría con meter nuestros handlers en el directorio handler/dataHandler y vincularlos a los logs que corresponda en el archivo de configuración.

Lanzando la aplicación

Una vez tenemos todo preparado bastaría con ejecutar:
$ node app.js

Comento a continuación el código:
var config = require('./config.json'); //Cargo el archivo de configuración

//Creo el objeto logger para escribir el log del app
var logger = require('./logger.js');
logger = new logger(config.appLogFile);

//Creo el objeto spaw que se encargará de crear un proceso a partir de un comando
var spawn = require('child_process').spawn;

var tails = new Array(); //El array que almacenará los procesos
var dataHandlers = new Array(); //El array que almacenará los handlers


/*APP*******************************************************/

for(key in config.logsToTail) //Itero los logs especificados en el archivo de configuración
{

tails.push(spawn('tail', ['-f', config.logsToTail[key].logFile]));//Creo el proceso hijo mediante el comando “tail -f” que permanecerá abierto

dataHandlers[key] = require('./handler/dataHandler/'+config.logsToTail[key].dataHandler+'.js'); //Incluyo el handler
dataHandlers[key] = new dataHandlers[key](config.logsToTail[key].name, logger, config); //Creo el objeto handler pasándole los parámetros requeridos

//Cuando el proceso devuelva algún dato, lo capturo y lo remito al handler que le corresponda
tails[key].stdout.on('data', function(data){
dataHandlers[key].handle(data.toString());
});

}

El proceso Node se mantendrá a la escucha de sus “procesos hijo”, enviándonos un correo electrónico cuando se produzca una alerta. En versiones posteriores tengo pensado emitir los datos por socket y pintarlos en el navegador en tiempo real; además tengo que controlar que cada error sólo se remita una vez en un periodo específico de tiempo, aunque de momento esto (por lo menos a mí) ya me es de utilidad ;-)