Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

🪓 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.

License

NotificationsYou must be signed in to change notification settings

cabinjs/axe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

build statuscode stylestyled with prettiermade with lasslicensenpm downloads

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 forForward Email,Lad, andCabin.

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 ofconsole.log('someVariable', someVariable) and another developer will simply writeconsole.log(someVariable). Even if both developers wrote in the style ofconsole.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 usingslow functions likelodash'somit, we use a more performant approach.

Axe adheres to theLog4j log levels, which have been established for 21+ years (since 2001). This means that you can use anycustom logger (or the defaultconsole), but we strictly support the following log levels:

  • trace
  • debug
  • info
  • warn
  • error
  • fatal

Axe normalizes invocation of logger methods to be called withonly 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 thelog method of Axe's returnedlogger 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(newError('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(newError('Oops!'),newError('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(newError('Oops!'),newError('Another Error!'),newError('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 uselogger.log, then the log level used will beinfo, but it will still use the logger's nativelog method (as opposed to usinginfo). If you invokelogger.log (or any other logging method, e.g.logger.info,logger.warn, orlogger.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',newError('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/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
logger.warn(newError('uh oh!'),'hello');// Error: uh oh!//     at Object.<anonymous> (/Users/user/Projects/axe/test.js:9:13)//     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

Axe has support for format specifiers, and you can even use format specifiers in the browser (usesformat-util – has limited number of format specifiers) and Node (uses the built-inutil.format method – supports all format specifiers). This feature is built-in thanks to smart detection usingformat-specifiers.

logger.info('favorite color is %s','blue');// 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.

Themost impactful feature of Axe is that itmakes logger output human-friendly and readable when there are multiple errors.

Normallyconsole output (and most other loggers) by default will output the following unreadable stack trace:

> console.log(new Error('hello'), new Error('world'));Error: hello    at REPL6:1:13    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) Error: world    at REPL6:1:33    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)

However with Axe, errors and stack traces are much more readable (we usemaybe-combine-errors under the hood):

> logger.log(new Error('hello'), new Error('world'));Error: hello    at REPL7:1:12    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)Error: world    at REPL7:1:32    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)

Lastly, Axe works in both server-side and client-side environments (with Node and the browser).

Application Metadata and Information

If you've read theForeword, you'll know that Axe invokes logger methods with two normalized arguments,message (String or Error) andmeta (Object).

Axe will automatically add the following metadata and information to themeta Object argument passed to logger methods:

PropertyTypeDescription
meta.levelStringThe log level invoked (e.g."info").
meta.errObjectParsed error information usingparse-err.
meta.original_errObjectIf and only ifmeta.err already existed, this field is preserved asmeta.original_err on the metadata object.
meta.original_metaObjectIf and only ifmeta already existed as an argument and was not an Object (e.g. an Array), this field is preserved asmeta.original_meta on the metadata object.
meta.appObjectApplication information parsed usingparse-app-info.This is not added in Browser environments. See below nested properties.
meta.app.nameStringName of the app frompackage.json.
meta.app.versionStringVersion of the apppackage.json.
meta.app.nodeStringVersion if node.js running the app.
meta.app.hashStringThe latest Git commit hash; not available when not in a Git repository or if there is no Git commit hash.
meta.app.tagStringThe latest Git tag; not available when not in a Git repository or if there is no Git tag.
meta.app.environmentStringThe value ofprocess.env.NODE_ENV.
meta.app.hostnameStringName of the computer.
meta.app.pidNumberProcess ID as inprocess.pid.
meta.app.clusterObjectNodecluster information.
meta.app.osObjectNodeos information.
meta.app.worker_threadsObjectNodeworker_threads information.

As of v11.0.0 Axe will outputmeta.app by default unless you passomittedFields: [ 'app' ] or specify the process environment variable ofAXE_OMIT_META_FIELDS=app node app.js when you start your app.

