Logging in Node.js! Why, How and What?
Ever heard of "Production is down!". The moment you heard that, the only thought that would've come to your mind is - My day is now ruined We need to fix it and quickly.
In this post, I'll explain What is logging? How to setup logging? and What data to log? through fastify (opens in a new tab), express (opens in a new tab) as the server framework and pino (opens in a new tab) as the logging framework for Node.js.
Logging
Logging is a technique that allows anyone to see and observe what's going on inside a service or application whether it's a server, database or a process running in the background.
It also enables reporting of crashes or failures like server outage, so that we can quickly fix those issue by looking into those logs and finding the root cause.
How to log?
With fastify
, logging comes out-of-the-box (opens in a new tab) and it uses pino
under the hood. Here's how you can use it:
npm install fastify
const fastify = require("fastify")({
// this will enable the request logging
logger: true,
});
fastify.get("/", async () => "Ok");
fastify.get("/log", async (req, reply) => {
// custom log
req.log.info("this is a custom log message");
return "Ok";
});
fastify.listen({ port: 8080 });
Now run the server from the terminal
node server.js
And, visit http://localhost:8080 (opens in a new tab) in the browser and see the logs printing, or http://localhost:8080/log (opens in a new tab) to see the custom log message.
{"level":30,"time":1654844524714,"pid":17118,"hostname":"DESKTOP","msg":"Server listening at http://127.0.0.1:8080"}
{"level":30,"time":1654844542572,"pid":17118,"hostname":"DESKTOP","reqId":"req-1","req":{"method":"GET","url":"/","hostname":"localhost:8080","remoteAddress":"127.0.0.1","remotePort":45384},"msg":"incoming request"}
{"level":30,"time":1654844542577,"pid":17118,"hostname":"DESKTOP","reqId":"req-1","res":{"statusCode":200},"responseTime":4.297284999862313,"msg":"request completed"}
{"level":30,"time":1654844632191,"pid":17118,"hostname":"DESKTOP","reqId":"req-2","req":{"method":"GET","url":"/log","hostname":"localhost:8080","remoteAddress":"127.0.0.1","remotePort":45414},"msg":"incoming request"}
{"level":30,"time":1654844632192,"pid":17118,"hostname":"DESKTOP","reqId":"req-2","msg":"this is a custom log message"}
{"level":30,"time":1654844632192,"pid":17118,"hostname":"DESKTOP","reqId":"req-2","res":{"statusCode":200},"responseTime":0.5779439993202686,"msg":"request completed"}
However, If you are using express
(opens in a new tab) you need to install express-pino-logger
(opens in a new tab)
npm install express express-pino-logger
const app = require("express")();
const pino = require("express-pino-logger")();
// Enables logging by registering it as a middleware
app.use(pino);
app.get("/", (req, res) => {
res.send("Ok");
});
app.get("/log", (req, res) => {
// custom log
req.log.info("this is a custom log message");
req.send("Ok");
});
app.listen(3000);
By default
fastify
logs request and response separately unlike theexpress
which logs both at the same time
What to log?
For a server environment, having observability on the request <-> response life cycle is very crucial. Everything depends on this life cycle working flawlessly, if something happens in-between then we need to make sure that we can get an idea when things went wrong. Things that we could/should log are:
-
Incoming and Outgoing Request - To see from where we are receiving the request and To see whether we are sending the correct response. If you are working with
fastify
orexpress
+express-pino-logger
, then request logging will be configured automatically for you. -
Information - These are useful when you are debugging something like a webhook for payment service or anything that falls outside of request-response lifecycle.
-
Unhandled Errors - These are reasons behind the most server crashes and most developers choose not to care about these. In Node.js, these errors are commonly generated by unhandled promises (opens in a new tab) but adding the following code should be enough to handle those errors.
process.on("unhandledRejection", (err) => {
// Here we can just use `console` instead of `pino`
console.error(err);
process.exit(1);
});
What not to log?
But, don't go and start logging everything you find. There are certain precautions we have to take to do logging in an ethical manner and protect your user's data.
- Remove Sensitive Headers - Headers like
Authorization
,Cookie
,token
etc. always contains sensitive information i.e., access tokens, secret keys, etc. which can easily compromise user data and privacy. Therefore these headers should not be logged unless absolutely necessary. To achieve this we can use use pino'sredaction
(opens in a new tab) api:
const fastify = require("fastify")({
// We can also use the pino's configuration
// https://github.com/pinojs/pino/blob/master/docs/api.md#pinooptions-stream
logger: {
redact: [
"req.headers.authorization",
"req.headers.token",
"req.headers.cookie",
],
},
});
fastify.get("/", async () => "Ok");
fastify.listen({ port: 8080 });
With express
const app = require("express")();
const pino = require("express-pino-logger")({
redact: [
"req.headers.authorization",
"req.headers.token",
"req.headers.cookie",
],
});
app.use(pino);
Now, those header will be removed before everything gets logged.
- Don't log the response body - Response body should be treated as a contract between the server and the client. No other function or service should be able to see them. By default,
pino
doesn't logs the response body so we can rest easy.
Best Practices
- Use
pino-pretty
(opens in a new tab) in development to have pretty log message instead of printing json.
npm install -D pino-pretty
{
// ...
"scripts": {
"start": "node server.js",
"dev": "node server.js | pino-pretty"
}
}
-
Use log management services like AWS Cloudwatch (opens in a new tab) or Azure Monitor (opens in a new tab) to monitor your logs. They provide tools to query and manipulate your logs to exactly find what you are looking for.
-
Use appropriate log codes like info, warn, error etc. to have a better understanding of the log messages and also allows you to filter them easily.
-
Use
stdout
orstderr
(opens in a new tab) for the logs destination as these are standard in all linux server environments. Most logging framework and cloud services are already configured to use these by default. -
Make sure to checkout the full documentaion: fastify (opens in a new tab) and pino (opens in a new tab)