winston-syslog
Version:
A syslog transport for winston
516 lines (460 loc) • 14 kB
JavaScript
/*
* syslog.js: Transport for logging to a remote syslog consumer
*
* (C) 2011 Squeeks and Charlie Robbins
* MIT LICENCE
*
*/
const dgram = require('dgram');
const net = require('net');
const secNet = require('tls');
const utils = require('./utils');
const glossy = require('glossy');
const winston = require('winston');
const Transport = require('winston-transport');
const { MESSAGE, LEVEL } = require('triple-beam');
const _noop = () => { };
// Ensure we have the correct winston here.
if (Number(winston.version.split('.')[0]) < 3) {
throw new Error('Winston-syslog requires winston >= 3.0.0');
}
const levels = Object.keys({
debug: 0,
info: 1,
notice: 2,
warning: 3,
warn: 3,
error: 4,
err: 4,
crit: 5,
alert: 6,
emerg: 7
});
//
// ### function Syslog (options)
// #### @options {Object} Options for this instance.
// Constructor function for the Syslog Transport capable of sending
// RFC 3164 and RFC 5424 compliant messages.
//
class Syslog extends Transport {
//
// Expose the name of this Transport on the prototype
//
get name() {
return 'syslog';
}
constructor(options = {}) {
//
// Inherit from `winston-transport`.
//
super(options);
//
// Setup connection state
//
this.connected = false;
this.congested = false;
this.retries = 0;
this.queue = [];
this.inFlight = 0;
//
// Merge the options for the target Syslog server.
//
this.setOptions(options);
//
// Setup our Syslog and network members for later use.
//
this.socket = null;
var Producer = options.customProducer || glossy.Produce;
this.producer = new Producer({
type: this.type,
appName: this.appName,
pid: this.pid,
facility: this.facility
});
}
setOptions(options) {
this.host = options.host || 'localhost';
this.port = options.port || 514;
this.path = options.path || null;
this.protocol = options.protocol || 'udp4';
this.protocolOptions = options.protocolOptions || {};
this.endOfLine = options.eol;
this.parseProtocol(this.protocol);
// this options provides a secure socket for sending logs to the syslog server based on
// TLSSocket options (https://nodejs.org/api/tls.html#class-tlstlssocket).
// Therefore, TLSSocket options are configurable through the code.
this.secureProtocol = options.secureProtocol;
this.ciphers = options.ciphers;
this.ecdhCurve = options.ecdhCurve;
this.rejectUnauthorized = options.rejectUnauthorized;
this.requestCert = options.requestCert;
// end options for secure socket
//
// Merge the default message options.
//
this.localhost =
typeof options.localhost !== 'undefined'
? options.localhost
: 'localhost';
this.type = options.type || 'BSD';
this.facility = options.facility || 'local0';
this.pid = options.pid || process.pid;
this.appName = options.appName || options.app_name || process.title;
}
parseProtocol(protocol = this.protocol) {
const parsedProtocol = utils.parseProtocol(protocol);
this.protocolType = parsedProtocol.type;
this.protocolFamily = parsedProtocol.family;
this.isDgram = parsedProtocol.isDgram;
if (this.protocolType === 'unix' && !this.path) {
throw new Error('`options.path` is required on unix dgram sockets.');
}
}
//
// ### function chunkMessage (buffer, callback)
// #### @buffer {Buffer} Syslog message buffer.
// #### @callback {function} Continuation to respond to when complete.
// Syslog messages sent over the UDP transport must be 64KB bytes or less. In
// order to avoid silent failures messages should be chunked when the buffer
// to write is larger than the maximum message size.
//
chunkMessage(buffer, callback = _noop) {
if (!this.connected) {
this.queue.push(buffer);
} else {
// Maximum payload for the UDP transport
// 65535 − 8 bytes UDP header − 20 bytes IP header
// https://tools.ietf.org/html/rfc5426#section-3.2
// This makes sense on loopback interfaces with MTU = 65536
// For non-loopback messages, it's impossible to know in advance
// the MTU of each interface through which a packet might be routed
// https://nodejs.org/api/dgram.html
const MAX_UDP_PAYLOAD = 65507;
let offset = 0;
while (offset < buffer.length) {
this.inFlight++;
const length =
offset + MAX_UDP_PAYLOAD > buffer.length
? buffer.length - offset
: MAX_UDP_PAYLOAD;
this._sendChunk(
buffer,
{ offset: offset, length: length, port: this.port, host: this.host },
callback
);
offset += length;
}
}
}
//
// ### function _sendChunk (buffer, options, callback)
// #### @buffer {Buffer} Syslog message buffer.
// #### @options {object} Options for the message send method.
// #### @callback {function} Continuation to respond to when complete.
// Sends a single chunk from an oversize UDP buffer.
//
_sendChunk(buffer, options, callback) {
try {
this.socket.send(
buffer,
options.offset,
options.length,
options.port,
options.host,
callback
);
} catch (err) {
return callback(err);
}
}
//
// ### function log (info, callback)
// #### @info {object} All relevant log information
// #### @callback {function} Continuation to respond to when complete.
// Core logging method exposed to Winston. Logs the `msg` and optional
// metadata, `meta`, to the specified `level`.
//
log(info, callback) {
let level = info[LEVEL];
if (!~levels.indexOf(level)) {
return callback(
new Error('Cannot log unknown syslog level: ' + info[LEVEL])
);
}
level = level === 'warn' ? 'warning' : level;
const output = info[MESSAGE];
const syslogMsg = this.producer.produce({
severity: level,
host: this.localhost,
appName: this.appName,
date: new Date(),
message: this.endOfLine ? output + this.endOfLine : output
});
//
// Attempt to connect to the socket
//
this.connect(err => {
if (err) {
//
// If there was an error enqueue the message
//
this.queue.push(syslogMsg);
return callback();
}
//
// On any error writing to the socket, enqueue the message
//
const onError = logErr => {
if (logErr) {
this.queue.push(syslogMsg);
this.emit('error', logErr);
}
this.emit('logged', info);
this.inFlight--;
};
const onCongestion = () => {
onError(new Error('Congestion Error'));
};
const sendDgram = () => {
const buffer = Buffer.from(syslogMsg);
if (this.protocolType === 'udp') {
this.chunkMessage(buffer, onError);
} else if (this.protocol === 'unix') {
this.inFlight++;
this.socket.send(buffer, 0, buffer.length, this.path, onError);
} else if (this.congested) {
this.queue.push(syslogMsg);
} else {
this.socket.once('congestion', onCongestion);
this.inFlight++;
this.socket.send(buffer, e => {
this.socket.removeListener('congestion', onCongestion);
onError(e);
});
}
};
//
// Write to the `tcp*`, `udp*`, or `unix` socket.
//
if (this.isDgram) {
sendDgram();
} else {
this.socket.write(syslogMsg, 'utf8', onError);
}
callback(null, true);
});
}
//
// ### function close ()
// Closes the socket used by this transport freeing the resource.
//
close() {
const max = 6;
let attempt = 0;
const _close = () => {
if (attempt >= max || (this.queue.length === 0 && this.inFlight <= 0)) {
if (this.socket) {
if (this.socket.destroy) {
// https://nodejs.org/api/net.html#net_socket_destroy_exception
this.socket.destroy();
} else if (this.socket.close) {
// https://nodejs.org/api/dgram.html#dgram_socket_close_callback
// https://www.npmjs.com/package/unix-dgram#socketclose
this.socket.close();
}
}
this.emit('closed', this.socket);
} else {
attempt++;
setTimeout(_close, 200 * attempt);
}
};
_close();
}
connectDgram(callback) {
if (this.protocol === 'unix-connect') {
return this._unixDgramConnect(callback);
} else if (this.protocol === 'unix') {
this.socket = require('unix-dgram').createSocket('unix_dgram');
} else if (!this.socket) {
// UDP protocol
const proto = this.protocol === 'udp' ? 'udp4' : this.protocol;
// https://nodejs.org/api/all.html#dgram_class_dgram_socket
this.socket = dgram.createSocket({
type: proto
})
.on('listening', () => {
this.connected = true;
let msg = this.queue.shift();
while (msg) {
this.chunkMessage(msg);
msg = this.queue.shift();
}
});
this.socket.bind();
}
return callback(null);
}
//
// ### function connect (callback)
// #### @callback {function} Continuation to respond to when complete.
// Connects to the remote syslog server using `dgram` or `net` depending
// on the `protocol` for this instance.
//
connect(callback) {
//
// If the socket already exists then respond
//
if (this.socket) {
return !this.socket.readyState ||
this.socket.readyState === 'open' ||
this.socket.connected
? callback(null)
: callback(true);
}
//
// Create the appropriate socket type.
//
if (this.isDgram) {
return this.connectDgram(callback);
}
// Set Secure options for TLS
const options = {
secureProtocol: this.secureProtocol,
ciphers: this.ciphers,
ecdhCurve: this.ecdhCurve,
rejectUnauthorized: this.rejectUnauthorized,
requestCert: this.requestCert
};
this.socket = /^tls[4|6]?$/.test(this.protocol)
? new secNet.TLSSocket(this.socket, options) // Set options for TLSSocket
: new net.Socket();
this.socket.setKeepAlive(true);
this.socket.setNoDelay();
this.setupEvents();
const connectConfig = Object.assign({}, this.protocolOptions, {
host: this.host,
port: this.port
});
if (this.protocolFamily) {
connectConfig.family = this.protocolFamily;
}
this.socket.connect(connectConfig);
//
// Indicate to the callee that the socket is not ready. This
// will enqueue the current message for later.
//
callback(true);
}
setupEvents() {
const readyEvent = 'connect';
//
// On any error writing to the socket, emit the `logged` event
// and the `error` event.
//
const onError = logErr => {
if (logErr) {
this.emit('error', logErr);
}
this.emit('logged');
this.inFlight--;
};
//
// Listen to the appropriate events on the socket that
// was just created.
//
this.socket
.on(readyEvent, () => {
//
// When the socket is ready, write the current queue
// to it.
//
this.socket.write(this.queue.join(''), 'utf8', onError);
this.emit('logged');
this.queue = [];
this.retries = 0;
this.connected = true;
})
.on('error', function () {
//
// TODO: Pass this error back up
//
})
.on('close', () => {
//
// Attempt to reconnect on lost connection(s), progressively
// increasing the amount of time between each try.
//
const interval = Math.pow(2, this.retries);
this.connected = false;
setTimeout(() => {
this.retries++;
this.socket.connect(this.port, this.host);
}, interval * 1000);
})
.on('timeout', () => {
if (this.socket.destroy) {
// https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback
this.socket.destroy();
} else if (this.socket.close) {
// https://nodejs.org/api/dgram.html#dgram_socket_close_callback
// https://www.npmjs.com/package/unix-dgram#socketclose
this.socket.close();
}
});
}
_unixDgramConnect(callback) {
const self = this;
const flushQueue = () => {
let sentMsgs = 0;
this.queue.forEach(msg => {
const buffer = Buffer.from(msg);
if (!this.congested) {
if (this.protocolType === 'udp') {
this.chunkMessage(buffer, () => {
++sentMsgs;
});
} else {
this.socket.send(buffer, function () {
++sentMsgs;
});
}
}
});
this.queue.splice(0, sentMsgs);
};
this.socket = require('unix-dgram').createSocket('unix_dgram');
this.socket.on('error', err => {
this.emit('error', err);
if (err.syscall === 'connect') {
this.socket.close();
this.socket = null;
return callback(err);
}
if (err.syscall === 'send') {
this.socket.close();
this.socket = null;
}
});
this.socket.on('connect', function () {
this.on('congestion', () => {
self.congested = true;
});
this.on('writable', () => {
self.congested = false;
flushQueue();
});
flushQueue();
callback();
});
this.socket.connect(this.path);
}
}
//
// Define a getter so that `winston.transports.Syslog`
// is available and thus backwards compatible.
//
winston.transports.Syslog = Syslog;
module.exports = {
Syslog
};