Fast, flexible stream based Logger using extendable Transports.
In short code help. If using VS Code for example the code help is quite good. If you are a Typescript fan you will particularly appreciate the care that's been taken for accurate typings. Comments are detailed and building Transform parsers for various outputs are quite flexible.
npm install blurp
The easiest way to use blurp is to use the default logger instance.
By default the following log levels are enabled:
fatal, error, warn, info, debug, trace
import blurp from 'blurp';
blurp.log('hello blurp');
blurp.log('My name is %s', 'Milton', { phone: '5551212', age: 55 });
blurp.error(new Error(`Some error`));
Beyond the default Logger you can create new Loggers as well as Child Loggers which allow the creation of Loggers using a parent but with injected metadata making the querying of logs more comprehensive.
The below will create a Logger labled "syslog" using standard syslog levels. We add the ConsoleTransport and the pre-bundled Transform stack console to build our message.
import blurp, { transforms } from 'blurp';
// OR ES5
const blurp = require('blurp').default;
// NOTE: If using Typescript you'll need to pass in your types.
// It is possible to have this inferred but you can't pass in Transports
// or Transforms. You'd need to create the Logger first then add
// your Transports etc after the fact by doing for ex: syslog.transport(transport1, transport2)
type Levels = 'emerg' | 'alert' | 'crit' | 'err' | 'warning' | 'notice' | 'info' | 'debug';
// If NOT USING Typescript you don't need "Levels" below.
const syslog = blurp.createLogger<Levels>('express', {
level: 'warning',
levels: ['emerg', 'alert', 'crit', 'err', 'warning', 'notice', 'info', 'debug'],
transports: [
new blurp.ConsoleTransport()
],
transforms: [
transforms.stack.console()
]
});
syslog.warn('My name is %s', 'John', { age: 33 });
The above, using the console() Transform will output the following:
16:57:15.1 WARN: default: My name is John age: 33
Child Loggers are simply Loggers that share all of its parent settings but insert meta data or you might think of them as flags for each logged method.
const child = blurp.child('mylabel', { module: 'user' });
child.warn('My name is %s', 'John', { age: 33 });
// "module" is added as metadata automatically
// on each logged message.
{
level: "warn",
message: "My name is John",
age: 33,
module: "user"
}
You can easily mute a Logger or it's child Loggers.
blurp.mute();
blurp.unmute();
// OR
blurp.mute('some-child', 'another-child');
// OR
blurp.unmute('*') // unmutes all children.
You can also bypass transforms and write directly to your stream.
blurp.write('some string without line ending');
blrup.writeLn('some string with a line ending');
One of the main reasons to use a Logger such as blurp is that it uses Transports. Transports allow a single logged message to be output to muliple destinations automatically. All with unique transforms (formatting) and destinations.
Blurp has two common built in Transports as follows:
ConsoleTransport, FileTransport
To create a custom Transport import the base Transport class and extend.
import { Transport } from 'blurp';
class MyTransport extends Transport {
constructor(options) {
super('my-transport-name', options);
}
log(payload, cb) {
const { [SOURCE]: source, [OUTPUT]: output } = payload;
console.log(output);
cb();
}
}
Same as above but using Typescript
import { Transport } from 'blurp';
interface IMyTransportOptions<L extends string> extends ITransportOptions<L> {
// your options here
}
const DEFAULTS: IMyTransportOptions<any> = {
// defaults here
};
class MyTransport<L extends string> extends Transport<L, IMyTransportOptions<L>> {
constructor(options?: IMyTransportOptions<L>) {
super('my-transport-name', { ...DEFAULTS, ...options });
}
log(payload: IPayload<L>, cb: Callback) {
const { [SOURCE]: source, [OUTPUT]: output } = payload;
console.log(output);
cb();
}
}
To understand how blurp works it is necessary to understand how each log message is bundled into its payload object.
I know how it goes, no one closely reads these things but please read the following closely, it will pay off!
If not using Typescript it is not necessary to remember the above interface name, just there to make more sense of things for Typescript users. Before explaining the payload let's take a quick look at one.
Basic Payload
const payload = {
[CONFIG]: {
label: 'default',
levels: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'],
colors: {
fatal: ['bgRed', 'yellow'],
error: ['red', 'bold'],
warn: 'yellow',
info: 'cyan',
debug: 'magenta',
trace: 'blue'
},
elapsed: 21
}
[SOURCE]: {
level: 'info',
message: 'Some log message'
}
[OUTPUT]: 'info Some log message' // example if using .console() Transform on ConsoleTransport
level: 'info',
message: 'Some log message'
}
Each payload contains an object decorated by a Symbol. Why a Symbol? Symbols won't be enumerated when outputing the payload object in say pretty print. However they allow us to add the info we need on a single object that our Transforms may need.
You may wonder if our Logger knows of it's log levels why attach in the payload? Our Transforms would need the log levels passed in order to use the colors you've already defined. Hence adding the handful of basic properties is quite helpful and simplifies our Transform methods.
Why duplicate the source of the primary payload properites? These properties are READ ONLY and are used for checking original values that have not been mutated. For example say you want your log levels to display in uppercase with ansi colors, that would render using the payload.level
useless for checking the log payload's current level. The same goes for other properties.
The output key is a string value that is to be used as the final output value. This value is set by returning the value from a Format Transform. More on that below. The output property is always a string as it is used to log to a console, or to write to a file such as JSON or perhaps in a PUT/POST to a server.
There are only TWO default properties. level & message. Every other property must be extended by you using a custom Transform or using one of the built in helper Transforms. This is by design and makes blurp far less opinionated.
Transforms are simple functions that can be combined that manipulate your log payload (shown above) formatting it to the desired output for your Transport.
There are two basic forms of Transforms:
Modifier, Format
There is a third form of sorts called a Stack. Ultimately it is a Format Transform but a stack merely combines multiple modifier and format Transforms to create a singular method for all internally bundled Transforms.
Current built in transforms are listed below. Soon as there is time we'll detail these and their options in the mean time please check source!
- align
- case
- colorize
- errorify
- extend
- generic
- label
- mask
- meta
- pad
- private
- process
- sort
- splat
- timestamp
- trace
- delimited
- json
- pretty
- terminal
- file
Although there's a built in sort Transform we'll use it to illustrate how simple it is to create it. A modifier Transform does exactly what it sounds like. It modifies the payload object then returns the modified payload object to continue down the stack.
const options = {
props: ['timestamp', 'level', 'message']
};
function sortTransform(payload, options?) {
// Let's get any props not defined in our options, we'll append them.
const remainingProps = Object.keys(payload).filter(k => !options.props.includes(k));
// Append our remaining props.
const props = [...options.props, ...remainingProps];
// Reduce into new ordered object and then
// return the newly ordered Payload for next Transform
return props.reduce((sorted, prop) => {
sorted[prop] = payload[prop];
return sorted;
}, {});
}
see /src/transforms/modifiers/sort.ts - for creating this Transform in Typescript.
Format Transforms format the log payload for final output. Unlike modifier Transforms instead of returning the payload object, they return a string value that automatically gets set to the [OUTPUT] property key in the payload.
import { inspect } from 'util';
const options = {
colorize: true
};
function prettyFormat(payload, options?) {
return inspect(payload, null, null, options.colorize);
}
see /src/transforms/formats/pretty.ts - for creating this Transform in Typescript.
To use the transform we need to call a special method that wraps for use with blurp.
const sort = blurp.createModifier(sortTransform);
const pretty = blurp.createFormatter(prettyFormat);
Now that each Transform is wrapped we can use them and even combine them into a single method!
// Duplicate of above here so you it all together.
const { combine } = blurp;
const sort = blurp.createModifier(sortTransform);
const pretty = blurp.createFormatter(prettyFormat);
// Call our Transforms passing in any options.
const combined = combine(
sort(),
pretty({ colorize: false })
);
const logger = blurp.createLogger('app', {
transports: [
new ConsoleTransport()
],
transforms: [
combined
]
})
Blurp can automatically handle uncaughtException and uncaughtRejection errors on the Node process. Simply pass { exceptions: true, rejections: true }
in your options when initializing the Logger or add to an existing Logger as shown below:
const consoleTransport = blurp.transports.get('console');
blurp.exceptions.handle(consoleTransport);
It is possible to stream activity on all Transports. To do this we use the firehose.
blurp.firehose(); // bind to all Transports.
const options {
// your opts to pass to each
// Transport.firehose() method.
};
blurp.firehose(options, 'console', 'file'); // bind w/ options to ONLY console and file.
Each Transport can create a .query()
method which allows the parent Logger to bind to it for querying previous logs. Much like the Transport itself you will need to define how this is to happen. That said there are some helpers that make the heavy lifting a little easier.
NOTE Some additional filtering capabilities beyond the below will be available soon!
const options = {
timestampKey: 'ts', // key containing timestamps.
level: undefined, // the level to query (default: undefined or all levels)
from: undefined, // including this date (default: previous 24 hrs from now)
to: undefined, // including this date (default: Date.now())
limit: 10, // max rows (0 = unlimited)
start: 0, // starting row by 0 index
sort: 'desc', // ascending or descending order.
transform: JSON.parse // parse each row using JSON.parse.
};
const query = blurp.query(options);
The above creates the query instance. This makes queries reusable. Let's get the results.
// Example results object
const results = {
queried: 10, // total number queried.
success: 8, // the total successfully matched.
skipped: 2, // how many were skipped didn't match criteria.
rows: [], // your parsed rows will be here.
filtered: [], // coming soon!
errors: [], // Errors are defined as tuples [transform.label, error]
show: Function // helper function to show the resutls in terminal/console.
};
const query = blurp.query(options);
// Get results and show in console.
query.exec((results) => {
results.show(); // or query.show();
});
Same as above but with async/await
async function queryLogs() {
const query = blurp.query(options);
const results = await query.exec();
// Do something with results.
}
Same as above but as a Promise
const query = blurp.query(options);
query.exec().then(results => {
// do something.
});
A lot unfortunately. As time permits we'll improve the Readme and create real docs.
Tests are coming and are for sure needed. This lib will be used in a production app so they are coming to make at a min us warm and fuzzy ha ha.
See LICENSE.md