For a Node.js web server written using express.js framework, we will explore how to implement structured logging as JSON data overall. We will see how to convert Apache combined logging message format for requests to JSON data as well as for custom log messages.
The project structure will be as follows.
jsloop-server
├── log-archive
│ └── log-2025-11-29.json
├── logs
│ ├── error.json
│ └── logs.json
├── node_modules
├── package-lock.json
├── package.json
└── src
├── controller
│ └── main.js
├── server.js
└── service
├── db.js
├── logger.js
└── middleware
└── logger-mw.js
We will use winston for general logging and morgan for web request logging. For log rotation we will use winston-daily-rotate-file package. Install all these using the below command.
npm install winston morgan winston-daily-rotate-file --save
// logger.js
const { createLogger, transports, format } = require("winston");
const DailyRotateFile = require('winston-daily-rotate-file');
const path = require("path");
const logger = createLogger({
level: "info",
format: format.combine(
format.timestamp({format: "YYYY-MM-DD HH:mm:ss"}),
format.errors({ stack: true }),
format.splat(),
format.json()
),
transports: [
new transports.File({
filename: path.join(__dirname, "../../logs/error.json"),
level: "error"
}),
new transports.File({
filename: path.join(__dirname, "../../logs/logs.json")
})
]
});
if (process.env.NODE_ENV !== "production") {
logger.add(
new transports.Console({
format: format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.json()
)
})
);
}
logger.add(new DailyRotateFile({
dirname: "log-archive",
filename: "log-%DATE%.json",
datePattern: "YYYY-MM-DD",
maxFiles: "14d"
}));
module.exports = logger;
Here we specify the logging format for winston. We log errors to error.json and all logs to logs.json. Logs will be present under logs folder under the root directory. For development, we display JSON logs in the console as well. Finally we specify log rotation configuration. Rotated logs will be present under log-archive under the root directory.
// logger-mw.js
// Middleware to convert apache combined logging format to json format for requests logged by morgan.
const morgan = require("morgan");
const log = require("../logger");
morgan.token("remote-addr", (req) => req.ip || req.connection.remoteAddress);
morgan.token("http-version", (req) => req.httpVersion);
const jsonFormat = (tokens, req, res) => {
return JSON.stringify({
method: tokens.method(req, res),
url: tokens.url(req, res),
status: Number(tokens.status(req, res)),
contentLength: Number(tokens.res(req, res, "content-length")) || 0,
referrer: tokens.referrer(req, res),
userAgent: tokens["user-agent"](req, res),
remoteAddress: tokens["remote-addr"](req, res),
httpVersion: tokens["http-version"](req, res),
responseTimeMs: Number(tokens["response-time"](req, res)),
});
};
const morganMiddleware = morgan(jsonFormat, {
stream: {
write: (message) => {
try {
// message is JSON string from jsonFormat
const data = JSON.parse(message);
log.info(data); // Winston gets structured fields
} catch (e) {
// Fallback in case of error.
log.info({ raw: message.trim() });
}
}
}
});
module.exports = morganMiddleware;
This is an express.js middleware that converts apache combined log messages from morgan to JSON format.
// server.js
const express = require("express");
const app = express();
const router = express.Router();
const morganMW = require("./service/middleware/logger-mw");
const mainCtrlr = require("./controller/main");
const log = require("./service/logger");
const port = process.env.PORT || 3000;
app.use(morganMW);
router.get("/health", mainCtrlr.health);
app.use("/", router);
app.listen(port, () => {
log.info(`Server started on port ${port}`);
});
Here we register the middleware in the app using app.use() method. This will now log request in JSON format. We can do application level logging using the log.info() and similar methods.
Sample log messages are given below.
{"level":"info","message":"Server started on port 3000","timestamp":"2025-11-29 01:44:48"}
{"level":"info","message":"check db access: 1","timestamp":"2025-11-29 01:44:48"}
{"level":"info","message":{"contentLength":29,"httpVersion":"1.1","method":"GET","remoteAddress":"::1","responseTimeMs":1.735,"status":200,"url":"/health","userAgent":"curl/8.7.1"},"timestamp":"2025-11-29 02:11:33"}