[Node.js] Logs in Local Timezone on Morgan and Winston

HsuCheng Tseng
Frontend Weekly
Published in
4 min readJan 22, 2018

We want our solid logging system including stdout, the access.log for each API request, and the ap.log at the same time. After setting all these up, an annoying thing emerge — they all based on UTC+0, not according to the local timezone. This article will walk through Morgan.js and Winston.js, then attempt to solve this issue by adding some simple configuration. The dependencies versions use in this post are morgan-1.8.1 and winston-3.0.0-rc.

Morgan

Morgan is the default Express logger middleware that helping us preserve access log. The basic setup is very simple with nearly zero configurations needed.

Basic Usage

Because Morgan had been extracted from the main codebase of Express, we must add the dependency manually.

npm install --save morgan

Then add these lines to your node.js code. The parameter combined here is one of the predefined log formats.

const morgan = require('morgan');
const accessLogStream = fs.createWriteStream(
path.join(__dirname, 'logs', 'access.log'),
{ flags: 'a' }
);
app.use(morgan('combined', { stream: accessLogStream }))

Now, all access log will be save to __dirname/logs/access.log.

Custom Format

This is how to define a custom format. The string start with a : mark is a token, by using several predefined token, we can easily build a customized log format depending on needed.

morgan.format('myformat', '[:date[clf]] ":method :url" :status :res[content-length] - :response-time ms');app.use(morgan('myformat', { stream: accessLogStream }));

Sometime we want more beside predefined token. The following is how to add a custom token into our first custom format. In this example, we tend to add a pid, which represent a de-identification user alias.

morgan.token('pid', (req, res) => {
return _.get(req, 'session.user.pid') || 'Guest';
});
morgan.format('myformat', ':pid - [:date[clf]] ":method :url" :status :res[content-length] - :response-time ms');
app.use(morgan('myformat', { stream: accessLogStream }));

Deal with Timezone

This article will use a lib called moment-timezone here to transform timezone.

const moment = require('moment-timezone');console.log(moment().tz(yourTimezone).format());

Now we gonna specify an new date token to replace the old one. Since we already know how to build a custom token, so it won’t be too difficult. The last thing we need to do is passing the timezone information to our newly generated custom token like :date[yourTimezone], it’ll be retrieve as the third argument in the callback. The following is what the final setup look like.

morgan.token('date', (req, res, tz) => {
return moment().tz(tz).format();
})
morgan.format('myformat', '[:date[Asia/Taipei]] ":method :url" :status :res[content-length] - :response-time ms');
app.use(morgan('myformat', { stream: accessLogStream }));

P.S. The correct timezone string can be query this way on your node.js server.

$ node
> Intl.DateTimeFormat().resolvedOptions().timeZone
'Asia/Taipei'

Winston

Winston is a logger library similar to log4j, it supports priority logging by giving different logging level. It is been designed for store your logs to different destinations based on its transports abstract layer.

Basic Usage

This is the basic setup, with this has been set up, it will preserve the log string to both your console and your file in __dirname/logs/ap.log. In this example, if you calling it like logger.info('hello log'), it will output an info object {"message":"hello log","level":"info"}.

const { createLogger, transports } = require('winston');const logger = createLogger({
level: 'info',
transports: [
new transports.Console(),
new transports.File({
filename: path.join(__dirname, 'logs', 'ap.log'),
})
]
});

Now we want to add a custom output format on it by modifying several lines. We can use the additional built-in tokens provided by Winston, to include label and timestamp.

const { createLogger, format, transports } = require('winston');
const { combine, label, timestamp, printf } = format;

const myFormat = printf(info => `${info.timestamp} [${info.level}]: ${info.label} - ${info.message}`);
const logger = createLogger({
level: 'info',
format: combine(
label({ label: 'main' }),
timestamp(),
myFormat
),
transports: [
new transports.Console(),
new transports.File({
filename: path.join(__dirname, 'logs', 'ap.log'),
options: { flags: 'a', mode: 0o666 }
})
]
});

And the output will look like this.

2018-01-21T12:43:42.064Z [info]: main - hello log

Here we must fix the same issue again, figuring out how to change the output to fit the correct timezone.

Custom Token

This is how to create a custom token on our own, each of the arguments are info object and options respectively.

const appendTimestamp = format((info, opts) => {
if(opts.tz)
info.timestamp = moment().tz(opts.tz).format();
return info;
});

This is the final setup.

const { createLogger, format, transports } = require('winston');
const { combine, label, printf } = format;
const myFormat = printf(info => `${info.timestamp} [${info.level}]: ${info.label} - ${info.message}`);const appendTimestamp = format((info, opts) => {
if(opts.tz)
info.timestamp = moment().tz(opts.tz).format();
return info;
});
const logger = createLogger({
level: 'info',
format: combine(
label({ label: 'main' }),
appendTimestamp({ tz: 'Asia/Taipei' }),
myFormat
),
transports: [
new transports.Console(),
new transports.File({
filename: path.join(__dirname, 'logs', 'ap.log'),
options: { flags: 'a', mode: 0o666 }
})
]
});

Finally we get the correct output we want.

2018-01-21T20:53:46+08:00 [info]: main - hello log

--

--