clay-log

An isomorphic logging module for Clay projects

Usage no npm install needed!

<script type="module">
  import clayLog from 'https://cdn.skypack.dev/clay-log';
</script>

README

Clay Log

An ismorphic logging module for Clay projects built around PinoJS

Why use Clay Log?

The purpose of this logging module is to wrap the instantiation of Pino for your Clay projects so that it can be used in multiple environments and output logs for the both local debugging (pretty printing) and log aggregators (such as the ELK stack). Use cases include:

  • Server-side
    • Amphora
    • Amphora Renderers (Amphora HTML)
    • Amphora plugins (Amphora Search)
    • Clay-related microservicestog
    • model.js files
  • Client-side
    • Kiln
    • Kiln plugins (Clay Space Edit)
    • model.js files

Setup

Instantiate the clay-log module.

const clayLog = require('clay-log');

var log = clayLog.init({
  name: 'amphora'
});

logger('info', 'some cool message', { additional: 'info' });

Want to attach some piece of data to every log? Use the meta property. It accepts an Object, like so:

var log = clayLog.init({
  name: 'amphora',
  meta: {
    important: 'information'
  }
});

Maybe you're wanting to get a new instance of a logger with its own associated metadata? Use the meta method. It accepts an Object as an argument and will spawn a new logger.

var loggerOne = clayLog.init({
  name: 'amphora',
  meta: {
    important: 'information'
  }
});

var loggerTwo = clayLog.meta({ another: 'piece of info' });

loggerOne('info', 'some cool message', { additional: 'info' });
loggerTwo('info', 'some different cool message');

If you'd like human-readable logs, Use the pretty property.

var loggerOne = clayLog.init({
  name: 'some-cli-tool',
  pretty: true
});

If you're using the module in a command-line tool, it's useful to be able to specify where the logs should output to. By default, they will output to stdout, but this can be changed by the output property.

var loggerOne = clayLog.init({
  name: 'some-cli-tool',
  output: process.stderr
});

Usage

Once you have a logging instance you're free to begin logging. Logging levels are the same as the default PinoJS levels:

  • info
  • trace
  • debug
  • warn
  • error

To use simply do the following:

var loggingInstance = clayLog.init({
  name: 'coolClayProject',
  meta: {
    important: 'information'
  }
});

loggingInstance('info', 'my cool message!', { additional: 'info' });

The arguments are in the following order:

  1. Logging level
  2. Message (String)
  3. Accompanying log information (Object)

Safety Considerations :warning:

The safest way to use the third position argument (data) is to wrap an object rather than pass an object directly. The object provided should have enumerable properties.