Axe will omit from metadata all properties via the default Array frommeta.omittedFields option (seeOptions 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.

hello world {  level:'info',  app: {    name:'axe',    version:'10.0.0',    node:'v16.15.1',    hash:'5ecd389b2523a8e810416f6c4e3ffa0ba6573dc2',    tag:'v10.0.0',    environment:'development',    hostname:'users-MacBook-Air.local',    pid: 3477,    cluster: { isMaster: true, isWorker: false, schedulingPolicy: 2 },    os: {      arch:'arm64',      cpus: [Array],      endianness:'LE',      freemem: 271433728,      priority: 0,      homedir:'/Users/user',      hostname:'users-MacBook-Air.local',      loadavg: [Array],      network_interfaces: [Object],      platform:'darwin',      release:'21.3.0',      tmpdir:'/var/folders/rl/gz_3j8fx4s98k2kb0hknfygm0000gn/T',      totalmem: 17179869184,      type:'Darwin',      uptime: 708340,      user: [Object],      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'    },    worker_threads: {      isMainThread: true,      resourceLimits: {},      threadId: 0,      workerData: null    }  }}

Note that you can also combinemeta.omittedFields withmeta.pickedFields andmeta.remappedFields (in case you want to output specific properties frommeta.app and exclude others – seeOptions for more insight).

Install

Node

npm:

npm install axe

Browser

SeeBrowser usage below for more information.

Usage

Options

PropertyTypeDefault ValueDescription
showStackBooleantrueAttempts to parse a boolean value fromprocess.env.AXE_SHOW_STACK).If this value istrue, then ifmessage is an instance of an Error, it will be invoked as the first argument to logger methods. If this isfalse, then only theerr.message will be invoked as the first argument to logger methods. Basically iftrue it will calllogger.method(err) and iffalse it will calllogger.method(err.message). If you passerr as the first argument to a logger method, then it will show the stack trace viaerr.stack typically.
metaObjectSee belowStores all meta config information (see the following nested properties below).
meta.showBooleantrueAttempts to parse a boolean value fromprocess.env.AXE_SHOW_META – meaning you can pass a flagAXE_SHOW_META=true node app.js when needed for debugging), whether or not to output metadata to logger methods. If set tofalse, then fields will not be omitted nor picked; the entire meta object will be hidden from logger output.
meta.remappedFieldsObject{}Attempts to parse an Object mapping fromprocess.env.AXE_REMAPPED_META_FIELDS (, and: delimited, e.g.REMAPPED_META_FIELDS=foo:bar,beep.boop:beepBoop to remapmeta.foo tometa.bar andmeta.beep.boop tometa.beepBoop). Note that this will clean up empty objects by default unless you set the optionmeta.cleanupRemapping tofalse). Supports dot-notation.
meta.omittedFieldsArray[]Attempts to parse an array value fromprocess.env.AXE_OMIT_META_FIELDS (, delimited) - meaning you can pass a flagAXE_OMIT_META_FIELDS=user,id node app.js), determining which fields to omit in the metadata passed to logger methods. Supports dot-notation.
meta.pickedFieldsArray[]Attempts to parse an array value fromprocess.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 frommeta.request andmeta.responseonlymeta.request.headers andmeta.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 viaReflect.ownKeys (not recursive yet).
meta.cleanupRemappingBooleantrueWhether or not to cleanup empty objects after remapping operations are completed)
meta.hideHTTPBooleantrueWhether to suppress HTTP metadata (prevents logger invocation with second argmeta) ifmeta.is_http istrue (viaparse-request v5.1.0+). If you manually setmeta.is_http = true and this istrue, thenmeta arg will be suppressed as well.
meta.hideMetaString or Boolean"hide_meta"If this value is provided as a String, then ifmeta[config.hideMeta] istrue, it will suppress the entire metadata objectmeta (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.
silentBooleanfalseWhether or not to invoke logger methods. Pre and post hooks will still run even if this option is set tofalse.
loggerObjectconsoleDefaults toconsole withconsole-polyfill added automatically, thoughyou can bring your own logger. Seecustom logger – you can pass an instance ofpino,signale,winston,bunyan, etc.
nameString or Booleanfalse ifNODE_ENV is"development" otherwise the value ofprocess.env.HOSTNAME oros.hostname()The default name for the logger (defaults tofalse in development environments, which does not setlogger.name) – this is useful if you are using a logger likepino which prefixes log output with the name set here.
levelString"info"The default level of logging to invokelogger methods for (defaults toinfo, which includes all logs including info and higher in severity (e.g.info,warn,error,fatal)
levelsArray['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.
appInfoBooleantrueAttempts to parse a boolean value fromprocess.env.AXE_APP_INFO) - whether or not to parse application information (usingparse-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 atrue value in the meta object.

This special property is a Symbol viaSymbol.for('axe.silent'). Using a Symbol will prevent logs from being suppressed inadvertently (e.g. if a meta object containedis_silent: true however you did not explicitly setis_silent: true, as it might have been the result of another package or Object parsed.

To do so, simply declareconst silentSymbol = Symbol.for('axe.silent') and then use it as follows:

constAxe=require('axe');constsilentSymbol=Symbol.for('axe.silent');constlogger=newAxe();logger.info('hello world');// <--- outputs to console "hello world"logger.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 usingCabin's middleware which parses HTTP requests into themeta Object properly:

constAxe=require('axe');constsilentSymbol=Symbol.for('axe.silent');constlogger=newAxe();constIGNORED_CONTENT_TYPES=['application/javascript; charset=utf-8','application/manifest+json','font','image','text/css'];//// set the silent symbol in axe to true for successful asset responses//for(constleveloflogger.config.levels){logger.pre(level,function(err,message,meta){if(meta.is_http&&meta.response&&meta.response.status_code&&meta.response.status_code<400&&(meta.response.status_code===304||(meta.response.headers&&meta.response.headers['content-type']&&IGNORED_CONTENT_TYPES.some((c)=>meta.response.headers['content-type'].startsWith(c)))))meta[silentSymbol]=true;return[err,message,meta];});}

Supported Platforms

  • Node: v14+

  • Browsers (see.browserslistrc):

    npx browserslist
    and_chr 107and_ff 106and_qq 13.1and_uc 13.4android 107chrome 107chrome 106chrome 105edge 107edge 106edge 105firefox 106firefox 105firefox 102ios_saf 16.1ios_saf 16.0ios_saf 15.6ios_saf 15.5ios_saf 14.5-14.8kaios 2.5op_mini allop_mob 64opera 91opera 90safari 16.1safari 16.0safari 15.6samsung 18.0samsung 17.0

Node

constAxe=require('axe');constlogger=newAxe();logger.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.

<scriptsrc="https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=Promise"></script><scriptsrc="https://unpkg.com/axe"></script><scripttype="text/javascript">(function(){// make a new logger instanceconstlogger=newAxe();logger.info('hello world');// or you can override console everywhereconsole=newAxe();console.info('hello world');});</script>

Required Browser Features

We recommend usinghttps://cdnjs.cloudflare.com/polyfill (specifically with the bundle mentioned inVanillaJS above):

<scriptsrc="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 likebrowserify,webpack, orrollup, then install the package as you would withNode above.

Custom logger

By default, Axe uses the built-inconsole (withconsole-polyfill for cross-browser support).

However you might want to use something fancier, and as such we supportany logger out of the box.

Loggers supported include, but are not limited to:

Just pass your custom logging utility as thelogger option:

constsignale=require('signale');constAxe=require('axe');constlogger=newAxe({logger:signale});logger.info('hello world');

InLad, we have an approach similar to the following, where non-production environments useconsola, and production environments usepino.

constAxe=require('axe');constconsola=require('consola');constpino=require('pino')({customLevels:{log:30},hooks:{// <https://github.com/pinojs/pino/blob/master/docs/api.md#logmethod>logMethod(inputArgs,method){returnmethod.call(this,{// <https://github.com/pinojs/pino/issues/854>// message: inputArgs[0],msg:inputArgs[0],meta:inputArgs[1]});}}});constisProduction=process.env.NODE_ENV==='production';constlogger=newAxe({logger:isProduction ?pino :consola});logger.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 setlogger.config.silent = true.

constAxe=require('axe');constlogger=newAxe({silent:true});logger.info('hello world');

Stack Traces and Error Handling

Please see Cabin's documentation forstack traces and error handling for more information.

If you're not usingcabin, 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, andmeta). 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 argumentserr,message, andmeta 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 canawait 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 withprocess.exit(0). Note that the resolved Promise returns an Array of the returned values from post hooks, executed serially viap-map-series. If no post hooks exist, then logger invocations will return an Object consisting of{ method, err, message, meta }, whereasmethod is the logger method invoked with (e.g.logger.error() will have amethod: '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 methodlogger.pre(level, fn) orlogger.post(level, fn). Here are a few examples below:

constAxe=require('axe');constlogger=newAxe({hooks:{pre:[function(level,err,message,meta){message=message.replace(/world/gi,'planet earth');return[err,message,meta];}]}});logger.info('hello world');// hello planet earth
constAxe=require('axe');constlogger=newAxe();logger.pre('error',(err,message,meta)=>{if(errinstanceofError)err.is_beep_boop=true;return[err,message,meta];});logger.error(newError('oops'));// Error: oops//     at Object.<anonymous> (/Users/user/Projects/axe/test.js:39: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 {//   is_beep_boop: true// }
constfs=require('node:fs');constpath=require('node:path');constAxe=require('axe');constlogger=newAxe();logger.post('error',async(err,message,meta){// store the logawaitfs.promises.appendFile(path.join(__dirname,'logs.txt'),JSON.stringify({ err, message, meta},null,2)+'\n');return{method:'error', err, message, meta};});// wait until logger stores the logconst[log]=awaitlogger.error(newError('oops'));console.log(log);// will wait to store logs before exiting processprocess.exit(0);

For more examples of hooks, see our below sections onSend Logs to HTTP Endpoint,Send Logs to Slack), andSuppress Logger Data below.

Symbols

If you use Symbols (e.g.new Symbol('hello') orSymbol.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 theSymbol.keyFor(symbol) value or thesymbol.description value. In the case ofconst symbol = new Symbol('hello') andconst 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 asobj[symbol], then the dot-notation value would beobj.hello.

Remapping

If you would like to remap fields, such asresponse.headers toresponseHeaders, then you can use environment variables or pass an object with configuration mapping.

constlogger=newAxe({meta:{remappedFields:{'response.headers':'responseHeaders'}}});logger.info('foo bar',{response:{headers:{'X-Hello-World':true}}});// foo bar { responseHeaders: { 'X-Hello-World': true }}

Omitting

If you would like to omit fields, such asresponse.headers from a response, so you are only left with the status code:

constlogger=newAxe({meta:{omittedFields:['response.headers']}});logger.info('foo bar',{response:{status:200,headers:{'X-Hello-World':true}}});// foo bar { response: { status: 200 }}

Picking

If you would like to pick certain fields, such asresponse.status from a response:

constlogger=newAxe({meta:{pickedFields:['response.status']}});logger.info('foo bar',{response:{status:200,headers:{'X-Hello-World':true}}});// foo bar { response: { status: 200 }}

Aliases

We have provided helper/safety aliases forlogger.warn andlogger.error oflogger.warning andlogger.err respectively.

Methods

A few extra methods are available, which were inspired bySlack'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 recommendsuperagent, however there are plenty of alternatives such asaxios andky.

  1. You will also need to install additional packages:

    npm install axe cuid parse-err fast-safe-stringify superagent
  2. Implementation example is provided below (and you can also refer to theForward Email code base):

    constAxe=require('axe');constcuid=require('cuid');constparseErr=require('parse-err');constsafeStringify=require('fast-safe-stringify');constsuperagent=require('superagent');constlogger=newAxe();// <https://github.com/cabinjs/axe/#send-logs-to-http-endpoint>asyncfunctionhook(err,message,meta){//// return early if we wish to ignore this// (this prevents recursion; see end of this fn)//if(meta.ignore_hook)return;try{constrequest=superagent.post(`https://api.example.com/v1/log`)// if the meta object already contained a request ID then re-use it// otherwise generate one that gets re-used in the API log request// (which normalizes server/browser request id formatting).set('X-Request-Id',meta&&meta.request&&meta.request.id ?meta.request.id :cuid()).set('X-Axe-Version',logger.config.version).timeout(5000);// if your endpoint is protected by an API token// note that superagent exposes `.auth()` method// request.auth(API_TOKEN);constresponse=awaitrequest.type('application/json').retry(3).send(safeStringify({err:parseErr(err), message, meta}));logger.info('log sent over HTTP',{ response,ignore_hook:true});}catch(err){logger.fatal(err,{ignore_hook:true});}}for(constleveloflogger.config.levels){logger.post(level,hook);}

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:

  1. You will need to install the@slack/web-api package locally:

    npm install @slack/web-api
  2. Create and copy to your clipboard a new Slack bot token athttps://my.slack.com/services/new/bot.

  3. Implementation example is provided below:

    ReplaceINSERT-YOUR-TOKEN with the token in your clipboard

    constos=require('os');constAxe=require('axe');const{ WebClient}=require('@slack/web-api');// create our application logger that uses hooksconstlogger=newAxe({logger:console,// optional (e.g. pino, signale, consola)level:'info'// optional (defaults to info)});// create an instance of the Slack Web Client API for posting messagesconstweb=newWebClient('INSERT-YOUR-TOKEN',{// https://slack.dev/node-slack-sdk/web-api#logging  logger,logLevel:logger.config.level});asyncfunctionhook(err,message,meta){//// return early if we wish to ignore this// (this prevents recursion; see end of this fn)//if(meta.ignore_hook)return;// otherwise post a message to the slack channeltry{constresult=awaitweb.chat.postMessage({channel:'monitoring',username:'Axe',icon_emoji:':axe:',attachments:[{title:err&&err.message||message,color:'danger',text:err&&err.stack||message,fields:[{title:'Level',value:meta.level,short:true},{title:'Environment',value:meta.app.environment,short:true},{title:'Hostname',value:meta.app.hostname,short:true},{title:'Hash',value:meta.app.hash,short:true}]}]});// finally log the result from slacklogger.info('slack message sent',{ result});}catch(err){logger.fatal(err,{ignore_hook:true});}}// bind custom hooks for "fatal" and "error" log levelslogger.post('error',hook);logger.post('fatal',hook);// test out the slack integrationlogger.error(newError('Uh oh something went wrong!'));

Send Logs to Sentry

See below example and the reference athttps://docs.sentry.io/platforms/node/ for more information.

npm install @sentry/node
constAxe=require('axe');constSentry=require('@sentry/node');constlogger=newAxe();Sentry.init({// TODO: input your DSN here from Sentry once you're logged in at:// https://docs.sentry.io/platforms/node/#configuredsn:"https://examplePublicKey@o0.ingest.sentry.io/0",});for(constleveloflogger.config.levels){logger.post(level,(err,message,meta)=>{// https://docs.sentry.io/clients/node/usage/if(err){Sentry.captureException(err,meta);}else{Sentry.captureMessage(message,meta);}});}// do stufflogger.error(newError('uh oh'));

Send Logs to Datadog

See below example and the reference athttps://docs.datadoghq.com/logs/log_collection/nodejs/?tab=winston30#agentless-logging.

Be sure to replaceDATADOG_API_KEY andDATADOG_APP_NAME with your Datadog API key and application name.

npm install axe cuid parse-err fast-safe-stringify superagent
constAxe=require('axe');constcuid=require('cuid');constparseErr=require('parse-err');constsafeStringify=require('fast-safe-stringify');constsuperagent=require('superagent');constlogger=newAxe();// TODO: use env var or replace this const with a stringconstDATADOG_API_KEY=process.env.DATADOG_API_KEY;// TODO: use env var or replace this const with a stringconstDATADOG_APP_NAME=process.env.DATADOG_APP_NAME;// <https://github.com/cabinjs/axe/#send-logs-to-datadog>asyncfunctionhook(err,message,meta){//// return early if we wish to ignore this// (this prevents recursion; see end of this fn)//if(meta.ignore_hook)return;try{constrequest=superagent.post(`https://http-intake.logs.datadoghq.com/api/v2/logs?dd-api-key=${DATADOG_API_KEY}&ddsource=nodejs&service=${DATADOG_APP_NAME}`)// if the meta object already contained a request ID then re-use it// otherwise generate one that gets re-used in the API log request// (which normalizes server/browser request id formatting).set('X-Request-Id',meta&&meta.request&&meta.request.id ?meta.request.id :cuid()).set('X-Axe-Version',logger.config.version).timeout(5000);constresponse=awaitrequest.type('application/json').retry(3).send(safeStringify({err:parseErr(err), message, meta}));logger.info('log sent over HTTP',{ response,ignore_hook:true});}catch(err){logger.fatal(err,{ignore_hook:true});}}for(constleveloflogger.config.levels){logger.post(level,hook);}

Send Logs to Papertrail

See below example and the reference athttps://www.papertrail.com/help/configuring-centralized-logging-from-nodejs-apps/.

Be sure to replacePAPERTRAIL_TOKEN with your Papertrail token.

npm install axe cuid parse-err fast-safe-stringify superagent
constAxe=require('axe');constcuid=require('cuid');constparseErr=require('parse-err');constsafeStringify=require('fast-safe-stringify');constsuperagent=require('superagent');constlogger=newAxe();// TODO: use env var or replace this const with a stringconstPAPERTRAIL_TOKEN=process.env.PAPERTRAIL_TOKEN;// <https://github.com/cabinjs/axe/#send-logs-to-papertrail>asyncfunctionhook(err,message,meta){//// return early if we wish to ignore this// (this prevents recursion; see end of this fn)//if(meta.ignore_hook)return;try{constrequest=superagent.post('https://logs.collector.solarwinds.com/v1/log')// if the meta object already contained a request ID then re-use it// otherwise generate one that gets re-used in the API log request// (which normalizes server/browser request id formatting).set('X-Request-Id',meta&&meta.request&&meta.request.id ?meta.request.id :cuid()).set('X-Axe-Version',logger.config.version).timeout(5000);request.auth('',PAPERTRAIL_TOKEN);constresponse=awaitrequest.type('application/json').retry(3).send(safeStringify({err:parseErr(err), message, meta}));logger.info('log sent over HTTP',{ response,ignore_hook:true});}catch(err){logger.fatal(err,{ignore_hook:true});}}for(constleveloflogger.config.levels){logger.post(level,hook);}

Suppress Logger Data

This is an example of using a custom hook to manipulate logger arguments to suppress sensitive data.

constAxe=require('.');constlogger=newAxe();for(constleveloflogger.config.levels){constfn=logger.config.logger[level];logger.config.logger[level]=function(message,meta){// replace any messages "beep" -> "boop"if(typeofmessage==='string')message=message.replace(/beep/g,'boop');// mask the property "beep" in the meta object "data"if(meta?.data?.beep)meta.data.beep=Array.from({length:meta.data.beep.length}).fill('*').join('');returnReflect.apply(fn,this,[message,meta]);};}logger.warn('hello world beep');// hello world booplogger.info('start',{data:{foo:'bar',beep:'boop'// <--- we're suppressing "beep" -> "****"}});// start { data: { foo: 'bar', beep: '****' }}logger.error(newError('oops!'),{data:{beep:'beep-boop-beep'// this becomes "**************"}});// Error: oops!//     at Object.<anonymous> (/Users/user/Projects/axe/test.js:30: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 { data: { beep: '**************' }}

Contributors

NameWebsite
Nick Baughhttp://niftylettuce.com
Alexis Tylerhttps://wvvw.me/
shadowgate15https://github.com/shadowgate15
Spencer Snyderhttps://spencersnyder.io

License

MIT ©Nick Baugh

About

🪓 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.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors4

  •  
  •  
  •  
  •  

[8]ページ先頭

©2009-2025 Movatter.jp