@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
JavaScript
'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;