Papyrus
Lightweight, modular JSON logger for Deno
with support for external formatter and transports.
Features
- 5 logging levels:
trace
,debug
,info
,warn
anderror
Debug
features- All logs output as
JSON
strings - Decorate your logs with
bindings
- Create
child loggers
inherit their parentâs bindings and settings - Support for external
formatters
andtransports
How to use
Basic usage:
import Papyrus from "https://deno.land/x/papyrus/mod.ts";
const logger = new Papyrus({
useLabels: true,
});
logger.trace("Hello World!");
logger.debug("Hello World!");
logger.info("Hello World!");
logger.warn("Hello World!");
logger.error("Hello World!");
// Outputs:
// {"level":"trace","time":1589371200000,"message":"Hello World!"}
// {"level":"debug","time":1589371200000,"message":"Hello World!"}
// {"level":"info","time":1589371200000,"message":"Hello World!"}
// {"level":"warn","time":1589371200000,"message":"Hello World!"}
// {"level":"error","time":1589371200000,"message":"Hello World!"}
Using child loggers
import Papyrus from "https://deno.land/x/papyrus/mod.ts";
const parent = new Papyrus({
bindings: {pid: Deno.pid},
name: "parentLogger",
useLabels: true,
});
parent.info("Hello from parent!");
const child = parent.child({
bindings: {host: "127.0.0.1"},
name: "childLogger"
});
child.info("Hello from child!");
// Outputs:
// {"level":"info","name":"parentLogger","time":1589371200000,"pid":6210,"message":"Hello from parent!"}
// {"level":"info","name":"childLogger","time":1589371200000,"pid":6210,"host":"127.0.0.1","message":"Hello from child!"}
Table of contents
- How to use
- Table of contents
- Creating a logger
- Logging with bindings
- Child loggers
- Options
- Logging
- Output
- Formatter
- Transport
- Plugins
- Contributions
Creating a logger
Import Papyrus from Deno.land:
import Papyrus from "https://deno.land/x/papyrus/mod.ts";
// Or
import { Papyrus } from "https://deno.land/x/papyrus/mod.ts";
Nameless logger with default options
A nameless logger will be created with the default options:
const logger = new Papyrus();
Named logger with default options
A named logger will be created with the default options:
const logger = new Papyrus("myLogger");
Customized logger
An object of type PapyrusOptions
can be used to specify options:
const logger = new Papyrus({
// options...
});
Logging with bindings
Bindings are properties that are bound to the logger. In other therms, any log output by the logger will contain itâs bindings. Bindings are typically found before the logâs message.
const logger = new Papyrus({
bindings: {pid: Deno.pid}
});
logger.info("Hello World!");
// Outputs:
// {"level":2,"time":1589371200000,"pid":3876,"message":"Hello World!"}
If the mergeBindings
property is false, all bindings will be grouped in a plain object under the bindings
property.
const logger = new Papyrus({
bindings: {pid: Deno.pid},
mergeBindings: false
});
logger.info("Hello World!");
// Outputs:
// {"level":2,"time":1589371200000,"bindings":{"pid":4309},"message":"Hello World!"}
Child loggers
Child loggers can be created from a Papyrus
logger by calling the child
method. The child
method requires options of type ChildOptions
that contains at least a name property.
A child logger inherits of all of its parentâs bindings.
const logger = new Papyrus({
bindings: {pid: Deno.pid}
}).child({
bindings: {host: "127.0.0.1"},
name: "myChildLogger"
});
logger.info("Hello World!");
// Outputs:
// {"level":2,"name":"childLogger","time":1589371200000,"pid":4022,"host":"127.0.0.1","message":"Hello World!"}
Options
PapyrusOptions
Papyrus
can be configured through an object that implements the interface PapyrusOptions
. All properties are optional.
interface PapyrusOptions {
bindings?: KeyValuePair;
destination?: DestinationOptions | DestinationOptions[];
enabled?: boolean;
formatter?: Formatter;
level?: Level | keyof typeof Level;
mergeBindings?: boolean;
mergePayload?: boolean;
name?: string;
time?: boolean;
useLabels?: boolean;
}
Property | Type | Default | Description |
---|---|---|---|
bindings |
KeyValuePair | { } | Object that contains the bindings |
enabled |
boolean | true | Disables the logger and its children when true |
formatter |
Formatter | undefined | Defines a Formatter to use |
level |
string | number | âtraceâ | Sets the min. level to a key of Level |
mergeBindings |
boolean | true | The bindings will be merged with Log if true |
mergePayload |
boolean | true | The payload will be merged with Log if true |
name |
string | undefined | The logs will include this name, which must be unique |
time |
boolean | true | The logs will not include the time when false |
transport |
TransportOptions | TransportOptions[ ] | [ ] | Defines a Transport , or an array or Transport to use |
useLabels |
boolean | false | The logs will be output with a numeric level when false |
ChildOptions
A ChildOptions
configuration object must be provided when creating a child logger.
interface ChildOptions {
bindings?: KeyValuePair;
name: string;
}
TransportOptions
A TransportOptions
configuration object is used when specifying a Transport
.
interface TransportOptions {
use?: Transport;
formatter?: Formatter;
}
Property | Type | Default | Description |
---|---|---|---|
use | Transport | undefined | Defines a Transport to use |
formatter | Formatter | undefined | Defines a Formatter to use |
Logging
Level enum
All levels are defined in a numeric enum.
enum Level {
trace, // Value: 0
debug, // Value: 1
info, // Value: 2
warn, // Value: 3
error, // Value: 4
}
Log methods and debug features
Instances of Papyrus
have five logging methods that one can use, each specific to a level. The methods are trace
, debug
, info
, warn
and error
.
const logger = new Papyrus();
logger.trace("Level is trace");
logger.debug("Level is debug");
logger.info("Level is info");
logger.warn("Level is warn");
logger.error("Level is error");
// Outputs:
// {"level":0,"time":1589371200000,"message":"Level is trace"}
// {"level":1,"time":1589371200000,"message":"Level is debug"}
// {"level":2,"time":1589371200000,"message":"Level is info"}
// {"level":3,"time":1589371200000,"message":"Level is warn"}
// {"level":4,"time":1589371200000,"message":"Level is error"}
Itâs possible to configure your logger so that it doesnât output logs below a given level. Letâs repeat the example of above with the level set to warn
.
const logger = new Papyrus({level: Level.warn});
logger.trace("Level is trace");
logger.debug("Level is debug");
logger.info("Level is info");
logger.warn("Level is warn");
logger.error("Level is error");
// Outputs:
// {"level":3,"time":1589371200000,"message":"Level is warn"}
// {"level":4,"time":1589371200000,"message":"Level is error"}
Only the logs with a level of warn
or above were output.
Note that we imported the level directly from the enum in this example. The configurations {level: "warn"}
and {level: 3}
would have given the same result.
Logging with payload
A payload is like bindings, except that itâs log-specific. A payload is typically found after the logâs message.
const logger = new Papyrus();
logger.info("Hello World!", {a: "A"});
// Outputs:
// {"level":2,"time":1589371200000,"message":"Hello World!","a":"A"}
If the mergePayload
property is false, all properties from the payload will be grouped in a plain object under the payload
property.
const logger = new Papyrus({mergePayload: false});
logger.info("Hello World!", {a: "A"});
// Outputs:
// {"level":2,"time":1589371200000,"message":"Hello World!","payload":{"a":"A"}}
Output
Route of a log
Internally, Papyrus
logs are defined as plain objects implementing Log
. The log is then sent to the formatter, which may return the log as a modified Log
object or as a JSON string. At this point, any log which is not already a string will be stringified using the JSON.stringify()
function. The string is finally sent to the transport(s) which will consume the log.
Papyrus
supports several transports, and each can have their own formatter.
Interfaces of a log
A log is transmitted to a formatter or a transport as an object implementing the Log
interface, which is a generic interface for any type of log, or as a JSON string representing this object. In formatters and transports, it can be interesting to differentiate messages from errors. Which is why Papyrus
is exporting the interfaces LogWithMessage
and LogWithError
.
All logs contain a level
property.
interface Log {
level: number | string;
name?: string;
time?: number | string;
bindings?: KeyValuePair;
message?: string | number | boolean;
error?: {
message: string;
name: string;
stack?: string;
};
payload?: KeyValuePair;
}
interface LogWithMessage {
level: number | string;
name?: string;
time?: number | string;
bindings?: KeyValuePair;
message?: string | number | boolean;
payload?: KeyValuePair;
}
interface LogWithError {
level: number | string;
name?: string;
time?: number | string;
bindings?: KeyValuePair;
error: {
message: string;
name: string;
stack?: string;
}
payload?: KeyValuePair;
}
Formatter
Whatâs a formatter?
A formatter is a class that implements the Formatter
interface. It can modify an object of type Log
, or convert it into a string. There are two kind of formatters:
- Editors: These formatters will alter, add or delete properties of
Log
. In principle, they should always return a object that implementsLog
, or a JSON representation of such object. - Prettifiers: These formatters will convert
Log
to a string formatted to be easier to read than a JSON string.
interface Formatter {
format: (data: string | Log, _v: number) => string | Log;
}
The _v
property represents the version of the Log
interface and will always be the same value for a given version of Papyrus
. is hidden and defines the version of the Log
interface. Should the Log
interface be updated in the future, the value of _v
will be incremented. For now, it is always 1
.
How to create a formatter?
Here is an example of a formatter that returns the log it receives as a Log
:
class IdentityFormatter implements Formatter {
public format(log: Log | string): Log {
if(typeof log === "string") return JSON.parse(log);
else return log;
}
}
Transport
Whatâs a transport?
A transport is a class that implements the Transport
interface. Itâs the final destination of the log. For example, it can:
- Send the log to the console
- Write the log into a file
- Save the log in a database
interface Transport {
log: (data: string, _v: number) => void;
}
How to create a Transport?
Here is an example of a transport sends the log to the console as a string:
class ConsoleTransport implements Transport {
public log(log: Log |string): void {
if(typeof log === "string") console.log(log);
else JSON.stringify(log);
}
}
Plugins
- Papyrus-File: A transport for writing into files
- Papyrus-Pretty: A formatter that prettifies your logs
Contributions
PRs are welcome!