diff --git a/.npm/package/npm-shrinkwrap.json b/.npm/package/npm-shrinkwrap.json index 21e3e9c..58c8671 100644 --- a/.npm/package/npm-shrinkwrap.json +++ b/.npm/package/npm-shrinkwrap.json @@ -2,32 +2,6 @@ "dependencies": { "getenv": { "version": "0.5.0" - }, - "winston": { - "version": "2.1.0", - "dependencies": { - "async": { - "version": "1.0.0" - }, - "colors": { - "version": "1.0.3" - }, - "cycle": { - "version": "1.0.3" - }, - "eyes": { - "version": "0.1.8" - }, - "isstream": { - "version": "0.1.2" - }, - "pkginfo": { - "version": "0.3.1" - }, - "stack-trace": { - "version": "0.0.9" - } - } } } } diff --git a/package.js b/package.js index 29243f4..36bb3aa 100644 --- a/package.js +++ b/package.js @@ -8,7 +8,6 @@ Package.describe({ Npm.depends({ "getenv": "0.5.0", - "winston": "2.1.0", "babel-plugin-transform-decorators-legacy": "1.3.4" }); @@ -49,7 +48,9 @@ Package.onUse(function(api) { 'source/injector.coffee', 'source/injector_annotations.coffee', 'source/module.coffee', - 'source/application.coffee' + 'source/application.coffee', + 'source/loggers/adapter.js', + 'source/loggers/console-adapter.js', ]); // Test helpers diff --git a/source/configuration.js b/source/configuration.js index 3c7095b..35ec25a 100644 --- a/source/configuration.js +++ b/source/configuration.js @@ -1,40 +1,3 @@ - if (Meteor.isServer) { - - let getenv = Npm.require('getenv'); - // Wrapper - Space.getenv = getenv; - - Space.configuration = Space.getenv.multi({ - log: { - enabled: ['SPACE_LOG_ENABLED', false, 'bool'], - minLevel: ['SPACE_LOG_MIN_LEVEL', 'info', 'string'] - } - }); - - // Pass down to the client - _.deepExtend(Meteor.settings, { - public: { - log: { - enabled: Space.configuration.log.enabled, - minLevel: Space.configuration.log.minLevel - } - } - }); - - __meteor_runtime_config__.PUBLIC_SETTINGS = Meteor.settings.public; - -} - -if (Meteor.isClient) { - - let log = Meteor.settings.public.log; - - // Guard and defaults when not loaded on server - Space.configuration = { - log: { - enabled: log && log.enabled || false, - minLevel: log && log.minLevel || 'info' - } - }; + Space.getenv = Npm.require('getenv'); } diff --git a/source/logger.js b/source/logger.js index bf6488c..d28ea89 100644 --- a/source/logger.js +++ b/source/logger.js @@ -1,104 +1,100 @@ -let config = Space.configuration; +const Logger = Space.Object.extend('Space.Logger', { -if (Meteor.isServer) { - winston = Npm.require('winston'); -} - -Space.Object.extend(Space, 'Logger', { - - _logger: null, - _minLevel: 6, - _state: 'stopped', - - _levels: { - 'error': 3, - 'warning': 4, - 'warn': 4, - 'info': 6, - 'debug': 7 + STATES: { + stopped: 'stopped', + running: 'running' }, Constructor() { - if (Meteor.isServer) { - this._logger = new winston.Logger({ - transports: [ - new winston.transports.Console({ - colorize: true, - prettyPrint: true - }) - ] - }); - this._logger.setLevels(winston.config.syslog.levels); + this._state = this.STATES.stopped; + this._adapters = {}; + }, + + addAdapter(id, adapter, shouldOverride = false) { + if (!id || typeof id !== 'string') { + throw new Error(this.constructor.ERRORS.invalidId); } - if (Meteor.isClient) { - this._logger = console; + if (this.hasAdapter(id) && !shouldOverride) { + throw new Error(this.constructor.ERRORS.mappingExists(id)); } + this._adapters[id] = adapter; }, - setMinLevel(name) { - let newCode = this._levelCode(name); - if (this._minLevel !== newCode) { - this._minLevel = newCode; - if (Meteor.isServer) { - this._logger.transports.console.level = name; - } - } + overrideAdapter(id, adapter) { + return this.addAdapter(id, adapter, true); + }, + + getAdapter(id) { + return this._adapters[id] || null; + }, + + hasAdapter(id) { + return (this._adapters[id] !== null && this._adapters[id] !== undefined); + }, + + removeAdapter(id) { + if (this._adapters[id]) {delete this._adapters[id];} + }, + + getAdapters() { + return this._adapters; }, start() { - if (this._is('stopped')) { - this._state = 'running'; + if (this.isInState(this.STATES.stopped)) { + this._state = this.STATES.running; } }, stop() { - if (this._is('running')) { - this._state = 'stopped'; + if (this.isInState(this.STATES.running)) { + this._state = this.STATES.stopped; } }, - debug(message) { - check(message, String); - this._log('debug', arguments); + debug(...args) { + this._log('debug', args); }, - info(message) { - check(message, String); - this._log('info', arguments); + info(...args) { + this._log('info', args); }, - warning(message) { - check(message, String); - if (Meteor.isClient) - this._log('warn', arguments); - if (Meteor.isServer) - this._log('warning', arguments); + warning(...args) { + this._log('warning', args); }, - error(message) { - check(message, String); - this._log('error', arguments); + error(...args) { + this._log('error', args); }, - _levelCode(name) { - return this._levels[name]; + isInState(expectedState) { + return (this._state === expectedState); }, - _is(expectedState) { - if (this._state === expectedState) return true; + isRunning() { + return this.isInState(this.STATES.running); }, - _log(level, message) { - if(this._is('running') && this._levelCode(level) <= this._minLevel) { - this._logger[level].apply(this._logger, message); + isStopped() { + return this.isInState(this.STATES.stopped); + }, + + _log(level, args) { + if (!this.isInState(this.STATES.running)) {return;} + + for (let adapter of Object.values(this.getAdapters())) { + adapter[level].apply(adapter, args); } } - }); -Space.log = new Space.Logger(); +Logger.ERRORS = { + mappingExists(id) { + return `Adapter with id '${id}' would be overwritten. Use method + 'overrideAdapter' for that`; + }, + invalidId: 'Cannot map or or non string values' +}; -if (config.log.enabled) { - Space.log.setMinLevel(config.log.minLevel); - Space.log.start(); -} +export default Logger; diff --git a/source/loggers/adapter.js b/source/loggers/adapter.js new file mode 100644 index 0000000..6a10d66 --- /dev/null +++ b/source/loggers/adapter.js @@ -0,0 +1,44 @@ +const LoggingAdapter = Space.Object.extend('Space.Logger.LoggingAdapter', { + + _lib: null, + Constructor(lib) { + if (!lib) { + throw new Error(this.ERRORS.undefinedLibrary); + } + this.setLibrary(lib); + }, + + setLibrary(lib) { + this._lib = lib; + }, + + getLibrary() { + return this._lib || null; + }, + + debug(...args) { + this._log('debug', args); + }, + + info(...args) { + this._log('info', args); + }, + + warning(...args) { + this._log('warning', args); + }, + + error(...args) { + this._log('error', args); + }, + + _log(level, args) { + this._lib[level].apply(this._lib, args); + }, + + ERRORS: { + undefinedLibrary: 'Logging library is required' + } +}); + +export default LoggingAdapter; diff --git a/source/loggers/console-adapter.js b/source/loggers/console-adapter.js new file mode 100644 index 0000000..4d28e0a --- /dev/null +++ b/source/loggers/console-adapter.js @@ -0,0 +1,14 @@ +import LoggingAdapter from './adapter'; + +const ConsoleLogger = LoggingAdapter.extend('Space.Logger.ConsoleAdapter', { + + Constructor() { + LoggingAdapter.call(this, console); + }, + + warning(...args) { + return this._log('warn', args); + } +}); + +export default ConsoleLogger; diff --git a/source/module.coffee b/source/module.coffee index 32719e6..4000f80 100644 --- a/source/module.coffee +++ b/source/module.coffee @@ -21,8 +21,14 @@ class Space.Module extends Space.Object initialize: (@app, @injector, isSubModule=false) -> return if not @is('constructed') # only initialize once if not @injector? then throw new Error @ERRORS.injectorMissing + @_state = 'configuring' - Space.log.debug("#{@constructor.publishedAs}: initialize") + unless isSubModule + @log = @_setupLogger() + else + @log = @injector.get('log') + @log.debug("#{@constructor.publishedAs}: initialize") + # Setup basic mappings required by all modules if this the top-level module unless isSubModule @injector.map('Injector').to @injector @@ -114,7 +120,7 @@ class Space.Module extends Space.Object # calling the instance hooks before, on, and after _runLifeCycleAction: (action, func) -> @_invokeActionOnRequiredModules action - Space.log.debug("#{@constructor.publishedAs}: #{action}") + @log.debug("#{@constructor.publishedAs}: #{action}") this["before#{Space.capitalizeString(action)}"]?() func?() this["on#{Space.capitalizeString(action)}"]?() @@ -125,7 +131,7 @@ class Space.Module extends Space.Object @_invokeActionOnRequiredModules '_runOnInitializeHooks' # Never run this hook twice if @is('configuring') - Space.log.debug("#{@constructor.publishedAs}: onInitialize") + @log.debug("#{@constructor.publishedAs}: onInitialize") @_state = 'initializing' # Inject required dependencies into this module @injector.injectInto this @@ -135,7 +141,7 @@ class Space.Module extends Space.Object _autoMapSingletons: -> @_invokeActionOnRequiredModules '_autoMapSingletons' if @is('initializing') - Space.log.debug("#{@constructor.publishedAs}: _autoMapSingletons") + @log.debug("#{@constructor.publishedAs}: _autoMapSingletons") @_state = 'auto-mapping-singletons' # Map classes that are declared as singletons @injector.map(singleton).asSingleton() for singleton in @singletons @@ -143,7 +149,7 @@ class Space.Module extends Space.Object _autoCreateSingletons: -> @_invokeActionOnRequiredModules '_autoCreateSingletons' if @is('auto-mapping-singletons') - Space.log.debug("#{@constructor.publishedAs}: _autoCreateSingletons") + @log.debug("#{@constructor.publishedAs}: _autoCreateSingletons") @_state = 'auto-creating-singletons' # Create singleton classes @injector.create(singleton) for singleton in @singletons @@ -153,7 +159,7 @@ class Space.Module extends Space.Object @_invokeActionOnRequiredModules '_runAfterInitializeHooks' # Never run this hook twice if @is('auto-creating-singletons') - Space.log.debug("#{@constructor.publishedAs}: afterInitialize") + @log.debug("#{@constructor.publishedAs}: afterInitialize") @_state = 'initialized' # Call custom lifecycle hook if existant @afterInitialize?() @@ -165,11 +171,23 @@ class Space.Module extends Space.Object this[hook] ?= -> this[hook] = _.wrap(this[hook], wrapper) + _setupLogger: -> + config = @_loggingConfig(@configuration) + logger = new Space.Logger() + logger.start() if config.enabled == true + return logger + + _loggingConfig: () -> + config = {} + _.deepExtend(config, @configuration) + _.deepExtend(config, @constructor.prototype.configuration) + return config.log or {} + _mapSpaceServices: -> - @injector.map('log').to Space.log + @injector.map('log').to @log _mapMeteorApis: -> - Space.log.debug("#{@constructor.publishedAs}: _mapMeteorApis") + @log.debug("#{@constructor.publishedAs}: _mapMeteorApis") # Map Meteor standard packages @injector.map('Meteor').to Meteor if Package.ejson? diff --git a/tests/unit/logger.tests.js b/tests/unit/logger.tests.js index acab9e7..475e925 100644 --- a/tests/unit/logger.tests.js +++ b/tests/unit/logger.tests.js @@ -1,112 +1,203 @@ -describe("Space.Logger", function() { +import Logger from '../../source/logger.js'; +import LoggingAdapter from '../../source/loggers/adapter.js'; - beforeEach(function() { - this.log = new Space.Logger(); - }); +const TestAdapter = LoggingAdapter.extend('TestAdapter', { + Constructor(lib) { + return this.setLibrary(lib); + } +}); - afterEach(function() { - this.log.stop(); - }); +describe("Logger", function() { - it('extends Space.Object', function() { - expect(Space.Logger).to.extend(Space.Object); + beforeEach(() => { + this.lib = { + debug: sinon.spy(), + info: sinon.spy(), + warning: sinon.spy(), + error: sinon.spy() + }; + this.testAdapter = new TestAdapter(this.lib); + this.logger = new Logger(); }); - it("is available of both client and server", function() { - if (Meteor.isServer || Meteor.isClient) - expect(this.log).to.be.instanceOf(Space.Logger); + it('extends Space.Object', () => { + expect(Logger).to.extend(Space.Object); }); - it("only logs after starting", function() { - this.log.start(); - this.log._logger.info = sinon.spy(); - let message = 'My Log Message'; - this.log.info(message); - expect(this.log._logger.info).to.be.calledWithExactly(message); + it("is available of both client and server", () => { + expect(this.logger).to.be.instanceOf(Logger); }); - it("it can log a debug message to the output channel when min level is equal but not less", function() { - this.log.start(); - this.log.setMinLevel('debug'); - this.log._logger.debug = sinon.spy(); - let message = 'My log message'; - this.log.debug(message); - expect(this.log._logger.debug).to.be.calledWithExactly(message); - this.log._logger.debug = sinon.spy(); - this.log.setMinLevel('info'); - this.log.debug(message); - expect(this.log._logger.debug).not.to.be.called; - }); + describe('adapters', () => { + it('throws error if id does not exists', () => { + const adapter = new TestAdapter(sinon.spy()); + expect(() => this.logger.addAdapter(undefined, adapter)).to.throw( + Logger.ERRORS.invalidId + ); + }); - it("it can log an info message to the output channel when min level is equal or higher, but not less", function() { - this.log.start(); - this.log.setMinLevel('info'); - this.log._logger.info = sinon.spy(); - this.log._logger.debug = sinon.spy(); - let message = 'My log message'; - this.log.info(message); - expect(this.log._logger.info).to.be.calledWithExactly(message); - expect(this.log._logger.debug).not.to.be.called; - this.log._logger.info = sinon.spy(); - this.log.setMinLevel('warning'); - this.log.info(message); - expect(this.log._logger.info).not.to.be.called; - }); + it('throws error if id is not a string value', () => { + const adapter = new TestAdapter(sinon.spy()); + expect(() => this.logger.addAdapter(adapter)).to.throw( + Logger.ERRORS.invalidId + ); + }); - it.server("it can log a warning message to the output channel when min level is equal or higher, but not less", function() { - this.log.start(); - this.log.setMinLevel('warning'); - this.log._logger.warning = sinon.spy(); - this.log._logger.info = sinon.spy(); - let message = 'My log message'; - this.log.warning(message); - expect(this.log._logger.warning).to.be.calledWithExactly(message); - expect(this.log._logger.info).not.to.be.called; - this.log._logger.warning = sinon.spy(); - this.log.setMinLevel('error'); - this.log.warning(message); - expect(this.log._logger.warning).not.to.be.called; - }); + it('throws error if adapter would be overridden', () => { + const adapterId = 'testAdapter'; + const adapter = new TestAdapter(sinon.spy()); + + this.logger.addAdapter(adapterId, adapter); + expect(() => this.logger.addAdapter(adapterId, adapter)).to.throw( + Logger.ERRORS.mappingExists(adapterId) + ); + }); + + it('adds adapter', () => { + const adapterId = 'testAdapter'; + const adapter = new TestAdapter(sinon.spy()); - it.client("it can log a warning message to the output channel when min level is equal or higher, but not less", function() { - this.log.start(); - this.log.setMinLevel('warning'); - this.log._logger.warn = sinon.spy(); - this.log._logger.info = sinon.spy(); - let message = 'My log message'; - this.log.warning(message); - expect(this.log._logger.warn).to.be.calledWithExactly(message); - expect(this.log._logger.info).not.to.be.called; - this.log._logger.warn = sinon.spy(); - this.log.setMinLevel('error'); - this.log.warning(message); - expect(this.log._logger.warn).not.to.be.called; + this.logger.addAdapter(adapterId, adapter); + expect(this.logger.getAdapter(adapterId)).to.equal(adapter); + expect(this.logger.hasAdapter(adapterId)).to.be.true; + }); + + it('allows to override adapter', () => { + const adapterId = 'testAdapter'; + const adapter = new TestAdapter(sinon.spy()); + const overridingAdapter = new TestAdapter(sinon.spy()); + + this.logger.addAdapter(adapterId, adapter); + expect(() => { + this.logger.overrideAdapter(adapterId, overridingAdapter); + }).to.not.throw(Error); + expect(this.logger.getAdapter(adapterId)).to.equal(overridingAdapter); + }); + + it('resolves adapter by id', () => { + consoleAdapter = new TestAdapter(sinon.spy()); + fileAdapter = new TestAdapter(sinon.spy()); + + this.logger.addAdapter('console', consoleAdapter); + this.logger.addAdapter('file', fileAdapter); + expect(this.logger.getAdapter('console')).to.equal(consoleAdapter); + expect(this.logger.getAdapter('file')).to.equal(fileAdapter); + expect(this.logger.getAdapter('non-existing-adapter')).to.be.null; + }); + + it('removes adapter', () => { + const adapterId = 'testAdapter'; + const adapter = new TestAdapter(sinon.spy()); + + this.logger.addAdapter(adapterId, adapter); + this.logger.removeAdapter(adapterId); + expect(this.logger.getAdapter(adapterId)).to.be.null; + expect(this.logger.hasAdapter(adapterId)).to.be.false; + }); + + it('returns adapters', () => { + const adapters = { + console: new TestAdapter(sinon.spy()), + file: new TestAdapter(sinon.spy()) + }; + this.logger.addAdapter('console', adapters.console); + this.logger.addAdapter('file', adapters.file); + expect(this.logger.getAdapters()).to.be.eql(adapters); + }); }); - it("it can log an error message to the output channel when min level is equal", function() { - this.log.start(); - this.log.setMinLevel('error'); - this.log._logger.error = sinon.spy(); - this.log._logger.info = sinon.spy(); - let message = 'My log message'; - this.log.error(message); - expect(this.log._logger.error).to.be.calledWithExactly(message); - expect(this.log._logger.info).not.to.be.called; - this.log._logger.info = sinon.spy(); - this.log.setMinLevel('debug'); - this.log.error(message); - expect(this.log._logger.error).to.be.calledWithExactly(message); + it("only logs after starting", () => { + this.logger.addAdapter('my-logger', this.testAdapter); + const message = 'My log message'; + + expect(this.logger.isRunning()).to.be.false; + expect(this.logger.isStopped()).to.be.true; + this.logger.info(message); + expect(this.lib.info).to.not.be.called; + + this.logger.start(); + expect(this.logger.isRunning()).to.be.true; + expect(this.logger.isStopped()).to.be.false; + this.logger.info(message); + expect(this.lib.info).to.be.calledOnce; + expect(this.lib.info.calledWith(message)).to.be.true; }); - it("allows logging output to be stopped", function() { - this.log._logger.info = sinon.spy(); - this.log.start(); - expect(this.log._is('running')).to.be.true; - this.log.stop(); - let message = 'My Log Message'; - this.log.info(message); - expect(this.log._logger.info).not.to.be.called; - expect(this.log._is('stopped')).to.be.true; + it("allows logging output to be stopped", () => { + this.logger.addAdapter('my-logger', this.testAdapter); + const message = 'My log message'; + + expect(this.logger.isRunning()).to.be.false; + expect(this.logger.isStopped()).to.be.true; + this.logger.start(); + expect(this.logger.isRunning()).to.be.true; + expect(this.logger.isStopped()).to.be.false; + this.logger.info(message); + expect(this.lib.info.calledWith(message)).to.be.true; + + this.logger.stop(); + expect(this.logger.isRunning()).to.be.false; + expect(this.logger.isStopped()).to.be.true; + + this.logger.info(message); + expect(this.lib.info).to.not.be.calledTwice; }); + describe('logging', () => { + it('allows multiple logging adapters to log same message', () => { + const firstLib = {debug: sinon.spy()}; + const firstAdapter = new TestAdapter(firstLib); + const secondLib = {debug: sinon.spy()}; + const secondAdapter = new TestAdapter(secondLib); + const message = 'My log message'; + + this.logger.addAdapter('first', firstAdapter); + this.logger.addAdapter('second', secondAdapter); + this.logger.start(); + + this.logger.debug(message); + expect(firstLib.debug.calledWith(message)).to.be.true; + expect(firstLib.debug).to.be.calledOnce; + expect(secondLib.debug.calledWith(message)).to.be.true; + expect(secondLib.debug).to.be.calledOnce; + }); + + describe('logs message as', () => { + it("debug", () => { + this.logger.addAdapter('my-logger', this.testAdapter); + this.logger.start(); + + const message = 'My log message'; + this.logger.debug(message); + expect(this.lib.debug.calledWith(message)).to.be.true; + }); + + it("info", () => { + this.logger.addAdapter('my-logger', this.testAdapter); + this.logger.start(); + + const message = 'My log message'; + this.logger.info(message); + expect(this.lib.info.calledWith(message)).to.be.true; + }); + + it("warning", () => { + this.logger.addAdapter('my-logger', this.testAdapter); + this.logger.start(); + + const message = 'My log message'; + this.logger.warning(message); + expect(this.lib.warning.calledWith(message)).to.be.true; + }); + + it("error", () => { + this.logger.addAdapter('my-logger', this.testAdapter); + this.logger.start(); + + const message = 'My log message'; + this.logger.error(message); + expect(this.lib.error.calledWith(message)).to.be.true; + }); + }); + }); }); diff --git a/tests/unit/module.unit.coffee b/tests/unit/module.unit.coffee index 859ad5f..ea64987 100644 --- a/tests/unit/module.unit.coffee +++ b/tests/unit/module.unit.coffee @@ -103,6 +103,7 @@ describe 'Space.Module - #start', -> beforeEach -> @module = new Space.Module() + @module.log = {debug: sinon.spy()} @module.start() @module._runLifeCycleAction = sinon.spy() @@ -117,6 +118,7 @@ describe 'Space.Module - #stop', -> beforeEach -> @module = new Space.Module() + @module.log = {debug: sinon.spy()} @module.start() @module.stop() @module._runLifeCycleAction = sinon.spy() @@ -132,6 +134,7 @@ describe 'Space.Module - #reset', -> beforeEach -> @module = new Space.Module() + @module.log = {debug: sinon.spy()} @module._runLifeCycleAction = sinon.spy() it.server 'rejects attempts to reset when in production', ->