Tripitaka
Tripitaka is a low dependency, no frills logger for Node.js
Install / Use
/learn @acuminous/TripitakaREADME
Tripitaka
Tripitaka is a low dependency, no frills logger, designed to play nicely with tools like fluentd and Elasticsearch. It is named after the buddhist monk from the TV series, Monkey due to shared values of simplicity and mindfulness, and also because Tripitaka is a term given to ancient collections of Buddhist scriptures, which loosely connects with logging. I wrote Tripitaka because, sadly my previous logger of choice, winston has fallen into disrepair.
TL;DR
const { Logger } = require("tripitaka");
const logger = new Logger();
const book = {
title: "Monkey",
author: "Wu Ch'eng-en",
ISBN10: "9780140441116",
};
logger.info("Retrieved book", { book });
NODE_ENV=production node index.js
{"level":"INFO","message":"Retrieved book","book":{"title":"Monkey","author":"Wu Ch'eng-en","ISBN10":"9780140441116"},"timestamp":"2022-05-27T18:21:17.371Z"}
Design Principles
Tripitaka intentionally ships with only two transports. A streams-based transport which will write to stdout and stderr (or other streams which you supply), and an event emitter based transport which will emit events using the global process object (or another emitter which you supply). This library holds the opinion that external files, database and message brokers are all far better handled with a data collector such as fluentd, but you can write your own transports if you so wish. Tripitaka also eschews child loggers. These are useful for stashing context, but more elegantly implemented via AsyncLocalStorage or continuation-local-storage. See the express example for how.
Usage
Tripitaka supports the same logging levels as console, i.e.
- logger.trace(message, context)
- logger.debug(message, context)
- logger.info(message, context)
- logger.warn(message, context)
- logger.error(message, context)
The function arguments are always the same, a message and a context, e.g.
logger.info("How blissful it is, for one who has nothing", {
env: process.env.NODE_ENV,
});
Assuming the default configuration, this will write the following to stdout when run in a production environment
{
"env": "production",
"message": "How blissful it is, for one who has nothing",
"level": "INFO"
}
If you use the context processor (enabled by default), the context may be an Object, Array or Error. Both errors and array are automatically nested under configurable attributes, which default to "error" and "items" respectively, e.g.
logger.info("How blissful it is, for one who has nothing", [1, 2, 3]);
logger.error("I forbid it!", new Error("Oooh, Demons!"));
{"items":[1,2,3],"message":"How blissful it is, for one who has nothing","level":"INFO"}
{"error":{"message":"Oooh, Demons!","stack":"..."},"message":"Oooh, Demons!","level":"ERROR"}
If you use the empty processor (enabled by default), and you neglect to log a message, Tripitaka will report this
logger.info({ env: process.env.NODE_ENV });
{
"message": "Empty message logged at Test._fn (/opt/acuminous/tripitaka/index.js:9:5)",
"env": "production"
}
The exception to this is when you are just logging an Error, in which case the log record message will default to the error message e.g.
logger.error(new Error("Oooh, Demons!"));
{
"error": { "message": "Oooh, Demons!", "stack": "..." },
"message": "Oooh, Demons!",
"level": "ERROR"
}
Customisation
You can customise this output through the use of processors and transports. By default Tripitaka ships with the following configuration.
const { Logger, Level, processors, transports } = require("tripitaka");
const { context, timestamp, json, human } = processors;
const { stream } = transports;
const logger = new Logger({
level: Level.INFO,
processors: [context(), timestamp(), process.env.NODE_ENV === "production" ? json() : human()],
transports: [stream()],
});
Suppressing logs
You can suppress logs by setting the logging level as when you create a Logger instance as above, or by calling logger.disable(). You can re-enable the logger by calling logger.enable().
Processors
A processor is a function you can use to mutate the Tripitaka log record before it is delivered to the transports. Since processors are chained together in an array, the record can be mutated over a series of steps.
The processor is called with a single object containing the following properties:
| name | type | notes | | ------- | ------ | ----------------------------------------------------------------------------------------- | | level | Level | | | message | string | | | ctx | object | | | record | any | Initialised to a shallow clone of the context. Be careful not to mutate nested attributes |
example
const logger = new Logger({
processors: [
context(),
({ record }) => {
return { ...record, timestamp: new Date() };
},
json(),
],
});
The out-of-the-box processors are as follows...
augment
Augments the record with the supplied source. If attributes are common to both the record and the source, the source wins. Use with AsyncLocalStorage as a substitute for child loggers. See the express example for how.
| name | type | required | default | notes | | ------ | ------------------ | -------- | ------- | ----- | | source | object or function | yes | | |
Object example
Use an object when the source data is static
const source = { env: process.env.NODE_ENV };
const logger = new Logger({
processors: [context(), augment({ source }), json()],
});
logger.info("How blissful it is, for one who has nothing");
{
"env": "production",
"message": "How blissful it is, for one who has nothing",
"level": "INFO"
}
Function example
Use a function when the source data is dynamic
const source = () => ({ timestamp: new Date() });
const logger = new Logger({
processors: [context(), augment({ source }), json()],
});
logger.info("How blissful it is, for one who has nothing");
{
"timestamp": "2021-03-28T17:43:12.012Z",
"message": "How blissful it is, for one who has nothing",
"level": "INFO"
}
The source function will be called with an object containing the following parameters
| name | type | notes | | ------- | ------------------ | ------------------------------------ | | level | Level | The log level | | message | string | The log message | | context | Object | The log context | | record | Object | The log record prior to augmentation |
buffer
The buffer processor outputs the record as a buffer, optionally encoding it before doing so. For this processor to work, the record must previously have been converted to a string.
| name | type | required | default | notes | | -------------- | ------ | -------- | ------- | ----- | | inputEncoding | string | no | | | | outputEncoding | string | no | | |
example
const logger = new Logger({
processors: [context(), json(), buffer({ outputEncoding: "hex" })],
});
logger.info("How blissful it is, for one who has nothing");
7b226c6576656c223a22494e464f222c226d657373616765223a22486f7720626c69737366756c2069742069732c20666f72206f6e652077686f20686173206e6f7468696e67227d
context
Performs a shallow copy of the context into the record. It also understands how to handle errors - without it they will not serialize correctly. It is best to put this processor first in the list of processors, as if another processor fires first, it may incorrectly handle the error object.
The processor operates with the following logic:
- If