Passing an object directly may result in a thrown error (if it's not really an object).

const obj = 'string';
log('info', 'this is unsafe', obj);  // throws

Passing an object directly may result in mutation. Be mindful of this when utilizing plug-ins.

const obj = { key: 'value' };
log('info', 'this is unsafe', obj);
obj._label // 'INFO'

These are safer ways to provide the data argument.

log('info', 'this is safer', { obj });
log('info', 'this is safer', { ...obj });
log('error', 'this is safer', { stack: err.stack });

Setting Minimum Log Level

For production instances it may not be necessary to log the same messages you do in dev environments. By default, Clay Log will only display info and above level logs, but this can be configured with an environment variable. Set process.env.LOG to a corresponding log level and that will be the minimum level that appears.

Errors

Want to be lazy? If you pass in an Error object as the only argument and everything will get formatted properly OR just pass an Error as a second argument and you'll be good.

var loggingInstance = clayLog.init({
  name: 'coolClayProject',
  meta: {
    important: 'information'
  }
});

loggingInstance(new Error('oh no!'));
loggingInstance('error', new Error('oh no!'));

_label Property

This property on the output log message is meant to make the logs more human searchable when using grep or importing into an ELK-like tool. Rather than making people remember the association between level and the different levels meanings, we supply a human-readable property.

Environment Variables

Config Value Decription
LOG The minimum log level to log trace,debug,info,warn,error. [Default: info]
CLAY_LOG_PRETTY Set to a 'truthy' value to enable pretty-printing.
CLAY_LOG_PLUGINS Comma-delimited list of plug-ins to enabled. Plug-ins are applied in the order listed.
CLAY_LOG_PLUGINS_PATH Absolute path (or path relative to the CWD) for directory containing additional plug-ins.

Pretty Printing

If you don't pass in a pretty property, pretty printing will controlled by the CLAY_LOG_PRETTY environment variable. The logs will be printed in human readable form. Otherwise they will be regular PinoJS JSON strings.

Plug-ins

A "plug-in" for clay-log is a wrapper function that is executed just before pino.<level>.

Plug-ins can be used to:

  • Rename keys in JSON message.
  • Manipulate values in a JSON message.
  • Provide additional data in a JSON message.
  • Take additional action based on log level or other message attributes.

An empty or unset value for CLAY_LOG_PLUGINS will disable plugins (the default).

Additional plug-ins can be loaded from CLAY_LOG_PLUGINS_PATH.

:warning: :warning: :warning:

  • As plug-in code is potentially executed for every message logged, there is a performance penalty to using them. :warning:
  • Plug-ins will only execute on log levels greater-then-or-equal-to what is set in LOG=.
  • Plug-ins from CLAY_LOG_PLUGINS_PATH will take priority over core plug-ins, be mindful of any naming conflicts.
  • If a plug-in specified in CLAY_LOG_PLUGINS is not found a warning message will be logged but application execution will not halt.
  • Plug-ins are only supported server-side at the moment, they will not be loaded on the client.

Core Plug-ins

Heap Logging

If CLAY_LOG_PLUGINS includes "heap" the following additional heap statistics will be included:

{
    "does_zap_garbage": 0,
    "heap_size_limit": 0,
    "malloced_memory": 0,
    "number_of_detatched_contexts": 0,
    "number_of_native_contexts": 0,
    "peak_malloced_memory": 0,
    "total_available_size": 0,
    "total_heap_size": 0,
    "total_heap_size_executable": 0,
    "total_physical_size": 0,
    "used_heap_size": 0
}

Sentry Reporting

If CLAY_LOG_PLUGINS includes "sentry" all error level logs will be reported to Sentry.

:warning: This plug-in requires @sentry/node to be installed as a peer dependency and SENTRY_DSN to be set as an environment variable.

Writing Core Plug-Ins

A plug-in is a wrapper function that accepts two arguments:

  • data: The extra context object that will be included in a log.
  • msg: The string summary of the log message.

A plug-in can be used to enhance data with additional context or to take additional actions (like reporting errors to another service).

Here is an example plug-in called env that will add the NODE_ENV to every log line with the warn or error level:

# ./plugins/env.js

// This utility should be used in all plug-ins. It provides an abstraction
// around the process used to wrap functions.
const { wrap } = require('./_utils');

// This is where your plug-in code will be defined. Anything in this block is
// executed **before** clay-log logs the message.
function wrapper(data, msg) {
    data.env = process.env.NODE_ENV;
}

// The export of a plug-in will always use the format `wrap(<plug-in-func>, [<levels>])`.
// Omitting `[<levels>]` will apply the plug-in to all active log-levels.
module.exports = wrap(wrapper, ['warn', 'error']);

Writing Custom Plug-Ins

A "custom" plug-in is a clay-log extension that lives outside of the clay-log project. Use a custom plug-in when extending or modifying log data with needs that are specific to your use-case but not useful for a broader audience.

To write a custom plugin:

  1. Designate where you will store your plug-ins in your project, eg. ./utils/clay-log-plugins.
  2. Set the CLAY_LOG_PLUGINS_PATH environment variable to ./utils/clay-log-plugins.
  3. Write your plug-in code.
# ./utils/clay-log-plugins/rename.js

// The utility wrapper import path changes a bit when developing a custom plug-in.
const { wrap } = require('clay-log/plugins/_utils');

// This is where your plug-in code will be defined. Anything in this block is
// executed **before** clay-log logs the message.
function wrapper(data, msg) {
    data.message = data.msg;
    delete data['msg'];
}

// The export of a plug-in will always use the format `wrap(<plug-in-func>, [<levels>])`.
// Omitting `[<levels>]` will apply the plug-in to all active log-levels.
module.exports = wrap(wrapper, ['warn', 'error']);
  1. Set CLAY_LOG_PLUGINS=rename (or whatever custom plug-in names you've used).