From 7b162e5395df907379e815c029b280d2a86a33c4 Mon Sep 17 00:00:00 2001 From: Johan Brook Date: Tue, 24 Feb 2015 21:05:41 +0100 Subject: [PATCH] Initial commit --- .eslintignore | 2 + .eslintrc | 22 ++++++++++ .gitignore | 2 + README.md | 82 +++++++++++++++++++++++++++++++++++ init.js | 20 +++++++++ logger.js | 93 ++++++++++++++++++++++++++++++++++++++++ loggers/local.js | 16 +++++++ loggers/loggers.js | 1 + loggers/loggly.js | 34 +++++++++++++++ package.js | 32 ++++++++++++++ tests/logger-spec.coffee | 59 +++++++++++++++++++++++++ 11 files changed, 363 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 README.md create mode 100644 init.js create mode 100644 logger.js create mode 100644 loggers/local.js create mode 100644 loggers/loggers.js create mode 100644 loggers/loggly.js create mode 100644 package.js create mode 100644 tests/logger-spec.coffee diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..ba07d2c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +app/.meteor +.npm/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..2aab5e3 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,22 @@ +{ + "rules": { + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "strict": 0, + "quotes": [2, "single"], + "no-underscore-dangle": 0, + "curly": 0 + }, + + "env": { + "browser": true, + "node": true, + "jquery": true, + "meteor": true + }, + + "globals": { + "Loggers": true, + "Logger": true, + "logger": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7065d6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.npm/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..7235626 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Lookback Logger + +A Meteor logger that logs to [Loggly](https://www.loggly.com/) or local `console.log`. + +## Install + +Lookback Logger is available on [Atmosphere](https://atmospherejs.com/lookback/logger) as `lookback:logger`: + +```bash +meteor add lookback:logger +``` + +## Usage + +A global `Logger` object is automatically exported from within the package and available in your app. It has the log level methods `debug`, `warn`, `error` and `info`. + +```js +Logger.info('Some logging'); +Logger.warn('Watch out!'); +Logger.error('Someone screwed up ...'); +Logger.debug('Herp-a-derp.'); +``` + +Each `Logger` method takes a message as a string, and optionally an array of tags: + +```js +Logger.info('Some logging', ['my-tag']); +``` +This can be shown in the Loggly interface. + +Additionally, a global `logger` factory function is exported. It is used to create a logger: + +```js +someLogger = logger(params); +``` + +`logger` takes an object of parameters: + +```js +myLogger = logger({ + // Local logging. Defaults to true. + local: true, + + // Loggly options. Defaults to {}. If set, local logging will be disabled. + loggly: { + // Options to send to the Loggly npm module. + logglyModuleOptions: { + token: '', + subdomain: 'foobar', + json: true + }, + // Params to send to Loggly with every request. + baseParams: { + environment: 'development', // Defaults to process.env.NODE_ENV + serverName: 'My Computer' + } + } +}); +``` +Please refer to the documentation for the [`loggly`](http://npmjs.org/package/loggly) npm module for documentation on the `logglyModuleOptions`. + +## Tests + +```bash +meteor test-packages lookback:logger +``` + +Locally: + +```bash +meteor test-packages ./ +``` + +## Version history + +- `1.0.0` - Initial publish. + +## Contributions + +Contributions are welcome. Please open issues and/or file Pull Requests. + +Made by [Lookback](http://lookback.io). diff --git a/init.js b/init.js new file mode 100644 index 0000000..39be1f1 --- /dev/null +++ b/init.js @@ -0,0 +1,20 @@ +// This really shouldn't be part of the package because its forces the +// developer to set Meteor.settings.logging rather than initing the Logger +// any way they want. +// +// The upside is however that other packages can use the logger package +// directly so we dont have to use a wrapper package just for the config. + +var settings = Meteor.settings.logging || {}; + +// Default to logging locally +var local = true; +if (typeof settings.local !== 'undefined') + local = settings.local; + +var loggly = settings.loggly; + +Logger = logger({ + local: local, + loggly: loggly +}); diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..b030720 --- /dev/null +++ b/logger.js @@ -0,0 +1,93 @@ +var loggly = Npm.require('loggly'); + +/** + @desc Gives us log levels that will be used with loggly. + E.g. calling MyLogger.info('Merge complete', 'Processing') will + log the message 'Merge complete' with the log level 'info' and the tag + 'Processing' to loggly or whatever logger you are using. + + @param {(function|array)} loggers - the function or array of functions to do the logging. + @return {object} - Logging methods for each log level +*/ +function attachLogLevels(loggers) { + if (_.isFunction(loggers)) + loggers = [loggers]; + + var levels = [ + 'error', + 'warn', + 'info', + 'debug' + ]; + + return loggers.reduce(function(memo, logger) { + levels.forEach(function(level) { + // Bind level value as first argument to each logger function + memo[level] = _.partial(logger, level); + }); + + return memo; + }, {}); +} + +/** + @example - Logger = logger({ + loggly: { + logglyModuleOptions: { + token: 'test', + subdomain: 'lookback', + json: true, + }, + baseParams: { + environment: 'development', + serverName: 'my dev env' // if you need more than the ip + } + }, + local: false, + }); + + Logger.debug('Init hyperdrive', ['this-is-a-tag', 'and-another-tag']); + + @param {object} options + + @param {object} [options.loggly] - options for loggly. + @param {object} options.loggly.logglyModuleOptions - options to init the loggly module + @param {object} options.loggly.baseParams - key/values sent to loggly with every request. + + @param {object} [options.local] - logs everything locally, and to loggly if thats configured. + useful if you need the log data immediately. + + @return {object} - Logger with log methods +*/ +logger = function(options) { + options = options || {}; + var logglyOptions = options.loggly; + + // log locally if loggly isnt enabled or if specifically requested. + var local = options.local || !logglyOptions; + + var loggers = []; + + if (logglyOptions) { + try { + var logglyClient = loggly.createClient(logglyOptions.logglyModuleOptions); + var baseParams = logglyOptions.baseParams || {}; + + // Use environment name from options or default to NODE_ENV. + baseParams.environment = baseParams.environment || process.env.NODE_ENV; + + baseParams.platform = 'site'; + + loggers.push(Loggers.loggly(logglyClient, baseParams)); + } catch (e) { + console.warn('Error creating Loggly client. No logs will be sent.'); + console.warn(e.message); + } + } + + if (local) { + loggers.push(Loggers.local); + } + + return attachLogLevels(loggers); +}; diff --git a/loggers/local.js b/loggers/local.js new file mode 100644 index 0000000..2150bbc --- /dev/null +++ b/loggers/local.js @@ -0,0 +1,16 @@ +// Local log +Loggers.local = function(level, message, tags) { + var method = (function() { + switch (level) { + case 'info': return 'info'; + case 'debug': return 'log'; + case 'warn': return 'warn'; + case 'error': return 'error'; + } + })(); + + if(tags) + console[method](message, tags); + else + console[method](message); +}; diff --git a/loggers/loggers.js b/loggers/loggers.js new file mode 100644 index 0000000..b5599c1 --- /dev/null +++ b/loggers/loggers.js @@ -0,0 +1 @@ +Loggers = {}; diff --git a/loggers/loggly.js b/loggers/loggly.js new file mode 100644 index 0000000..9b8b2ed --- /dev/null +++ b/loggers/loggly.js @@ -0,0 +1,34 @@ +Loggers.loggly = function(logglyClient, baseParams) { + baseParams = baseParams || {}; + + // @desc Log a message with a level and one or more tags to loggly + // + // @param {string} level + // @param {string} message + // @param {(string|array)} [tags] + return function log(level, message, tags) { + if (tags && _.isString(tags)) + tags = [tags]; + + var toLog = { + message: message, + level: level + }; + + _.extend(toLog, baseParams); + + // If the log fails for some reason (bad config, timeout or whatever) + // at least log it to console. + var callback = function(err) { + if (err) { + console.error('Error while logging', { log: toLog, tags: tags }); + console.error(err); + } + }; + + if (tags) + logglyClient.log(toLog, tags, callback); + else + logglyClient.log(toLog, callback); + }; +}; diff --git a/package.js b/package.js new file mode 100644 index 0000000..c3158c5 --- /dev/null +++ b/package.js @@ -0,0 +1,32 @@ +Package.describe({ + name: 'lookback:logger', + summary: 'Logger for Meteor with Loggly integration', + git: 'https://github.com/lookback/meteor-logger', + version: '0.0.1' +}); + +Npm.depends({ + 'loggly': '1.0.8' +}); + +var where = 'server'; + +Package.onUse(function (api) { + api.use('underscore', where); + + api.addFiles([ + 'loggers/loggers.js', + 'loggers/local.js', + 'loggers/loggly.js', + 'logger.js', + 'init.js' + ] + , where); + + api.export(['Logger', 'logger'], where); +}); + +Package.onTest(function(api) { + api.use(['coffeescript', 'practicalmeteor:munit', 'lookback:logger'], where); + api.addFiles(['tests/logger-spec.coffee'], where); +}); diff --git a/tests/logger-spec.coffee b/tests/logger-spec.coffee new file mode 100644 index 0000000..5c4fd39 --- /dev/null +++ b/tests/logger-spec.coffee @@ -0,0 +1,59 @@ +Should = should() + +describe 'Logger', -> + + beforeEach -> + spies.create 'info', console, 'info' + spies.create 'warn', console, 'warn' + spies.create 'debug', console, 'log' + spies.create 'error', console, 'error' + + afterEach -> + spies.restoreAll() + + it 'should exist as an object', -> + Logger.should.be.defined + Logger.should.be.an 'object' + + it 'should log locally per default', -> + Logger.info 'Foo' + spies.info.should.have.been.calledWith 'Foo' + + describe 'Local Logging', -> + + it 'should log info', -> + Logger.info 'Foo' + spies.info.should.have.been.calledWith 'Foo' + + it 'should log debug', -> + Logger.debug 'Foo' + spies.debug.should.have.been.calledWith 'Foo' + + it 'should log warn', -> + Logger.warn 'Foo' + spies.warn.should.have.been.calledWith 'Foo' + + it 'should log error', -> + Logger.error 'Foo' + spies.error.should.have.been.calledWith 'Foo' + + it 'should log tags', -> + ['info', 'warn', 'error', 'debug'].forEach (method) -> + Logger[method]('Foo', ['bar', 'baz']) + spies[method].should.have.been.calledWith 'Foo', ['bar', 'baz'] + +describe 'logger', -> + + it 'should exist as a function', -> + logger.should.be.defined + logger.should.be.a 'function' + + it 'should return a logger object', -> + log = logger() + log.should.be.an 'object' + + it 'should have all methods', -> + log = logger() + + 'info debug error warn'.split(' ').forEach (method) -> + log[method].should.be.a 'function'