Morgan is a popular used HTTP request logger for Node.js applications, especially those built with Express or similar frameworks. It provides a straightforward, consistent way to log incoming HTTP requests, capturing details like methods, URLs, response times, and status codes.
Morgan comes with predefined log formats—ranging from Apache-style combined logs to simple one-line entries—and also allows custom token-based formats for more fine-grained control.
In this guide, you’ll learn how to set up Morgan, integrate it with popular frameworks, refine your log output, and route logs to various destinations.
Prerequisites
Before you begin, ensure you have a recent version of
Node.js and npm
installed. This article also
assumes basic familiarity with building an Express application.
Setting up the project directory
In this section, you'll create a directory for a simple Express application and set up the necessary configurations to get started with logging using Morgan.
First, create a new directory for your project and navigate into it. Then, initialize a new Node.js project using npm
:
mkdir morgan-logging && cd morgan-logging
npm init -y
To use modern JavaScript features such as import
statements, enable ECMAScript Modules in your project by updating the package.json
file:
npm pkg set type=module
This command sets the type
field to module
in your package.json
, allowing you to use import
and export
syntax in your JavaScript files.
Next, install Express, a minimal and flexible Node.js web application framework:
npm install express
Open your preferred code editor and create an index.js
file in the root of your project directory with the following contents:
import express from "express";
const app = express();
const PORT = 3000
app.get("/", (req, res) => {
res.send("Welcome to the Home Page!");
});
app.get("/about", (req, res) => {
res.send("About Us");
});
app.listen(PORT, () => {
console.log("Server running on http://localhost:3000");
});
With that, you are now ready to proceed to the next section.
Why do you need Morgan?
Let’s start by testing our Express application without Morgan to understand the limitations of operating without a dedicated logging middleware. This demonstration will help illustrate why adding Morgan can significantly enhance your application's observability and debugging capabilities.
First, launch the server with:
node --watch index.js
Server running on http://localhost:3000
Next, open your browser and visit the following endpoints:
http://localhost:3000/
http://localhost:3000/about
After loading these pages, recheck your console. Notice that only the initial startup message is displayed:
Server running on http://localhost:3000
The output has no logs indicating that the endpoints were accessed. This lack of request visibility introduces several significant drawbacks:
- Without logs, you can’t see which routes are being accessed or how frequently
- Identifying and diagnosing issues becomes challenging without a record of incoming requests and their outcomes
- You have no data on response times or status codes, making assessing and improving performance hard.
- Suspicious activities or potential security threats go unnoticed without logging.
These limitations underscore the need for a logging solution like Morgan, which provides comprehensive and consistent logging of HTTP requests, improving your ability to monitor, troubleshoot, and secure your application.
Getting started with Morgan
In this section, you will enhance your existing Express application by adding Morgan. With Morgan, your application will gain the ability to log detailed information about incoming HTTP requests, making monitoring traffic and troubleshooting issues easier.
First, install Morgan on your machine:
npm install morgan
Next, update your index.js
file to use one of Morgan’s built-in formats. For example, let’s use the “combined” format, which closely resembles the Apache combined log style:
import express from "express";
import morgan from "morgan";
const app = express();
const PORT = 3000;
app.use(morgan("combined"));
app.get("/", (req, res) => {
res.send("Welcome to the Home Page!");
});
...
Upon saving the changes, Node.js will automatically restart the server. Revisit the following endpoints:
http://localhost:3000/
http://localhost:3000/about
You will now see more detailed log entries that include the status code, response size, and user-agent:
::1 - - [09/Dec/2024:10:04:38 +0000] "GET / HTTP/1.1" 304 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
::1 - - [09/Dec/2024:10:04:43 +0000] "GET /about HTTP/1.1" 304 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
These entries follow the Apache combined log format and provide comprehensive details such as the client’s IP address (::1
for local IPv6), timestamp, HTTP method and URL (GET /
), status code (304
), response size, referrer, and the User-Agent string.
This structured information provides valuable insights into how users interact with your application.
Understanding Morgan’s logging formats
Morgan provides several predefined log formats:
- combined: Similar to the Apache combined format; verbose and includes details like referrers and user-agents.
- common: A standard Apache common log output without referrers and user agents.
- dev: A concise color-coded output for development.
- short: A shorter version of the common format.
- tiny: The smallest output format, including basic request info and status code.
Switching formats is as easy as changing the string passed to morgan()
:
...
app.use(morgan("tiny"));
app.get("/", (req, res) => {
res.send("Welcome to the Home Page!");
});
...
Now visit the http://localhost:3000/
in your browser. You’ll see a simplified output such as the following in the terminal output:
GET / 304 - - 8.003 ms
This minimal format is easier to scan during local development, but less
comprehensive than combined
.
Customizing Morgan’s output
Morgan allows you to customize its log outputs to better suit your needs. In addition to using predefined log formats, you can create your own by defining custom format strings or even supplying a function that returns a log line.
Using custom tokens
Tokens are placeholders that Morgan substitutes with request-specific data at
runtime. Morgan includes built-in tokens such as :method
, :url
, :status
,
and :response-time
.
Creating custom tokens can significantly enhance the usefulness of your logs. One particularly valuable addition is a UUID (Universally Unique Identifier) for each request, ensuring that every log entry can be uniquely traced throughout your application.
To begin, install the uuid
package:
npm install uuid
Update index.js
to generate and log a UUID with each request:
import { v4 as uuidv4 } from "uuid";
...
const PORT = 3000;
app.use((req, res, next) => {
req.id = uuidv4();
next();
});
// Define a custom Morgan token for Request ID
morgan.token("id", (req) => req.id);
// Use Morgan with the custom token in the log format
app.use(
morgan(":id :method :url :status :res[content-length] - :response-time ms")
);
app.get("/", (req, res) => {
...
});
Now, visit the endpoints again:
http://localhost:3000/
http://localhost:3000/about
c13071fa-6f9e-490b-8c50-58f943eb9356 GET / 304 - - 2.924 ms
72fe0fd4-f7c0-496c-bb5a-d94f16c1a6c2 GET /about 304 - - 4.642 ms
Each log line now includes a unique UUID for every request, allowing you to trace individual requests through your application more effectively.
Using a custom format function
Building on the custom UUID token, you can achieve even greater control over your log output by defining a custom format function. Instead of relying on a predefined format string, you’ll supply a function that determines exactly how each log entry is structured. This level of customization is especially useful for producing machine-readable formats like JSON, which integrate smoothly with centralized logging systems.
To implement a custom format function, pass a logging function to Morgan that receives (tokens, req, res)
and returns a string. This approach gives you the flexibility to include any data you need and format it however you like:
...
morgan.token("id", (req) => req.id);
app.use(
morgan(function (tokens, req, res) {
return JSON.stringify({
requestId: tokens.id(req, res),
method: tokens.method(req, res),
url: tokens.url(req, res),
status: parseInt(tokens.status(req, res), 10),
responseTime: `${tokens["response-time"](req, res)} ms`,
});
})
);
...
With this configuration in place, your logs will now appear as JSON:
{"requestId":"b60ce6d8-9702-4b57-8f08-d158f10ea1f1","method":"GET","url":"/","status":304,"responseTime":"3.255 ms"}
{"requestId":"aa34c908-7aea-4b94-9e28-2c357e71bfc6","method":"GET","url":"/about","status":304,"responseTime":"1.708 ms"}
Producing JSON logs makes it easier for automated systems to parse and index your log data. This structured format is especially beneficial in production environments where logs are aggregated, analyzed, and monitored through centralized platforms.
Prettifying logs for development
When developing your application, having human-readable and color-coded logs can significantly improve the debugging experience. Morgan’s dev
format is handy during development, providing colorized output and highlighting essential details like status codes.
To enable the dev
format, adjust your code as follows:
...
const PORT = 3000;
app.use(morgan("dev"));
app.get("/", (req, res) => {
...
})
...
After refreshing the endpoint, you’ll see output similar to the following:
The dev
format uses colors to distinguish successful requests (green) from
errors (red) and so on, improving readability at a glance.
Centralizing and monitoring your logs
As your application grows, logs generated by different services, servers, and containers become scattered and difficult to analyze. Without a unified view, identifying issues, tracking performance trends, and maintaining security standards is time-consuming and error-prone. Centralizing your logs in a single platform transforms scattered data into actionable intelligence, making your application more resilient and high-performing.
Better Stack provides a great solution for centralizing and monitoring your Node.js logs. By directing Morgan’s structured, JSON-formatted logs to Better Stack, you gain the ability to:
- View all logs from your entire infrastructure in one place, eliminating the need to check multiple sources.
- Filter and query logs based on response times, status codes, or URLs
- Detect irregular patterns, suspicious activities, and errors before they escalate, maintaining a clear audit trail.
- Collaborate more effectively by sharing dashboards, alerts, and search results so everyone can respond with the same context.
You can route Morgan’s output to Better Stack using our Node.js client library, which requires a source token from Better Stack.
To get the source token, sign up for a free account with Better Stack. Once you’re logged in, navigate to the Sources link on the left and click the Connect source button:
Give your source a descriptive name, then select JavaScript • Node.js
under the logs section. Finally, click the Connect source button:
After the source is created, copy the Source token to your clipboard:
Now, update your application’s logging configuration to output JSON logs as shown earlier:
...
app.use((req, res, next) => {
req.id = uuidv4();
next();
});
morgan.token("id", (req) => req.id);
app.use(
morgan(function (tokens, req, res) {
return JSON.stringify({
requestId: tokens.id(req, res),
method: tokens.method(req, res),
url: tokens.url(req, res),
status: parseInt(tokens.status(req, res), 10),
responseTime: `${tokens["response-time"](req, res)} ms`,
});
})
);
...
To send these logs to Better Stack, install the Logtail client and integrate it with Morgan like this:
...
import { Logtail } from "@logtail/node"
...
const logtail = new Logtail("<your_better_stack_source_token>");
...
app.use(
morgan((tokens, req, res) => {
const entry = {
requestId: tokens.id(req, res),
method: tokens.method(req, res),
url: tokens.url(req, res),
status: parseInt(tokens.status(req, res), 10),
response_time: tokens["response-time"](req, res),
};
logtail.info("HTTP request", entry);
return null; // We return null so nothing prints to stdout
})
);
After saving your changes, the server will restart. Revisit the endpoints.
Return to Better Stack, and your source will update to show “Logs received!”. Click See Live tail to view your logs in real-time:
You’ll now see your logs appear within the Better Stack interface:
With that, you can centralize and monitor your logs using Better Stack
Final thoughts
This article focused on Morgan, a straightforward yet effective middleware for logging HTTP requests in Node.js applications. While Morgan is an excellent choice for enhancing HTTP request visibility, it’s not your only option.
For instance, if you’re already using a comprehensive logging library like Pino, you might consider integrating pino-http instead of Morgan to handle HTTP request logging. This approach consolidates your logging stack under a single tool, reducing complexity and overhead.
Thanks for reading, and happy logging!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for usBuild on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github