Logule is a pretty, but heavily configurable logging utility for nodejs. It allows multiple transports as well as being configurable per user, per app and per module via localized config files.
Logging is a simple yet deceptively hairy problem. You don't want foreign modules to spam your app with needless messages, but you also want them to say something if you are passing down invalid arguments. You want logs to look pretty, but you also want a consistent style.
What you really want, is not simply configurability, but a hierarchy of configurability and suppression. You want to be able to:
- Mute specific log levels globablly
- Mute non-fatal info from certain branches of code
- Mute chatty modules
- Unmute new/experimental modules during development
as well as being able to configure how your:
- module logs by default
- app logs by default (perhaps overriding individual module defaults)
- apps log by default (by providing sensible overall configs)
Manipulating these settings should be super easy, as it's most useful during development and debug sessions where time is of the essence.
Finally, you should be able to get trace/debug messages from a module that's not behaving correctly, without spamming people not using logule!
Logule strives to adhere these goals and beyond that has since 1.0 maintained a stable API. Features so far has been greatly improved via issues/pull requests contributions, so please follow this path if there is anything you feel deserves attention.
- Basic Usage
- Namespaces
- Subs
- Logging Methods
- Mute API
- Configuration
- Branch Based Filtration
- Colors
- npm Usage
- Installation
- Running Tests
- License
File scope usage; pass down the module
object at the start.
var log = require('logule').init(module);
log.error("this is an error message")
log.warn("<- heed this")
log.info("info %s %d %j", "message", 1, {a: 2})
log.debug("this message has to be turned on");
To add a namespace to this module, add a second parameter to init()
.
log = require('logule').init(module, 'BUILD');
log.trace("Trying to compile client.js");
log.error("Failed");
Namespaces inherit from the call tree in order of registration. If your entry point required the 'BUILD' module, and this has a namespace, then 'BUILD' becomes the next namespace in the chain. When printing to stdout
there's a default limit of nesting printed out of 3.
In this case, the entry point that required 'BUILD' had no namespace. See Filtering Branches for a more detailed example of how namespaces nest.
Subs are copies of logger instances that do not save settings to the call tree. This allows you to have muted/unmuted logger instances inside a particular file without inadvertently muting or unmuting levels (resp.) from dependent modules.
var log = require('logule').init(module, 'myFile');
var spammy = log.sub('spammy').unmute('trace');
// pass spammy logger to external/internal code
A log.sub()
will maintain the the original namespaces and mute settings of log
as well as config suppressions (so unmute does not override config suppress
). It can also optionally append one extra namespace to the ones existing, in this case, 'external' will be appended.
See Filtering Branches for a more detailed example of how subs are used.
There are 7 log levels (and one method getter) available available on log
:
For debug info to help out during bad times. Suppressed by default, safe to leave in.
For temporary debug messages that you sprinkle liberally throughout your nasty functions. Prepends filename.js:lineNum to the message. Note these fetch new stacks at each call and are not meant to be used in production. They are always shown in the default config, so don't leave these in code.
var log = require('logule').init(module, 'broken');
log.debug('dumping lines to console');
log.line();
log.line();
For info messages that are too common to see all the time. Suppressed by default, safe to leave in.
For messages that show events/standard progressions. These will be shown by default in any module you embed logule in when installed on a new box (i.e. without a custom config). Ensure you're not talking when you should not need to - consider `trace.
For warnings. Shown by default.
For errors. Shown by default in bold. Not meant to handle Error objects, just the logging please.
H̸̡̪̯ͨ͊̽̅̾̎Ȩ̬̩̾͛ͪ̈́̀́͘ ̶̧̨̱̹̭̯ͧ̾ͬC̷̙̲̝͖ͭ̏ͥͮ͟Oͮ͏̮̪̝͍M̲̖͊̒ͪͩͬ̚̚͜Ȇ̴̟̟͙̞ͩ͌͝S̨̥̫͎̭ͯ̿̔̀ͅ
log.zalgo('core melting')
Disclaimer; this is mostly for fun. Find your own use case or suppress it. Note that zalgolization is only applied to stdout, not to the filestream
or emitter
transports.
Levels can be retrieved as single functions that do not chain by using this method.
Useful if you have a debug module that only needs log.debug
, so you can pass down a perhaps namespaced function to it.
var dbg = log.get('debug');
dbg("works like log.debug - but this function does not chain and expose other methods");
Note that get()
result obeys the mute API and config suppression entirely like the individual level methods.
Warning: while you simulate this with log.debug.bind(log)
, this would chain and allow modules to break out of the constricted environment.
These methods are shortcuts for modifying the private list of muted levels.
Muting affects by default stdout
and filestream
, but the config allows changing this to any combination of transports being affected.
Suppress logs for passed in methods.
log.mute('warn', 'info');
log.info('muted').warn('muted').error('works');
Unmutes logs for passed in methods.
log.mute('warn');
var l2 = log.sub('forModuleX').unmute('warn', 'info');
log.warn('muted');
l2.warn('works!');
NB: unmute does not override config suppression. It has lower precedence.
A convenience for muting all levels passed in, and unmuting all others.
log.muteOnly('debug', 'trace'); // unmutes everything except trace & debug
log.muteOnly(); // muteOnly nothing === unmute everything
A convenience for unmuting all levels passed in, and muting the others.
log.unmuteOnly('error'); // mutes everything except error
log.unmuteOnly(); // unmuteOnly nothing === mute everything
Configuration of colors, style, date formatting, transports, setting mutability and global suppression levels are done via config files. The default configuration file results in output looking like the images herein.
Configs are located via the confortable module. This module performs priority based config searches. In particular, it is used here with the following path priorities:
-
- execution directory
- 2a). if (
execDir
outside$HOME
)$HOME
- 2b). if (
execDir
inside$HOME
) Up to and including$HOME
in..
increments -
- directory of
module.parent
- directory of
Step 3 enables modules to bundle their own default config which can be overriden by apps by utilizing step 2.
The found config file is merged one level deep with the default config, so you don't have to include more in your config than what you disagree with.
Logule supports 3 transports: stdout
, filestream
and emitter
.
These all have similar options in the config, but by default only stdout
is enabled.
NB: Logule does not strip colors. If you log pre-colored strings, those colors will show up in the other transports as well!
All transports have the following options:
suppress
- list of levels to globally suppress from the transportmutable
- a boolean to indicate whether the mute API affects the transporttimestamp
- an object/string to indicate timesta
All logs are by default written directly to process.stdout
If emitter
has an enabled
attribute set to true
, logule will expose an EventEmitter
instance on require('logule').stream
.
Then you can listen to "log"
events:
var logule = require('logule');
var e = logule.emitter;
var log = logule.init(module);
e.on('log', function (obj) {
// plain replacement logging
console.log(obj.time.valueOf() + ' - ' + obj.level + ' - ' + obj.message);
});
The types of the keys in obj
are as follows:
{
time : Date,
level: String,
namespaces : [String],
message : String
}
In the case of level being 'line'
, a line
key is also available with the short location identifier string line()
typically outputs.
If filestream
has a file
attribute filled in (with a cwd relative path), this file will be appended to with JSON log messages (one message per line - so JSON.parse forEach line in file.split('\n') will work).
By default the individual JSON messages use the current format:
{
"time": "2012-11-08T11:08:25.092Z",
"level": "error",
"namespaces": ["build"],
"message": "message part, without stdout formatting"
}
Where the time
value may differ depending on the timestamp
config option.
NB: A "line"
key is present if level is "line"
like in the EventEmitter.
The first four blocks in the default config describes the default style set used by the stdout
transport and are all of the form levelName : fn
where fn
is any function in the module dye. Functions can even be composed by delimiting them with a dot; e.g. bold.blue
.
The delimiters
object contains the subtle default styling of the delimiters joining the optional timestamp, the log level, the optional namespaces and the message.
The levels
object contain the styling for the log levels. By default we only apply bold
to the critical messages.
The messages
object contain styling fot the messages (i.e. the result of util.format(args..)
). By default only zalgo gets message level formatting.
The colors
object contain the misc. styling used:
namespace
- the default blue namespacestimestamp
- the default grey optional timestampcallsite
- the default green file.js:line prefix added by .line()
NB: Levels already listed in the delimiters
, levels
or messages
objects, can be disabled by overriding them in your personal/module's config with the value of "none"
.
Timestamps can be configured via the "timestamp" key in the config which is overloaded as follows:
"none"
- nothing prepended; log output starts at type, e.g. theINFO
partprecision
- prepends HH:MM:SS:MSS + delimiter via above + paddedgetMilliseconds
dateMethod
- prepends the result of any custom method onDate.prototype
object
- full customization mode
Example of using the custom mode:
"timestamp": {
"date" : true,
"reverse" : false,
"delimiter" : "-",
"precise" : false
}
This prepends the date, in the form YYYY-MM-DD (i.e. normal non-reversed european style), and adds the timestamp after the date without precision (i.e. just toLocaleTimeString
).
Most sensible methods on Date.prototype
are:
toJSON
- default for filevalueOf
toLocaleTimeString
- default for stdouttoGMTString
Note that the first two can be perfectly serialized/deserialized with Date.parse
and are thusly a good format for the filestream JSON transport.
Set the suppress
flag to globally turn all listed log methods into chaining no-ops.
Alternatively list the exceptions under allow
instead if you like to suppress most levels.
See the Branch based filtration section for more granular control.
By default, trace
and debug
messages are suppressed.
Controlling global levels is done via config files, but the levels not globally suppressed therein can temporarily muted/unmuted at any branch point and these settings will propagate down the call tree.
NB: The following techniques require your disired transport(s) to be mutable
in the config.
To get the most out of call tree filtration consider the following example of an application structure:
a.js
└──┬b.js
└───c.js
When just using mute/unmute on an instance returned directly by init()
logule will remember the call tree and apply the same rules to the ones further down the tree by default:
// a.js
var l = require('logule').init(module, 'app').mute('info');
var b = require('./b');
// b.js
var l = require('logule').init(module);
var c = require('./c');
l.info('muted');
l.unmute('info');
l.info('works');
// c.js
var l = require('logule').init(module, 'leaf');
l.info('works');
With the following code, a.js
sets the an app default of no info messages, which is overridden by b.js
, so the unmute propagates to c.js
. Note that the app
namespace set in a.js
propagates down to both b.js
and c.js
, but c.js
will show two namespaces: app
and leaf
provided the config setting nesting >= 2
.
Note that any mute
/unmute
calls to a sub()
does not propagate to other files:
// a.js as above
// b.js
var l = require('logule').init(module).sub().unmute('info');
l.info('works');
// c.js
var l = require('logule').init(module);
l.info('still muted');
In short tree based log levels is the safe, overridable version of log levels. To enforce strict suppression of certain levels, the config file is the way to go.
Say you want to mute warnings in the file c.js
above. If you own the file, you easily just edit the first line to be:
// c.js
var l = require('logule').init(module).mute('warn', 'info');
However, if you don't own the file, perhaps it's deep down in the npm hierarchy for instance, you can propagate more simply from b.js
.
// b.js
var l = require('logule').init(module).mute('warn', 'info').sub().unmute('warn', 'info');
var c = require('./c');
l.warn('unmuted, but down the call tree it is muted');
Here we mute main logger from b.js
(the one from init
), but unmute a sub
that will be used inside this file to preserve the same behaviour inside b.js
only.
Essentially the reverse process of Muting chatty modules, there are two cases, you own the file c.js (modify imports line to mute it):
// c.js
var l = require('logule').init(module, 'leaf').unmute('info');
l.info('works');
Or if you don't own the file (so unmute above in the hierarchy):
// b.js
var l = require('logule').init(module).unmute('info').sub().mute('info');
l.info('works');
This preserves muting of b.js
, but opens up for its descendants.
The ASNI color code wrapping and zalgolizer is provided by dye, which does not introduce implicit global dependencies on String.prototype
.
Logging is a
Published libraries that log should not depend on a logging library directly
When logging with logule >=2
, the practice is to put logule
inside package.json
peerDependencies
and NOT the normal dependencies
. This ensures all modules use the same code and thus logule can encapsulate everything needed to process ALL the logs an application uses. Logule's API is stable, so simply restricting to "logule": "~2"
will suffice.
In "logule": "~1"
, bundling of separate copies per npm module was the standard and so logule then adopted the method of communicating with other copies via process.logule
to compensate for not having any one central piece of code where all logs went through. Ultimately, decisions were being made on behalf of the config so this worked well.
$ npm install logule
Install development dependencies
$ npm install
Run the tests
$ npm test
MIT-Licensed. See LICENSE file for details.