Anyhow

Drop-in (and slightly improved) logging wrapper for Winston, Bunyan, pino, Google Cloud Logging and console.
Why?
The idea for Anyhow came after a conflict of interests regarding logging libraries in personal and work projects. Some of these projects were using winston. A few other bunyan. Some were simply streaming to the console.
By using Anyhow we can achieve a consistent logging mechanism regardless of what library is actually doing the logging. Install Anyhow, replace the log calls and let it delegate the actual logging to the correct library. It also has some handy features like compacting the messages, pre-processing arguments, extracting error details and stylizing the console output.
Basic usage
const logger = require("anyhow")
logger.setup()
logger.setup("winston")
logger.options = {
compact: true,
maxDepth: 6,
appName: "Anyhow",
levels: ["info", "warn", "error"],
styles: {
debug: ["gray"],
info: ["white"],
warn: ["yellow"],
error: ["red", "bold"]
},
preprocessorOptions: {
maskedFields: ["password", "token"],
clone: true
}
}
logger.info("My app has started")
logger.info({someJson: "hello world"}, "Some string", 123, new Date())
try {
oops.itFailed()
} catch (ex) {
logger.error("MyApp.method", ex)
}
logger.debug("This will not be logged", myObject)
logger.setOptions({levels: ["debug", "info", "warn", "error"]})
logger.debug("Now it's logged", anotherObject)
logger.setOptions({levels: ["warn", "error"]})
logger.info("Won't log because we only enabled warn and error")
logger.warn("This warning will be logged")
logger.log("warn", "This will be called as warn", someExtraObject, 123, true)
Enforcing a specific library
logger.setup("winston")
const winstonLogger = require("winston").createLogger(options)
logger.setup({name: "winston", instance: winstonLogger})
logger.setup("bunyan")
const bunyanLogger = require("bunyan").createLogger(options)
logger.setup({name: "winston", instance: bunyanLogger})
logger.setup("pino")
logger.setup("gcloud")
const googleOptions = {
logName: "anyhow-testing",
projectId: env.GCP_PROJECT_ID,
credentials: {
client_email: env.GCP_CLIENT_EMAIL,
private_key: env.GCP_PRIVATE_KEY.replace(/\\n/g, "\n")
}
}
logger.setup("gcloud", googleOptions)
logger.setup("none")
Changing settings
logger.setOptions({separator: ", "})
logger.info("This is", "now separated", "by comma")
logger.setOptions({compact: false})
logger.info(someComplexObject, somethingElse)
logger.setOptions({styles: {warn: ["red", "italic"]}})
logger.warn("Console output now shows yellow italic for this")
logger.info("Info is still default gray")
logger.setOptions({styles: null})
logger.warn("No console styles anymore, even if chalk is installed")
logger.setOptions({levelOnConsole: true})
logger.info("This will now have 'INFO:' on the beginning of the message")
logger.options = {timestamp: true}
logger.info("This", "should be separated with a bar now")
Preprocessors
const user = {
name: "John Doe",
password: "mypass",
token: "sometoken",
registered: new Date(),
foo: function Foo() {},
team: {
a: {
b: {
c: {
}
}
}
}
}
logger.info(user)
logger.setOptions({preprocessors: ["cleanup"]})
logger.info(user)
logger.setOptions({preprocessors: ["maskSecrets"]})
logger.info(user)
logger.setOptions({preprocessors: ["friendlyErrors"]})
try{
axios.get("https://my.api.com/something-to-fail")
} catch (ex) {
logger.error(ex)
}
const numToString = (args) => args.map(a => `@ ${a.toString()}`)
logger.setOptions({preprocessors: [numToString]})
logger.info(1, 2, new Date("2000-01-01T00:00:00"))
logger.setOptions({preprocessors: ["friendlyErrors", "maskSecrets", numToString]})
Uncaught and unhandled errors
logger.setOptions({uncaughtExceptions: true})
let notFunction = true
notFunction()
logger.setOptions({unhandledRejections: true})
let failFunction = async function() {
throw new Error("Oh no!")
}
failFunction()
Options
appName: string, "Anyhow"
Optional, the name of your app / tool / service.
compact: boolean, true
Defines if messages should be compacted (remove line breaks and extra spaces, minify the JSON output, flatten arrays, etc).
levels: string[], ["info", "warn", "error"]
Defines which logging levels are enabled. The standard logging levels
are ["debug", "info", "warn", "error"]. Debug should usually not be enabled in production.
maxDepth: number, 6
The maximum tree depth to follow when processing and logging arrays and objects.
levelOnConsole: boolean, false
If true it will prepend the log level (INFO, WARN, ERROR etc...) to the message on the console.
preprocessors: string / function[], null
Array of preprocessors to be enabled, passed as functions or strings. Preprocessor functions should accept a single
array containing the arguments to be parsed. The following built-in preprocessors strings are available:
cleanup
Cleanup the message output by removing non-relevant data from logged objects and replacing
functions / custom objects with [Function] / [object Type] strings.
friendlyErrors
Extract the exception code, status and message instead of logging the full exception object.
Supports axios and fetch exceptions out-of-the-box.
maskSecrets
Replace sensitive credentials with [***]. The actual field names to be masked are set
under the preprocessorOptions
, see below.
preprocessorOptions: object
Additional options to be passed to the preprocessors:
clone: boolean, true
Boolean, if set to false then objects will not be cloned before running the preprocessors.
Only set to false if you are dealing exclusively with JSON data that can be mutated by the logger.
errorStack: boolean, false
Boolean, if set to true then exception stack traces will also be logged.
maskedFields: string[], default below
Array of strings, property names (case insensitive) that should be masked with the maskSecrets preprocessor. Defaults to:
authorization, password, passcode, secret, token, accesstoken, access_token, refreshtoken, refresh_token, clientsecret, client_secret, apikey, api_key, apisecret, api_secret, privatekey, private_key
uncaughtException: boolean, false
Boolean, if true it will log uncaught exceptions to the console (and will NOT quit execution).
unhandledRejections: boolean, false
Boolean, if true it will log uncaught exceptions to the console (and will NOT quit execution).
separator: string, " | "
String, defines the default separator between logged objects. For instance if you do a
info(123, "ABC")
, output will be "123 | ABC".
styles: object
Object with keys defining the styles for each level on console output. This will only be effective
if you also have the chalk module installed. By default
debug
is gray, info
white, warn
yellow and error
bold red. To disable, set it to null.
timestamp: boolean, false
Boolean, if true it will prepend log messages with a timestamp.
Methods
console(level, args) -> string
Log to console directly, regardless of which library is currently active. First argument is
the level
string, and second is array of things to be logged.
Returns the final, parsed message that was logged.
log(level, args) -> string
Main logging method. First argument is the level
string, and second is array of things to be logged.
Please note that only "info", "warn" and "error" levels are enabled by default.
Returns the final, parsed message that was logged.
debug(...args) -> string
Shortcut to log("debug", args). Please note that "debug" is not included on the default levels
.
info(...args) -> string
Shortcut to log("info", args).
warn(...args) -> string
Shortcut to log("warn", args).
error(...args) -> string
Shortcut to log("error", args).
Version 3 breaking changes
If you are using the default options, there's nothing to worry about - the logging methods have the same
signature and are backwards-compatible. Otherwise, please use the new options
object:
New options
anyhow.appName = "My App"
anyhow.compact = true
anyhow.levels = ["debug", "info", "warn", "error"]
anyhow.preprocessor = someFunction
anyhow.options = {
appName: "My App",
compact: true,
levels: ["debug", "info", "warn", "error"],
preprocessors: [someFunction]
}
anyhow.options = {
appName: "My App",
compact: true
}
anyhow.setOptions({appName: "MyApp", compact: true})
API documentation
You can browse the full API documentation at https://anyhow.devv.com.
Or check these following projects that are using Anyhow for logging: