From 5e601c32a0d646f7dafbcd8a45de0cf4ccbe8574 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 10 Feb 2012 17:12:47 -0800 Subject: [PATCH] working product. examples all pass --- .gitignore | 2 + .npmignore | 3 + LICENSE | 22 ++++ Makefile | 11 ++ README | 79 +++++++++++ examples/file_utilization.js | 34 +++++ examples/file_utilization.json | 21 +++ examples/max_utilization.js | 28 ++++ examples/max_utilization_helper.js | 73 +++++++++++ examples/medium.js | 38 ++++++ examples/simple.js | 27 ++++ index.js | 2 + lib/coffee/color.coffee | 62 +++++++++ lib/coffee/config.coffee | 170 ++++++++++++++++++++++++ lib/coffee/helper.coffee | 163 +++++++++++++++++++++++ lib/coffee/reqLog.coffee | 190 +++++++++++++++++++++++++++ lib/color.js | 53 ++++++++ lib/config.js | 144 ++++++++++++++++++++ lib/helper.js | 203 +++++++++++++++++++++++++++++ lib/reqLog.js | 179 +++++++++++++++++++++++++ package.json | 19 +++ 21 files changed, 1523 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 examples/file_utilization.js create mode 100644 examples/file_utilization.json create mode 100644 examples/max_utilization.js create mode 100644 examples/max_utilization_helper.js create mode 100644 examples/medium.js create mode 100644 examples/simple.js create mode 100644 index.js create mode 100644 lib/coffee/color.coffee create mode 100644 lib/coffee/config.coffee create mode 100644 lib/coffee/helper.coffee create mode 100644 lib/coffee/reqLog.coffee create mode 100644 lib/color.js create mode 100644 lib/config.js create mode 100644 lib/helper.js create mode 100644 lib/reqLog.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..646ac51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +node_modules/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..b95370d --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +.git* +examples/ +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9d002a6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2012 Barret Schloerke + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..760c510 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ + + +clean: + @ rm lib/*.js + +compile: + @exec coffee --compile --bare -o lib/ lib/coffee/*.coffee + +watch: + @exec coffee --compile --bare --watch -o lib/ lib/coffee/*.coffee & + diff --git a/README b/README index e69de29..8eaba67 100644 --- a/README +++ b/README @@ -0,0 +1,79 @@ +reqLog +=========== + +reqLog is a Node.js logging library that can + +* print log messages according to your own functions +* be customized per module and per logging level +* have any amount of logging levels +* print statements in full color (background too!!!) +* print statements with console qualities such as "italics" or "bold" +* print messages with module name and current line number so you know from where it was called +* be configured from file or by a module + +Usage +----- +Use npm or download. Then add to your code: + + var log = require('reqLog').with(module); + +*module* is object defined automatically by Node.js. If you don't want automatic module names, replace it with your desired string name. + + log.info(arg1, arg2, arg3); + +Strings arguments are not inspected unless the log level allows for printing of the statement. This avoids unnecessary JSON.stringify calls. + +Examples +-------- + + var log = require('reqLog').with(module); + log.info('Info message'); + log.debug('Debug message'); + log.warn('Warning message'); + log.error('Error message'); + log.trace('Trace message'); + log.info('Array =', [1, 2, 3, 4], '; Object = ', {one: 1, two: 2}); + + +Output samples +-------------- + + 2010-10-02 20:39:03.570 INFO main:5 - Info message + 2010-10-02 20:39:03.588 DEBUG main:6 - Debug message + 2010-10-02 20:39:03.589 WARN main:7 - Warning message + 2010-10-02 20:39:03.590 ERROR main:8 - Error message + 2010-10-02 20:39:03.590 TRACE main:9 - Trace message + 2010-10-02 20:39:03.590 INFO main:10 - Array = [ 1, 2, 3, 4 ], Object = { one: 1, two: 2 } + +Advance Example +--------------- + + var log = require('./examples/max_utilization_helper').with(module); + + // qualifiers to make it a 'req' object + var req = {session: { user: {email: jobs@metamarkets.com } } , route: {}, res: {}, next: {}}; + var fourtyTwo = 42; + log.info(req, "The answer to life the universe and everything: '", fourtyTwo, "'") + // "2012-02-00T00:00:00.000Z; main; jobs@metamarkets.com; INFO ; main:18; The answer to life the universe and everything: '!¿!fourtyTwo!¿!'; 1" + + log.info(req, "Info counter should be at 2. Counter value:") + // "2012-02-00T00:00:00.001Z; main; jobs@metamarkets.com; INFO ; main:21; Info counter should be at 2. Counter value:; 2" + + log.debug(req, "Debug counter should be at 1. Counter value:") + // "2012-02-00T00:00:00.001Z; main; jobs@metamarkets.com; DEBUG; main:24; Debug counter should be at 1. Counter value:; 1" + + log.trace(req, "this should not execute. Level is not included (too low in stack)") + // + + +Take a look at the examples directory for different uses. + + +Changes +------- +0.0.1 - First npm release + + +License +------- +Released under MIT License. Enjoy and Fork! diff --git a/examples/file_utilization.js b/examples/file_utilization.js new file mode 100644 index 0000000..68c7916 --- /dev/null +++ b/examples/file_utilization.js @@ -0,0 +1,34 @@ + +reqLog = require('../index.js').options({ + file: "./file_utilization.json" +}) + + +// // ./file.utilization.json +// { +// "logLevels": +// [ { "title": "test" , "color": "magenta"} +// , { "title": "trace", "color": "cyan" } +// , { "title": "debug", "color": "blue" } +// , { "title": "info" , "color": "green" } +// , { "title": "sql" , "color": "magenta"} +// , { "title": "req" , "color": "white" } +// , { "title": "warn" , "color": "yellow" } +// , { "title": "error", "color": "red" , "background": "yellow", "attr": ["bold", "underline"] } +// ] + +// , "fileLevels": +// [ ["*", "error"] +// ["main", "req"] +// ["first_file", "trace"] +// ["second_file", "info"] +// ["custom_file", ["debug", "sql", "warn", "error"]] +// ] +// } + +// // Can produce output with the following levels +// '*' : ['error'] +// 'main' : ['req', 'warn', 'error'] +// 'first_file' : ['trace', 'debug', 'info', 'sql', 'req' ,'warn', 'error'] +// 'second_file': ['info', 'sql', 'req' ,'warn', 'error'] +// 'custom_file': ['debug', 'sql', 'warn', 'error'] // <- does not follow traditional model!!! diff --git a/examples/file_utilization.json b/examples/file_utilization.json new file mode 100644 index 0000000..609470e --- /dev/null +++ b/examples/file_utilization.json @@ -0,0 +1,21 @@ + +{ + "logLevels": + [ { "title": "test" , "color": "magenta"} + , { "title": "trace", "color": "cyan" } + , { "title": "debug", "color": "blue" } + , { "title": "info" , "color": "green" } + , { "title": "sql" , "color": "magenta"} + , { "title": "req" , "color": "white" } + , { "title": "warn" , "color": "yellow" } + , { "title": "error", "color": "red" , "background": "yellow", "attr": ["bold", "underline"] } + ] + +, "fileLevels": + [ ["*", "error"] + ["main", "req"] + ["first_file", "trace"] + ["second_file", "info"] + ["custom_file", ["debug", "sql", "warn", "error"]] + ] +} diff --git a/examples/max_utilization.js b/examples/max_utilization.js new file mode 100644 index 0000000..af54442 --- /dev/null +++ b/examples/max_utilization.js @@ -0,0 +1,28 @@ + + +var log = require('./max_utilization_helper').with(module); + +// // Can produce output with the following file and levels +// '*' : ['error'] +// 'main' : ['debug', 'info', 'sql', req', 'warn', 'error'] +// 'first_file' : ['trace', 'debug', 'info', 'sql', 'req' ,'warn', 'error'] +// 'second_file': ['info', 'sql', 'req' ,'warn', 'error'] +// 'custom_file': ['debug', 'sql', 'warn', 'error'] // <- does not follow traditional model!!! + + +var iden = function(d) {return d;}; + + // qualifiers to make it a 'req' object +var req = {session: { user: {email: "jobs@metamarkets.com" } } , route: {}, res: {}, next: iden}; +var fourtyTwo = 42; +log.info(req, "The answer to life the universe and everything: '", fourtyTwo, "'") +// "2012-02-00T00:00:00.000Z; main; jobs@metamarkets.com; INFO ; main:18; The answer to life the universe and everything: '!¿!fourtyTwo!¿!'; 1" + +log.info(req, "Info counter should be at 2. Counter value:") +// "2012-02-00T00:00:00.001Z; main; jobs@metamarkets.com; INFO ; main:21; Info counter should be at 2. Counter value:; 2" + +log.debug(req, "Debug counter should be at 1. Counter value:") +// "2012-02-00T00:00:00.001Z; main; jobs@metamarkets.com; DEBUG; main:24; Debug counter should be at 1. Counter value:; 1" + +log.trace(req, "this should not execute. Level is not included (too low in stack)") +// diff --git a/examples/max_utilization_helper.js b/examples/max_utilization_helper.js new file mode 100644 index 0000000..96505dc --- /dev/null +++ b/examples/max_utilization_helper.js @@ -0,0 +1,73 @@ + +var sys = require('sys'); + +function save_to_db(str) { + // do stuff +} +function save_to_console(str) { + sys.puts(str); + // do stuff +} +function save_to_file(str) { + // do stuff +} + + +function my_log_hook_fn(str) { + save_to_db(str); + save_to_console(str); + save_to_file(str); +} + +function my_custom_counter(mod) { + var levelCounterMap = {}; + function counter_by_level(level, args) { + levelCounterMap[level] || (levelCounterMap[level] = 0); + levelCounterMap[level]++; + return levelCounterMap[level]; + } + return counter_by_level; +} + + +var reqLog = require('../index.js') +reqLog.options( + { color : true // false // true; to force color + , logger : my_log_hook_fn // console.log + , loggerContext : null // console + , seperator : '; ' + , fnArr: + [ reqLog.helper.iso_date() // no options + , reqLog.helper.session_user_email() // no options + , reqLog.helper.level() // no options + , reqLog.helper.file_and_line() // no options + , "-" // plain string to always be inserted + , reqLog.helper.single_line_message_ignore_express_req_at_first({seperator: "!¿!"}) + , my_custom_counter // must execute on function that takes args ['module'], then on a function that takes args ['level', 'args'] + ] + + , logLevels: + [ { title: "silly", color: "red" , background: "cyan", attr: ["bold", "italic", "underline", "blink", "inverse", "hidden"]} + , { title: "test" , color: "magenta"} + , { title: "trace", color: "cyan" } + , { title: "debug", color: "blue" } + , { title: "info" , color: "green" } + , { title: "sql" , color: "magenta"} + , { title: "req" , color: "white" } + , { title: "warn" , color: "yellow" } + , { title: "error", color: "red" , background: "black", attr: ["bold", "underline"] } + ] + , fileLevels: + [ ["*", "error"] + , ["main", "debug"] + , ["first_file", "trace"] + , ["second_file", "info"] + , ["custom_file", ["debug", "sql", "warn", "error"] ] + ] + } +); + + +exports.with = function(mod) { + return reqLog.with(mod); +}; diff --git a/examples/medium.js b/examples/medium.js new file mode 100644 index 0000000..bfbb28f --- /dev/null +++ b/examples/medium.js @@ -0,0 +1,38 @@ + +reqLog = require('../index.js').options({ + logLevels: + [ { title: "test" , color: "magenta"} + , { title: "trace", color: "cyan" } + , { title: "debug", color: "blue" } + , { title: "info" , color: "green" } + , { title: "sql" , color: "magenta"} + , { title: "req" , color: "white" } + , { title: "warn" , color: "yellow" } + , { title: "error", color: "red" , background: "black", attr: ["bold", "underline"] } + ] +, fileLevels: + [ ["*", "error"] + , ["main", "req"] + , ["first_file", "trace"] + , ["second_file", "info"] + // , ["main", ["debug", "sql", "warn", "error"]] // swith this in for main for custom logging! + ] + +}) + +var log = reqLog.with("main"); + +// // Can produce output with the following file and levels +// '*' : ['error'] +// 'main' : ['req', 'warn', 'error'] +// 'first_file' : ['trace', 'debug', 'info', 'sql', 'req' ,'warn', 'error'] +// 'second_file': ['info', 'sql', 'req' ,'warn', 'error'] +// 'custom_file': ['debug', 'sql', 'warn', 'error'] // <- does not follow traditional model!!! +log.test( "Test" , 0); +log.trace("Trace" , 1); +log.debug("Debug" , 2); +log.info( "Info" , 3); +log.sql( "Sql" , 4); +log.req( "Req" , 5); +log.warn( "Warn" , 6); +log.error("Error" , 7); diff --git a/examples/simple.js b/examples/simple.js new file mode 100644 index 0000000..eda44d5 --- /dev/null +++ b/examples/simple.js @@ -0,0 +1,27 @@ + +var log = require('../index.js').with(module); + + +// // Uses the default log levels and file levels +// config.defaultLogLevels = [ +// { title: 'test' , color: 'magenta'} +// { title: 'trace', color: 'cyan' } +// { title: 'debug', color: 'blue' } +// { title: 'info' , color: 'green' } +// { title: 'warn' , color: 'yellow' } +// { title: 'error', color: 'red' , background: 'yellow', attr: ['bold', 'underline']} +// ] + +// config.defaultFileLevels = [ +// ["*", "trace"] +// ] + + +// // All files will produce output with the following levels +// '*': ['trace', 'debug', 'info', 'warn', 'error'] +log.test( "Test" , 0); +log.trace("Trace" , 1); +log.debug("Debug" , 2); +log.info( "Info" , 3); +log.warn( "Warn" , 4); +log.error("Error" , 5); diff --git a/index.js b/index.js new file mode 100644 index 0000000..2b41d8a --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ + +module.exports = require('./lib/reqLog'); diff --git a/lib/coffee/color.coffee b/lib/coffee/color.coffee new file mode 100644 index 0000000..544a970 --- /dev/null +++ b/lib/coffee/color.coffee @@ -0,0 +1,62 @@ + + +# methods: [ +# {title: "test", color: "cyan", attr: ["bold", "blink", "inverse"]} +# ] + + + +attrMap = { + off : 0 + bold : 1 + italic : 3 + underline: 4 + blink : 5 + inverse : 7 + hidden : 8 +} + +fontMap = { + black : 30 + red : 31 + green : 32 + yellow : 33 + blue : 34 + magenta: 35 + cyan : 36 + white : 37 +} + + +backgroundMap = { + black : 40 + red : 41 + green : 42 + yellow : 43 + blue : 44 + magenta: 45 + cyan : 46 + white : 47 +} + + +exports.ansi_str = ({font, background, attr}) -> + + fontNum = fontMap[font] or font or null + + backgroundNum = backgroundMap[background] or background or null + + items = ['0'] + items.push(fontNum) if fontNum + items.push(backgroundNum) if backgroundNum + + for item in (attr or []) + itemNum = attrMap[item] or item or null + if itemNum? + items.push(itemNum) + + return '\x1B[' + items.join(';') + 'm' + + +exports.ANSI_END = '\x1B[0m' + diff --git a/lib/coffee/config.coffee b/lib/coffee/config.coffee new file mode 100644 index 0000000..ed14ed5 --- /dev/null +++ b/lib/coffee/config.coffee @@ -0,0 +1,170 @@ + + +_ = require 'underscore' + +color = require './color' + +log = _.identity + + +exports.defaultLogLevels = [ + { title: 'test' , color: 'magenta'} + { title: 'trace', color: 'cyan' } + { title: 'debug', color: 'blue' } + { title: 'info' , color: 'green' } + { title: 'warn' , color: 'yellow' } + { title: 'error', color: 'red' , background: 'black', attr: ['bold', 'underline']} +] + +exports.defaultFileLevels = [ + ["*", "trace"] +] + + + +# @return array of items that contain {title: title.toUpperCase(), rawTitle, pos, ansiColor start} +parse_log_levels = (methods) -> + unless _.isArray(methods) + throw "methods is defined, but is not an array" + + titlesSeen = {} + + maxTitleSize = 0 + ret = [] + + for method, pos in methods + unless method.title + throw "title not found for the #{ pos } method" + + title = method.title.toUpperCase() + maxTitleSize = Math.max(maxTitleSize, title.length) + + if titlesSeen[title] + throw "title already found for '#{ title }'. Must be unique values when taken 'toUpperCase'" + titlesSeen[title] = true + + ret.push { + title : title + rawTitle : method.title + pos : pos + ansiColor: color.ansi_str { + font : method.color + background: method.background + attr : method.attr + } + } + + # pad the title to align nicely + for item in ret + while (item.title.length < maxTitleSize) + item.title += ' ' + + return ret + + +# @return map of title to ansiColors +log_levels_to_map_of_color = (logLevels) -> + + ret = {} + for logLevel in logLevels + ret[logLevel.title] = logLevel.ansiColor + return ret + + +# @return array of rawTitles of each logLevel +log_levels_to_raw_title_arr = (logLevels) -> + return _.pluck(logLevels, "rawTitle") + +# @return map of file to map of levels: true +# @returnEx +# input = { +# ["*", "test"] +# ["myFile", ["debug", "warn"]] +# } +# { +# '*': { +# TEST : true +# TRACE: true +# DEBUG: true +# INFO : true +# WARN : true +# ERROR: true +# } +# myFile: { +# DEBUG: true +# WARN : true +# } +# } +parse_file_levels = (fileLevels, logLevels) -> + # log levels is passed as properly parsed log levels. no need to check + + unless _.isArray(fileLevels) + throw "fileLevels is defined, but is not an array" + + logLevelNameArr = log_levels_to_raw_title_arr(logLevels) + + + ret = {} + for row, pos in fileLevels + unless _.isArray(row) and row.length is 2 + throw "level in row #{ pos } is defined, but is not an array of length 2" + rowFile = row[0] + rowLevels = row[1] + + # upgrade a string level to a + if _.isString(rowLevels) + # "info" turns into ["info", "warn", "error"] + logPos = logLevelNameArr.indexOf(rowLevels) + unless logPos isnt -1 + throw "level in row '#{ pos }' is not a defined log level" + + rowLevels = logLevelNameArr.slice(logPos) + else + unless _.isArray(rowLevels) + throw "level in row '#{ pos }' is not a string or an array" + + # is array. check for all strings + unless _.uniq(rowLevels).length is rowLevels.length + throw "log levels in row '#{ pos }' are not unique" + + for rowLevel in rowLevels + unless _.include(logLevelNameArr, rowLevel) + throw "log level called '#{ rowLevel }' in row '#{ pos }' is not a valid log level" + + # rowLevels are now valid + + rowMap = {} + for rowLevel in rowLevels + rowMap[rowLevel] = true + + ret[rowFile] = rowMap + + return ret + + +exports.parse = ({logLevels, fileLevels}) -> + logLevels ?= exports.defaultLogLevels + fileLevels ?= exports.defaultFileLevels + + parsedLogLevels = parse_log_levels(logLevels) + + parsedFileLevels = parse_file_levels(fileLevels, parsedLogLevels) + + return { + logLevels + fileLevels + parsedLogLevels + colorMap : log_levels_to_map_of_color(parsedLogLevels) + fileLevelMap: parsedFileLevels + } + + + + + + + + + + + diff --git a/lib/coffee/helper.coffee b/lib/coffee/helper.coffee new file mode 100644 index 0000000..b95258c --- /dev/null +++ b/lib/coffee/helper.coffee @@ -0,0 +1,163 @@ + + + +_ = require 'underscore' +util = require 'util' + + + +is_express_req_obj = (obj) -> + obj or= {} + return _.has(obj, "route") and _.has(obj, "res") and _.has(obj, "next") + +item_by_keys = (obj, keyArr) -> + item = obj + for key in keyArr + return null unless _.has(item, key) + item = item[key] + + return item.toString() + + +# Take an array of keys to retrieve an object +arg_by_keys_wrapper = (keyArr) -> + throw "array is not supplied" unless keyArr + return (mod) -> + return (level, args) -> + return item_by_keys(args, keyArr) + + +req_item_by_keys = (keyArr) -> + return (mod) -> + return (level, args) -> + reqObj = args[0] or {} + return null unless is_express_req_obj(reqObj) + + return item_by_keys(reqObj, keyArr) + + +exports.iso_date = (opts = {}) -> + return (mod) -> + return (level, args) -> + return (new Date()).toISOString() + + +exports.level = (opts = {}) -> + return (mod) -> + return (level, args, realTitle) -> + return realTitle + + +exports.session_user_id = (defArr = ["session", "user", "id"]) -> + return req_item_by_keys(defArr) + +exports.session_user_email = (defArr = ["session", "user", "email"]) -> + return req_item_by_keys(defArr) + +exports.line_number = (opts = {}) -> + return (mod) -> + return (level, args) -> + try + throw new Error() + catch e + # now magic will happen: get line number from callstack + # console.error(e.stack) # TODO REMOVE + return e.stack.split('\n')[3].split(':')[1]; + +exports.module_title = (opts = {}) -> + return (mod) -> + return _.once (level, args, realTitle, moduleTitle) -> + # if moduleTitle + # return moduleTitle + + unless mod + return '' + + unless mod.id + return mod + + if (mod.id is '.') + return 'main' + else + return mod.id.replace(process.env.PWD, "").replace(/.js$/, "").replace(/^\//, "") + + +exports.file_and_line = (opts = {}) -> + return (mod) -> + line_fn = exports.line_number(opts)(mod) + mod_title_fn = exports.module_title(opts)(mod) + return (level, args) -> + return mod_title_fn(level, args) + ":" + line_fn(level, args) + + +exports.single_line_message = (opts = {}) -> + showHidden = opts.showHidden + depth = opts.depth + seperator = opts.seperator or "" + return (mod) -> + return (level, args) -> + ret = [] + for item in (args or []) + if _.isString(item) + ret.push(item) + else + ret.push(util.inspect(item, showHidden, depth)) + + ret = ret.join(seperator) + ret = ret.replace(/[^\\]\\n/g, '') # remove all '\n' but not '\\n' as that is an escaped '\n' and not something introduced by util.inspect + return ret + +exports.message = (opts = {}) -> + showHidden = opts.showHidden + depth = opts.depth + seperator = opts.seperator or "" + return (mod) -> + return (level, args) -> + ret = [] + for item in (args or []) + if _.isString(item) + ret.push(item) + else + ret.push(util.inspect(item, showHidden, depth)) + + ret = ret.join(seperator) + return ret + + +do_but_ignore_req_at_first = (fn) -> + + return (opts = {}) -> + fn_after_opts = fn(opts) + return (mod) -> + fn_after_mod = fn_after_opts(mod) + return (level, args) -> + args or= [] + if is_express_req_obj(args[0] or null) + return fn_after_mod(level, args.slice(1)) + else + return fn_after_mod(level, args) + + + +exports.single_line_message_ignore_express_req_at_first = do_but_ignore_req_at_first(exports.single_line_message) +exports.message_ignore_express_req_at_first = do_but_ignore_req_at_first(exports.message) + + + +exports.string = (x) -> + return _.once (mod) -> + return _.once (level, args) -> + return x + + + + + + + + + + + + + diff --git a/lib/coffee/reqLog.coffee b/lib/coffee/reqLog.coffee new file mode 100644 index 0000000..f0cea0c --- /dev/null +++ b/lib/coffee/reqLog.coffee @@ -0,0 +1,190 @@ + +sys = require('sys') +fs = require('fs') + +_ = require 'underscore' + +config = require './config' +color = require './color' +helper = require './helper' +exports.helper = helper + +# initalize with empty inputs +exports._status = status = { + logger : sys.puts + loggerContext: sys + file : null + color : process.env.TERM?.indexOf('color') >= 0 + seperator : " " + fnArr: [ + helper.iso_date() + helper.session_user_email() + helper.level() + helper.file_and_line() + helper.string("-") + helper.message_ignore_express_req_at_first({seperator: " "}) + ] +} + +add_levels_to_status = ({logLevels, fileLevels} = {}) -> + parsedDefaults = config.parse({logLevels, fileLevels}) + + status.logLevels = parsedDefaults.logLevels + status.parsedLogLevels = parsedDefaults.parsedLogLevels + status.fileLevels = parsedDefaults.fileLevels + + status.colorMap = parsedDefaults.colorMap + status.fileLevelMap = parsedDefaults.fileLevelMap + status.defaultLogLevel = _.first(status.logLevels).title # default to the lowest level + return + +# init with nothing +add_levels_to_status() + +exports.options = (opts) -> + if _.has(opts, "logger") or _.has(opts, "loggerContext") + unless _.has(opts, "logger") + throw "'logger' not supplied. Must be a function to match the 'loggerContext' (which may have a value of null)" + unless _.has(opts, "loggerContext") + throw "'loggerContext' not supplied. A logger context must be supplied to avoid errors. Ex. reqLog.opts({logger: console.log, loggerContext: console})" + + status.logger = opts.logger + status.loggerContext = opts.loggerContext + + if _.has(opts, "color") + status.color = opts.color + + if _.has(opts, "seperator") + status.seperator = opts.seperator + + if _.has(opts, "file") + status.file = opts.file + + fileDataString = fs.readFileSync(opts.file) + fileData = JSON.parse(fileDataString) + + status.logLevels = fileData.logLevels + status.fileLevels = fileData.fileLevels + + else + if _.has(opts, "logLevels") + status.logLevels = opts.logLevels + if _.has(opts, "fileLevels") + status.fileLevels = opts.fileLevels + + if _.has(opts, "fnArr") + status.fnArr = opts.fnArr + for item, fnPos in status.fnArr + if _.isString(item) + do (item) -> + status.fnArr[fnPos] = _.once( () -> return _.once( () -> return item)) + + else if _.isFunction(item) + # keep calm carry on + null + else + throw "function at position '#{ fnPos }' is not a string that matches a helper method name or a function defined by you" + + add_levels_to_status { + logLevels : status.logLevels + fileLevels: status.fileLevels + } + return exports + + + + + + + + + + + +# returns a object that contains all log +exports.with = (mod) -> + + moduleTitle = helper.module_title({})(mod)() # get function then execute it. only has one answer + # console.error("fileLevelMap", status.fileLevelMap) + # console.error("moduleTitle", moduleTitle) + + moduleLevelMap = status.fileLevelMap[moduleTitle] or status.fileLevelMap['*'] + unless moduleLevelMap? + throw "No levels found for module: '#{ moduleTitle }'. Please add the default file level of '*' or one that matches the module name." + + has_level = (lvl) -> + # makes 'falsy' -> false; 'truthy' -> true + return not (not moduleLevelMap[lvl]) + + logFn = status.logger + loggerContext = status.logger + seperatorItem = status.seperator + + ret = { + _inColor : status.color + _title : moduleTitle + _logLevels : status.logLevels + _moduleLevels : _.keys(moduleLevelMap) + _has_level : has_level + _seperator : seperatorItem + _logFn : logFn + _loggerContext: loggerContext + } + + for levelInfo in status.parsedLogLevels + # go through the keys (level names) + do (levelInfo) -> + level = levelInfo.rawTitle + + # console.log("--Adding level: ", level, levelInfo) + unless _.isFunction(logFn) + # don't do squat unless there is a log function + ret[level] = _.identity + return + + unless moduleLevelMap[level] + # level does not exist for this module... add method that doesn't crash the user + ret[level] = _.identity + return + + # setup all fns to be init'ed with the module + levelFns = (fn(mod) for fn in status.fnArr) + + if status.color + + ansiColorStart = levelInfo.ansiColor + ansiColorEnd = color.ANSI_END + + # color + ret[level] = (args...) -> + outputs = [] + for fn in levelFns + fnOutput = fn(level, args, levelInfo.title, moduleTitle) + # remove all null or undefined values. helpers should make them strings to avoid the reduction + if fnOutput? + outputs.push(fnOutput) + + str = (ansiColorStart + outputs.join(seperatorItem) + ansiColorEnd) + + # console.error("-- str: " + str) + logFn.call(loggerContext, str) + return + + + else + # no color + ret[level] = (args...) -> + outputs = [] + for fn in levelFns + fnOutput = fn(level, args) + # remove all null or undefined values. helpers should make them strings to avoid the reduction + if fnOutput? + outputs.push(fnOutput) + + joinedOutput = outputs.join(seperatorItem) + + logFn.call(loggerContext, joinedOutput) + return + + return ret + diff --git a/lib/color.js b/lib/color.js new file mode 100644 index 0000000..f1923a2 --- /dev/null +++ b/lib/color.js @@ -0,0 +1,53 @@ +var attrMap, backgroundMap, fontMap; +attrMap = { + off: 0, + bold: 1, + italic: 3, + underline: 4, + blink: 5, + inverse: 7, + hidden: 8 +}; +fontMap = { + black: 30, + red: 31, + green: 32, + yellow: 33, + blue: 34, + magenta: 35, + cyan: 36, + white: 37 +}; +backgroundMap = { + black: 40, + red: 41, + green: 42, + yellow: 43, + blue: 44, + magenta: 45, + cyan: 46, + white: 47 +}; +exports.ansi_str = function(_arg) { + var attr, background, backgroundNum, font, fontNum, item, itemNum, items, _i, _len, _ref; + font = _arg.font, background = _arg.background, attr = _arg.attr; + fontNum = fontMap[font] || font || null; + backgroundNum = backgroundMap[background] || background || null; + items = ['0']; + if (fontNum) { + items.push(fontNum); + } + if (backgroundNum) { + items.push(backgroundNum); + } + _ref = attr || []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + item = _ref[_i]; + itemNum = attrMap[item] || item || null; + if (itemNum != null) { + items.push(itemNum); + } + } + return '\x1B[' + items.join(';') + 'm'; +}; +exports.ANSI_END = '\x1B[0m'; \ No newline at end of file diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..30715ec --- /dev/null +++ b/lib/config.js @@ -0,0 +1,144 @@ +var color, log, log_levels_to_map_of_color, log_levels_to_raw_title_arr, parse_file_levels, parse_log_levels, _; +_ = require('underscore'); +color = require('./color'); +log = _.identity; +exports.defaultLogLevels = [ + { + title: 'test', + color: 'magenta' + }, { + title: 'trace', + color: 'cyan' + }, { + title: 'debug', + color: 'blue' + }, { + title: 'info', + color: 'green' + }, { + title: 'warn', + color: 'yellow' + }, { + title: 'error', + color: 'red', + background: 'black', + attr: ['bold', 'underline'] + } +]; +exports.defaultFileLevels = [["*", "trace"]]; +parse_log_levels = function(methods) { + var item, maxTitleSize, method, pos, ret, title, titlesSeen, _i, _len, _len2; + if (!_.isArray(methods)) { + throw "methods is defined, but is not an array"; + } + titlesSeen = {}; + maxTitleSize = 0; + ret = []; + for (pos = 0, _len = methods.length; pos < _len; pos++) { + method = methods[pos]; + if (!method.title) { + throw "title not found for the " + pos + " method"; + } + title = method.title.toUpperCase(); + maxTitleSize = Math.max(maxTitleSize, title.length); + if (titlesSeen[title]) { + throw "title already found for '" + title + "'. Must be unique values when taken 'toUpperCase'"; + } + titlesSeen[title] = true; + ret.push({ + title: title, + rawTitle: method.title, + pos: pos, + ansiColor: color.ansi_str({ + font: method.color, + background: method.background, + attr: method.attr + }) + }); + } + for (_i = 0, _len2 = ret.length; _i < _len2; _i++) { + item = ret[_i]; + while (item.title.length < maxTitleSize) { + item.title += ' '; + } + } + return ret; +}; +log_levels_to_map_of_color = function(logLevels) { + var logLevel, ret, _i, _len; + ret = {}; + for (_i = 0, _len = logLevels.length; _i < _len; _i++) { + logLevel = logLevels[_i]; + ret[logLevel.title] = logLevel.ansiColor; + } + return ret; +}; +log_levels_to_raw_title_arr = function(logLevels) { + return _.pluck(logLevels, "rawTitle"); +}; +parse_file_levels = function(fileLevels, logLevels) { + var logLevelNameArr, logPos, pos, ret, row, rowFile, rowLevel, rowLevels, rowMap, _i, _j, _len, _len2, _len3; + if (!_.isArray(fileLevels)) { + throw "fileLevels is defined, but is not an array"; + } + logLevelNameArr = log_levels_to_raw_title_arr(logLevels); + ret = {}; + for (pos = 0, _len = fileLevels.length; pos < _len; pos++) { + row = fileLevels[pos]; + if (!(_.isArray(row) && row.length === 2)) { + throw "level in row " + pos + " is defined, but is not an array of length 2"; + } + rowFile = row[0]; + rowLevels = row[1]; + if (_.isString(rowLevels)) { + logPos = logLevelNameArr.indexOf(rowLevels); + if (logPos === -1) { + throw "level in row '" + pos + "' is not a defined log level"; + } + rowLevels = logLevelNameArr.slice(logPos); + } else { + if (!_.isArray(rowLevels)) { + throw "level in row '" + pos + "' is not a string or an array"; + } + if (_.uniq(rowLevels).length !== rowLevels.length) { + throw "log levels in row '" + pos + "' are not unique"; + } + for (_i = 0, _len2 = rowLevels.length; _i < _len2; _i++) { + rowLevel = rowLevels[_i]; + if (!_.include(logLevelNameArr, rowLevel)) { + throw "log level called '" + rowLevel + "' in row '" + pos + "' is not a valid log level"; + } + } + } + rowMap = {}; + for (_j = 0, _len3 = rowLevels.length; _j < _len3; _j++) { + rowLevel = rowLevels[_j]; + rowMap[rowLevel] = true; + } + ret[rowFile] = rowMap; + } + return ret; +}; +exports.parse = function(_arg) { + var fileLevels, logLevels, parsedFileLevels, parsedLogLevels; + logLevels = _arg.logLevels, fileLevels = _arg.fileLevels; + if (logLevels != null) { + logLevels; + } else { + logLevels = exports.defaultLogLevels; + }; + if (fileLevels != null) { + fileLevels; + } else { + fileLevels = exports.defaultFileLevels; + }; + parsedLogLevels = parse_log_levels(logLevels); + parsedFileLevels = parse_file_levels(fileLevels, parsedLogLevels); + return { + logLevels: logLevels, + fileLevels: fileLevels, + parsedLogLevels: parsedLogLevels, + colorMap: log_levels_to_map_of_color(parsedLogLevels), + fileLevelMap: parsedFileLevels + }; +}; \ No newline at end of file diff --git a/lib/helper.js b/lib/helper.js new file mode 100644 index 0000000..caf6270 --- /dev/null +++ b/lib/helper.js @@ -0,0 +1,203 @@ +var arg_by_keys_wrapper, do_but_ignore_req_at_first, is_express_req_obj, item_by_keys, req_item_by_keys, util, _; +_ = require('underscore'); +util = require('util'); +is_express_req_obj = function(obj) { + obj || (obj = {}); + return _.has(obj, "route") && _.has(obj, "res") && _.has(obj, "next"); +}; +item_by_keys = function(obj, keyArr) { + var item, key, _i, _len; + item = obj; + for (_i = 0, _len = keyArr.length; _i < _len; _i++) { + key = keyArr[_i]; + if (!_.has(item, key)) { + return null; + } + item = item[key]; + } + return item.toString(); +}; +arg_by_keys_wrapper = function(keyArr) { + if (!keyArr) { + throw "array is not supplied"; + } + return function(mod) { + return function(level, args) { + return item_by_keys(args, keyArr); + }; + }; +}; +req_item_by_keys = function(keyArr) { + return function(mod) { + return function(level, args) { + var reqObj; + reqObj = args[0] || {}; + if (!is_express_req_obj(reqObj)) { + return null; + } + return item_by_keys(reqObj, keyArr); + }; + }; +}; +exports.iso_date = function(opts) { + if (opts == null) { + opts = {}; + } + return function(mod) { + return function(level, args) { + return (new Date()).toISOString(); + }; + }; +}; +exports.level = function(opts) { + if (opts == null) { + opts = {}; + } + return function(mod) { + return function(level, args, realTitle) { + return realTitle; + }; + }; +}; +exports.session_user_id = function(defArr) { + if (defArr == null) { + defArr = ["session", "user", "id"]; + } + return req_item_by_keys(defArr); +}; +exports.session_user_email = function(defArr) { + if (defArr == null) { + defArr = ["session", "user", "email"]; + } + return req_item_by_keys(defArr); +}; +exports.line_number = function(opts) { + if (opts == null) { + opts = {}; + } + return function(mod) { + return function(level, args) { + try { + throw new Error(); + } catch (e) { + return e.stack.split('\n')[3].split(':')[1]; + } + }; + }; +}; +exports.module_title = function(opts) { + if (opts == null) { + opts = {}; + } + return function(mod) { + return _.once(function(level, args, realTitle, moduleTitle) { + if (!mod) { + return ''; + } + if (!mod.id) { + return mod; + } + if (mod.id === '.') { + return 'main'; + } else { + return mod.id.replace(process.env.PWD, "").replace(/.js$/, "").replace(/^\//, ""); + } + }); + }; +}; +exports.file_and_line = function(opts) { + if (opts == null) { + opts = {}; + } + return function(mod) { + var line_fn, mod_title_fn; + line_fn = exports.line_number(opts)(mod); + mod_title_fn = exports.module_title(opts)(mod); + return function(level, args) { + return mod_title_fn(level, args) + ":" + line_fn(level, args); + }; + }; +}; +exports.single_line_message = function(opts) { + var depth, seperator, showHidden; + if (opts == null) { + opts = {}; + } + showHidden = opts.showHidden; + depth = opts.depth; + seperator = opts.seperator || ""; + return function(mod) { + return function(level, args) { + var item, ret, _i, _len, _ref; + ret = []; + _ref = args || []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + item = _ref[_i]; + if (_.isString(item)) { + ret.push(item); + } else { + ret.push(util.inspect(item, showHidden, depth)); + } + } + ret = ret.join(seperator); + ret = ret.replace(/[^\\]\\n/g, ''); + return ret; + }; + }; +}; +exports.message = function(opts) { + var depth, seperator, showHidden; + if (opts == null) { + opts = {}; + } + showHidden = opts.showHidden; + depth = opts.depth; + seperator = opts.seperator || ""; + return function(mod) { + return function(level, args) { + var item, ret, _i, _len, _ref; + ret = []; + _ref = args || []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + item = _ref[_i]; + if (_.isString(item)) { + ret.push(item); + } else { + ret.push(util.inspect(item, showHidden, depth)); + } + } + ret = ret.join(seperator); + return ret; + }; + }; +}; +do_but_ignore_req_at_first = function(fn) { + return function(opts) { + var fn_after_opts; + if (opts == null) { + opts = {}; + } + fn_after_opts = fn(opts); + return function(mod) { + var fn_after_mod; + fn_after_mod = fn_after_opts(mod); + return function(level, args) { + args || (args = []); + if (is_express_req_obj(args[0] || null)) { + return fn_after_mod(level, args.slice(1)); + } else { + return fn_after_mod(level, args); + } + }; + }; + }; +}; +exports.single_line_message_ignore_express_req_at_first = do_but_ignore_req_at_first(exports.single_line_message); +exports.message_ignore_express_req_at_first = do_but_ignore_req_at_first(exports.message); +exports.string = function(x) { + return _.once(function(mod) { + return _.once(function(level, args) { + return x; + }); + }); +}; \ No newline at end of file diff --git a/lib/reqLog.js b/lib/reqLog.js new file mode 100644 index 0000000..b57a417 --- /dev/null +++ b/lib/reqLog.js @@ -0,0 +1,179 @@ +var add_levels_to_status, color, config, fs, helper, status, sys, _, _ref; +var __slice = Array.prototype.slice; +sys = require('sys'); +fs = require('fs'); +_ = require('underscore'); +config = require('./config'); +color = require('./color'); +helper = require('./helper'); +exports.helper = helper; +exports._status = status = { + logger: sys.puts, + loggerContext: sys, + file: null, + color: ((_ref = process.env.TERM) != null ? _ref.indexOf('color') : void 0) >= 0, + seperator: " ", + fnArr: [ + helper.iso_date(), helper.session_user_email(), helper.level(), helper.file_and_line(), helper.string("-"), helper.message_ignore_express_req_at_first({ + seperator: " " + }) + ] +}; +add_levels_to_status = function(_arg) { + var fileLevels, logLevels, parsedDefaults, _ref2; + _ref2 = _arg != null ? _arg : {}, logLevels = _ref2.logLevels, fileLevels = _ref2.fileLevels; + parsedDefaults = config.parse({ + logLevels: logLevels, + fileLevels: fileLevels + }); + status.logLevels = parsedDefaults.logLevels; + status.parsedLogLevels = parsedDefaults.parsedLogLevels; + status.fileLevels = parsedDefaults.fileLevels; + status.colorMap = parsedDefaults.colorMap; + status.fileLevelMap = parsedDefaults.fileLevelMap; + status.defaultLogLevel = _.first(status.logLevels).title; +}; +add_levels_to_status(); +exports.options = function(opts) { + var fileData, fileDataString, fnPos, item, _len, _ref2; + if (_.has(opts, "logger") || _.has(opts, "loggerContext")) { + if (!_.has(opts, "logger")) { + throw "'logger' not supplied. Must be a function to match the 'loggerContext' (which may have a value of null)"; + } + if (!_.has(opts, "loggerContext")) { + throw "'loggerContext' not supplied. A logger context must be supplied to avoid errors. Ex. reqLog.opts({logger: console.log, loggerContext: console})"; + } + status.logger = opts.logger; + status.loggerContext = opts.loggerContext; + } + if (_.has(opts, "color")) { + status.color = opts.color; + } + if (_.has(opts, "seperator")) { + status.seperator = opts.seperator; + } + if (_.has(opts, "file")) { + status.file = opts.file; + fileDataString = fs.readFileSync(opts.file); + fileData = JSON.parse(fileDataString); + status.logLevels = fileData.logLevels; + status.fileLevels = fileData.fileLevels; + } else { + if (_.has(opts, "logLevels")) { + status.logLevels = opts.logLevels; + } + if (_.has(opts, "fileLevels")) { + status.fileLevels = opts.fileLevels; + } + } + if (_.has(opts, "fnArr")) { + status.fnArr = opts.fnArr; + _ref2 = status.fnArr; + for (fnPos = 0, _len = _ref2.length; fnPos < _len; fnPos++) { + item = _ref2[fnPos]; + if (_.isString(item)) { + (function(item) { + return status.fnArr[fnPos] = _.once(function() { + return _.once(function() { + return item; + }); + }); + })(item); + } else if (_.isFunction(item)) { + null; + } else { + throw "function at position '" + fnPos + "' is not a string that matches a helper method name or a function defined by you"; + } + } + } + add_levels_to_status({ + logLevels: status.logLevels, + fileLevels: status.fileLevels + }); + return exports; +}; +exports["with"] = function(mod) { + var has_level, levelInfo, logFn, loggerContext, moduleLevelMap, moduleTitle, ret, seperatorItem, _fn, _i, _len, _ref2; + moduleTitle = helper.module_title({})(mod)(); + moduleLevelMap = status.fileLevelMap[moduleTitle] || status.fileLevelMap['*']; + if (moduleLevelMap == null) { + throw "No levels found for module: '" + moduleTitle + "'. Please add the default file level of '*' or one that matches the module name."; + } + has_level = function(lvl) { + return !(!moduleLevelMap[lvl]); + }; + logFn = status.logger; + loggerContext = status.logger; + seperatorItem = status.seperator; + ret = { + _inColor: status.color, + _title: moduleTitle, + _logLevels: status.logLevels, + _moduleLevels: _.keys(moduleLevelMap), + _has_level: has_level, + _seperator: seperatorItem, + _logFn: logFn, + _loggerContext: loggerContext + }; + _ref2 = status.parsedLogLevels; + _fn = function(levelInfo) { + var ansiColorEnd, ansiColorStart, fn, level, levelFns; + level = levelInfo.rawTitle; + if (!_.isFunction(logFn)) { + ret[level] = _.identity; + return; + } + if (!moduleLevelMap[level]) { + ret[level] = _.identity; + return; + } + levelFns = (function() { + var _j, _len2, _ref3, _results; + _ref3 = status.fnArr; + _results = []; + for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) { + fn = _ref3[_j]; + _results.push(fn(mod)); + } + return _results; + })(); + if (status.color) { + ansiColorStart = levelInfo.ansiColor; + ansiColorEnd = color.ANSI_END; + return ret[level] = function() { + var args, fn, fnOutput, outputs, str, _j, _len2; + args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + outputs = []; + for (_j = 0, _len2 = levelFns.length; _j < _len2; _j++) { + fn = levelFns[_j]; + fnOutput = fn(level, args, levelInfo.title, moduleTitle); + if (fnOutput != null) { + outputs.push(fnOutput); + } + } + str = ansiColorStart + outputs.join(seperatorItem) + ansiColorEnd; + logFn.call(loggerContext, str); + }; + } else { + return ret[level] = function() { + var args, fn, fnOutput, joinedOutput, outputs, _j, _len2; + args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + outputs = []; + for (_j = 0, _len2 = levelFns.length; _j < _len2; _j++) { + fn = levelFns[_j]; + fnOutput = fn(level, args); + if (fnOutput != null) { + outputs.push(fnOutput); + } + } + joinedOutput = outputs.join(seperatorItem); + logFn.call(loggerContext, joinedOutput); + }; + } + }; + for (_i = 0, _len = _ref2.length; _i < _len; _i++) { + levelInfo = _ref2[_i]; + _fn(levelInfo); + } + return ret; +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c12f7f1 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name" : "reqLog", + "version" : "0.0.1", + "description": "Logging module that allows for custom levels and custom output support", + "author" : "Barret Schloerke", + "homepage" : "https://github.com/schloerke/reqLog", + "keywords" : ["log", "logging", "logger", "custom", "color", "request"], + "main" : "index", + "repository" : "git://github.com/schloerke/reqLog.git", + "dependencies": { + "underscore": "1.3.1" + }, + "devDependencies": { + "coffee": ">= 1.1.1" + }, + "contributors": [ + { "name": "Barret Schloerke", "email": "schloerke+github@gmail.com" } + ] +} \ No newline at end of file