nsyslog
Version:
Modular new generation log agent. Reads, transform, aggregate, correlate and send logs from sources to destinations
383 lines (346 loc) • 10.5 kB
JavaScript
const
Transporter = require("./"),
jsexpr = require("jsexpr"),
extend = require("extend"),
logger = require("../logger"),
{timer} = require('../util'),
Semaphore = require("../semaphore"),
MongoClient = require("mongodb").MongoClient;
const nofn = entry=>entry;
const RETRY = 2000;
const DEF_CONF = {
url : "mongodb://localhost:27017/test",
collection : "nsyslog",
interval : 100,
batch : 1000,
retry : true,
maxRetry : Number.MAX_VALUE,
maxPool : 5,
options : {
useUnifiedTopology: true,
autoReconnect : true,
reconnectTries : Number.MAX_VALUE
}
};
/**
* MongoSimpleTransporter is a transporter for sending log messages to a MongoDB collection.
*
* @extends Transporter
*/
class MongoSimpleTransporter extends Transporter {
/**
* Creates an instance of MongoSimpleTransporter.
*
* @param {string} id - The unique identifier for the transporter.
* @param {string} type - The type of the transporter.
*/
constructor(id,type) {
super(id,type);
this.connected = null;
}
/**
* Configures the transporter with the provided settings.
*
* @param {Object} config - Configuration object for the transporter.
* @param {string} [config.url="mongodb://localhost:27017/test"] - The MongoDB connection URL.
* @param {string} [config.collection="nsyslog"] - The MongoDB collection to store log messages.
* @param {number} [config.interval=100] - Interval for batch insertion in milliseconds.
* @param {number} [config.batch=1000] - Maximum number of messages per batch.
* @param {boolean} [config.retry=true] - Whether to retry inserting messages on failure.
* @param {number} [config.maxRetry=Number.MAX_VALUE] - Maximum number of retries for insertion.
* @param {number} [config.maxPool=5] - Maximum number of concurrent insertions.
* @param {Object} [config.options] - Additional options for the MongoDB client.
* @param {Function} callback - Callback function to signal completion.
*/
async configure(config, callback) {
this.config = config = extend(true,{},DEF_CONF, config || {});
this.url = config.url || DEFAULTS.url;
this.msg = config.format? jsexpr.expr(config.format) : nofn;
this.indexes = config.indexes || [];
this.options = config.options || {};
this.cn = jsexpr.expr(config.collection);
this.retry = config.retry;
this.maxRetry = config.maxRetry;
this.buffers = {};
this.idxmap = {};
this.sem = new Semaphore(config.maxPool||5);
callback();
}
/**
* Connects to the MongoDB server.
*/
async connect() {
let connected = false;
while(!connected) {
try {
this.server = await MongoClient.connect(this.url,this.options);
this.isConnected = true;
this.server.on('close', () => {
logger.warn(`${this.id}: MongoDB -> lost connection`,this.url);
this.isConnected = false;
});
this.server.on('reconnect', () => {
logger.warn(`${this.id}: MongoDB -> reconnect`,this.url);
this.isConnected = true;
});
this.db = this.server.db();
this.resume(()=>{});
connected = true;
}catch(err) {
logger.error(`${this.id}: Cannot stablish connection to mongo (${this.url})`);
logger.error(err);
await timer(2000);
}
}
}
/**
* Creates indexes on the specified MongoDB collection.
*
* @param {string} col - The MongoDB collection name.
*/
async createIndexes(col) {
// Create indexes
if(!this.idxmap[col]) {
try {
await Promise.all(this.indexes.map(idx=>{
return this.db.collection(col).createIndex(idx);
}));
this.idxmap[col] = true;
logger.debug(`${this.id}: Created indexes on collection ${col}`,this.indexes);
}catch(err) {
logger.error(`${this.id}: Error creating indexes on collection ${col}`);
logger.error(err);
}
}
}
/**
* Inserts a batch of log messages into the MongoDB collection.
*
* @param {string} col - The MongoDB collection name.
* @param {Array} arr - Array of log messages to insert.
*/
async insert(col, arr) {
var msgs = arr.map(e=>e.msg);
var cbs = arr.map(e=>e.callback);
var inserted = false;
var retries = this.maxRetry;
// Insert data (Retry on error)
await this.sem.take();
while(!inserted) {
try {
if(!this.isConnected) throw new Error('Mongo connection lost');
await this.db.collection(col).insertMany(msgs,this.options);
cbs.forEach(callback=>callback());
inserted = true;
}catch(err) {
logger.error(err);
// Duplicate key error
if(err.code==11000) {
msgs.forEach(msg=>delete msg._id);
await timer(RETRY);
}
else if(!this.retry) {
cbs.forEach(callback=>callback(err));
inserted = true;
}
else {
retries--;
if(!retries) {
logger.error(`${this.id}: Reached max retries (${this.maxRetry}), aborting insertion`);
cbs.forEach(callback=>callback(err));
inserted = true;
}
else {
await timer(RETRY);
}
}
}
}
this.sem.leave();
}
/**
* Handles the insertion loop for batch processing.
*
* @param {boolean} start - Whether to start or stop the loop.
*/
async insertLoop(start) {
let cfg = this.config, buffers = this.buffers;
let all = [];
if(start===false) {
clearTimeout(this.ival);
this.ival = null;
return;
}
// Await for connection
await this.connected;
// For each collection
Object.keys(buffers).forEach(async(col)=>{
this.createIndexes(col); // Create indexes
let buffer = buffers[col]; // Get buffer for collection
// While there are data in the collection
while(buffer.length) {
let arr = buffer.splice(0,cfg.batch); // Get a batch of data
all.push(this.insert(col,arr)); // Insert data
}
});
await Promise.all(all);
this.ival = setTimeout(()=>this.insertLoop(),cfg.interval);
}
/**
* Starts the transporter and initializes the MongoDB connection.
*
* @param {Function} callback - Callback function to signal completion.
*/
start(callback) {
this.connected = this.connect();
if(callback) callback();
}
/**
* Resumes the insertion loop.
*
* @param {Function} callback - Callback function to signal completion.
*/
resume(callback) {
this.insertLoop(true);
if(callback) callback();
}
/**
* Pauses the insertion loop.
*
* @param {Function} callback - Callback function to signal completion.
*/
pause(callback) {
this.insertLoop(false);
if(callback) callback();
}
/**
* Stops the transporter and closes the MongoDB connection.
*
* @param {Function} callback - Callback function to signal completion.
*/
stop(callback) {
this.insertLoop(false);
if(this.server && this.server.close)
this.server.close();
this.connected = null;
this.buffer = [];
if(callback) callback();
}
/**
* Transports a log entry by adding it to the buffer for batch insertion.
*
* @param {Object} entry - The log entry to be transported.
* @param {Function} callback - Callback function to signal completion.
*/
transport(entry,callback) {
let msg = this.msg(entry);
if(typeof(msg)!=='object') msg = {data:msg};
let col = this.cn(entry);
// Remove invalid keys
Object.keys(msg).forEach(k=>{
if(k.startsWith("$")) delete msg[k];
});
if(!this.buffers[col])
this.buffers[col] = [];
this.buffers[col].unshift({msg,callback});
}
}
/**
* MongoTransporter is a transporter for managing multiple MongoDB connections.
*
* @extends Transporter
*/
class MongoTransporter extends Transporter {
/**
* Creates an instance of MongoTransporter.
*
* @param {string} id - The unique identifier for the transporter.
* @param {string} type - The type of the transporter.
*/
constructor(id,type) {
super(id,type);
this.connected = null;
this.transporters = {}
}
/**
* Configures the transporter with the provided settings.
*
* @param {Object} config - Configuration object for the transporter.
* @param {string} [config.url] - The MongoDB connection URL.
* @param {Function} callback - Callback function to signal completion.
*/
async configure(config, callback) {
this.config = config;
this.url = jsexpr.expr(config.url);
callback();
}
/**
* Sends a command to all managed MongoDB connections.
*
* @param {string} cmd - The command to send (e.g., "resume", "pause", "stop").
* @param {Function} callback - Callback function to signal completion.
*/
async sendCommand(cmd, callback) {
let prall = Object.keys(this.transporters).map(url=>{
return new Promise((ok,rej)=>{
this.transporters[url][cmd]((err)=>err? rej(err) : ok());
});
});
try {
await Promise.all(prall);
if(callback) callback();
}catch(err) {
if(callback) callback(err);
}
}
/**
* Starts the transporter.
*
* @param {Function} callback - Callback function to signal completion.
*/
start(callback) {
if(callback) callback();
}
/**
* Resumes all managed MongoDB connections.
*
* @param {Function} callback - Callback function to signal completion.
*/
resume(callback) {
return this.sendCommand('resume',callback);
}
/**
* Pauses all managed MongoDB connections.
*
* @param {Function} callback - Callback function to signal completion.
*/
pause(callback) {
return this.sendCommand('pause',callback);
}
/**
* Stops all managed MongoDB connections.
*
* @param {Function} callback - Callback function to signal completion.
*/
stop(callback) {
return this.sendCommand('stop',callback);
}
/**
* Transports a log entry by delegating it to the appropriate MongoDB connection.
*
* @param {Object} entry - The log entry to be transported.
* @param {Function} callback - Callback function to signal completion.
*/
async transport(entry,callback) {
let url = this.url(entry);
let config = extend({},this.config,{url});
if(!this.transporters[url]) {
let tr = new MongoSimpleTransporter(this.id, this.type);
await new Promise(ok=>tr.configure(config,ok));
await new Promise(ok=>tr.start(ok));
this.transporters[url] = tr;
}
this.transporters[url].transport(entry, callback);
}
}
module.exports = MongoTransporter;