UNPKG

nsyslog

Version:

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

283 lines (247 loc) 8.58 kB
const logger = require('../logger'), extend = require('extend'), request = require('request'), Watermark = require('../watermark'), Queue = require('../queue'), jsexpr = require('jsexpr'), {timer, immediate} = require('../util'), TLS = require('../tls'), Input = require('.'), URL = require('url'); const DEFAULTS = { start: 'start', url: 'http://localhost', options: {}, tls: { rejectUnauthorized: false }, states: { start: { url: "http://localhost", options: {}, emit: [ { when: 'true', then: "${body}", next: 'start' } ] } } }; const PROTOS = { "http:": true, "https:": true }; /** * HTTPSMInput class for handling HTTP state machine-based input. * Extends the base Input class. */ class HTTPSMInput extends Input { /** * Constructor for HTTPSMInput. * @param {string} id - Unique identifier for the input. * @param {string} type - Type of the input. */ constructor(id, type) { super(id, type); this.timeout = 0; // Timeout for the next state transition } /** * Returns the mode of the input. * @returns {string} The mode of the input (pull). */ get mode() { return Input.MODE.pull; } /** * Configures the HTTPSMInput with the provided settings. * * This method initializes the input by merging the provided configuration * with the default settings. It sets up the state machine, prepares the * watermark for tracking state, and evaluates the starting state. Each state * is configured with its respective options, TLS settings, and emit conditions. * * @param {Object} config - Configuration object containing: * @param {string} [config.start="start"] - The initial state to start the state machine. * @param {Object} [config.states] - An object defining the states of the state machine. Each state includes: * @param {string} [config.states.url] - The endpoint URL to fetch data from. * @param {Object} [config.states.options] - HTTP request options, such as method, headers, and body. * @param {Array<Object>} [config.states.emit] - An array of conditions and actions to perform based on the response. * @param {string} [config.states.emit.when="true"] - A condition to evaluate for the response. * @param {string} [config.states.emit.next] - The next state to transition to. * @param {number} [config.states.emit.timeout=-1] - Timeout in milliseconds before the next fetch. * @param {Function} [config.states.emit.store] - A function to store data for use in subsequent states. * @param {Function} [config.states.emit.publish] - A function to publish data as the output of the input. * @param {Object} [config.states.emit.log] - Logging configuration for the state transition. * @param {string} [config.states.emit.log.level="info"] - Log level (e.g., "info", "error"). * @param {string} [config.states.emit.log.message=""] - Log message template. * @param {Function} callback - Callback function to signal completion. */ async configure(config, callback) { this.config = config = extend(true, {}, DEFAULTS, config); this.queue = new Queue(); this.watermark = new Watermark(config.$datadir); this.owm = config.watermark || {}; this.xpstart = jsexpr.expr(this.config.start || "start"); // Configure states let prall = Object.keys(this.config.states).map(async (key) => { let state = this.config.states[key]; state.name = key; state.options = extend(true, {}, this.config.options, state.options); state.tls = extend(true, {}, this.config.tls, state.tls); let tls = state.tls; if (tls) this.tls = await TLS.configure(tls, config.$path); state.url = jsexpr.expr(state.options.url || state.url || 'http://localhost'); state.options = jsexpr.expr(state.options); state.method = jsexpr.expr(state.method || 'GET'); state.options.agentOptions = tls ? state.tls : {}; state.emit = state.emit || []; state.emit.forEach(emit => { emit.when = jsexpr.eval(emit.when || 'true'); emit.publish = emit.publish ? jsexpr.expr(emit.publish) : null; emit.next = jsexpr.expr(emit.next || state.name); emit.store = emit.store ? jsexpr.expr(emit.store) : () => { }; emit.timeout = jsexpr.eval(`${emit.timeout || '-1'}`); if (emit.log) { emit.log.level = jsexpr.expr(emit.log.level || 'info'); emit.log.message = jsexpr.expr(emit.log.message || ""); } }); }); await Promise.all(prall); await this.watermark.start(); this.wm = await this.watermark.get(this.id); if (!this.wm.last) { this.wm.last = this.owm; } if (!this.wm.store) { this.wm.store = {}; } await this.watermark.save(this.wm); this.state = this.config.states[this.xpstart(this.wm.store)]; callback(); } /** * Fetches data from the current state and processes it. * @returns {Promise<Object|null>} Resolves with the fetched data or null. */ async fetch() { if (this.state.debug) debugger; let wm = this.wm; let last = wm.last; let store = wm.store; let state = this.state; let options = state.options(store); let url = state.url(store); let method = state.method(store); options.url = options.url || url || 'http://localhost'; options.method = options.method || method || 'GET'; logger.silly(`${this.id} http query`, url, options); let result = await new Promise(ok => { request(options, (error, res, body) => { ok({ error, res, body }); }); }); if (result.error) { throw result.error; } if (result.body) { let headers = result.res.headers; if (headers['content-type'] && headers['content-type'].indexOf('application/json') >= 0) { if (typeof (result.body) == 'string') { try { result.body = JSON.parse(result.body); } catch (ignore) { } } } } let emit = state.emit.find(emit => { if (emit.when(result)) return true; }); let msg = { headers: result.res.headers, httpVersion: result.res.httpVersion, url: url, statusCode: result.res.statusCode, originalMessage: emit && emit.publish ? emit.publish(result) : result.body }; wm.last = msg; if (emit) { wm.store = extend(true, wm.store, emit.store(result)); } await this.watermark.save(wm); // If "then" clause, publish message, otherwise only change state return emit ? { entry: emit.publish ? msg : null, emit, result } : null; } /** * Starts the HTTPSMInput and initializes the state machine. * @param {Function} callback - Callback function to signal completion. */ start(callback) { this.timeout = 0; callback(); } /** * Retrieves the next item from the state machine. * @param {Function} callback - Callback function to process the next item. */ async next(callback) { let entry, emit, result; do { // Wait timeout or wait forever if end state if (this.timeout >= 0) { await timer(this.timeout); } else { await new Promise(ok => { }); } try { // Fetch HTTP data ({ entry, emit, result } = await this.fetch()); // Entry matches an emit clause if (emit) { this.state = this.config.states[emit.next(result)] || this.state; this.timeout = emit.timeout(entry); if (emit.log) { logger[emit.log.level(result)](`${this.id} ${emit.log.message(result)}`); } logger.silly(`${this.id} Next query in ${this.timeout} ms`); } // If entry, publish if (entry) { callback(null, entry); } } catch (err) { logger.error(`${this.id} `, err); return callback(err); } } while (!entry); } /** * Stops the HTTPSMInput and performs cleanup. * @param {Function} callback - Callback function to signal completion. */ stop(callback) { callback(); } /** * Pauses the HTTPSMInput by halting state transitions. * @param {Function} callback - Callback function to signal completion. */ pause(callback) { this.stop(callback); } /** * Resumes the HTTPSMInput by restarting state transitions. * @param {Function} callback - Callback function to signal completion. */ resume(callback) { this.start(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.url}`; } } module.exports = HTTPSMInput;