UNPKG

winston-3-papertrail

Version:

A Papertrail transport for winston. Fork of http://github.com/kenperkins/winston-papertrail.git

308 lines (255 loc) 8.12 kB
/** * winston-papertrail.js * * Transport for logging to Papertrail service * https://papertrailapp.com/ * * Based on a previous version (1.x) by Ken Perkins. */ const Transport = require('winston-transport'); const syslogProducer = require('glossy').Produce; const net = require('net'); const os = require('os'); const tls = require('tls'); const util = require('util'); const { EventEmitter } = require('events'); const KEEPALIVE_INTERVAL = 15 * 1000; class PapertrailConnection extends EventEmitter { constructor (options) { super(); const DEFAULT_OPTIONS = { attemptsBeforeDecay: 5, connectionDelay: 1000, maxDelayBetweenConnections: 60000, maximumAttempts: 25, disableTls: false, flushOnClose: true, }; this.options = Object.assign({}, DEFAULT_OPTIONS, options); if (!this.options.host || !this.options.port) { throw new Error('Missing required parameters: host and port'); } this.connectionDelay = this.options.connectionDelay; this.currentRetries = 0; this.totalRetries = 0; this.loggingEnabled = true; this._shutdown = false; /** * Dev could instantiate a new logger and then call logger.log immediately. * We need a way to put incoming strings (from multiple transports) into * a buffer queue. */ this.deferredQueue = []; this.deferredQueueLength = 0; this.connect(); } connect () { if (this._shutdown || this._erroring) { return; } this.close(); try { if (this.options.disableTls) { this.stream = net.createConnection(this.options.port, this.options.host, this.onConnected.bind(this)); this.stream.setKeepAlive(true, KEEPALIVE_INTERVAL); this.stream.once('error', this.onErrored.bind(this)); this.stream.once('end', this.connect.bind(this)); } else { this.socket = net.createConnection(this.options.port, this.options.host, () => { this.socket.setKeepAlive(true, KEEPALIVE_INTERVAL); this.stream = tls.connect({ socket: this.socket, rejectUnauthorized: false, }, this.onConnected.bind(this)); this.stream.once('error', this.onErrored.bind(this)); this.stream.once('end', this.connect.bind(this)); }); this.socket.once('error', this.onErrored.bind(this)); } } catch (err) { this.onErrored(err); } } write (text, callback) { // If the stream is writable if (this.stream && this.stream.writable) { this.stream.write(text, callback); } else { // Otherwise, store it in a buffer and write it when we're connected this.deferredQueue.push({ buffer: text, callback, }); this.deferredQueueLength++; } } processBuffer () { if (this.deferredQueue.length === 0 || !this.stream || !this.stream.writable) { return; } while (this.deferredQueue.length > 0) { const queueItem = this.deferredQueue.shift(); const callback = () => { queueItem.callback(); this.deferredQueueLength--; if (this.deferredQueueLength === 0) { this.stream.emit('empty'); } }; this.stream.write(queueItem.buffer, callback); } } onConnected () { this.loggingEnabled = true; this.currentRetries = 0; this.totalRetries = 0; this.connectionDelay = this.options.connectionDelay; this.processBuffer(); this.emit('connect', `Connected to Papertrail at ${this.options.host}:${this.options.port}`); } onErrored (err) { this._erroring = true; this.emitSilentError(err); setTimeout(() => { if ((this.connectionDelay < this.options.maxDelayBetweenConnections) && (this.currentRetries >= this.attemptsBeforeDecay)) { this.connectionDelay = this.connectionDelay * 2; this.currentRetries = 0; } if (this.loggingEnabled && this.totalRetries >= this.options.maximumAttempts) { this.loggingEnabled = false; this.emitSilentError(new Error('Max entries eclipsed, disabling buffering')); } this._erroring = false; this.connect(); }, this.connectionDelay); } emitSilentError (err) { if (this.listenerCount('error') > 0) { this.emit('error', err); } else { console.error(`Papertrail connection error: ${err}`); } } clean () { try { if (this.socket) { this.socket.destroy(); this.socket = null; } this.stream.removeListener('end', this.connect); this.stream.removeListener('error', this.onErrored); this.stream.destroy(); this.stream = null; } catch (ignored) { } } close () { this._shutdown = true; if (this.stream) { if (this.options.flushOnClose && this.deferredQueueLength > 0) { this.stream.on('empty', () => { this.clean(); }); } else { this.clean(); } } this._shutdown = false; } } class PapertrailTransport extends Transport { /** * @param {PapertrailConnection} connection * @param options */ constructor (connection, options = {}) { super(options); const DEFAULT_OPTIONS = { inlineMeta: false, colorize: false, program: 'default', facility: 'daemon', hostname: os.hostname(), depth: null, levels: { silly: 7, debug: 7, verbose: 7, info: 6, warn: 4, error: 3, }, logFormat: function (level, message) { return level + ' ' + message; }, }; this.connection = connection; this.options = Object.assign({}, DEFAULT_OPTIONS, options); this.producer = new syslogProducer({ facility: this.options.facility }); } log (info, callback) { setImmediate(() => { this.emit('logged', info); }); // write to Papertrail const { level, message, meta } = info; let output = message; if (meta) { if (typeof meta !== 'object') { output += ' ' + meta; } else if (meta) { if (this.options.inlineMeta) { output += ' ' + util .inspect(meta, { showHidden: false, depth: this.options.depth, colors: this.options.colorize, }) .replace(/[\n\t]\s*/gm, ' '); } else { output += '\n' + util.inspect(meta, false, this.options.depth, this.options.colorize); } } } this.sendMessage(level, output, callback); } sendMessage (level, message, callback) { let lines = []; let msg = ''; let gap = ''; // Only split if we actually have a message if (message) { lines = message.split('\n'); } else { lines = ['']; } // If the incoming message has multiple lines, break them and format each // line as its own message for (let i = 0; i < lines.length; i++) { // don't send extra message if our message ends with a newline if (lines[i].length === 0 && i === lines.length - 1) { break; } if (i === 1) { gap = ' '; } // Strip escape characters (for colorization) const cleanedLevel = level.replace(/\u001b\[\d+m/g, ''); msg += this.producer.produce({ severity: this.options.levels[cleanedLevel] || cleanedLevel, host: this.options.hostname, appName: this.options.program, date: new Date(), message: this.options.logFormat(level, gap + lines[i]), }) + '\r\n'; } this.connection.write(msg, callback); } close () { this.connection.close(); } } module.exports = { PapertrailConnection, PapertrailTransport };