From e4b550ef8da44e54fab99bb71fc96a06239b4896 Mon Sep 17 00:00:00 2001 From: Glen Mailer Date: Sat, 1 Apr 2017 13:21:22 +0100 Subject: [PATCH] Support formatting of metadata, including 'flat' builtin Flat is useful as application insights doesn't allow nested properties, its left as an option so as not to break backwards compatibility --- README.md | 3 +- lib/winston-azure-application-insights.js | 150 ++++++++------ ...winston-azure-application-insights.test.js | 190 ++++++++++++------ 3 files changed, 228 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index c4b57e5..df3a1c1 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ Then you didn't specify a suitable instrumentation key. See the section above. * **level**: lowest logging level transport to be logged (default: `info`) * **silent**: Boolean flag indicating whether to suppress output (default: `false`) -* **treatErrorsAsExceptions**: Boolean flag indicating whether to treat errors as exceptions. +* **formatMeta**: Function that can be used to pre-process the log metadata before it is sent to azure. As application insights doesn't support nested properties, you can use the special value `"flat"` to flatten nested properties with underscores (default: metadata is not modified) +* **treatErrorsAsExceptions**: Boolean flag indicating whether to treat errors as exceptions. See section below for more details (default: `false`). **SDK integration options (required):** diff --git a/lib/winston-azure-application-insights.js b/lib/winston-azure-application-insights.js index fdceda8..21454f6 100644 --- a/lib/winston-azure-application-insights.js +++ b/lib/winston-azure-application-insights.js @@ -9,19 +9,47 @@ var WINSTON_LOGGER_NAME = 'applicationinsightslogger'; var WINSTON_DEFAULT_LEVEL = 'info'; var DEFAULT_IS_SILENT = false; +function noOp(x) { + return x; +} -// Remaping winston level on Application Insights +function flattenObject(source, target, prefix) { + Object.keys(source).forEach(function(key) { + var sourceVal = source[key]; + var fullKey = prefix + '_' + key; + if (sourceVal && typeof sourceVal === "object") { + flattenObject(sourceVal, target, fullKey) + } else { + target[fullKey] = sourceVal; + } + }); +} + +function flattenMeta(meta) { + var flat = {}; + Object.keys(meta).forEach(function(key) { + var val = meta[key]; + if (val && typeof val === "object") { + flattenObject(val, flat, key); + } else { + flat[key] = val; + } + }) + return flat; +} + +// Remaping winston level on Application Insights function getMessageLevel(winstonLevel) { - + // TODO: Find a way to get the actual level values from AI's SDK // They are defined in SDK's "Library/Contracts.ts" - + var levels = { - emerg: 4, // AI 'Critical' - alert: 4, // AI 'Critical' - crit: 4, // AI 'Critical' - error: 3, // AI 'Error' - warning: 2, // AI 'Warning' + emerg: 4, // AI 'Critical' + alert: 4, // AI 'Critical' + crit: 4, // AI 'Critical' + error: 3, // AI 'Error' + warning: 2, // AI 'Warning' warn: 2, // AI 'Warning' notice: 1, // AI 'Informational' info: 1, // AI 'Informational' @@ -29,88 +57,92 @@ function getMessageLevel(winstonLevel) { debug: 0, // AI 'Verbose' silly: 0 // AI 'Verbose' }; - - return winstonLevel in levels ? levels[winstonLevel] : levels.info; -} + + return winstonLevel in levels ? levels[winstonLevel] : levels.info; +} var AzureApplicationInsightsLogger = function (options) { - + options = options || {}; - + winston.Transport.call(this, options); - + if (options.client) { - + // If client is set, just use it. // We expect it to be already configured and started this.client = options.client - + } else if (options.insights) { - + // If insights is set, just use the default client // We expect it to be already configured and started this.client = options.insights.client; - + } else { // Setup insights and start it // If options.key is defined, use it. Else the SDK will expect // an environment variable to be set. - + appInsights .setup(options.key) .start(); - - this.client = appInsights.client; + + this.client = appInsights.client; } if (!this.client) { throw new Error('Could not get an Application Insights client instance'); } - + this.name = WINSTON_LOGGER_NAME; this.level = options.level || WINSTON_DEFAULT_LEVEL; this.silent = options.silent || DEFAULT_IS_SILENT; this.treatErrorsAsExceptions = !!options.treatErrorsAsExceptions; - + this.formatMeta = options.formatMeta || noOp; + if (this.formatMeta === "flat") { + this.formatMeta = flattenMeta; + } + // Setup AI here! }; -// -// Inherits from 'winston.Transport'. -// -util.inherits(AzureApplicationInsightsLogger, winston.Transport); - -// -// Define a getter so that 'winston.transport.AzureApplicationInsightsLogger' -// is available and thus backwards compatible -// -winston.transports.AzureApplicationInsightsLogger = AzureApplicationInsightsLogger; - -// -// ### function log (level, msg, [meta], callback) -// #### @level {string} Level at which to log the message. -// #### @msg {string} Message to log -// #### @meta {Object} **Optional** Additional metadata to attach -// #### @callback {function} Continuation to respond to when complete. -// Core logging method exposed to Winston. Metadata is optional. -// +// +// Inherits from 'winston.Transport'. +// +util.inherits(AzureApplicationInsightsLogger, winston.Transport); + +// +// Define a getter so that 'winston.transport.AzureApplicationInsightsLogger' +// is available and thus backwards compatible +// +winston.transports.AzureApplicationInsightsLogger = AzureApplicationInsightsLogger; + +// +// ### function log (level, msg, [meta], callback) +// #### @level {string} Level at which to log the message. +// #### @msg {string} Message to log +// #### @meta {Object} **Optional** Additional metadata to attach +// #### @callback {function} Continuation to respond to when complete. +// Core logging method exposed to Winston. Metadata is optional. +// AzureApplicationInsightsLogger.prototype.log = function (level, msg, meta, callback) { - - if (typeof meta === 'function') { - callback = meta; - meta = {}; + + if (typeof meta === 'function') { + callback = meta; + meta = {}; } - + callback = callback || function(){}; - - if (this.silent) { - return callback(null, true); - } + + if (this.silent) { + return callback(null, true); + } var aiLevel = getMessageLevel(level); - + if (this.treatErrorsAsExceptions && aiLevel >= getMessageLevel('error')) { var error; if (msg instanceof Error) { @@ -121,7 +153,7 @@ AzureApplicationInsightsLogger.prototype.log = function (level, msg, meta, callb } else { error = Error(msg); } - this.client.trackException(error, meta); + this.client.trackException(error, this.formatMeta(meta)); } else { @@ -130,7 +162,7 @@ AzureApplicationInsightsLogger.prototype.log = function (level, msg, meta, callb stack: meta.stack, name: meta.name }; - + for (var field in meta) { if(field === 'message' && !msg) { msg = meta[field]; @@ -138,16 +170,18 @@ AzureApplicationInsightsLogger.prototype.log = function (level, msg, meta, callb } else if (field === 'constructor') { continue; } - + errorMeta[field] = meta[field]; } - + meta = errorMeta; } - this.client.trackTrace(msg, aiLevel, meta); + this.client.trackTrace(msg, aiLevel, this.formatMeta(meta)); } - + return callback(null, true); }; exports.AzureApplicationInsightsLogger = AzureApplicationInsightsLogger; + +exports.flattenMeta = flattenMeta; diff --git a/test/winston-azure-application-insights.test.js b/test/winston-azure-application-insights.test.js index cfa032c..1e169a2 100644 --- a/test/winston-azure-application-insights.test.js +++ b/test/winston-azure-application-insights.test.js @@ -3,7 +3,7 @@ var assert = require('chai').assert, sinon = require('sinon'); - + var winston = require('winston'), appInsights = require("applicationinsights"), transport = require('../lib/winston-azure-application-insights'); @@ -17,126 +17,126 @@ afterEach(function() { describe ('winston-azure-application-insights', function() { describe('class', function() { describe('constructor', function() { - + beforeEach(function() { delete process.env['APPINSIGHTS_INSTRUMENTATIONKEY']; }); - + it('should fail if no instrumentation insights instance, client or key specified', function() { assert.throws(function() { new transport.AzureApplicationInsightsLogger(); }, /key not found/); }); - + it('should accept an App Insights instance with the insights option', function() { - + var aiLogger; - + assert.doesNotThrow(function() { appInsights.setup('FAKEKEY'); - + aiLogger = new transport.AzureApplicationInsightsLogger({ insights: appInsights }); }); - + assert.ok(aiLogger.client); }); - + it('should accept an App Insights client with the client option', function() { - + var aiLogger; - + assert.doesNotThrow(function() { aiLogger = new transport.AzureApplicationInsightsLogger({ client: appInsights.getClient('FAKEKEY') }); }); - + assert.ok(aiLogger.client); }); - + it('should accept an instrumentation key with the key option', function() { - + var aiLogger; - + assert.doesNotThrow(function() { aiLogger = new transport.AzureApplicationInsightsLogger({ key: 'FAKEKEY' }); }); - + assert.ok(aiLogger.client); }); - + it('should use the APPINSIGHTS_INSTRUMENTATIONKEY environment variable if defined', function() { - + var aiLogger; - + process.env['APPINSIGHTS_INSTRUMENTATIONKEY'] = 'FAKEKEY'; - + assert.doesNotThrow(function() { aiLogger = new transport.AzureApplicationInsightsLogger(); }); - + assert.ok(aiLogger.client); }); - + it('should set default logging level to info', function() { var aiLogger = new transport.AzureApplicationInsightsLogger({ key: 'FAKEKEY' }); - - assert.equal(aiLogger.level, 'info'); + + assert.equal(aiLogger.level, 'info'); }); - + it('should set logging level', function() { var aiLogger = new transport.AzureApplicationInsightsLogger({ key: 'FAKEKEY', level: 'warn' }); - - assert.equal(aiLogger.level, 'warn'); + + assert.equal(aiLogger.level, 'warn'); }); - + it('should set default silent to false', function() { var aiLogger = new transport.AzureApplicationInsightsLogger({ key: 'FAKEKEY' }); - - assert.notOk(aiLogger.silent); + + assert.notOk(aiLogger.silent); }); - + it('should set silent', function() { var aiLogger = new transport.AzureApplicationInsightsLogger({ key: 'FAKEKEY', silent: true }); - - assert.ok(aiLogger.silent); + + assert.ok(aiLogger.silent); }); - + it('should declare a Winston logger', function() { new transport.AzureApplicationInsightsLogger({ key: 'FAKEKEY' }); - + assert.ok(winston.transports.AzureApplicationInsightsLogger); }); }); - + describe('#log', function() { - + var aiLogger, clientMock, expectTrace; - + beforeEach(function() { aiLogger = new transport.AzureApplicationInsightsLogger({ key: 'FAKEKEY' }); clientMock = sinon.mock(appInsights.client); expectTrace = clientMock.expects("trackTrace"); }) - + afterEach(function() { clientMock.restore(); }); @@ -144,12 +144,12 @@ describe ('winston-azure-application-insights', function() { it('should not log if silent', function() { aiLogger.silent = true; - + expectTrace.never(); - + aiLogger.log('info', 'some log text...'); }); - + it('should log with correct log levels', function() { clientMock.expects("trackTrace").once().withArgs('emerg', 4); clientMock.expects("trackTrace").once().withArgs('alert', 4); @@ -163,16 +163,16 @@ describe ('winston-azure-application-insights', function() { clientMock.expects("trackTrace").once().withArgs('debug', 0); clientMock.expects("trackTrace").once().withArgs('silly', 0); clientMock.expects("trackTrace").once().withArgs('undefined', 1); - + [ 'emerg', 'alert', 'crit', 'error', 'warning', 'warn', 'notice', 'info', 'verbose', 'debug', 'silly', 'undefined'] .forEach(function(level) { aiLogger.log(level, level); }); }); }); - + describe('#log errors as exceptions', function() { - + var aiLogger, clientMock; @@ -182,7 +182,7 @@ describe ('winston-azure-application-insights', function() { ); clientMock = sinon.mock(aiLogger.client); }) - + afterEach(function() { clientMock.restore(); }); @@ -190,19 +190,19 @@ describe ('winston-azure-application-insights', function() { it('should not track exceptions with default option', function() { aiLogger = new transport.AzureApplicationInsightsLogger({ key: 'FAKEKEY' }); - + clientMock.expects("trackException").never(); - + aiLogger.log('error', 'error message'); }); - + it('should not track exceptions if the option is off', function() { aiLogger = new transport.AzureApplicationInsightsLogger({ key: 'FAKEKEY', treatErrorsAsExceptions: false }); - + clientMock.expects("trackException").never(); - + aiLogger.log('error', 'error message'); }); @@ -260,7 +260,7 @@ describe ('winston-azure-application-insights', function() { }); describe('winston', function() { - + function ExtendedError(message, arg1, arg2) { this.message = message; this.name = "ExtendedError"; @@ -276,7 +276,7 @@ describe ('winston-azure-application-insights', function() { expectTrace; beforeEach(function() { - + winstonLogger = new(winston.Logger)({ transports: [ new winston.transports.AzureApplicationInsightsLogger({ key: 'FAKEKEY' }) ] }); @@ -284,11 +284,11 @@ describe ('winston-azure-application-insights', function() { clientMock = sinon.mock(appInsights.client); expectTrace = clientMock.expects("trackTrace"); }) - + afterEach(function() { clientMock.restore(); }); - + it('should log from winston', function() { var logMessage = "some log text...", logLevel = 'error', @@ -329,5 +329,83 @@ describe ('winston-azure-application-insights', function() { winstonLogger.error(message, error); }); + + context('with formatMeta=flat', function() { + + beforeEach(function() { + + winstonLogger = new(winston.Logger)({ + transports: [ new winston.transports.AzureApplicationInsightsLogger({ key: 'FAKEKEY', formatMeta: 'flat' }) ] + }); + + }) + + it('should log from winston with nested fields flattened', function() { + var logMessage = "some log text...", + logLevel = 'warn', + logMeta = { + text: 'some meta text', + object: { + has: { + stuff: 'inside' + }, + and: 'other bits' + } + }; + + expectTrace.once().withExactArgs(logMessage, 2, { + text: 'some meta text', + object_has_stuff: 'inside', + object_and: 'other bits' + }); + + winstonLogger.log(logLevel, logMessage, logMeta); + }); + + it('should log errors with nested fields flattend', function() { + var error = new ExtendedError("errormessage", "arg1", {"isan": "object"}); + + expectTrace.once().withExactArgs(error.message, 3, { + arg1: error.arg1, + arg2_isan: error.arg2.isan, + name: error.name, + stack: error.stack + }); + + winstonLogger.error(error); + }); + }) + + context('with formatMeta as a function', function() { + + function removePassword(meta) { + if (meta.password) { + delete meta.password; + } + return meta; + } + + beforeEach(function() { + + winstonLogger = new(winston.Logger)({ + transports: [ new winston.transports.AzureApplicationInsightsLogger({ key: 'FAKEKEY', formatMeta: removePassword }) ] + }); + }) + + it('should log from winston applying formatMeta', function() { + var logMessage = "some log text...", + logLevel = 'debug', + logMeta = { + text: 'some meta text', + password: 'dont want this!', + }; + + expectTrace.once().withExactArgs(logMessage, 0, { + text: 'some meta text', + }); + + winstonLogger.log(logLevel, logMessage, logMeta); + }); + }) }); -}); \ No newline at end of file +});