Axe
:axe: Logger-agnostic wrapper that normalizes logs regardless of arg style. Great for large dev teams, old/new projects, and works w/Pino, Bunyan, Winston, console, and more. It is lightweight, performant, highly-configurable, and automatically adds OS, CPU, and Git information to your logs. Hooks, dot-notation remap, omit, and pick of metadata.
Install / Use
/learn @cabinjs/AxeREADME
Axe
Axe is a logger-agnostic wrapper that normalizes logs regardless of argument style. Great for large development teams, old and new projects, and works with Pino, Bunyan, Winston, console, and more. It is lightweight, performant, highly-configurable, and automatically adds OS, CPU, and Git information to your logs. It supports hooks (useful for masking sensitive data) and dot-notation remapping, omitting, and picking of log metadata properties. Made for [Forward Email][forward-email], [Lad][], and [Cabin][].
Table of Contents
Foreword
Axe was built to provide consistency among development teams when it comes to logging. You not only have to worry about your development team using the same approach to writing logs and debugging applications, but you also have to consider that open-source maintainers implement logging differently in their packages.
There is no industry standard as to logging style, and developers mix and match arguments without consistency. For example, one developer may use the approach of console.log('someVariable', someVariable) and another developer will simply write console.log(someVariable). Even if both developers wrote in the style of console.log('someVariable', someVariable), there still could be an underlying third-party package that logs differently, or uses an entirely different approach. Furthermore, by default there is no consistency of logs with stdout or using any third-party hosted logging dashboard solution. It will also be almost impossible to spot logging outliers as it would be too time intensive.
No matter how your team or underlying packages style arguments when invoked with logger methods, Axe will clean it up and normalize it for you. This is especially helpful as you can see outliers much more easily in your logging dashboards, and pinpoint where in your application you need to do a better job of logging at. Axe makes your logs consistent and organized.
Axe is highly configurable and has built-in functionality to remap, omit, and pick metadata fields with dot-notation support. Instead of using slow functions like lodash's omit, we use a more performant approach.
Axe adheres to the [Log4j][log4j] log levels, which have been established for 21+ years (since 2001). This means that you can use any custom logger (or the default console), but we strictly support the following log levels:
tracedebuginfowarnerrorfatal
Axe normalizes invocation of logger methods to be called with only two arguments: a String or Error as the first argument and an Object as the second argument. These two arguments are referred to as "message" and "meta" respectively. For example, if you're simply logging a message and some other information:
logger.info('Hello world', { beep: 'boop', foo: true });
// Hello world { beep: 'boop', foo: true }
Or if you're logging a user, or a variable in general:
logger.info('user', { user: { id: '1' } });
// user { user: { id: '1' } }
logger.info('someVariable', { someVariable: true });
// someVariable { someVariable: true }
You might write logs with three arguments (level, message, meta) using the log method of Axe's returned logger instance:
logger.log('info', 'Hello world', { beep: 'boop', foo: true });
// Hello world { beep: 'boop', foo: true }
Logging errors is just the same as you might do now:
logger.error(new Error('Oops!'));
// Error: Oops!
// at REPL3:1:14
// at Script.runInThisContext (node:vm:129:12)
// at REPLServer.defaultEval (node:repl:566:29)
// at bound (node:domain:421:15)
// at REPLServer.runBound [as eval] (node:domain:432:12)
// at REPLServer.onLine (node:repl:893:10)
// at REPLServer.emit (node:events:539:35)
// at REPLServer.emit (node:domain:475:12)
// at REPLServer.Interface._onLine (node:readline:487:10)
// at REPLServer.Interface._line (node:readline:864:8)
You might log errors like this:
logger.error(new Error('Oops!'), new Error('Another Error!'));
// Error: Oops!
// at Object.<anonymous> (/Users/user/Projects/axe/test.js:5:14)
// at Module._compile (node:internal/modules/cjs/loader:1105:14)
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
// at Module.load (node:internal/modules/cjs/loader:981:32)
// at Function.Module._load (node:internal/modules/cjs/loader:822:12)
// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
// at node:internal/main/run_main_module:17:47
//
// Error: Another Error!
// at Object.<anonymous> (/Users/user/Projects/axe/test.js:5:34)
// at Module._compile (node:internal/modules/cjs/loader:1105:14)
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
// at Module.load (node:internal/modules/cjs/loader:981:32)
// at Function.Module._load (node:internal/modules/cjs/loader:822:12)
// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
// at node:internal/main/run_main_module:17:47
Or even multiple errors:
logger.error(new Error('Oops!'), new Error('Another Error!'), new Error('Woah!'));
// Error: Oops!
// at Object.<anonymous> (/Users/user/Projects/axe/test.js:6:3)
// at Module._compile (node:internal/modules/cjs/loader:1105:14)
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
// at Module.load (node:internal/modules/cjs/loader:981:32)
// at Function.Module._load (node:internal/modules/cjs/loader:822:12)
// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
// at node:internal/main/run_main_module:17:47
//
// Error: Another Error!
// at Object.<anonymous> (/Users/user/Projects/axe/test.js:7:3)
// at Module._compile (node:internal/modules/cjs/loader:1105:14)
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
// at Module.load (node:internal/modules/cjs/loader:981:32)
// at Function.Module._load (node:internal/modules/cjs/loader:822:12)
// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
// at node:internal/main/run_main_module:17:47
//
// Error: Woah!
// at Object.<anonymous> (/Users/user/Projects/axe/test.js:8:3)
// at Module._compile (node:internal/modules/cjs/loader:1105:14)
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
// at Module.load (node:internal/modules/cjs/loader:981:32)
// at Function.Module._load (node:internal/modules/cjs/loader:822:12)
// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
// at node:internal/main/run_main_module:17:47
As you can see, Axe combines multiple errors into one – for an easy to read stack trace.
If you simply use logger.log, then the log level used will be info, but it will still use the logger's native log method (as opposed to using info). If you invoke logger.log (or any other logging method, e.g. logger.info, logger.warn, or logger.error), then it will consistently invoke the internal logger with these two arguments.
logger.log('hello world');
// hello world
logger.info('hello world');
// hello world
logger.warn('uh oh!', { amount_spent: 50 });
// uh oh! { amount_spent: 50 }
As you can see - this is exactly what you'd want your logger output to look like. Axe doesn't change anything out of the ordinary. Now here is where Axe is handy - it will automatically normalize argument style for you:
logger.warn({ hello: 'world' }, 'uh oh');
// uh oh { hello: 'world' }
logger.warn('uh oh', 'foo bar', 'beep boop');
// uh oh foo bar beep boop
logger.warn('hello', new Error('uh oh!'));
// Error: uh oh!
// at Object.<anonymous> (/Users/user/Projects/axe/test.js:5:22)
// at Module._compile (node:internal/modules/cjs/loader:1105:14)
// at Object.Module._extensions..js (node:internal/modules/
