UNPKG

@mmathias01/winston3-logstash-transport

Version:

A winston@3 replacement for both winston-logstash and winston-logstash-udp to facilitate either TCP or UDP traffic to logstash

512 lines (446 loc) 14.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var Transport = _interopDefault(require('winston-transport')); var debug = _interopDefault(require('diagnostics')); var os = require('os'); var dgram = require('dgram'); var tls = require('tls'); var net = require('net'); var fs = require('fs'); var stringify = _interopDefault(require('fast-safe-stringify')); var deepmerge = _interopDefault(require('deepmerge')); /** @module winston3-logstash-transport */ /** * Setup Debugging */ const name = 'winston:logstash'; // const _debug = debugModule(name); // const _debug = debug(name); const logger = { log: debug(`${name}:info`), warn: debug(`${name}:warn`), error: debug(`${name}:warn`), }; /** * Enumeration for LogstashProtocol * @readonly * @enum {string} */ const LogstashTransportProtocol = { UDP: 'udp4', UDP4: 'udp4', UDP6: 'udp6', TCP: 'tcp4', TCP4: 'tcp4', TCP6: 'tcp6' }; /** * @constant * @default * @type {LogstashTransportOptions} */ const defaultOptions = { silent: false, connection: { host: '127.0.0.1', port: 28777, mode: LogstashTransportProtocol.UDP4, maxRetries: 4, retryFrequency: 100, udp: { trailingLineFeed: false, trailingLineFeedChar: os.EOL }, ssl: { enabled: true, key: '', cert: '', ca: '', passphrase: '', rejectUnauthorized: false } }, formatting: { formatted: true, timestampField: 'timestamp', format: { application: process.title, serverName: os.hostname(), pid: process.pid, label: process.title } } }; /** * A Winston@3 transport for LogStash. * @extends Transport */ class LogstashTransport extends Transport { /** * Create a LogstashTransport. * @constructor * @param {LogstashTransportOptions} [options] - {@link LogstashTransportOptions} options object */ constructor(options = {}) { super(options); this.silent = options.silent; logger.log(`Provided options: ${stringify(options)}`); /** * @type {LogstashTransportOptions} */ this.options = this.mergeOptions(options); this.name = name; const { mode, host } = this.options.connection.mode; if ((mode === LogstashTransportProtocol.TCP6 || mode === LogstashTransportProtocol.UDP6) && host === '127.0.0.1') { logger.warn(`IPv4 localhost address(${host}), IPv6 mode (${mode}). Using IPv6 localhost address(::0)!`); this.options.connection.host = '::0'; } // Connection state this.logQueue = []; this.connectionState = 'NOT CONNECTED'; this.socketmode = null; this.socket = null; this.retries = -1; this.connect(); this.on('logged', info => { const message = info.message || info; logger.log(`Logged Event - ${message}`); }); } mergeOptions(options) { const deprecatedFields = [ 'mode', 'localhost', 'host', 'port', 'maxConnectRetries', 'timeoutConnectRetries', 'sslEnable', 'sslKey', 'sslCert', 'sslCA', 'sslPassPhrase', 'rejectUnauthorized', 'trailingLineFeed', 'trailingLineFeedChar', 'applicationName', 'appName', 'pid', 'label', 'level' ]; const deprecatedFieldsInUse = deprecatedFields.reduce((collector, fieldName) => { if (options[fieldName]) { collector.push(fieldName); } return collector; }, []); if (deprecatedFieldsInUse.length > 0) { const errMsg = `The fields ${stringify(deprecatedFieldsInUse)} are deprecated! Update your options shape!`; logger.error(errMsg); } // // Adjust the provided connection options to fit the new shape // const _connection = { // host: options.host || defaultOptions.connection.host, // port: options.port || defaultOptions.connection.host, // mode: options.mode || defaultOptions.connection.host, // maxRetries: options.maxConnectRetries || defaultOptions.connection.host, // retryFrequency: options.timeoutConnectRetries // ...(options.connection ? options.connection : {}) // }; // options.connection = _connection; // const _udp = { // trailingLineFeed: options.trailingLineFeed, // trailingLineFeedChar: options.trailingLineFeedChar, // ...(options.connection.upd ? options.connection.upd : {}) // }; // options.connection.udp = _udp; // const _ssl = { // enabled: options.sslEnable, // key: options.sslKey, // cert: options.sslCert, // ca: options.sslCA, // passphrase: options.sslPassPhrase, // rejectUnauthorized: options.rejectUnauthorized, // ...(options.connection.ssl ? options.connection.ssl : {}) // }; // _ssl.ca = _ssl.ca ? Array.isArray(_ssl.ca) ? _ssl.ca : [_ssl.ca] : undefined; // options.connection.ssl = _ssl; // // Adjust the provided formatting options to fit the new shape // const _logFormat = { // applicationName: options.applicationName || options.appName || process.title, // hostname: options.localhost, // pid: options.pid, // label: options.label, // level: options.level, // ...(options.logFormat ? options.logFormat : {}) // }; // options.logFormat = _logFormat; // Remove the deprecated fields for good measure deprecatedFieldsInUse.forEach(field => { delete options[field]; }); const mergedOptions = deepmerge(defaultOptions, options); // { ...defaultOptions, }; logger.log(`Merged options: ${stringify(mergedOptions)}`); // const ca = mergedOptions.connection.ssl.ca; // mergedOptions.connection.ssl.ca = Array.isArray(ca) ? ca : [ca]; return mergedOptions; } getFormattedOutput(info) { const { enabled, timestampField, format } = this.options.formatting; const { message: _message, level } = info; if (!enabled) { logger.log(`Formatting not enabled. Logging raw info object: ${info}`); return stringify(info); } const _log = { [timestampField]: new Date().toISOString(), message: typeof _message !== 'object' ? { data: _message } : _message, level: level }; const log = { ...format, ..._log }; logger.log(`Formatting enabled. Logging formatted log: ${info}`); return stringify(log); } log(info, callback) { if (this.silent) { callback(null, true); return; } const output = this.getFormattedOutput(info); if (this.connectionState !== 'CONNECTED') { this.logQueue.push({ output: output, callback: () => { this.emit('logged', info); callback(); // callback(err, !err); } }); } else { setImmediate(() => { try { this.deliver(output, () => { this.emit('logged', info); callback(); // callback(err, !err); }); } catch (err) { callback(); } }); } return; } deliverTCP(output, callback) { callback = callback || (() => {}); this.socket.write(output, undefined, callback); } deliverUDP(output, callback) { const { host, port } = this.options.connection; callback = callback || (() => {}); const buff = Buffer.from(output); this.socket.send(buff, 0, buff.length, port, host, callback); } deliver(output, callback) { const { trailingLineFeed, trailingLineFeedChar } = this.options.connection.udp; if (trailingLineFeed) { output = output.replace(/\s+$/, '') + trailingLineFeedChar; } switch (this.socketmode) { case 'tcp6': case 'tcp4': { this.deliverTCP(output, callback); break; } case 'udp6': case 'udp4': default: { this.deliverUDP(output, callback); break; } } } connectTCP() { const { connection: { host, port, ssl } } = this.options.connection; const options = { host, port }; if (ssl.enabled) { options.key = ssl.key ? fs.readFileSync(ssl.key) : null; options.cert = ssl.cert ? fs.readFileSync(ssl.cert) : null; options.passphrase = ssl.passphrase || null; options.rejectUnauthorized = ssl.rejectUnauthorized === true; if (ssl.ca) { options.ca = ssl.ca.map(value => { return fs.readFileSync(value); }); } this.socket = tls.connect(options, () => { this.socket.setEncoding('UTF-8'); this.announce(); this.connectionState = 'CONNECTED'; }); } else { this.socket = new net.Socket(); this.socket.connect(options, () => { this.socket.setKeepAlive(true, 60 * 1000); this.announce(); this.connectionState = 'CONNECTED'; }); } this.hookTCPSocketEvents(); } hookTCPSocketEvents() { this.socket.on('error', err => { this.connectionState = 'NOT CONNECTED'; if (this.socket && typeof this.socket !== 'undefined') { this.socket.destroy(); } this.socket = null; this.emit('close'); logger.error(`TCP socket error! ${err}`); }); this.socket.on('timeout', () => { if (this.socket.readyState !== 'open') { logger.info('TCP socket timed out'); this.socket.destroy(); } }); this.socket.on('connect', () => { this.connectionState = 'CONNECTED'; this.retries = 0; logger.info('TCP socket connected'); }); this.socket.on('close', () => { if (this.connectionState === 'TERMINATING') { return; } if (this.maxConnectRetries >= 0 && this.retries >= this.maxConnectRetries) { this.logQueue = []; this.silent = true; logger.error('Max retries reached, placing transport in OFFLINE/silent mode.'); // setImmediate(() => { // this.emit('error', new Error('Max retries reached, placing transport in OFFLINE/silent mode.')); // }); } else if (this.connectionState !== 'CONNECTING') { setTimeout(() => { this.connect(); }, this.timeoutConnectRetries); } }); } connectUDP() { this.socket = dgram.createSocket(this.options.connection.mode, { sendBufferSize: 60000 }); this.socket.on('error', err => { if (!/ECONNREFUSED/.test(err.message)) { logger.error(`UDP socket error! ${err}`); } }); this.socket.on('close', () => { logger.info('UDP socket closed'); this.connectionState = 'NOT CONNECTED'; }); if (this.socket.unref) { this.socket.unref(); } this.announce(); } connect() { if (this.connectionState !== 'CONNECTED') { this.socketmode = this.options.connection.mode; this.connectionState = 'CONNECTING'; switch (this.options.connection.mode) { case 'tcp6': case 'tcp4': { this.connectTCP(); break; } case 'udp6': case 'udp4': default: { this.connectUDP(); break; } } } } closeTCP() { this.socket.end(); this.socket.destroy(); this.socket = null; this.connectionState = 'NOT CONNECTED'; } closeUDP() { this.socket.close(); this.connectionState = 'NOT CONNECTED'; } close() { if (this.connectionState === 'CONNECTED' && this.socket) { this.connectionState = 'TERMINATING'; switch (this.socketmode) { case 'tcp6': case 'tcp4': { this.closeTCP(); break; } case 'udp6': case 'udp4': default: { this.closeUDP(); break; } } this.socketmode = null; } } flush() { while (this.logQueue.length > 0) { const elem = this.logQueue.shift(); this.deliver(elem.output, elem.callback); } } announce() { this.flush(); if (this.connectionState === 'TERMINATING') { this.close(); } else { this.connectionState = 'CONNECTED'; } } getQueueLength() { return this.logQueue.length; } } /** * Options for the Winston 3 LogstashTransport. * @typedef {object} LogstashTransportOptions * @property {boolean} [silent] - Offline / Silent mode enabled * @property {object} [connection] - LogStash connection information * @property {string} [connection.host] - The LogStash server ip or hostname * @property {string} [connection.port] - The LogStash server port number * @property {LogstashTransportProtocol} [connection.mode] - {@link LogstashTransportProtocol} to use to for LogStash. * @property {number} [connection.maxRetries] - The number of attempts to reconnect to make before erroring out * @property {number} [connection.retryFrequency] - The number of milliseconds to wait between connection attempts * @property {object} [connection.udp] - UDP specific connection parameters * @property {boolean} [connection.udp.trailingLineFeed] - Enable appending end of line character to UDP output * @property {string} [connection.udp.trailingLineFeedChar] - character(s) to append to UDP output * @property {object} [connection.ssl] - SSL specific connection parameters * @property {boolean} [connection.ssl.enabled] - Whether SSL/TLS connection should be attempted when connecting via TCP * @property {string} [connection.ssl.key] - The filepath to the SSL Key * @property {string} [connection.ssl.cert] - The filepath to the SSL Cert * @property {string|string[]} [connection.ssl.ca] - The filepath(s) to the CA Intermediary Certs * @property {string} [connection.ssl.passphrase] - SSL specific connection parameters * @property {boolean} [connection.ssl.rejectUnauthorized] - SSL specific connection parameters * @property {object} [formatting] - Base JSON object to send to LogStash * @property {boolean} [formatting.enabled] - Format logs with supplied format object? * @property {string} [formatting.timestampField] - The key name you want to send for timestamp * @property {object} [formatting.format] - If formatting is enabled the log will be merged into this before sending * @property {string} [formatting.format.application] - The application name sent to LogStash * @property {string} [formatting.format.serverName] - The hostname sent to LogStash * @property {number} [formatting.format.pid] - The Operating System process ID sent to LogStash * @property {string} [formatting.format.label] - The LogStash label to send with the information */ exports.LogstashTransport = LogstashTransport; exports.LogstashTransportProtocol = LogstashTransportProtocol; exports.default = LogstashTransport;