Versatile JSON Logger.
- JSON and Pretty Print log messages.
- Extend or alter logging schema to fit your needs.
- Customize built-in serializers by overwriting them to create your own logging schema.
- Middleware support.
- Allows full manipulation of output.
- Use in libraries and compose multiple Logality instances on the root project.
- Automatically detects the module filename and path and includes in the log.
👉 See how Logality compares to other popular loggers..
Install the module using NPM:
npm install logality --save
const Logality = require('logality');
const logality = Logality();
const log = logality.get();
log.info('Hello World!');
Logality requires to be initialized and configured once, then use the instance throughout your application.
You can configure Logality during instantiation, here are the available configuration options:
appName
{string} An arbitrary string to uniquely identify the service (logger instance).prettyPrint
{boolean|Object} If true will format and prettify the event and context, default isfalse
. You may define additional options to configure pretty printing, they can be combined:prettyPrint.noTimestamp
{boolean} Do not print timestamp.prettyPrint.noFilename
{boolean} Do not print Log filename source.prettyPrint.onlyMessage
{boolean} Only print the log message (no context).
minLevel
{number|string} Define the minimum level to be logged and ignore lower log levels. See log levels for input values, accepts both the string or numeric representations of the levels.serializers
{Object} You can define custom serializers or overwrite logality's. Read more about Serializers bellow.async
{boolean} Set to true to enable the asynchronous API for logging, see more bellow. Read more on the async option bellow.output
{Function(logContext:Object, isPiped:boolean)} Replace the output process of logality with a custom one. Read more on the custom output documentation bellow.
const Logality = require('logality');
const logality = Logality({
appName: 'service-something',
prettyPrint: false,
serializers: [(logContext) => {}],
async: false,
output: (logMessage) => {
process.stdout.write(logMessage);
},
});
- Message {string} The text (string) Log message input from the user.
- Context {Object} The Context (or bindings) input from the user.
- LogContext {Object} Log Context (the schema) used internally by logality for processing and ultimately output.
- LogMessage {String} The serialized
LogContext
into a string for output.
👉 Click here or on the Flow Chart for Full Resolution.
When logging has a transactional requirement, such as storing logs to a database or sending through an API, you can enable asynchronous mode.
When Async is enabled all logs should be prefixed with the await
keyword.
Both the middleware defined through use()
and the output function if defined will be expected to execute
asynchronously.
To enable the async API all you have to do is set the option async
to true. All logging methods will now return
a promise for you to handle:
const Logality = require('logality');
const logality = Logality({
appName: 'service-audit',
async: true,
});
/** ... */
async function createUser (userData) => {
await log.info('New user creation', {
userData,
});
}
The custom output function will receive two arguments and is the final operation in the execution flow. The input arguments are:
logContext
Object logContext is a native JS Object representing the entire log message.isPiped
boolean This argument indicates if the inbound "logContext" is the output of a piped instance or not (comes from a library).
Depending on what value is returned by your custom output function different actions are performed by Logality.
This is what you would typically want to always return. When an object is returned from your custom output function you pass the responsibility of serializing the Log Context into a string to Logality.
As per the Logality Flow Diagram, there are a few more steps that are done after your custom output returns an Object value:
- Logality checks your
prettyPrint
setting and:- If it's true will format your Log Context into a pretty formatted string message.
- If it's false will serialize using
JSON.stringify
.
- Logality will then output that serialized stream by writing to the
process.stdout
stream.
When you return a string, Logality will skip the serialization of your Log
Message and will directly invoke the output by writing to the process.stdout
stream.
This technique gives you the freedom to implement your own output format and/or create your pretty output formats.
When your custom output does not return anything, Logality will assume that you have handled everything and will not perform any further actions.
In those cases your custom output function is responsible for serializing the Log Context and outputting it to the medium you see fit (stdout or a database).
ℹ️ Note: This is the recommended way to apply filters on what messages you want to be logged.
To get a logger you have to invoke the get()
method. That method will detect and use the module filename that it was invoked from so it is advised that you use the get()
method in each module to have proper log messages.
const log = logality.get();
log(level, message, context);
The get()
method will return the log()
method partialed with arguments.
The full argument requirements of log()
, are:
logality.log(filename, level, message, context);`
When using get()
you will receive the logger function with the filename
argument already filled out. That is why you don't need to input the filename
argument when you are using logality.get()
.
The partialed and returned log
function will also have level helpers as
illustrated in "Log Levels" section.
Using any log level function (e.g. log.info()
), your first argument is the "message". This is any arbitrary string to describe what has happened. It is the second argument, "context" that you will need to put any and all data you also want to attach with the logging message.
log.info(message, context);
The context
argument is an object literal, parsed by what are called "Serializers". Serializers
will take your data as input and format them in an appropriate, logging schema
compliant output.
You may extend logality with new serializers or you may overwrite the existing ones.
Use pipe()
to link multiple logality instances to the root instance:
const Logality = require('logality');
const parentLogality = Logality();
const childLogality = Logality();
parentLogality.pipe(childLogality);
What this does is pipe all the output of the piped (child) logality instances to the "parent" Logality. This is particularly useful if a library is using Logality and you want to pipe its output or you want to have multiple classes of log streams (i.e. for audit logging purposes).
pipe()
Accepts a single Logality instance or an Array of Logality instances.
ℹ️ Note: The LogContext of the child instance, will go through all the middleware and custom output functions defined in the parent instance.
ℹ️ Note: This is the case when the second argument
isPiped
will have atrue
value.
You can add multiple Middleware that will be invoked after all the serializers are applied (built-in and custom defined) and before the "Write to output" method is called.
The middleware will receive the "Log Message" as a native Javascript Object and you can mutate or process it.
All middleware with use()
are synchronous. To support async middleware you have to enable the async
mode when instantiating.
const Logality = require('logality');
const logality = Logality();
logality.use((context) => {
delete context.user;
});
const Logality = require('logality');
const logality = Logality({
async: true,
});
logality.use(async (context) => {
await db.write(context);
});
Logality automatically calculates and formats a series of system information which is then included in the output. When you log using:
log.info('Hello World!');
Logality, when on production, will output the following (expanded) JSON string:
{
"severity": 6,
"level": "info",
"dt": "2018-05-18T16:25:57.815Z",
"message": "hello world",
"event": {},
"context": {
"runtime": {
"application": "testLogality"
},
"source": {
"file_name": "/test/spec/surface.test.js"
},
"system": {
"hostname": "localhost",
"pid": 36255,
"process_name": "node ."
}
}
}
severity
{number} Message severity expressed in an integer (7 lowest, 0 higher), see bellow fow values.level
{string} Message severity expressed in a unique string, see bellow fow values.dt
{string} An ISO8601 date.message
{string} Any message provided to the logger.event
{Object} When the log was triggered by an event, the metadata of that event are stored here. Logality supports many kinds of events as explained in the Serializers section.context
{Object} Context related to the log message.context.runtime.application
{string} Name of the service, define this when first instantiating the locality service.context.source.file_name
{string} The module where the log originated.context.system.hostname
{string} The local system's hostname.context.system.pid
{string} The local process id.context.system.process_name
{string} The local process name.
As per the Log Schema, the logging levels map to those of Syslog RFC 5424:
Syslog Level | Level Enum | Description |
---|---|---|
0 | emergency |
System is unusable |
1 | alert |
Action must be taken immediately |
2 | critical |
Critical conditions |
3 | error |
Error Conditions |
4 | warn |
Warning Conditions |
5 | notice |
Normal, but significant condition |
6 | info |
Informational messages |
7 | debug |
Debug-level messages |
Each one of the "Level Enum" values is an available function at the logger that is returned using the get()
method:
const Logality = require('logality');
const logality = new Logality();
const log = logality.get();
log.debug('This is message of level: Debug');
log.info('This is message of level: Info');
log.notice('This is message of level: Notice');
log.warn('This is message of level: warning');
log.error('This is message of level: Error');
log.critical('This is message of level: Critical');
log.alert('This is message of level: Alert');
log.emergency('This is message of level: Emergency');
Serializers are triggered by defined keys in the context
object.
Every serializer is configured to listen to a specific context key, for example
the user serializer expects the user
key in the context:
log.info('User Logged in', {
user: udo,
});
If no serializer is configured for the user
property, the data will be
ignored. Logality has implemented the following serializers out of the box:
Serializes a User Data Object.
// a user logged in
const user = login(username, password);
// Let log the event
log.info('User Logged in', { user: user });
id
The user's id.email
The user's email.
"context": {
"user": {
"id": 10,
"email": "[email protected]",
}
}
Serializes a Javascript Error Object or an Exception.
const err = new Error('Broke');
log.error('Something broke', { error: err });
A native JS Error Object, or similar:
name
{string} Name of the error.message
{string} The error's message.stack
{string} The stack trace. Logality will automatically parse the stack trace to a JSON object.
"event":{
"error":{
"name":"Error",
"message":"Broke",
"backtrace": "Stack Trace...",
}
}
Serializes an Express.JS Request Object.
function index(req, res) {
log.info('Index Request', { req: req });
}
Express JS Request Object.
"event":{
"http_request": {
"headers": {},
"host": "localhost",
"method": "GET",
"path": "/",
"query_string": "",
"scheme": "http"
}
}
event.http_request
{Object} When the request object is passed the following additional data are stored:event.http_request.headers
{Object} Key-value pairs of all the HTTP headers, excluding sensitive headers.event.http_request.host
{string} The hostname.event.http_request.method
{string} HTTP method used.event.http_request.path
{string} The request path.event.http_request.query_string
{string} Quer string used.event.http_request.scheme
{string} One of "http" or "https".
Serializes any data that is passed as JSON.
// Custom log
log.info('Something happened', {
custom: {
any: 'value',
},
});
Anything
"context": {
"custom": {
"any": "value"
}
}
You can define your own serializers or overwrite the existing ones when you first instantiate Logality. There are three parameters when creating a serializer:
- Context Name The name on your
context
object that will trigger the serializer. - Output Path The path in the JSON output where you want the serializer's value to be stored. Use dot notation to signify the exact path.
- Value The serialized value to output on the log message.
The Context Name is the key on which you define your serializer. So for instance when you set a serializer on the user key like so mySerializers.user = userSerializer
the keyword user
will be used.
Output Path and Value are the output of your serializer function and are expected as separate keys in the object you must return:
path
{string} Path to save the value, use dot notation.value
{*} Any value to store on that path.
An Example:
const Logality = require('logality');
mySerializers = {
user: function (user) {
return {
path: 'context.user',
value: {
id: user.id,
email: email.id,
type: user.type,
},
};
},
order: function (order) {
return {
path: 'context.order',
value: {
order_id: order.id,
sku_id: order.sku,
total_price: order.item_price * order.quantity,
quantity: order.quantity,
},
};
},
};
const logality = new Logality({
appName: 'service-something',
serializers: mySerializers,
});
In some cases you may need to write to more than one keys in the log context. To be able to do that, simply return an Array instead of an Object like so:
const Logality = require('logality');
mySerializers = {
user: function (user) {
return [
{
path: 'context.user',
value: {
id: user.id,
email: email.id,
type: user.type,
},
},
{
path: 'context.request',
value: {
user_id: user.id,
},
},
];
},
};
const logality = new Logality({
appName: 'service-something',
serializers: mySerializers,
});
This is the initializing module. During your application bootstrap and before
you anything else, you need to require this module and invoke the init()
function to initialize logality.
const Logality = require('logality');
const logger = (module.exports = {});
// Will store the logality reference.
logger.logality = null;
/**
* Initialize the logging service.
*
* @param {Object} bootOpts boot options. This module will check for:
* @param {string=} bootOpts.appName Set a custom appname for the logger.
* @param {WriteStream|null} bootOpts.wstream Optionally define a custom
* writable stream.
*/
logger.init = function (bootOpts = {}) {
// check if already initialized.
if (logger.logality) {
return;
}
const appName = bootOpts.appName || 'app-name';
logger.logality = new Logality({
prettyPrint: process.env.ENV !== 'production',
appName,
wstream: bootOpts.wstream,
});
// Create the get method
logger.get = logger.logality.get.bind(logger.logality);
};
Then, in any module you want to log something you fetch the logality instance from your logger service.
const log = require('../services/log.service').get();
/* ... */
function register (userData) => {
log.info('New user registration', {
userData
});
}
ℹ️ Note: You can view a real-world example of Logality being used in production in this Discord Bot Project.
Comparison table as of 16th of April 2021.
Logality | Winston | Bunyan | Pino | |
---|---|---|---|---|
JSON Output | ✅ | ✅ | ✅ | ✅ |
Pretty Print | ✅ | ✅ | ❌ | ✅ |
Custom Log Levels | ❌ | ✅ | ✅ | ✅ |
Serializers | ✅ | ❌ | ✅ | ✅ |
Middleware | ✅ | ✅ | ❌ | ✅ |
Mutate JSON Schema | ✅ | ✅ | ❌ | ❌ |
Output Destination | ✅ | ✅ | ✅ | ✅ |
Mutate Output | ✅ | ✅ | ❌ | ❌ |
Async Operation | ✅ | ❌ | ❌ | ❌ |
Filename Detection | ✅ | ❌ | ❌ | ❌ |
Speed Optimised | ❌ | ❌ | ❌ | ✅ |
Used in Libraries | ✅ | ❌ | ❌ | ❌ |
- Update the changelog bellow ("Release History").
- Ensure you are on master and your repository is clean.
- Type:
npm run release
for patch version jump.npm run release:minor
for minor version jump.npm run release:major
for major major jump.
- v3.1.3, 19 Nov 2021
- Will now safely JSON serialize BitInt values. Handles also edge case on pretty print.
- Updated all dependencies to latest.
- v3.1.1, 26 Sep 2021
- Removed emojis for UTF-8 chars and corrected formating of pretty print.
- v3.1.0, 26 Sep 2021
- Added new options for pretty print (noTimestamp, noFilename, onlyMessage).
- Added log level filtering.
- Added codecoverage report.
- Replaced figures package with emojis.
- Updated all dependencies to latest.
- v3.0.4, 31 May 2021
- Updated all dependencies to latest.
- Tweaked eslint and prettier configurations.
- v3.0.3, 16 Apr 2021
- Updated all dependencies to latest.
- Tweaked, fixed and updated README, added comparison chart with popular loggers.
- v3.0.2, 30 Oct 2020
- Updated all dependencies to latest.
- v3.0.1, 03 Jul 2020
- Updated all dependencies to latest.
- v3.0.0, 04 Apr 2020
- Introduced middleware for pre-processing log messages.
- Introduced the pipe() method to link multiple Logality instances together, enables using logality in dependencies and libraries.
- Breaking Change Replaced "wstream" with "output" to customize logality's output.
- v2.1.2, 24 Feb 2020
- Removed http serializer when pretty print is enabled.
- Replaced aged grunt with "release-it" for automated releasing.
- v2.1.1, 19 Feb 2020
- Added the "objectMode" configuration.
- Implemented multi-key serializers feature.
- Fixed async logging issues and tests.
- v2.1.0, 18 Feb 2020
- Added Async feature.
- v2.0.1, 18 Feb 2020
- Fixed issue with null http headers on sanitizer helper.
- v2.0.0, 29 Jan 2020 :: Extensible Serializers
- Enables new serializers and allows over-writing the built-in ones.
- Backwards compatible.
- v1.1.0, 05 Jun 2018 :: JSON Log Schema Version: 4.1.0
- Added
prettyPrint
option, thank you Marius.
- Added
- v1.0.0, 21 May 2018 :: JSON Log Schema Version: 4.1.0
- Big Bang
Copyright Thanasis Polychronakis Licensed under the ISC license