├── src ├── browserSuffix.coffee ├── nodeSuffix.coffee ├── browserPrefix.coffee ├── meteorBrowserPrefix.coffee ├── meteorServerPrefix.coffee ├── nodePrefix.coffee └── logger.coffee ├── .gitignore ├── .npmignore ├── versions.json ├── package.js ├── package.json ├── gulpfile.coffee ├── test └── pince-test.coffee └── README.md /src/browserSuffix.coffee: -------------------------------------------------------------------------------- 1 | window.Logger = Logger 2 | -------------------------------------------------------------------------------- /src/nodeSuffix.coffee: -------------------------------------------------------------------------------- 1 | module.exports = Logger 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npm 3 | dist/ 4 | .build* 5 | -------------------------------------------------------------------------------- /src/browserPrefix.coffee: -------------------------------------------------------------------------------- 1 | __baseLogLevel = 'info' 2 | __specificLogLevels = {} 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .filenametags 3 | .cscope.files 4 | .tags 5 | .build 6 | .npm 7 | src/ 8 | test/ 9 | versions.json 10 | package.js 11 | gulpfile.coffee 12 | -------------------------------------------------------------------------------- /src/meteorBrowserPrefix.coffee: -------------------------------------------------------------------------------- 1 | __baseLogLevel = Meteor.settings?.public?.logLevel ? 'info' 2 | __specificLogLevels = Meteor.settings?.public?.specificLogLevels ? {} 3 | -------------------------------------------------------------------------------- /src/meteorServerPrefix.coffee: -------------------------------------------------------------------------------- 1 | clc = Npm.require 'cli-color' 2 | 3 | __baseLogLevel = Meteor.settings?.public?.logLevel ? 'info' 4 | __specificLogLevels = Meteor.settings?.public?.specificLogLevels ? {} 5 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "coffeescript", 5 | "1.0.4" 6 | ], 7 | [ 8 | "meteor", 9 | "1.1.3" 10 | ], 11 | [ 12 | "momentjs:moment", 13 | "2.8.4" 14 | ], 15 | [ 16 | "underscore", 17 | "1.0.1" 18 | ] 19 | ], 20 | "pluginDependencies": [], 21 | "toolVersion": "meteor-tool@1.0.35", 22 | "format": "1.0" 23 | } -------------------------------------------------------------------------------- /src/nodePrefix.coffee: -------------------------------------------------------------------------------- 1 | clc = require 'cli-color' 2 | 3 | # Log level stuff 4 | LOG_PREFIX = 'MADEYE_LOGLEVEL' 5 | __baseLogLevel = process.env[LOG_PREFIX] || 'info' 6 | __specificLogLevels = {} 7 | for k,v of process.env 8 | continue unless k.indexOf("#{LOG_PREFIX}_") == 0 9 | continue if k == LOG_PREFIX 10 | name = k.substr "#{LOG_PREFIX}_".length 11 | name = name.split('_').join(':') 12 | specificLogLevels[name] = v 13 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "A logger for Meteor inspired by log4j and commons-logging.", 3 | version: "0.0.9", 4 | name: "jag:pince", 5 | git: "https://github.com/mad-eye/pince.git", 6 | }); 7 | 8 | Npm.depends({ 9 | "cli-color": "0.2.3" 10 | }); 11 | 12 | Package.onUse(function (api, where) { 13 | api.use('coffeescript@1.0.6'); 14 | api.use('momentjs:moment@2.10.3'); 15 | 16 | api.export('Logger'); 17 | api.add_files('dist/pince-meteor-browser.coffee', 'client'); 18 | api.add_files('dist/pince-meteor-server.coffee', 'server'); 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pince", 3 | "version": "0.0.9", 4 | "description": "A logger for node inspired by log4j and commons-logging.", 5 | "keywords": [ 6 | "logging", 7 | "logger", 8 | "log4j" 9 | ], 10 | "private": false, 11 | "homepage": "https://github.com/mad-eye/pince", 12 | "author": { 13 | "name": "James A. Gill", 14 | "url": "https://github.com/jagill" 15 | }, 16 | "license": "MIT", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/mad-eye/pince" 20 | }, 21 | "main": "./dist/pince-node.js", 22 | "dependencies": { 23 | "moment": "2.4.0", 24 | "cli-color": "0.2.3" 25 | }, 26 | "devDependencies": { 27 | "coffee-script": "1.9.0", 28 | "chai": "^3.2.0", 29 | "gulp": "^3.9.0", 30 | "gulp-coffee": "^2.3.1", 31 | "gulp-concat": "^2.6.0", 32 | "mocha": "^2.2.5", 33 | "sinon": "^1.15.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | coffee = require 'gulp-coffee' 3 | concat = require 'gulp-concat' 4 | 5 | inSrc = (path) -> 'src/' + path 6 | 7 | paths = 8 | browser: [ 9 | 'browserPrefix.coffee', 10 | 'logger.coffee', 11 | 'browserSuffix.coffee', 12 | ].map inSrc 13 | node: [ 14 | 'nodePrefix.coffee', 15 | 'logger.coffee', 16 | 'nodeSuffix.coffee', 17 | ].map inSrc 18 | 'meteor-server': [ 19 | 'meteorServerPrefix.coffee', 20 | 'logger.coffee', 21 | ].map inSrc 22 | 'meteor-browser': [ 23 | 'meteorBrowserPrefix.coffee', 24 | 'logger.coffee', 25 | ].map inSrc 26 | 27 | build = (name, compile=true) -> 28 | stream = gulp.src paths[name] 29 | .pipe concat("pince-#{name}.coffee") 30 | stream = stream.pipe coffee() if compile 31 | return stream.pipe gulp.dest 'dist/' 32 | 33 | gulp.task 'build-node', -> 34 | build 'node' 35 | 36 | gulp.task 'build-browser', -> 37 | build 'browser' 38 | 39 | gulp.task 'build-meteor-server', -> 40 | # Due to annoying meteor/coffee namespacing, need to leave it as coffeescript. 41 | build 'meteor-server', false 42 | 43 | gulp.task 'build-meteor-browser', -> 44 | # Due to annoying meteor/coffee namespacing, need to leave it as coffeescript. 45 | build 'meteor-browser', false 46 | 47 | gulp.task 'build-test', -> 48 | gulp.src 'test/*.coffee' 49 | .pipe coffee() 50 | .pipe gulp.dest 'dist/' 51 | 52 | gulp.task 'build', ['build-node', 'build-browser', 'build-meteor-server', 'build-meteor-browser'] 53 | 54 | gulp.task 'default', ['build', 'build-test'] 55 | -------------------------------------------------------------------------------- /test/pince-test.coffee: -------------------------------------------------------------------------------- 1 | sinon = require 'sinon' 2 | {assert} = require 'chai' 3 | Logger = require './pince-node' 4 | 5 | describe 'Logger', -> 6 | beforeEach -> 7 | Logger._output = sinon.spy() 8 | 9 | describe 'basic', -> 10 | data = null 11 | 12 | beforeEach -> 13 | Logger.setLevel 'info' 14 | log = new Logger('test') 15 | log.info 'foo' 16 | data = Logger._output.getCall(0).args[0] 17 | 18 | it 'should log messages', -> 19 | assert.deepEqual data.messages, ['foo'] 20 | 21 | it 'should assign correct level', -> 22 | assert.equal data.level, 'info' 23 | 24 | it 'should assign correct name', -> 25 | assert.equal data.name, 'test' 26 | 27 | it 'should assign a timestamp', -> 28 | assert.ok data.timestamp 29 | 30 | describe 'levels', -> 31 | log = null 32 | 33 | beforeEach -> 34 | Logger.setLevel 'info' 35 | log = new Logger('test') 36 | 37 | it 'should log messages of equal level', -> 38 | log.info 'foo' 39 | assert.isTrue Logger._output.called 40 | 41 | it 'should log messages of higher level', -> 42 | log.warn 'foo' 43 | assert.isTrue Logger._output.called 44 | 45 | it 'should not log messages of lower level', -> 46 | log.debug 'foo' 47 | assert.isFalse Logger._output.called 48 | 49 | describe 'individual levels', -> 50 | routerLog = controllerLog = null 51 | 52 | beforeEach -> 53 | Logger._output = sinon.spy() 54 | Logger.setLevel('info') 55 | Logger.setLevel('controller', 'trace') 56 | routerLog = new Logger('router') 57 | controllerLog = new Logger('controller') 58 | 59 | it 'should not apply to differently named routers', -> 60 | routerLog.trace "Can't hear me!" 61 | assert.isFalse Logger._output.called 62 | 63 | it 'should apply to correctly named routers', -> 64 | controllerLog.trace "Can hear me." 65 | data = Logger._output.getCall(0).args[0] 66 | assert.equal data.name, 'controller' 67 | assert.equal data.level, 'trace' 68 | 69 | describe 'multiple individual levels', -> 70 | routerLog = controllerLog = null 71 | 72 | beforeEach -> 73 | Logger._output = sinon.spy() 74 | Logger.setLevels router:'debug', controller:'warn' 75 | routerLog = new Logger('router') 76 | controllerLog = new Logger('controller') 77 | 78 | it 'should allow named routers', -> 79 | routerLog.info "Finally! Someone is listening to me." 80 | data = Logger._output.getCall(0).args[0] 81 | assert.equal data.name, 'router' 82 | assert.equal data.level, 'info' 83 | 84 | it 'should filter named routers', -> 85 | controllerLog.info "Hello? Hello??" 86 | assert.isFalse Logger._output.called 87 | 88 | describe 'hierarchical levels', -> 89 | routerLog = controllerLog = null 90 | 91 | beforeEach -> 92 | Logger._output = sinon.spy() 93 | routerLog = new Logger('myPackage:router') 94 | controllerLog = new Logger('myPackage:controller') 95 | Logger.setLevel 'warn' 96 | Logger.setLevel 'myPackage', 'info' 97 | Logger.setLevel 'myPackage:controller', 'debug' 98 | 99 | it 'should not apply to differently named loggers', -> 100 | otherLog = new Logger('other') 101 | otherLog.info 'Crickets.' 102 | assert.isFalse Logger._output.called 103 | 104 | it 'should apply top level to non-specific sub-loggers -- pass', -> 105 | routerLog.info 'You can see this.' 106 | assert.isTrue Logger._output.called 107 | 108 | it 'should apply top level to non-specific sub-loggers -- filter', -> 109 | routerLog.debug 'You cannot see this; myPackage level is set to info.' 110 | assert.isFalse Logger._output.called 111 | 112 | it 'should apply specific level to specific sub-loggers', -> 113 | controllerLog.debug 'You can see this, myPackage:controller level is set to debug.' 114 | assert.isTrue Logger._output.called 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pince 2 | ===== 3 | 4 | Pince is a lightweight logger that combines some of the best 5 | properties of log4j and node. It's equally usable in Node, (most) 6 | browsers, and Meteor (client and server). 7 | 8 | It was developed for [MadEye](https://madeye.io). 9 | 10 | Features: 11 | 12 | * Log levels: error, warn, info, debug, trace 13 | * Dynamically change the log level on the client. 14 | * Change the log level with no code changes on the server. 15 | * Logging is very lightweight when there's nothing listening to it. 16 | * Each logger has a name -- you can set levels individually by name! 17 | * Names can be hierarchically namespaced by separating them with ':'s, like 18 | `myLibrary:aModule:thisObject`. 19 | * Set log levels by any level of the namespace hierarcy! 20 | 21 | 22 | Installation (Node.js) 23 | ---------------------- 24 | To install, `npm install pince`. 25 | 26 | In any file you wish to make a logger, require it via 27 | `Logger = require('pince');` 28 | 29 | Installation (Meteor) 30 | --------------------- 31 | To install, just `meteor add jag:pince`. The global `Logger` symbol will be 32 | there waiting for you. 33 | 34 | By default, on the server Meteor will prepend a string to logs that includes 35 | a timestamp (amongst other things). To silence Meteor's prefix, run meteor 36 | with the `--raw-logs` flag: `meteor --raw-logs`. 37 | 38 | Installation (Browser) 39 | ---------------------- 40 | Just source `pince-browser.js`. 41 | 42 | Usage 43 | ---------- 44 | 45 | Set the log level: 46 | ```javascript 47 | //Default is info 48 | Logger.setLevel('trace'); 49 | ``` 50 | 51 | Make a new logger: 52 | ```javascript 53 | var log = new Logger('router'); 54 | log.info("Routing."); 55 | //2013-10-31 11:29:36.097 info: [router] Routing. 56 | log.trace("Setting up routes..."); 57 | //2013-10-31 11:29:36.101 trace: [router] Setting up routes... 58 | ``` 59 | 60 | Set individual levels: 61 | ```javascript 62 | Logger.setLevel('info'); 63 | Logger.setLevel('controller', 'trace'); 64 | var routerLog = new Logger('router'); 65 | var controllerLog = new Logger('controller'); 66 | 67 | routerLog.trace("Can't hear me!"); 68 | //Nothing 69 | controllerLog.trace("Can hear me."); 70 | //2013-10-31 11:31:21.906 trace: [controller] Can hear me. 71 | 72 | Logger.setLevels({router:'debug', controller:'warn'}); 73 | routerLog.info("Finally! Someone is listening to me."); 74 | //2013-10-31 11:32:48.374 info: [router] Finally! Someone is listening to me. 75 | controllerLog.info("Hello? Hello??"); 76 | //Nothing 77 | ``` 78 | 79 | Hierarchically name and set levels: 80 | ```javascript 81 | var routerLog = new Logger('myPackage:router'); 82 | var controllerLog = new Logger('myPackage:controller'); 83 | Logger.setLevel('myPackage', 'info'); 84 | Logger.setLevel('myPackage:controller', 'debug'); 85 | 86 | routerLog.info('You can see this.'); 87 | routerLog.debug('You cannot see this; myPackage level is set to info.'); 88 | controllerLog.debug('You can see this, myPackage:controller level is set to debug.'); 89 | ``` 90 | 91 | Control the output formatting: 92 | ```javascript 93 | var log = new Logger('router'); 94 | log.info('A message.'); 95 | //2013-10-31 11:32:48.374 info: [router] A message. 96 | Logger.setFormat('%N:%L [%T] %M'); 97 | log.info('A message.'); 98 | //router:info [2013-10-31 11:32:48.374] A message. 99 | Logger.setDateFormat('YYYY_MM_DD_HH_mm_ss'); 100 | log.info('A message.'); 101 | //router:info [2013_10_31_11_32_48] A message. 102 | ``` 103 | 104 | To control the formatting, use `Logger.setFormat(str)` and 105 | `Logger.setDateFormat(str)`. The former controls the overall format, and includes the escape characters: 106 | * `%T` The timestamp string (as controlled by `setDateFormat()`). 107 | * `%L` The logging level, eg `info`. 108 | * `%N` The name of the logger, eg `myPackage:router`. 109 | * `%M` The message to log. 110 | 111 | In addition, you can control the appearance of the timestamp. The timestamp format string has the special sequences: 112 | * `YYYY` The full 4-digit date. 113 | * `MM` The 2-digit month (1-12). 114 | * `DD` The 2-digit date (1-31). 115 | * `HH` The 2-digit hour (00-23). 116 | * `mm` The 2-digit minute (00-59). 117 | * `ss` The 2-digit second (00-59). 118 | * `SSS` The 3-digit millisecond (000-999). 119 | -------------------------------------------------------------------------------- /src/logger.coffee: -------------------------------------------------------------------------------- 1 | 2 | ## Formatting 3 | 4 | if typeof window != 'undefined' 5 | noop = (x) -> x 6 | colors = 7 | error: noop 8 | warn: noop 9 | info: noop 10 | debug: noop 11 | trace: noop 12 | else 13 | colors = 14 | error: clc.red.bold 15 | warn: clc.yellow 16 | info: clc.bold 17 | debug: clc.blue 18 | trace: clc.blackBright 19 | 20 | # Pad integer d to n places with preceding 0s 21 | pad = (d, n) -> 22 | d = d.toString() 23 | while d.length < n 24 | d = '0' + d 25 | return d 26 | 27 | # Map token to replacement values from date d 28 | __dateFormatStr = "YYYY-MM-DD HH:mm:ss.SSS" 29 | 30 | __dateFormatTokens = 31 | 'YYYY': (d) -> pad(d.getFullYear(), 4) 32 | 'MM': (d) -> pad(d.getMonth(), 2) 33 | 'DD': (d) -> pad(d.getDate(), 2) 34 | 'HH': (d) -> pad(d.getHours(), 2) 35 | 'mm': (d) -> pad(d.getMinutes(), 2) 36 | 'ss': (d) -> pad(d.getSeconds(), 2) 37 | 'SSS': (d) -> pad(d.getMilliseconds(), 3) 38 | 39 | formatDate = (date, formatStr=__dateFormatStr) -> 40 | str = formatStr 41 | for token, valueFn of __dateFormatTokens 42 | value = valueFn date 43 | str = str.replace(token, value) 44 | return str 45 | 46 | __formatStr = "%T %L: [%N] %M" 47 | 48 | __formatTokens = 49 | '%T': (data) -> data.formattedTimestamp 50 | '%L': (data) -> colors[data.level](data.level) 51 | '%N': (data) -> data.name 52 | '%M': (data) -> data.messages[0] 53 | 54 | formatLog = (data, formatStr=__formatStr) -> 55 | str = formatStr 56 | for token, valueFn of __formatTokens 57 | value = valueFn data 58 | str = str.replace(token, value) 59 | output = data.messages[1..] 60 | output.unshift(str) 61 | return output 62 | 63 | 64 | ## END Formatting 65 | 66 | __levels = ['error', 'warn', 'info', 'debug', 'trace'] 67 | __levelnums = {} 68 | __levelnums[l] = i for l, i in __levels 69 | 70 | findLevelFor = (name) -> 71 | #Check first for any specific levels 72 | level = __specificLogLevels[name] 73 | 74 | #Check hierarchically up the : chain 75 | parentName = name 76 | while (parentName.indexOf(':') > -1) and not level? 77 | lastIdx = parentName.lastIndexOf(':') 78 | parentName = parentName.substr 0, lastIdx 79 | level ?= __specificLogLevels[parentName] 80 | 81 | level ?= __baseLogLevel 82 | return level 83 | 84 | shouldLog = (name, level) -> 85 | allowedLevelNum = __levelnums[findLevelFor name] 86 | if __levelnums[level] > allowedLevelNum 87 | return false 88 | return true 89 | 90 | class Logger 91 | # Class Methods 92 | @setLevel = (name, level) -> 93 | unless level 94 | level = name 95 | name = null 96 | 97 | unless level of __levelnums 98 | throw new Error("Level #{level} unknown.") 99 | 100 | if name 101 | __specificLogLevels[name] = level 102 | else 103 | __baseLogLevel = level 104 | 105 | return 106 | 107 | #levels is an object {name:level} which sets each name to level 108 | @setLevels: (levels) -> 109 | for name, level of levels 110 | __specificLogLevels[name] = level 111 | return 112 | 113 | @setDateFormat: (str) -> 114 | __dateFormatStr = str 115 | 116 | @setFormat: (str) -> 117 | __formatStr = str 118 | 119 | @_log = (data) -> 120 | return unless shouldLog data.name, data.level 121 | @_output data 122 | 123 | @_output = (data) -> 124 | # 2013-10-31 11:32:48.374 info: [router] Finally! Someone is 125 | data.formattedTimestamp = formatDate(data.timestamp, __dateFormatStr) 126 | output = formatLog(data, __formatStr) 127 | switch data.level 128 | when 'trace', 'debug' then fn = console.log 129 | when 'info' then fn = console.info ? console.log 130 | when 'warn' then fn = console.warn ? console.log 131 | when 'error' then fn = console.error ? console.log 132 | fn.apply console, output 133 | 134 | # Instance Methods 135 | constructor: (@name) -> 136 | 137 | trace: (messages...) -> @log 'trace', messages 138 | debug: (messages...) -> @log 'debug', messages 139 | info: (messages...) -> @log 'info', messages 140 | warn: (messages...) -> @log 'warn', messages 141 | error: (messages...) -> @log 'error', messages 142 | 143 | # log takes messages as an array. It will ignore additional arguments. 144 | log: (level, messages) -> 145 | unless Array.isArray messages 146 | messages = [messages] 147 | 148 | data = {level, messages} 149 | data.name = @name 150 | data.timestamp = new Date() 151 | 152 | Logger._log data 153 | --------------------------------------------------------------------------------