@mistereo/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
347 lines (306 loc) • 8.44 kB
JavaScript
const util = require('util');
const __ = require('@outofsync/lodash-ex');
const Transport = require('winston-transport');
const os = require('os');
const dgram = require('dgram');
const tls = require('tls');
const net = require('net');
const fs = require('fs');
class LogstashTransport extends Transport {
constructor(options) {
const defaults = {
mode: 'udp4',
localhost: os.hostname(),
host: '127.0.0.1',
port: 28777,
applicationName: process.title,
pid: process.pid,
silent: false,
maxConnectRetries: 4,
timeoutConnectRetries: 100,
sslEnable: false,
sslKey: '',
sslCert: '',
sslCA: '',
sslPassPhrase: '',
sniServerName: null,
rejectUnauthorized: false,
label: process.title,
trailingLineFeed: false,
trailingLineFeedChar: os.EOL,
level: 'info',
formatted: true
};
options = options || {};
options.applicationName = options.applicationName || options.appName || process.title;
options = __.merge(defaults, options);
super(options);
this.silent = options.silent;
// Assign all options to local properties
__.forEach(options, (value, key) => {
this[key] = value;
});
this.name = 'logstashTransport';
if (this.mode === 'tcp') { this.mode = 'tcp4'; }
if (this.mode === 'udp') { this.mode = 'udp4'; }
if (this.mode.substr(3, 4) === '6' && this.host === '127.0.0.1') {
this.host = '::0';
}
// Connection state
this.logQueue = [];
this.connectionState = 'NOT CONNECTED';
this.socketmode = null;
this.socket = null;
this.retries = -1;
this.connect();
}
tryStringify(data) {
let msg;
// when options.formatted is false this will always be skipped
if (typeof data !== 'object') {
const strData = data;
data = { data: strData };
}
try {
msg = JSON.stringify(data);
} catch (err) {
msg = util.inspect(data, { depth: null });
}
return msg;
}
log(info, callback) {
if (this.silent) {
callback(null, true);
return;
}
if (info.message) {
let output;
if (!this.formatted) {
output = this.tryStringify(info);
} else {
const msg = this.tryStringify(info.message);
output = JSON.stringify({
timestamp: new Date().toISOString(),
message: msg,
level: info.level,
label: this.label,
application: this.applicationName,
serverName: this.localhost,
pid: this.pid
});
}
if (this.connectionState !== 'CONNECTED') {
this.logQueue.push({
message: 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(message, callback) {
callback = callback || (() => {});
this.socket.write(message, undefined, callback);
}
deliverUDP(message, callback) {
callback = callback || (() => {});
const buff = Buffer.from(message);
this.socket.send(buff, 0, buff.length, this.port, this.host, callback);
}
deliver(message, callback) {
if (this.trailingLineFeed) {
message = message.replace(/\s+$/, '') + this.trailingLineFeedChar;
}
switch (this.socketmode) {
case 'tcp6':
case 'tcp4': {
this.deliverTCP(message, callback);
break;
}
case 'udp6':
case 'udp4':
default: {
this.deliverUDP(message, callback);
break;
}
}
}
connectTCP() {
const options = {
host: this.host,
port: this.port
};
if (this.sslEnable) {
options.key = this.sslKey ? fs.readFileSync(this.sslKey) : null;
options.cert = this.sslCert ? fs.readFileSync(this.sslCert) : null;
options.passphrase = this.sslPassPhrase || null;
options.rejectUnauthorized = (this.rejectUnauthorized === true);
options.servername = this.sniServerName;
if (this.ca) {
options.ca = [];
__.forEach(this.ca, (value) => {
options.ca.push(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', () => {
this.connectionState = 'NOT CONNECTED';
if (this.socket && (typeof this.socket !== 'undefined')) {
this.socket.destroy();
}
this.socket = null;
this.emit('close');
// if (!(/ECONNREFUSED/).test(err.message) && !(/socket has been ended/).test(err.message)) {
// this.emit('close');
// console.log(err);
// // setImmediate(() => {
// // this.emit('error', err);
// // });
// } else {
// this.emit('close');
// }
});
this.socket.on('timeout', () => {
if (this.socket.readyState !== 'open') {
this.socket.destroy();
}
});
this.socket.on('connect', () => {
this.connectionState = 'CONNECTED';
this.retries = 0;
});
this.socket.on('close', () => {
if (this.connectionState === 'TERMINATING') {
return;
}
if (this.maxConnectRetries >= 0 && this.retries >= this.maxConnectRetries) {
this.logQueue = [];
this.silent = true;
console.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.mode, { sendBufferSize: 60000 });
this.socket.on('error', (err) => {
// Do nothing
if (!(/ECONNREFUSED/).test(err.message)) {
console.error(err);
// setImmediate(() => {
// this.emit('error', err);
// });
}
});
this.socket.on('close', () => {
this.connectionState = 'NOT CONNECTED';
});
if (this.socket.unref) {
this.socket.unref();
}
this.announce();
}
connect() {
if (this.connectionState !== 'CONNECTED') {
this.socketmode = this.mode;
this.connectionState = 'CONNECTING';
switch (this.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.message, elem.callback);
}
}
announce() {
this.flush();
if (this.connectionState === 'TERMINATING') {
this.close();
} else {
this.connectionState = 'CONNECTED';
}
}
getQueueLength() {
return this.logQueue.length;
}
}
module.exports = LogstashTransport;