diff --git a/examples/using-winston-transport.js b/examples/using-winston-transport.js new file mode 100644 index 0000000..26fd6fa --- /dev/null +++ b/examples/using-winston-transport.js @@ -0,0 +1,27 @@ +var winston = require('winston'), + WinstonCloudWatch = require('../index'); + +function format(message, ...rest) { + // I am inventing a custom formatter + return ` + ${message} + ${rest.map(r => ` * ${r}`).join('\n')} + ` +} + +function customFormatter({level, message, [Symbol.for('splat')]: args = []}) { + return `${level} - ${format(message, ...args)}`; +} + +var transport = winston.createLogger({ + exitOnError: false, + transports: [ + new WinstonCloudWatch({ + logGroupName: 'testing', + logStreamName: 'first', + awsRegion: 'us-east-1', + })], + format: winston.format.printf(customFormatter) +}); + +transport.info('some text', {foo: 'bar'}, new Error('wtf')); diff --git a/index.js b/index.js index 4e7c882..ead9026 100644 --- a/index.js +++ b/index.js @@ -1,150 +1,146 @@ -'use strict'; - -var util = require('util'), - winston = require('winston'), - AWS = require('aws-sdk'), - cloudWatchIntegration = require('./lib/cloudwatch-integration'), - isEmpty = require('lodash.isempty'), - assign = require('lodash.assign'), - isError = require('lodash.iserror'), - stringify = require('./lib/utils').stringify, - debug = require('./lib/utils').debug, - defaultFlushTimeoutMs = 10000; - -var WinstonCloudWatch = function(options) { - winston.Transport.call(this, options); - this.level = options.level || 'info'; - this.name = options.name || 'CloudWatch'; - this.logGroupName = options.logGroupName; - this.retentionInDays = options.retentionInDays || 0; - this.logStreamName = options.logStreamName; - - var awsAccessKeyId = options.awsAccessKeyId; - var awsSecretKey = options.awsSecretKey; - var awsRegion = options.awsRegion; - var messageFormatter = options.messageFormatter ? options.messageFormatter : function(log) { - return [ log.level, log.message ].join(' - ') - }; - this.formatMessage = options.jsonMessage ? stringify : messageFormatter; - this.proxyServer = options.proxyServer; - this.uploadRate = options.uploadRate || 2000; - this.logEvents = []; - this.errorHandler = options.errorHandler; - - if (options.cloudWatchLogs) { - this.cloudwatchlogs = options.cloudWatchLogs; - } else { - if (this.proxyServer) { - AWS.config.update({ - httpOptions: { - agent: require('proxy-agent')(this.proxyServer) - } - }); - } - - var config = {}; +const winston = require('winston'), + Transport = require('winston-transport'), + AWS = require('aws-sdk'), + { LEVEL, MESSAGE } = require('triple-beam'), + cloudWatchIntegration = require('./lib/cloudwatch-integration'), + isEmpty = require('lodash.isempty'), + assign = require('lodash.assign'), + isError = require('lodash.iserror'), + stringify = require('./lib/utils').stringify, + debug = require('./lib/utils').debug + +const defaultFlushTimeoutMs = 10000 + + +module.exports = class WinstonCloudwatch extends Transport { + constructor(opts) { + super(opts) + this.setOptions(opts) + debug('constructor finished') + } - if (awsAccessKeyId && awsSecretKey && awsRegion) { - config = { accessKeyId: awsAccessKeyId, secretAccessKey: awsSecretKey, region: awsRegion }; - } else if (awsRegion && !awsAccessKeyId && !awsSecretKey) { - // Amazon SDK will automatically pull access credentials - // from IAM Role when running on EC2 but region still - // needs to be configured - config = { region: awsRegion }; + log(info, callback) { + debug('log (called by winston)', info); + if (!isEmpty(info.message) || isError(info.message)) { + this.add(info); } - if (options.awsOptions) { - config = assign(config, options.awsOptions); + if (!/^uncaughtException: /.test(info.message)) { + // do not wait, just return right away + return callback(null, true); } - this.cloudwatchlogs = new AWS.CloudWatchLogs(config); - } + debug('message not empty, proceeding') - debug('constructor finished'); -}; - -util.inherits(WinstonCloudWatch, winston.Transport); - -WinstonCloudWatch.prototype.log = function (info, callback) { - debug('log (called by winston)', info); - - if (!isEmpty(info.message) || isError(info.message)) { - this.add(info); + // clear interval and send logs immediately + // as Winston is about to end the process + clearInterval(this.intervalId); + this.intervalId = null; + this.submit(callback); } - if (!/^uncaughtException: /.test(info.message)) { - // do not wait, just return right away - return callback(null, true); - } + add(log) { + debug('add log to queue', log); - debug('message not empty, proceeding') + if (!isEmpty(log.message) || isError(log.message)) { + this.logEvents.push({ + message: this.formatMessage(log), + timestamp: new Date().getTime() + }); + } - // clear interval and send logs immediately - // as Winston is about to end the process - clearInterval(this.intervalId); - this.intervalId = null; - this.submit(callback); -}; + if (!this.intervalId) { + debug('creating interval'); + this.intervalId = setInterval(() => { + this.submit((err) => { + if (err) { + debug('error during submit', err, true); + this.errorHandler ? this.errorHandler(err) : console.error(err); + } + }); + }, this.uploadRate); + } + } -WinstonCloudWatch.prototype.add = function(log) { - debug('add log to queue', log); + submit(callback) { + var groupName = typeof this.logGroupName === 'function' ? + this.logGroupName() : this.logGroupName + var streamName = typeof this.logStreamName === 'function' ? + this.logStreamName() : this.logStreamName + var retentionInDays = this.retentionInDays - var self = this; + if (isEmpty(this.logEvents)) { + return callback() + } - if (!isEmpty(log.message) || isError(log.message)) { - self.logEvents.push({ - message: self.formatMessage(log), - timestamp: new Date().getTime() - }); + cloudWatchIntegration.upload( + this.cloudwatchlogs, + groupName, + streamName, + this.logEvents, + retentionInDays, + callback + ) } - if (!self.intervalId) { - debug('creating interval'); - self.intervalId = setInterval(function() { - self.submit(function(err) { - if (err) { - debug('error during submit', err, true); - self.errorHandler ? self.errorHandler(err) : console.error(err); - } - }); - }, self.uploadRate); + kthxbye(callback) { + clearInterval(this.intervalId); + this.intervalId = null; + this.flushTimeout = this.flushTimeout || (Date.now() + defaultFlushTimeoutMs); + + this.submit(((error) => { + if (error) return callback(error); + if (isEmpty(this.logEvents)) return callback(); + if (Date.now() > this.flushTimeout) return callback(new Error('Timeout reached while waiting for logs to submit')); + else setTimeout(this.kthxbye.bind(this, callback), 0); + })); } -}; - -WinstonCloudWatch.prototype.submit = function(callback) { - var groupName = typeof this.logGroupName === 'function' ? - this.logGroupName() : this.logGroupName; - var streamName = typeof this.logStreamName === 'function' ? - this.logStreamName() : this.logStreamName; - var retentionInDays = this.retentionInDays; - if (isEmpty(this.logEvents)) { - return callback(); + setOptions(options) { + this.level = options.level || 'info'; + this.name = options.name || 'CloudWatch'; + this.logGroupName = options.logGroupName; + this.retentionInDays = options.retentionInDays || 0; + this.logStreamName = options.logStreamName; + + var awsAccessKeyId = options.awsAccessKeyId; + var awsSecretKey = options.awsSecretKey; + var awsRegion = options.awsRegion; + var messageFormatter = options.messageFormatter ? + options.messageFormatter : (log) => log[MESSAGE] + this.formatMessage = options.jsonMessage ? stringify : messageFormatter; + this.proxyServer = options.proxyServer; + this.uploadRate = options.uploadRate || 2000; + this.logEvents = []; + this.errorHandler = options.errorHandler; + + if (options.cloudWatchLogs) { + this.cloudwatchlogs = options.cloudWatchLogs; + } else { + if (this.proxyServer) { + AWS.config.update({ + httpOptions: { + agent: require('proxy-agent')(this.proxyServer) + } + }); + } + + var config = {}; + + if (awsAccessKeyId && awsSecretKey && awsRegion) { + config = { accessKeyId: awsAccessKeyId, secretAccessKey: awsSecretKey, region: awsRegion }; + } else if (awsRegion && !awsAccessKeyId && !awsSecretKey) { + // Amazon SDK will automatically pull access credentials + // from IAM Role when running on EC2 but region still + // needs to be configured + config = { region: awsRegion }; + } + + if (options.awsOptions) { + config = assign(config, options.awsOptions); + } + + this.cloudwatchlogs = new AWS.CloudWatchLogs(config); + } } - - cloudWatchIntegration.upload( - this.cloudwatchlogs, - groupName, - streamName, - this.logEvents, - retentionInDays, - callback - ); -}; - -WinstonCloudWatch.prototype.kthxbye = function(callback) { - clearInterval(this.intervalId); - this.intervalId = null; - this.flushTimeout = this.flushTimeout || (Date.now() + defaultFlushTimeoutMs); - - this.submit((function(error) { - if (error) return callback(error); - if (isEmpty(this.logEvents)) return callback(); - if (Date.now() > this.flushTimeout) return callback(new Error('Timeout reached while waiting for logs to submit')); - else setTimeout(this.kthxbye.bind(this, callback), 0); - }).bind(this)); -}; - -winston.transports.CloudWatch = WinstonCloudWatch; - -module.exports = WinstonCloudWatch; +} diff --git a/package-lock.json b/package-lock.json index 0df4ce3..ba06b49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "winston-cloudwatch", - "version": "2.3.1", + "version": "2.3.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3877,8 +3877,7 @@ "triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", - "dev": true + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, "tunnel-agent": { "version": "0.6.0", diff --git a/package.json b/package.json index e432a95..3106464 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "license": "MIT", "typings": "typescript/winston-cloudwatch.d.ts", "peerDependencies": { - "winston": "^3.0.0" + "winston": "^3.0.0", + "winston-transport": "^4.3.0" }, "dependencies": { "async": "^3.1.0", @@ -31,7 +32,8 @@ "lodash.find": "^4.6.0", "lodash.isempty": "^4.4.0", "lodash.iserror": "^3.1.1", - "proxy-agent": "^3.1.1" + "proxy-agent": "^3.1.1", + "triple-beam": "^1.3.0" }, "devDependencies": { "@types/node": "13.11.0", diff --git a/test/index.js b/test/index.js index 7775e46..e5c929b 100644 --- a/test/index.js +++ b/test/index.js @@ -1,9 +1,27 @@ -describe('index', function() { +var sinon = require('sinon'), + should = require('should'), + mockery = require('mockery'); + + +// the following does not, and should not, mock winston, +// to actually test the transport +describe.only('Transport', function() { - var sinon = require('sinon'), - should = require('should'), - mockery = require('mockery'); + var winston = require('winston'), + WinstonCloudWatch = require('../index.js'), + clock = sinon.useFakeTimers(), + Transport = require('winston-transport'), + transport; + it('should derive from Winston transport', function() { + transport = new WinstonCloudWatch({}); + transport.should.be.an.instanceOf(Transport) + }); +}); + +describe('index', function() { + + // TODO check that the API is still this one var stubbedWinston = { transports: {}, Transport: function() {} @@ -303,5 +321,4 @@ describe('index', function() { clock.tick(1); }); }); - });