UNPKG

winston-syslog

Version:
516 lines (460 loc) 14 kB
/* * 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 };