UNPKG

nsyslog

Version:

Modular new generation log agent. Reads, transform, aggregate, correlate and send logs from sources to destinations

367 lines (327 loc) 10.2 kB
const logger = require('../logger'), Input = require('./'), extend = require('extend'), jsexpr = require('jsexpr'), Semaphore = require('../semaphore'), Watermark = require("../watermark"), {timer} = require('../util'), MongoClient = require("mongodb").MongoClient; const IVAL_WM = 2000; // Interval for watermark updates in milliseconds const DEFAULTS = { url: "mongodb://localhost:27017/test", // Default MongoDB connection URL collection: "test", // Default collection name maxCursors: 5, // Maximum number of concurrent cursors query: {}, // Default query sort: {}, // Default sort order watermark: {}, // Default watermark configuration options: { useUnifiedTopology: true, autoReconnect: true, reconnectTries: Number.MAX_VALUE } }; /** * MongoInput class for handling MongoDB-based input. * Extends the base Input class. */ class MongoInput extends Input { /** * Constructor for MongoInput. * @param {string} id - Unique identifier for the input. * @param {string} type - Type of the input. */ constructor(id, type) { super(id, type); this.wmival = null; // Watermark interval timer this.connected = null; // Connection status } /** * Configures the MongoInput with the provided settings. * * @param {Object} config - Configuration object containing: * @param {string} [config.url="mongodb://localhost:27017/test"] - MongoDB connection URL. * @param {string|Array<string>} [config.collection="test"] - Collection(s) to monitor. * @param {Object} [config.query={}] - Query to filter documents. * @param {Object} [config.sort={}] - Sort order for documents. * @param {Object} [config.watermark={}] - Initial watermark configuration. * @param {number} [config.maxCursors=5] - Maximum number of concurrent cursors. * @param {Object} [config.options] - MongoDB connection options. * @param {Function} callback - Callback function to signal completion. */ async configure(config, callback) { this.config = config = extend(true, {}, DEFAULTS, config || {}); this.url = config.url || DEFAULTS.url; this.options = config.options || DEFAULTS.options; this.owm = config.watermark || DEFAULTS.watermark; this.query = jsexpr.expr(config.query || DEFAULTS.query); this.sem = new Semaphore(config.maxCursors || DEFAULTS.maxCursors); this.sort = jsexpr.expr(config.sort || DEFAULTS.sort); this.cursors = {}; this.watermark = new Watermark(config.$datadir); await this.watermark.start(); this.wm = await this.watermark.get(this.id); this.wm.cols = this.wm.cols || {}; let cols = config.collection || DEFAULTS.collection; if (!Array.isArray(cols)) cols = [cols]; this.collection = cols.map(c => { if (c.startsWith('/')) return new RegExp(c.replace(/^\/|\/$/g, '')); else return c; }); callback(); } /** * Returns the mode of the input. * @returns {string} The mode of the input (pull). */ get mode() { return Input.MODE.pull; } /** * Fetches collections from MongoDB that match the configured patterns * and initializes their watermarks. * * @param {number} [next] - Interval for fetching collections in milliseconds. * @returns {Promise<Array>} Matched collections. */ async fetchCollections(next) { let cols = await this.db.listCollections().toArray(); cols = cols.filter(col => { let name = col.name; return this.collection.reduce((res, pattern) => { return res || (typeof (pattern) == 'string' ? name == pattern : pattern.test(name)); }, false); }); let colmap = cols.reduce((map, c) => { map[c.name] = c; return map; }, cols); // Remove obsolete watermarks Object.keys(this.wm).forEach(cname => { if (!colmap[cname]) delete this.wm.cols[cname]; }); // Add new watermarks cols.forEach(c => { if (!this.wm.cols[c.name]) this.wm.cols[c.name] = this.owm; }); // Save watermarks await this.saveWatermark(next); if (next) { this.wmival = setTimeout(() => this.fetchCollections(next), next); } logger.debug(`${this.id} Matched collections: `, cols.map(c => c.name)); this.cols = cols; return this.cols; } /** * Fetches data from a specific collection. * * @param {string} colname - Name of the collection to fetch data from. * @returns {Promise<Object|null>} Cursor for the fetched data. */ async fetchData(colname) { let cursor = null; await this.sem.take(); try { let owm = this.wm.cols[colname] || this.owm; let query = this.query(owm); let sort = this.sort(owm); logger.silly(`${this.id}: Query: ${JSON.stringify(query)}`); cursor = await this.db.collection(colname).find(query).sort(sort); this.cursors[colname] = cursor; } catch (err) { this.sem.leave(); delete this.cursors[colname]; logger.error(err); } this.sem.leave(); return cursor; } /** * Saves the current watermark state to persistent storage. * @returns {Promise<void>} */ async saveWatermark() { try { await this.watermark.save(this.wm); logger.debug(`${this.id}: Watermark saved`); } catch (err) { logger.error(err); } } /** * Establishes a connection to the MongoDB server and initializes collections. * Reconnects automatically if the connection is lost. * @returns {Promise<void>} */ async connect() { let connected = false; while (!connected) { try { this.server = await MongoClient.connect(this.url, this.options); this.server.on('close', () => logger.warn(`${this.id}: MongoDB -> lost connection`, this.url)); this.server.on('reconnect', () => logger.warn(`${this.id}: MongoDB -> reconnect`, this.url)); this.db = this.server.db(); await this.fetchCollections(IVAL_WM); connected = true; } catch (err) { logger.error(`${this.id}: Cannot stablish connection to mongo (${this.url})`); logger.error(err); await timer(2000); } } } /** * Starts the MongoInput and establishes a connection to MongoDB. * @param {Function} callback - Callback function to signal completion. */ async start(callback) { this.connected = this.connect(); if (callback) callback(); } /** * Retrieves the next item from the MongoDB collections. * * @param {Function} callback - Callback function to process the next item. */ async next(callback) { await this.connected; let data = false; let len = this.cols.length; let counter = 0, ccol = null; while (!data) { let col = this.cols.shift(), cname = col ? col.name : null, cursor = cname ? this.cursors[cname] : null; if (col) this.cols.push(col); ccol = col; counter++; // No matching cols if (!col) { logger.warn(`${this.id}: No matching collections...`); await timer(1000); } // No cursor, fetch data else if (!cursor) { this.cursors[cname] = this.fetchData(cname); logger.debug(`${this.id}: Query for collection ${cname} started`); } // Doing query, is a promise else if (cursor.then) { logger.debug(`${this.id}: Query for collection ${cname} already running`); } // Is an actual cursor else if (cursor.hasNext) { logger.silly(`${this.id}: Cursor for ${cname} is opened`); try { let hasNext = await cursor.hasNext(); if (!hasNext) { await cursor.close(); delete this.cursors[cname]; } else { data = await cursor.next(); this.wm.cols[cname] = data; } } catch (err) { logger.error(err); } } // If no data read in any collection, wait if (!data && (counter % len) == 0) { logger.debug(`${this.id}: No data read in any collection. Waiting...`); await timer(1000); } } if (callback) { callback(null, { id: this.id, type: this.type, collection: ccol.name, database: this.db.name, originalMessage: data }); } } /** * Stops the MongoInput and performs cleanup. * Closes all active cursors and the MongoDB connection. * * @param {Function} callback - Callback function to signal completion. */ async stop(callback) { await this.pause(); let pall = Object.keys(this.cursors).map(cname => { let cursor = this.cursors[cname]; if (!cursor) return Promise.resolve(); else if (cursor.then) { return cursor.then(c => c.close()); } else { return cursor.close(); } }); await Promise.all(pall); if (this.server && this.server.close) await this.server.close(); this.connected = null; if (callback) callback(); } /** * Pauses the MongoInput by halting watermark updates and saving the current state. * * @param {Function} callback - Callback function to signal completion. */ async pause(callback) { clearTimeout(this.wmival); await this.saveWatermark(); if (callback) callback(); } /** * Resumes the MongoInput by restarting watermark updates. * * @param {Function} callback - Callback function to signal completion. */ async resume(callback) { await this.fetchCollections(IVAL_WM); if (callback) callback(); } /** * Generates a unique key for the input entry. * * @param {Object} entry - Input entry object. * @returns {string} Unique key for the entry. */ key(entry) { return `${entry.input}:${entry.type}@${entry.database}:${entry.collection}`; } } if (module.parent) { module.exports = MongoInput; } else { // Example usage of MongoInput let input = new MongoInput("mongo", "mongo"); logger.setLevel('debug'); input.configure({ $datadir: '/tmp/nsyslog', url: 'mongodb://localhost/logicalog', collection: ["/loghost.logline.*/"], query: { line: { $gt: '${line}' } }, watermark: { line: 0 }, maxCursors: 10 }, () => { input.start(() => { function next() { input.next((err, item) => { if (err) { logger.error(err); process.exit(1); } else { logger.debug(item); setImmediate(next, 1000); } }); } next(); }); }); }