How To Log Like A Boss With Node.js

Eren Yatkin
Nov 12 · 5 min read
Image for post
Image for post
Credits https://www.baileyjavinscarter.com/wp-content/uploads/2019/08/logging-accident-1030x429.jpg

Last couple weeks were tough for me. The reason behind the challenging and long working hours for weeks was launching the new version of a backend which had been developed for seven years. From day to day until this September, the codebase got bigger and bigger with every new features and their bug fixes. I joined this journey at July 2019 and put so much effort with my colleagues.

First, we split the application into two logical partition. One for customers and one for service providers. Then we decided to start this overhauling on “service providers” because business logic were heavy at this side. Then we split the codebase into microservices. More pressure, more things to ignore.

When they put more pressure C-level decided to ignore things. At the end of the day we had a monolithic backend with some of the features and bunch of microservices. As you can imagine things became complicated. But we had one thing to hold on to! That was our logging strategy. We logged every step. When a bug occurred, we just opened up CloudWatch logs and queried, believe me after five minutes or little less, the problem was right in your eyes.

How we did it

We are using Node.js as backend. When it comes to logging we have plenty options. They are all good but one of them is a masterpiece and that is Winston. I love Winston because it is easily configurable and community is good. It has nice documentation. You can log wherever you want with the transporters. Also you can create your custom transport. You can configure Winston individually for all the log levels from where and how to log.

But the best practice is decoupling data source from the code. We configured Winston to log nicely to console and we grabbed the whole outs with fluentd then populated the CloudWatch.

CloudWatch Log Insights is not a budget friendly service at all but it enables you to interactively search and analyze your log data. You can perform queries to help you more efficiently and effectively respond to operational issues. Queries can be saved and used to create a dashboard for your needs.

What do we need?

  • NodeJs^ 10.xx.x

I added Fluentd as a requirement but I will not cover how to install it or run it. Docs on AWS about FluentD and logging to CloudWatch is very good. Please follow this link for fluentd.

Let’s begin!

First, we need to create Node.js project and install the packages.

mkdir winston-logger && cd winston-logger 
npm init -y #this will skip prompts
npm install --save winston winston-cloudwatch winston-slack-webhook-transport
touch logger.js
touch index.js

We are ready to log! Winston provides levelled logs such as error, verbose, silly, info, debug. You can configure Winston individually for all the log levels from where and how to log. I just simply used info for test purposes but I will configure levels later on.

logger.js

const winston = require('winston');module.exports = winston;

index.js

const logger = require('./logger');
logger.info('Test Log', {});

If you run with node index.js you will get:

[winston] Attempt to write logs with no transports {"message":"Test Log","level":"info"}

Because we have not configured it. I added a variable called logger and created an instance. By default, Winston can log data into Console, File, Http. I choose File for now and will look into other transport options later on. You need to provide log level and file name. Verbose logging will log every level. If you want to log specific level, change the level to info, error or warn.

logger.js

const winston = require('winston');const logger = winston.createLogger({
new winston.transports.File({ level: 'verbose', filename: 'log.out' }),
],
});
module.exports = logger;

Let’s run our application. As you can see, there is a file called log.out in our project directory. Well we did it.

log.out

{"message": "Test Log","level": "info"}

Let’s make it more readable with just providing format. Also I added transport strategies for different log levels. As you can see, from now on we will log everything into console but only errors into file.

logger.js

const winston = require('winston');const logger = winston.createLogger({
format: winston.format.printf((info) => JSON.stringify(info, null, 2)),
transports: [
new winston.transports.File({ level: 'error', filename: 'log.out' }),
new winston.transports.Console({ level: 'verbose' }),
],
});
module.exports = logger;

Let’s run it, just as I said right? Now, I will add some default values and combine formats.

logger.js

const winston = require('winston');const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.printf((info) => {
let logData = { ...info };
if (info.error instanceof Error) {
logData.error = {
message: info.error.message,
stack: info.error.stack,
};
}
return JSON.stringify(logData, null, 2);
}),
),
transports: [
new winston.transports.File({ level: 'error', filename: 'log.out' }),
new winston.transports.Console({ level: 'verbose' }),
],
});
module.exports = logger;

index.js

const logger = require('./logger');
logger.info('Test Log info', { error: new Error('test') });

Let’s break it down, I combined the formats together using winston.format.combine. This functions take format options as arguments, you can use default functions such as timestamp(), printf(), simple(), colorize(). We can pass optional object to log but passing an Error will be empty. Because Error object doesn’t have its enumerable properties, that’s why it prints an empty object.

So, I checked the parameter type and changed it with stack of the error. When you run the code, output will be like below.

{
"error": {
"message": "test",
"stack": "Error: test\\n at Object.<anonymous> (C:\\\\Users\\\\Eren\\\\Desktop\\\\winston-logger\\\\index.js:114:39)\\n at Module._compile (internal/modules/cjs/loader.js:1137:30)\\n
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1157:10)\\n at Module.load (internal/modules/cjs/loader.js:985:32)\\n at Function.Module._load (internal/modules/cjs/loader.js:878:14)\\n at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)\\n at internal/main/run_main_module.js:17:47"
},
"level": "info",
"message": "Test Log info",
"timestamp": "2020-11-10 23:03:29"
}

Bonus

Also the final version of the logger.js. Do not forget to provide the keys for Slack and CloudWatch.

logger.js

const winston = require('winston');
const SlackHook = require('winston-slack-webhook-transport');
const WinstonCloudWatch = require('winston-cloudwatch');
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.printf((info) => {
let logData = { ...info };
if (info.error instanceof Error) {
logData.error = {
message: info.error.message,
stack: info.error.stack,
};
}
return JSON.stringify(logData, null, 2);
}),
),
transports: [
new winston.transports.File({ level: 'error', filename: 'log.out' }),
new winston.transports.Console({ level: 'verbose' }),
],
});
logger.add(
new SlackHook({
webhookUrl: 'SLACK.WEBHOOK_URL',
channel: '#SLACK.CHANNEL',
username: 'SLACK.USERNAME',
iconEmoji: ':warning:',
formatter: (info) => ({
text: `_${info.timestamp}_\\n*${info.level.toUpperCase()}:*\\n>${info.message}`,
attachments: [
{
text: `\\`\\`\\`${JSON.stringify(logData, null, 2)}\\`\\`\\``,
},
],
}),
level: 'error',
}),
);
logger.add(
new WinstonCloudWatch({
logGroupName: 'CLOUD_WATCH_LOG_GROUP',
logStreamName: 'CLOUD_WATCH_STREAM_NAME',
level: 'verbose',
messageFormatter: (info) => JSON.stringify(logData, null, 2),
}),
);
module.exports = logger;

So we reached at the end, you can now log like a boss. Having ability to log is always good when it comes to hunting bug. I hope this configuration helps you. Let me know if you have any questions or recommendations in the comments. You can follow me for more contents like this.

Stay tuned!

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store