nsyslog
Version:
Modular new generation log agent. Reads, transform, aggregate, correlate and send logs from sources to destinations
290 lines (274 loc) • 10.3 kB
JavaScript
/**
* @file wrapper.js
* @description Módulo que implementa un wrapper para los inputs de NSyslog, unificando la interfaz
* para tipos de inputs "push" y "pull" y conectándolos con el sistema de procesamiento.
*/
const
Input = require('./'),
logger = require('../logger'),
jsexpr = require('jsexpr'),
mingo = require('mingo'),
Stats = require('../stats'),
Readable = require('stream').Readable,
{FILTER_ACTION} = require('../constants'),
{timer} = require('../util'),
MODE = Input.MODE;
const stats = Stats.fetch('main');
/**
* @function getFilter
* @description Crea una función de filtro basada en la definición proporcionada
* @private
* @param {Object} def - Definición del input que contiene la configuración del filtro
* @returns {Function|boolean} - Función de filtrado o false si no hay filtro definido
*/
function getFilter(def) {
let filter = def.then.filter;
if(filter) {
// Si el filtro es un objeto, se utiliza mingo para crear una consulta MongoDB-like
if(typeof(filter)=='object') {
let query = new mingo.Query(filter);
return (entry)=>query.test(entry);
}
// Si el filtro es una cadena, se evalúa como una expresión JavaScript usando jsexpr
else if(typeof(filter)=='string') {
try {
return jsexpr.eval(filter);
}catch(err) {
// En caso de error en la expresión, se registra y se devuelve false
logger.error(`Invalid expression '${filter}' will be ignored.`,err);
return false;
}
}
// Para cualquier otro tipo de filtro, se devuelve una función que siempre retorna el valor del filtro
else {
return ()=>filter;
}
}
// Si no hay filtro definido, se devuelve false
else {
return false;
}
}
/**
* @function next
* @description Convierte la API basada en callbacks del input a una basada en Promises
* @private
* @param {Input} input - La instancia de input a consultar
* @returns {Promise<Object>} - Promise que resuelve al próximo objeto de datos del input
*/
function next(input) {
// Convierte la función next() basada en callbacks a una basada en Promises
// para facilitar el uso con async/await en el stream de lectura
return new Promise((ok,rej)=>{
input.next((err,data)=>{
if(!err) ok(data);
else rej(err);
});
});
}
/**
* InputWrapper class
* @extends Input
* @description <p>InputWrapper is a special subclass of {@link Input} intended to work
* within the NSyslog core engine. Since inputs can be either <b>push</b> or <b>pull</b>,
* InputWrapper unifies both kinds of inputs to a single implementation that manages
* both situations.</p>
* <p>But that's not the only thing it does. Its other task is to push read entries to
* the NSyslog input stream, in order to be processed by the flows.</p>
* @example
* const InputWrapper = require('nsyslog').Core.InputWrapper;
* const {BypassStream, QueueStream} = require('nsyslog').Core.Streams;
*
* let pushWrapper = new InputWrapper(myPushInput);
* let pullWrapper = new InputWrapper(myPullInput);
* pushWrapper.start(buffer)
*
* @param {Input} input Input instance
*/
class InputWrapper extends Input {
constructor(input) {
super(input.id,input.type);
/** @property {Input} input Input instance */
this.input = input;
/** @property {Input} instance Input instance (again) */
this.instance = input;
/**
* @protected
* @property {Stream} stream Internal stream (only for pull inputs)
*/
this.stream = null;
/**
* @protected
* @property {Stream} queueStream Output stream where input will be piped to
*/
this.queueStream = null;
}
/**
* Starts the input
* @param {Duplex} queueStream Duplex stream to pipe read entries
* @param {Function} callback Callback function
*/ start(queueStream,callback) {
// Obtiene referencias al input y su configuración
let input = this.input;
let def = input.$def;
// Configura el filtro según la definición, o null si el input está deshabilitado
let filter = def.disabled? null : getFilter(def);
// Determina las acciones a tomar cuando el filtro coincide o no coincide
// con valores predeterminados si no están especificados
let filterMatch = FILTER_ACTION[def.then.match] || FILTER_ACTION.process;
let filterNoMatch = FILTER_ACTION[def.then.nomatch] || FILTER_ACTION.block;
// MODO PULL: Creamos un stream propio que activamente solicita datos del input
if(input.mode==MODE.pull) {
// Creamos un stream legible en modo objeto con un límite máximo de pendientes
let stream = new Readable({
objectMode:true,
highWaterMark:def.maxPending,
// Implementación del método _read que será llamado cuando el consumidor necesite más datos
async read() { // Bucle infinito para continuar solicitando datos mientras el stream esté activo
while(true) {
try {
// Solicita el siguiente objeto del input
let obj = await next(input);
// Si no hay objeto o tiene un timer definido, espera un tiempo antes de reintentar
if(!obj || (obj.$$timer!==undefined)) {
await timer(obj.$$timer || 100);
}
else {
// Enriquece el objeto con metadatos del input
obj.input = input.id;
obj.type = input.type;
obj.$key = input.key(obj);
// Aplica la lógica de filtrado:
// Caso 1: No hay filtro configurado, se envía el objeto directamente
if(!filter) {
return this.push(obj);
}
// Caso 2: El filtro evaluado sobre el objeto da true
else if(filter && filter(obj)) {
// Se envía el objeto solo si la acción para coincidencias no es bloquear
if(filterMatch!=FILTER_ACTION.block) {
return this.push(obj);
}
}
// Caso 3: El filtro evaluado sobre el objeto da false
else if(filter && !filter(obj)) {
// Se envía el objeto solo si la acción para no-coincidencias no es bloquear
if(filterNoMatch!=FILTER_ACTION.block) {
return this.push(obj);
}
}
} }catch(err) {
// Registra el error en las estadísticas
stats.fail('input',input.id);
// Espera 5 segundos antes de reintentar (evita bucles de error rápidos)
await timer(5000);
}
}
}
});
// Guarda referencias al stream y queueStream para poder controlarlos más tarde
this.stream = stream;
this.queueStream = queueStream;
// Mantiene una referencia al input original en el stream
this.stream.instance = input;
// Inicia el input subyacente
input.start((err)=>{
// Si hay error, emite un evento para notificarlo
if(err) this.emit('stream_error',err);
// Registra cada entrada en las estadísticas
stream.on('data',data=>stats.emit('input',input.id));
// Conecta el stream al destino proporcionado
stream.pipe(queueStream);
});
}
// MODO PUSH: El input notificará asíncronamente cuando tenga datos disponibles
else {
// No necesitamos crear un stream, el input enviará los datos directamente
this.stream = null;
this.queueStream = queueStream;
// Inicia el input subyacente, que llamará al callback con cada objeto recibido
input.start((err,obj)=>{
// Si hay error, se registra en las estadísticas
if(err) {
stats.fail('input',input.id);
}
else {
// Enriquece el objeto con metadatos del input, igual que en el modo pull
obj.input = input.id;
obj.type = input.type;
obj.$key = input.key(obj); // Aplica la misma lógica de filtrado que en el modo pull:
// Caso 1: No hay filtro configurado, se envía el objeto directamente
if(!filter) {
stats.emit('input',input.id);
queueStream.write(obj);
}
// Caso 2: El filtro evaluado sobre el objeto da true
else if(filter && filter(obj)) {
if(filterMatch!=FILTER_ACTION.block) {
stats.emit('input',input.id);
queueStream.write(obj);
}
}
// Caso 3: El filtro evaluado sobre el objeto da false
else if(filter && !filter(obj)) {
if(filterNoMatch!=FILTER_ACTION.block) {
stats.emit('input',input.id);
queueStream.write(obj);
}
}
}
});
callback();
}
}
/**
* @method pause
* @description Pausa la lectura de datos del input
* @param {Function} callback - Función de callback a llamar cuando la pausa se complete
*/ pause(callback) {
// Si existe un stream (modo pull), lo pausa para detener la lectura
if(this.stream) {
this.stream.pause();
//this.stream.unpipe();
}
// Notifica al input subyacente que debe pausarse
this.input.pause(callback);
}
/**
* @method resume
* @description Reanuda la lectura de datos del input
* @param {Function} callback - Función de callback a llamar cuando la reanudación se complete
*/ resume(callback) {
// Si existe un stream (modo pull), reconecta el pipe y reanuda la lectura
if(this.stream) {
this.stream.pipe(this.queueStream);
//this.stream.resume();
}
// Notifica al input subyacente que debe reanudar la lectura
this.input.resume(callback);
}
/**
* @method stop
* @description Detiene el input y libera los recursos asociados
* @param {Function} callback - Función de callback a llamar cuando la detención se complete
* @returns {Promise<void>} - Promise que se resuelve cuando el input se detiene
*/ async stop(callback) {
// Si existe un stream (modo pull), desconecta el pipe para detener el flujo de datos
if(this.stream) {
this.stream.unpipe();
}
try {
// Detiene el input subyacente y espera a que termine
await this.input.stop(err=>{
// Registra la detención correcta del input
logger.info(`Input ${this.input.id} stopped`);
callback(err);
});
}catch(err) {
// Registra errores durante la detención del input
logger.error(`Input ${this.input.id} abnormal stop`,err);
callback(err);
}
}
}
module.exports = InputWrapper;