See all blog posts ↑

Building a foolproof logger lib with Node.js

Chopper Lee on

Build a logger lib with Node.js for good error tracking.

When building a big Node.js app, as it gets bigger and you add more components, you ought to consider integrating a logging system to trace events on each component or store error information for detection later. We’re working on several small projects as part of a larger contract with a big food & beverage client in China, so we found it important to have a trace log feature to keep an eye on things. Let’s explore writing a logger lib with Node.js for better project-sharing.

First, I need to define the lib’s features:

  • Configurability for multi log steams, such as file storage for dev and syslog for production.
  • logger template, pre-defined template log template will help different project’s component sharing neat log format easily.
  • logger level can be changed in the code.

Second, I need pick some tools because I don’t want to start from scratch:

  • bunyan and winston are both very popular logging frameworks. The biggest difference is bunyan uses the JSON format, which makes logging big pieces of data more direct. Due to its simpler use for implementing the logger template, I’ll use bunyan.
  • bunyan-syslog is a plugin lib, which can help transfer logging data to Syslog. It works fine on my Mac, but watch out for your OS and version of Node.js, because it doesn’t support everything.
  • Also, for implementing logger template feature, I need an object validator lib for defining the logger schema. I use JOI lib, which has complete methods and active project management.

Let’s begin writing the lib.

Here is my lib’s bunyan-logger, you can see the details and ask questions there.

First step, write a steam factory for getting the logger steam configuration.

const bsyslog = require('bunyan-syslog');
exports.syslog = (opt) => {
  return {
    type: 'raw',
    stream: bsyslog.createBunyanStream({
      host: opt.syslogHost || 'localhost',
      port: parseInt(opt.syslogPort || 514, 10),
      facility: bsyslog.facility.local0,
      type: 'sys'
    })
  };
};
exports.file = (opt) => {
  return {
    path: opt.filePath || './app.log'
  };
};

Next, implement the logger instance code.

const LOGGER = Symbol();
class BunyanLogger {
  this.streams = BunyanLogger.setStreams(opt);
  this[LOGGER] = bunyan.createLogger({
    name: opt.name || 'APP',
    streams: this.streams
  });
}

I recommend using the ES2015 class syntax. With it, I can clearly define a logger class with a static or not static method, a constructor, and a getter-setter method.

Define the logger template for the last step. After setting up some log schema, I can use them to format and verify the raw data with the JOI lib:

formatted(payload) {
  let httpSchema = Joi.object().keys({
    type: Joi.string(),
    direction: Joi.string(),
    actionType: Joi.string(),
    target: Joi.string().required(),
    targetModule: Joi.string(),
    transactionId: Joi.string(),
    appId: Joi.string(),
    statusCode: Joi.number().allow(null)
  });
}
switch (this.format) {
    case 'request':
      Joi.validate({
        type: 'HTTP',
        direction: 'IN',
        actionType: 'REQ',
        target: payload.method + ':' + payload.originalUrl,
        targetModule: payload.targetModule.toUpperCase(),
        transactionId: payload.transactionId,
        appId: payload.appId,
        statusCode: payload.statusCode
      }, httpSchema, (err, value)=> {
        if (err) {
          throw err;
        }
        formatedLog = value;
      });
      break;
    case 'response':
      Joi.validate({
        type: 'HTTP',
        direction: 'OUT',
        actionType: 'RESP',
        target: payload.method + ':' + payload.originalUrl,
        targetModule: payload.targetModule.toUpperCase(),
        transactionId: payload.transactionId,
        appId: payload.appId,
        statusCode: payload.statusCode
      }, httpSchema, (err, value)=> {
        if (err) {
          throw err;
        }
        formatedLog = value;
      });
      break;
    case 'error':
      Joi.validate({
        type: 'ERROR',
        targetModule: payload.targetModule.toUpperCase(),
        message: payload.message,
        code: payload.code
      }, errSchema, (err, value)=> {
        if (err) {
          throw err;
        }
        formatedLog = value;
      });
      break;
    default:
      formatedLog = {
        type: 'MESSAGE',
        message: payload.message
      };
      break;
  }
  return formattedLog;
}

Test the code with mocha

Testing the code is not only useful for checking whether the lib’s features work correctly or not, but also a good way to show how to use the lib.

For my lib, I’ll run these commands:
- test/logger.js, check creating logger instance
- test/logger.stream.js, check setting up different logger stream
- test/logger.template.js, check using different template for raw log data

I hope this helps anyone wanting to try building their own lib. Give it a shot and let us know how you did over Twitter or email!