: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. 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.
Installations
npm install axe
Developer Guide
Typescript
Yes
Module System
CommonJS, UMD
Min. Node Version
>=14
Node Version
18.20.4
NPM Version
10.7.0
Releases
Contributors
Unable to fetch Contributors
Languages
JavaScript (95.7%)
HTML (2.79%)
TypeScript (1.13%)
Shell (0.39%)
Love this project? Help keep it running — sponsor us today! 🚀
Developer
Download Statistics
Total Downloads
724,442
Last Day
27
Last Week
27
Last Month
9,238
Last Year
163,809
GitHub Statistics
50 Stars
237 Commits
9 Forks
3 Watching
1 Branches
4 Contributors
Bundle Size
35.52 kB
Minified
11.23 kB
Minified + Gzipped
Package Meta Information
Latest Version
13.0.0
Package Id
axe@13.0.0
Unpacked Size
257.34 kB
Size
68.70 kB
File Count
9
NPM Version
10.7.0
Node Version
18.20.4
Publised On
26 Nov 2024
Total Downloads
Cumulative downloads
Total Downloads
724,442
Last day
0%
27
Compared to previous day
Last week
-98.9%
27
Compared to previous week
Last month
1.6%
9,238
Compared to previous month
Last year
-19.6%
163,809
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Dependencies
15
Dev Dependencies
31
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, 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 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:
trace
debug
info
warn
error
fatal
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:
1logger.info('Hello world', { beep: 'boop', foo: true }); 2// Hello world { beep: 'boop', foo: true }
Or if you're logging a user, or a variable in general:
1logger.info('user', { user: { id: '1' } }); 2// user { user: { id: '1' } }
1logger.info('someVariable', { someVariable: true }); 2// someVariable { someVariable: true }
You might write logs with three arguments (level, message, meta)
using the log
method of Axe's returned logger
instance:
1logger.log('info', 'Hello world', { beep: 'boop', foo: true }); 2// Hello world { beep: 'boop', foo: true }
Logging errors is just the same as you might do now:
1logger.error(new Error('Oops!')); 2 3// Error: Oops! 4// at REPL3:1:14 5// at Script.runInThisContext (node:vm:129:12) 6// at REPLServer.defaultEval (node:repl:566:29) 7// at bound (node:domain:421:15) 8// at REPLServer.runBound [as eval] (node:domain:432:12) 9// at REPLServer.onLine (node:repl:893:10) 10// at REPLServer.emit (node:events:539:35) 11// at REPLServer.emit (node:domain:475:12) 12// at REPLServer.Interface._onLine (node:readline:487:10) 13// at REPLServer.Interface._line (node:readline:864:8)
You might log errors like this:
1logger.error(new Error('Oops!'), new Error('Another Error!')); 2 3// Error: Oops! 4// at Object.<anonymous> (/Users/user/Projects/axe/test.js:5:14) 5// at Module._compile (node:internal/modules/cjs/loader:1105:14) 6// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10) 7// at Module.load (node:internal/modules/cjs/loader:981:32) 8// at Function.Module._load (node:internal/modules/cjs/loader:822:12) 9// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) 10// at node:internal/main/run_main_module:17:47 11// 12// Error: Another Error! 13// at Object.<anonymous> (/Users/user/Projects/axe/test.js:5:34) 14// at Module._compile (node:internal/modules/cjs/loader:1105:14) 15// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10) 16// at Module.load (node:internal/modules/cjs/loader:981:32) 17// at Function.Module._load (node:internal/modules/cjs/loader:822:12) 18// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) 19// at node:internal/main/run_main_module:17:47
Or even multiple errors:
1logger.error(new Error('Oops!'), new Error('Another Error!'), new Error('Woah!')); 2 3// Error: Oops! 4// at Object.<anonymous> (/Users/user/Projects/axe/test.js:6:3) 5// at Module._compile (node:internal/modules/cjs/loader:1105:14) 6// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10) 7// at Module.load (node:internal/modules/cjs/loader:981:32) 8// at Function.Module._load (node:internal/modules/cjs/loader:822:12) 9// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) 10// at node:internal/main/run_main_module:17:47 11// 12// Error: Another Error! 13// at Object.<anonymous> (/Users/user/Projects/axe/test.js:7:3) 14// at Module._compile (node:internal/modules/cjs/loader:1105:14) 15// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10) 16// at Module.load (node:internal/modules/cjs/loader:981:32) 17// at Function.Module._load (node:internal/modules/cjs/loader:822:12) 18// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) 19// at node:internal/main/run_main_module:17:47 20// 21// Error: Woah! 22// at Object.<anonymous> (/Users/user/Projects/axe/test.js:8:3) 23// at Module._compile (node:internal/modules/cjs/loader:1105:14) 24// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10) 25// at Module.load (node:internal/modules/cjs/loader:981:32) 26// at Function.Module._load (node:internal/modules/cjs/loader:822:12) 27// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) 28// 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.
1logger.log('hello world'); 2// hello world
1logger.info('hello world'); 2// hello world
1logger.warn('uh oh!', { amount_spent: 50 }); 2// 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:
1logger.warn({ hello: 'world' }, 'uh oh'); 2// uh oh { hello: 'world' }
1logger.warn('uh oh', 'foo bar', 'beep boop'); 2// uh oh foo bar beep boop
1logger.warn('hello', new Error('uh oh!')); 2 3// Error: uh oh! 4// at Object.<anonymous> (/Users/user/Projects/axe/test.js:5:22) 5// at Module._compile (node:internal/modules/cjs/loader:1105:14) 6// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10) 7// at Module.load (node:internal/modules/cjs/loader:981:32) 8// at Function.Module._load (node:internal/modules/cjs/loader:822:12) 9// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) 10// at node:internal/main/run_main_module:17:47
1logger.warn(new Error('uh oh!'), 'hello'); 2 3// Error: uh oh! 4// at Object.<anonymous> (/Users/user/Projects/axe/test.js:9:13) 5// at Module._compile (node:internal/modules/cjs/loader:1105:14) 6// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10) 7// at Module.load (node:internal/modules/cjs/loader:981:32) 8// at Function.Module._load (node:internal/modules/cjs/loader:822:12) 9// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) 10// at node:internal/main/run_main_module:17:47
Axe has support for format specifiers, and you can even use format specifiers in the browser (uses format-util – has limited number of format specifiers) and Node (uses the built-in util.format method – supports all format specifiers). This feature is built-in thanks to smart detection using format-specifiers.
1logger.info('favorite color is %s', 'blue'); 2// favorite color is blue
As you can see, Axe makes your logs consistent in both Node and browser environments.
Axe's goal is to allow you to log in any style, but make your log output more readable, organized, and clean.
The most impactful feature of Axe is that it makes logger output human-friendly and readable when there are multiple errors.
Normally console
output (and most other loggers) by default will output the following unreadable stack trace:
1> console.log(new Error('hello'), new Error('world')); 2Error: hello 3 at REPL6:1:13 4 at Script.runInThisContext (node:vm:129:12) 5 at REPLServer.defaultEval (node:repl:566:29) 6 at bound (node:domain:421:15) 7 at REPLServer.runBound [as eval] (node:domain:432:12) 8 at REPLServer.onLine (node:repl:893:10) 9 at REPLServer.emit (node:events:539:35) 10 at REPLServer.emit (node:domain:475:12) 11 at REPLServer.Interface._onLine (node:readline:487:10) 12 at REPLServer.Interface._line (node:readline:864:8) Error: world 13 at REPL6:1:33 14 at Script.runInThisContext (node:vm:129:12) 15 at REPLServer.defaultEval (node:repl:566:29) 16 at bound (node:domain:421:15) 17 at REPLServer.runBound [as eval] (node:domain:432:12) 18 at REPLServer.onLine (node:repl:893:10) 19 at REPLServer.emit (node:events:539:35) 20 at REPLServer.emit (node:domain:475:12) 21 at REPLServer.Interface._onLine (node:readline:487:10) 22 at REPLServer.Interface._line (node:readline:864:8)
However with Axe, errors and stack traces are much more readable (we use maybe-combine-errors under the hood):
1> logger.log(new Error('hello'), new Error('world')); 2Error: hello 3 at REPL7:1:12 4 at Script.runInThisContext (node:vm:129:12) 5 at REPLServer.defaultEval (node:repl:566:29) 6 at bound (node:domain:421:15) 7 at REPLServer.runBound [as eval] (node:domain:432:12) 8 at REPLServer.onLine (node:repl:893:10) 9 at REPLServer.emit (node:events:539:35) 10 at REPLServer.emit (node:domain:475:12) 11 at REPLServer.Interface._onLine (node:readline:487:10) 12 at REPLServer.Interface._line (node:readline:864:8) 13 14Error: world 15 at REPL7:1:32 16 at Script.runInThisContext (node:vm:129:12) 17 at REPLServer.defaultEval (node:repl:566:29) 18 at bound (node:domain:421:15) 19 at REPLServer.runBound [as eval] (node:domain:432:12) 20 at REPLServer.onLine (node:repl:893:10) 21 at REPLServer.emit (node:events:539:35) 22 at REPLServer.emit (node:domain:475:12) 23 at REPLServer.Interface._onLine (node:readline:487:10) 24 at REPLServer.Interface._line (node:readline:864:8)
Lastly, Axe works in both server-side and client-side environments (with Node and the browser).
Application Metadata and Information
If you've read the Foreword, you'll know that Axe invokes logger methods with two normalized arguments, message
(String or Error) and meta
(Object).
Axe will automatically add the following metadata and information to the meta
Object argument passed to logger methods:
Property | Type | Description |
---|---|---|
meta.level | String | The log level invoked (e.g. "info" ). |
meta.err | Object | Parsed error information using parse-err. |
meta.original_err | Object | If and only if meta.err already existed, this field is preserved as meta.original_err on the metadata object. |
meta.original_meta | Object | If and only if meta already existed as an argument and was not an Object (e.g. an Array), this field is preserved as meta.original_meta on the metadata object. |
meta.app | Object | Application information parsed using parse-app-info. This is not added in Browser environments. See below nested properties. |
meta.app.name | String | Name of the app from package.json . |
meta.app.version | String | Version of the app package.json . |
meta.app.node | String | Version if node.js running the app. |
meta.app.hash | String | The latest Git commit hash; not available when not in a Git repository or if there is no Git commit hash. |
meta.app.tag | String | The latest Git tag; not available when not in a Git repository or if there is no Git tag. |
meta.app.environment | String | The value of process.env.NODE_ENV . |
meta.app.hostname | String | Name of the computer. |
meta.app.pid | Number | Process ID as in process.pid . |
meta.app.cluster | Object | Node cluster information. |
meta.app.os | Object | Node os information. |
meta.app.worker_threads | Object | Node worker_threads information. |
As of v11.0.0 Axe will output meta.app
by default unless you pass omittedFields: [ 'app' ]
or specify the process environment variable of AXE_OMIT_META_FIELDS=app node app.js
when you start your app.
Axe will omit from metadata all properties via the default Array from meta.omittedFields
option (see Options below for more insight).
If the argument "meta" is an empty object, then it will not be passed as an argument to logger methods – because you don't want to see an empty {}
polluting your log metadata. Axe keeps your log output tidy.
1hello world { 2 level: 'info', 3 app: { 4 name: 'axe', 5 version: '10.0.0', 6 node: 'v16.15.1', 7 hash: '5ecd389b2523a8e810416f6c4e3ffa0ba6573dc2', 8 tag: 'v10.0.0', 9 environment: 'development', 10 hostname: 'users-MacBook-Air.local', 11 pid: 3477, 12 cluster: { isMaster: true, isWorker: false, schedulingPolicy: 2 }, 13 os: { 14 arch: 'arm64', 15 cpus: [Array], 16 endianness: 'LE', 17 freemem: 271433728, 18 priority: 0, 19 homedir: '/Users/user', 20 hostname: 'users-MacBook-Air.local', 21 loadavg: [Array], 22 network_interfaces: [Object], 23 platform: 'darwin', 24 release: '21.3.0', 25 tmpdir: '/var/folders/rl/gz_3j8fx4s98k2kb0hknfygm0000gn/T', 26 totalmem: 17179869184, 27 type: 'Darwin', 28 uptime: 708340, 29 user: [Object], 30 version: 'Darwin Kernel Version 21.3.0: Wed Dec 8 00:40:46 PST 2021; root:xnu-8019.80.11.111.1~1/RELEASE_ARM64_T8101' 31 }, 32 worker_threads: { 33 isMainThread: true, 34 resourceLimits: {}, 35 threadId: 0, 36 workerData: null 37 } 38 } 39}
Note that you can also combine meta.omittedFields
with meta.pickedFields
and meta.remappedFields
(in case you want to output specific properties from meta.app
and exclude others – see Options for more insight).
Install
Node
npm:
1npm install axe
Browser
See Browser usage below for more information.
Usage
Options
Property | Type | Default Value | Description | |
---|---|---|---|---|
showStack | Boolean | true | Attempts to parse a boolean value from process.env.AXE_SHOW_STACK ). If this value is true , then if message is an instance of an Error, it will be invoked as the first argument to logger methods. If this is false , then only the err.message will be invoked as the first argument to logger methods. Basically if true it will call logger.method(err) and if false it will call logger.method(err.message) . If you pass err as the first argument to a logger method, then it will show the stack trace via err.stack typically. | |
meta | Object | See below | Stores all meta config information (see the following nested properties below). | |
meta.show | Boolean | true | Attempts to parse a boolean value from process.env.AXE_SHOW_META – meaning you can pass a flag AXE_SHOW_META=true node app.js when needed for debugging), whether or not to output metadata to logger methods. If set to false , then fields will not be omitted nor picked; the entire meta object will be hidden from logger output. | |
meta.remappedFields | Object | {} | Attempts to parse an Object mapping from process.env.AXE_REMAPPED_META_FIELDS (, and : delimited, e.g. REMAPPED_META_FIELDS=foo:bar,beep.boop:beepBoop to remap meta.foo to meta.bar and meta.beep.boop to meta.beepBoop ). Note that this will clean up empty objects by default unless you set the option meta.cleanupRemapping to false ). Supports dot-notation. | |
meta.omittedFields | Array | [] | Attempts to parse an array value from process.env.AXE_OMIT_META_FIELDS (, delimited) - meaning you can pass a flag AXE_OMIT_META_FIELDS=user,id node app.js ), determining which fields to omit in the metadata passed to logger methods. Supports dot-notation. | |
meta.pickedFields | Array | [] | Attempts to parse an array value from process.env.AXE_PICK_META_FIELDS (, delimited) - meaning you can pass a flag, e.g. AXE_PICK_META_FIELDS=request.headers,response.headers node app.js which would pick from meta.request and meta.response only meta.request.headers and meta.response.headers ), This takes precedence after fields are omitted, which means this acts as a whitelist. Supports dot-notation. As of v11.2.0 this now supports Symbols, but only top-level symbols via Reflect.ownKeys (not recursive yet). | |
meta.cleanupRemapping | Boolean | true | Whether or not to cleanup empty objects after remapping operations are completed) | |
meta.hideHTTP | Boolean | true | Whether to suppress HTTP metadata (prevents logger invocation with second arg meta ) if meta.is_http is true (via parse-request v5.1.0+). If you manually set meta.is_http = true and this is true , then meta arg will be suppressed as well. | |
meta.hideMeta | String or Boolean | "hide_meta" | If this value is provided as a String, then if meta[config.hideMeta] is true , it will suppress the entire metadata object meta (the second arg) from being passed/invoked to the logger. This is useful when you want to suppress metadata from the logger invocation, but still persist it to post hooks (e.g. for sending upstream to your log storage provider). This helps to keep development and production console output clean while also allowing you to still store the meta object. | |
silent | Boolean | false | Whether or not to invoke logger methods. Pre and post hooks will still run even if this option is set to false . | |
logger | Object | console | Defaults to console with console-polyfill added automatically, though you can bring your own logger. See custom logger – you can pass an instance of pino , signale , winston , bunyan , etc. | |
name | String or Boolean | false if NODE_ENV is "development" otherwise the value of process.env.HOSTNAME or os.hostname() | The default name for the logger (defaults to false in development environments, which does not set logger.name ) – this is useful if you are using a logger like pino which prefixes log output with the name set here. | |
level | String | "info" | The default level of logging to invoke logger methods for (defaults to info , which includes all logs including info and higher in severity (e.g. info , warn , error , fatal ) | |
levels | Array | ['info','warn','error','fatal'] | An Array of logging levels to support. You usually shouldn't change this unless you want to prevent logger methods from being invoked or prevent hooks from being run for a certain log level. If an invalid log level is attempted to be invoked, and if it is not in this Array, then no hooks and no logger methods will be invoked. | |
appInfo | Boolean | true | Attempts to parse a boolean value from process.env.AXE_APP_INFO ) - whether or not to parse application information (using parse-app-info). |
Suppress Console Output and Logger Invocation
If you wish to suppress console output (e.g. prevent logger invocation) for a specific log – you can do so by setting a special property to have a true
value in the meta object.
This special property is a Symbol via Symbol.for('axe.silent')
. Using a Symbol will prevent logs from being suppressed inadvertently (e.g. if a meta object contained is_silent: true
however you did not explicitly set is_silent: true
, as it might have been the result of another package or Object parsed.
To do so, simply declare const silentSymbol = Symbol.for('axe.silent')
and then use it as follows:
1const Axe = require('axe'); 2 3const silentSymbol = Symbol.for('axe.silent'); 4 5const logger = new Axe(); 6 7logger.info('hello world'); // <--- outputs to console "hello world" 8logger.info('hello world', { [silentSymbol]: true }); // <--- **does not output to console**
Pre and post hooks will still run whether this is set to true or not – this is simply only for logger method invocation.
Another common use case for this is to suppress console output for HTTP asset requests that are successful.
Note that the example provided below assumes you are using Cabin's middleware which parses HTTP requests into the meta
Object properly:
1const Axe = require('axe'); 2 3const silentSymbol = Symbol.for('axe.silent'); 4 5const logger = new Axe(); 6 7const IGNORED_CONTENT_TYPES = [ 'application/javascript; charset=utf-8', 'application/manifest+json', 'font', 'image', 'text/css']; 8 9// 10// set the silent symbol in axe to true for successful asset responses 11// 12for (const level of logger.config.levels) { 13 logger.pre(level, function (err, message, meta) { 14 if ( 15 meta.is_http && 16 meta.response && 17 meta.response.status_code && 18 meta.response.status_code < 400 && 19 (meta.response.status_code === 304 || 20 (meta.response.headers && 21 meta.response.headers['content-type'] && 22 IGNORED_CONTENT_TYPES.some((c) => 23 meta.response.headers['content-type'].startsWith(c) 24 ))) 25 ) 26 meta[silentSymbol] = true; 27 return [err, message, meta]; 28 }); 29}
Supported Platforms
-
Node: v14+
-
Browsers (see .browserslistrc):
1npx browserslist
1and_chr 107 2and_ff 106 3and_qq 13.1 4and_uc 13.4 5android 107 6chrome 107 7chrome 106 8chrome 105 9edge 107 10edge 106 11edge 105 12firefox 106 13firefox 105 14firefox 102 15ios_saf 16.1 16ios_saf 16.0 17ios_saf 15.6 18ios_saf 15.5 19ios_saf 14.5-14.8 20kaios 2.5 21op_mini all 22op_mob 64 23opera 91 24opera 90 25safari 16.1 26safari 16.0 27safari 15.6 28samsung 18.0 29samsung 17.0
Node
1const Axe = require('axe'); 2 3const logger = new Axe(); 4 5logger.info('hello world');
Browser
This package requires Promise support, therefore you will need to polyfill if you are using an unsupported browser (namely Opera mini).
We no longer support IE as of Axe v10.0.0+.
VanillaJS
The browser-ready bundle is only 18 KB when minified and 6 KB when gzipped.
1<script src="https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=Promise"></script> 2<script src="https://unpkg.com/axe"></script> 3<script type="text/javascript"> 4 (function () { 5 // make a new logger instance 6 const logger = new Axe(); 7 logger.info('hello world'); 8 9 // or you can override console everywhere 10 console = new Axe(); 11 console.info('hello world'); 12 }); 13</script>
Required Browser Features
We recommend using https://cdnjs.cloudflare.com/polyfill (specifically with the bundle mentioned in VanillaJS above):
1<script src="https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=Promise"></script>
- Promise is not supported in op_mini all
Bundler
If you're using something like browserify, webpack, or rollup, then install the package as you would with Node above.
Custom logger
By default, Axe uses the built-in console
(with console-polyfill for cross-browser support).
However you might want to use something fancier, and as such we support any logger out of the box.
Loggers supported include, but are not limited to:
Just pass your custom logging utility as the
logger
option:
1const signale = require('signale'); 2const Axe = require('axe'); 3 4const logger = new Axe({ logger: signale }); 5 6logger.info('hello world');
In Lad, we have an approach similar to the following, where non-production environments use consola, and production environments use pino.
1const Axe = require('axe'); 2const consola = require('consola'); 3const pino = require('pino')({ 4 customLevels: { 5 log: 30 6 }, 7 hooks: { 8 // <https://github.com/pinojs/pino/blob/master/docs/api.md#logmethod> 9 logMethod(inputArgs, method) { 10 return method.call(this, { 11 // <https://github.com/pinojs/pino/issues/854> 12 // message: inputArgs[0], 13 msg: inputArgs[0], 14 meta: inputArgs[1] 15 }); 16 } 17 } 18}); 19 20const isProduction = process.env.NODE_ENV === 'production'; 21const logger = new Axe({ 22 logger: isProduction ? pino : consola 23}); 24 25logger.info('hello world');
Silent Logging
Silent logging is useful when you need to disable logging in certain environments for privacy reasons or to simply clean up output on stdout.
For example when you're running tests you can set logger.config.silent = true
.
1const Axe = require('axe'); 2 3const logger = new Axe({ silent: true }); 4 5logger.info('hello world');
Stack Traces and Error Handling
Please see Cabin's documentation for stack traces and error handling for more information.
If you're not using
cabin
, you can simply replace instances of the wordcabin
withaxe
in the documentation examples linked above.
Hooks
You can add synchronous "pre" hooks and/or asynchronous/synchronous "post" hooks with Axe. Both pre and post hooks accept four arguments (level
, err
, message
, and meta
). Pre hooks are required to be synchronous. Pre hooks also run before any metadata is picked, omitted, remapped, etc.
Both pre and post hooks execute serially – and while pre hooks are blocking, post-hooks will run in the background after logger methods are invoked (you can have a post hook that's a Promise or async function).
Pre hooks require an Array to be returned of [ err, message, meta ]
.
Pre hooks allow you to manipulate the arguments err
, message
, and meta
that are passed to the internal logger methods. This is useful for masking sensitive data or doing additional custom logic before writing logs.
Post hooks are useful if you want to send logging information to a third-party, store them into a database, or do any sort of custom processing.
As of v12, logger invocation (e.g. logger.info('hello')
) will return their post hooks as a Promise – which means you can await logger.error(err);
if needed – which is useful if you have jobs that need to store logs to a database using post hooks before the job shuts down with process.exit(0)
. Note that the resolved Promise returns an Array of the returned values from post hooks, executed serially via p-map-series
. If no post hooks exist, then logger invocations will return an Object consisting of { method, err, message, meta }
, whereas method
is the logger method invoked with (e.g. logger.error()
will have a method: 'error'
value).
You should properly handle any errors in your pre hooks, otherwise they will be thrown and logger methods will not be invoked.
We will catch errors for post hooks by default and log them as errors with your logger methods' logger.error
method).
Hooks can be defined in the options passed to an instance of Axe, e.g. new Axe({ hooks: { pre: [ fn ], post: [ fn ] } });
and/or with the method logger.pre(level, fn)
or logger.post(level, fn)
. Here are a few examples below:
1const Axe = require('axe'); 2 3const logger = new Axe({ 4 hooks: { 5 pre: [ 6 function (level, err, message, meta) { 7 message = message.replace(/world/gi, 'planet earth'); 8 return [err, message, meta]; 9 } 10 ] 11 } 12}); 13 14logger.info('hello world'); 15 16// hello planet earth
1const Axe = require('axe'); 2 3const logger = new Axe(); 4 5logger.pre('error', (err, message, meta) => { 6 if (err instanceof Error) err.is_beep_boop = true; 7 return [err, message, meta]; 8}); 9 10logger.error(new Error('oops')); 11 12// Error: oops 13// at Object.<anonymous> (/Users/user/Projects/axe/test.js:39:14) 14// at Module._compile (node:internal/modules/cjs/loader:1105:14) 15// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10) 16// at Module.load (node:internal/modules/cjs/loader:981:32) 17// at Function.Module._load (node:internal/modules/cjs/loader:822:12) 18// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) 19// at node:internal/main/run_main_module:17:47 { 20// is_beep_boop: true 21// }
1const fs = require('node:fs'); 2const path = require('node:path'); 3 4const Axe = require('axe'); 5 6const logger = new Axe(); 7 8logger.post('error', async (err, message, meta) { 9 // store the log 10 await fs.promises.appendFile( 11 path.join(__dirname, 'logs.txt'), 12 JSON.stringify({ err, message, meta }, null, 2) + '\n' 13 ); 14 return { method: 'error', err, message, meta }; 15}); 16 17// wait until logger stores the log 18const [ log ] = await logger.error(new Error('oops')); 19console.log(log); 20 21// will wait to store logs before exiting process 22process.exit(0);
For more examples of hooks, see our below sections on Send Logs to HTTP Endpoint, Send Logs to Slack), and Suppress Logger Data below.
Symbols
If you use Symbols (e.g. new Symbol('hello')
or Symbol.for('hello')
, and also wish to use these Symbols with remapping, picking, or omitting – then you must refer to them in dot-notation with either the Symbol.keyFor(symbol)
value or the symbol.description
value. In the case of const symbol = new Symbol('hello')
and const symbol = Symbol.for('hello')
, the dot-notation format would be simply be the key value "hello"
– however if the symbol is nested in an object such as obj[symbol]
, then the dot-notation value would be obj.hello
.
Remapping
If you would like to remap fields, such as response.headers
to responseHeaders
, then you can use environment variables or pass an object with configuration mapping.
1const logger = new Axe({ 2 meta: { 3 remappedFields: { 4 'response.headers': 'responseHeaders' 5 } 6 } 7}); 8 9logger.info('foo bar', { 10 response: { 11 headers: { 12 'X-Hello-World': true 13 } 14 } 15}); 16 17// foo bar { responseHeaders: { 'X-Hello-World': true } }
Omitting
If you would like to omit fields, such as response.headers
from a response, so you are only left with the status code:
1const logger = new Axe({ 2 meta: { 3 omittedFields: ['response.headers'] 4 } 5}); 6 7logger.info('foo bar', { 8 response: { 9 status: 200, 10 headers: { 11 'X-Hello-World': true 12 } 13 } 14}); 15 16// foo bar { response: { status: 200 } }
Picking
If you would like to pick certain fields, such as response.status
from a response:
1const logger = new Axe({ 2 meta: { 3 pickedFields: [ 'response.status' ] 4 } 5}); 6 7logger.info('foo bar', { 8 response: { 9 status: 200, 10 headers: { 11 'X-Hello-World': true 12 } 13 } 14}); 15 16// foo bar { response: { status: 200 } }
Aliases
We have provided helper/safety aliases for logger.warn
and logger.error
of logger.warning
and logger.err
respectively.
Methods
A few extra methods are available, which were inspired by Slack's logger and added for compatibility:
logger.setLevel(level)
- sets the loglevel
(String) severity to invokelogger
methods for (must be valid enumerable level)logger.getNormalizedLevel(level)
- gets the normalized loglevel
(String) severity (normalizes to known logger levels, e.g. "warning" => "warn", "err" => "error", "log" => "info")logger.setName(name)
- sets thename
(String) property (some loggers likepino
will prefix logs with the name set here)
Examples
Send Logs to HTTP Endpoint
This is an example of using hooks to send a POST request to an HTTP endpoint with logs of the "fatal" and "error" levels that occur in your application:
We recommend superagent, however there are plenty of alternatives such as axios and ky.
-
You will also need to install additional packages:
1npm install axe cuid parse-err fast-safe-stringify superagent
-
Implementation example is provided below (and you can also refer to the Forward Email code base):
1const Axe = require('axe'); 2const cuid = require('cuid'); 3const parseErr = require('parse-err'); 4const safeStringify = require('fast-safe-stringify'); 5const superagent = require('superagent'); 6 7const logger = new Axe(); 8 9// <https://github.com/cabinjs/axe/#send-logs-to-http-endpoint> 10async function hook(err, message, meta) { 11 // 12 // return early if we wish to ignore this 13 // (this prevents recursion; see end of this fn) 14 // 15 if (meta.ignore_hook) return; 16 17 try { 18 const request = superagent 19 .post(`https://api.example.com/v1/log`) 20 // if the meta object already contained a request ID then re-use it 21 // otherwise generate one that gets re-used in the API log request 22 // (which normalizes server/browser request id formatting) 23 .set( 24 'X-Request-Id', 25 meta && meta.request && meta.request.id ? meta.request.id : cuid() 26 ) 27 .set('X-Axe-Version', logger.config.version) 28 .timeout(5000); 29 30 // if your endpoint is protected by an API token 31 // note that superagent exposes `.auth()` method 32 // request.auth(API_TOKEN); 33 34 const response = await request 35 .type('application/json') 36 .retry(3) 37 .send(safeStringify({ err: parseErr(err), message, meta })); 38 39 logger.info('log sent over HTTP', { response, ignore_hook: true }); 40 } catch (err) { 41 logger.fatal(err, { ignore_hook: true }); 42 } 43} 44 45for (const level of logger.config.levels) { 46 logger.post(level, hook); 47}
Send Logs to Slack
This is an example of using hooks to send a message to Slack with logs of the "fatal" and "error" levels that occur in your application:
-
You will need to install the
@slack/web-api
package locally:1npm install @slack/web-api
-
Create and copy to your clipboard a new Slack bot token at https://my.slack.com/services/new/bot.
-
Implementation example is provided below:
Replace
INSERT-YOUR-TOKEN
with the token in your clipboard1const os = require('os'); 2 3const Axe = require('axe'); 4const { WebClient } = require('@slack/web-api'); 5 6// create our application logger that uses hooks 7const logger = new Axe({ 8 logger: console, // optional (e.g. pino, signale, consola) 9 level: 'info' // optional (defaults to info) 10}); 11 12// create an instance of the Slack Web Client API for posting messages 13const web = new WebClient('INSERT-YOUR-TOKEN', { 14 // https://slack.dev/node-slack-sdk/web-api#logging 15 logger, 16 logLevel: logger.config.level 17}); 18 19async function hook(err, message, meta) { 20 // 21 // return early if we wish to ignore this 22 // (this prevents recursion; see end of this fn) 23 // 24 if (meta.ignore_hook) return; 25 26 // otherwise post a message to the slack channel 27 try { 28 const result = await web.chat.postMessage({ 29 channel: 'monitoring', 30 username: 'Axe', 31 icon_emoji: ':axe:', 32 attachments: [ 33 { 34 title: err && err.message || message, 35 color: 'danger', 36 text: err && err.stack || message, 37 fields: [ 38 { 39 title: 'Level', 40 value: meta.level, 41 short: true 42 }, 43 { 44 title: 'Environment', 45 value: meta.app.environment, 46 short: true 47 }, 48 { 49 title: 'Hostname', 50 value: meta.app.hostname, 51 short: true 52 }, 53 { 54 title: 'Hash', 55 value: meta.app.hash, 56 short: true 57 } 58 ] 59 } 60 ] 61 }); 62 63 // finally log the result from slack 64 logger.info('slack message sent', { result }); 65 } catch (err) { 66 logger.fatal(err, { ignore_hook: true }); 67 } 68} 69 70// bind custom hooks for "fatal" and "error" log levels 71logger.post('error', hook); 72logger.post('fatal', hook); 73 74// test out the slack integration 75logger.error(new Error('Uh oh something went wrong!'));
Send Logs to Sentry
See below example and the reference at https://docs.sentry.io/platforms/node/ for more information.
1npm install @sentry/node
1const Axe = require('axe');
2const Sentry = require('@sentry/node');
3
4const logger = new Axe();
5
6Sentry.init({
7 // TODO: input your DSN here from Sentry once you're logged in at:
8 // https://docs.sentry.io/platforms/node/#configure
9 dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
10});
11
12for (const level of logger.config.levels) {
13 logger.post(level, (err, message, meta) => {
14 // https://docs.sentry.io/clients/node/usage/
15 if (err) {
16 Sentry.captureException(err, meta);
17 } else {
18 Sentry.captureMessage(message, meta);
19 }
20 });
21}
22
23// do stuff
24logger.error(new Error('uh oh'));
Send Logs to Datadog
See below example and the reference at https://docs.datadoghq.com/logs/log_collection/nodejs/?tab=winston30#agentless-logging.
Be sure to replace DATADOG_API_KEY
and DATADOG_APP_NAME
with your Datadog API key and application name.
1npm install axe cuid parse-err fast-safe-stringify superagent
1const Axe = require('axe'); 2const cuid = require('cuid'); 3const parseErr = require('parse-err'); 4const safeStringify = require('fast-safe-stringify'); 5const superagent = require('superagent'); 6 7const logger = new Axe(); 8 9// TODO: use env var or replace this const with a string 10const DATADOG_API_KEY = process.env.DATADOG_API_KEY; 11 12// TODO: use env var or replace this const with a string 13const DATADOG_APP_NAME = process.env.DATADOG_APP_NAME; 14 15// <https://github.com/cabinjs/axe/#send-logs-to-datadog> 16async function hook(err, message, meta) { 17 // 18 // return early if we wish to ignore this 19 // (this prevents recursion; see end of this fn) 20 // 21 if (meta.ignore_hook) return; 22 23 try { 24 const request = superagent 25 .post(`https://http-intake.logs.datadoghq.com/api/v2/logs?dd-api-key=${DATADOG_API_KEY}&ddsource=nodejs&service=${DATADOG_APP_NAME}`) 26 // if the meta object already contained a request ID then re-use it 27 // otherwise generate one that gets re-used in the API log request 28 // (which normalizes server/browser request id formatting) 29 .set( 30 'X-Request-Id', 31 meta && meta.request && meta.request.id ? meta.request.id : cuid() 32 ) 33 .set('X-Axe-Version', logger.config.version) 34 .timeout(5000); 35 36 const response = await request 37 .type('application/json') 38 .retry(3) 39 .send(safeStringify({ err: parseErr(err), message, meta })); 40 41 logger.info('log sent over HTTP', { response, ignore_hook: true }); 42 } catch (err) { 43 logger.fatal(err, { ignore_hook: true }); 44 } 45} 46 47for (const level of logger.config.levels) { 48 logger.post(level, hook); 49}
Send Logs to Papertrail
See below example and the reference at https://www.papertrail.com/help/configuring-centralized-logging-from-nodejs-apps/.
Be sure to replace PAPERTRAIL_TOKEN
with your Papertrail token.
1npm install axe cuid parse-err fast-safe-stringify superagent
1const Axe = require('axe'); 2const cuid = require('cuid'); 3const parseErr = require('parse-err'); 4const safeStringify = require('fast-safe-stringify'); 5const superagent = require('superagent'); 6 7const logger = new Axe(); 8 9// TODO: use env var or replace this const with a string 10const PAPERTRAIL_TOKEN = process.env.PAPERTRAIL_TOKEN; 11 12// <https://github.com/cabinjs/axe/#send-logs-to-papertrail> 13async function hook(err, message, meta) { 14 // 15 // return early if we wish to ignore this 16 // (this prevents recursion; see end of this fn) 17 // 18 if (meta.ignore_hook) return; 19 20 try { 21 const request = superagent 22 .post('https://logs.collector.solarwinds.com/v1/log') 23 // if the meta object already contained a request ID then re-use it 24 // otherwise generate one that gets re-used in the API log request 25 // (which normalizes server/browser request id formatting) 26 .set( 27 'X-Request-Id', 28 meta && meta.request && meta.request.id ? meta.request.id : cuid() 29 ) 30 .set('X-Axe-Version', logger.config.version) 31 .timeout(5000); 32 33 request.auth('', PAPERTRAIL_TOKEN); 34 35 const response = await request 36 .type('application/json') 37 .retry(3) 38 .send(safeStringify({ err: parseErr(err), message, meta })); 39 40 logger.info('log sent over HTTP', { response, ignore_hook: true }); 41 } catch (err) { 42 logger.fatal(err, { ignore_hook: true }); 43 } 44} 45 46for (const level of logger.config.levels) { 47 logger.post(level, hook); 48}
Suppress Logger Data
This is an example of using a custom hook to manipulate logger arguments to suppress sensitive data.
1const Axe = require('.'); 2 3const logger = new Axe(); 4 5for (const level of logger.config.levels) { 6 const fn = logger.config.logger[level]; 7 logger.config.logger[level] = function (message, meta) { 8 // replace any messages "beep" -> "boop" 9 if (typeof message === 'string') message = message.replace(/beep/g, 'boop'); 10 11 // mask the property "beep" in the meta object "data" 12 if (meta?.data?.beep) 13 meta.data.beep = Array.from({ length: meta.data.beep.length }) 14 .fill('*') 15 .join(''); 16 17 return Reflect.apply(fn, this, [message, meta]); 18 }; 19} 20 21logger.warn('hello world beep'); 22 23// hello world boop 24 25logger.info('start', { 26 data: { 27 foo: 'bar', 28 beep: 'boop' // <--- we're suppressing "beep" -> "****" 29 } 30}); 31 32// start { data: { foo: 'bar', beep: '****' } } 33 34logger.error(new Error('oops!'), { 35 data: { 36 beep: 'beep-boop-beep' // this becomes "**************" 37 } 38}); 39 40// Error: oops! 41// at Object.<anonymous> (/Users/user/Projects/axe/test.js:30:14) 42// at Module._compile (node:internal/modules/cjs/loader:1105:14) 43// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10) 44// at Module.load (node:internal/modules/cjs/loader:981:32) 45// at Function.Module._load (node:internal/modules/cjs/loader:822:12) 46// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) 47// at node:internal/main/run_main_module:17:47 { data: { beep: '**************' } }
Contributors
Name | Website |
---|---|
Nick Baugh | http://niftylettuce.com |
Alexis Tyler | https://wvvw.me/ |
shadowgate15 | https://github.com/shadowgate15 |
Spencer Snyder | https://spencersnyder.io |
License
MIT © Nick Baugh
![Empty State](/_next/static/media/empty.e5fae2e5.png)
No vulnerabilities found.
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
license file detected
Details
- Info: project has a license file: LICENSE:0
- Info: FSF or OSI recognized license: MIT License: LICENSE:0
Reason
0 existing vulnerabilities detected
Reason
security policy file detected
Details
- Info: security policy file detected: github.com/cabinjs/.github/SECURITY.md:1
- Info: Found linked content: github.com/cabinjs/.github/SECURITY.md:1
- Info: Found disclosure, vulnerability, and/or timelines in security policy: github.com/cabinjs/.github/SECURITY.md:1
- Info: Found text in security policy: github.com/cabinjs/.github/SECURITY.md:1
Reason
2 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 1
Reason
Found 1/28 approved changesets -- score normalized to 0
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/cabinjs/axe/ci.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/cabinjs/axe/ci.yml/master?enable=pin
- Warn: npmCommand not pinned by hash: .github/workflows/ci.yml:23
- Info: 0 out of 2 GitHub-owned GitHubAction dependencies pinned
- Info: 0 out of 1 npmCommand dependencies pinned
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: no topLevel permission defined: .github/workflows/ci.yml:1
- Info: no jobLevel write permissions found
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
branch protection not enabled on development/release branches
Details
- Warn: branch protection not enabled for branch 'master'
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 0 commits out of 3 are checked with a SAST tool
Score
4.2
/10
Last Scanned on 2025-02-03
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn MoreOther packages similar to axe
axe-core
Accessibility engine for automated Web UI testing
jest-axe
Custom Jest matcher for aXe for testing accessibility
@hint/hint-axe
hint that that checks using axe for accessibility related best practices
nightwatch-axe-verbose
For adding custom commands to allow you to run axe accessibility tests in your NightwatchJS test cases.