簡介
Morgan 是 Node.js Express 官方推薦 的 HTTP request logger middleware。
Morgan 是用來整理整個 HTTP 從接收 request 到發出 response 整個 HTTP request lifecycle 訊息並將原本存文字的訊息做 聚合(aggregation)處理,將原本過程產生多筆的純文字訊統整成有意義的資訊,並把重要訊息做分類及格式化處理,使之可以被處理成 JSON 等方便閱讀的資料格式。
沒有使用 Morgan 的話
首先會沒有明確的 HTTP request lifecycle 結束時間,在接收 request 要自己紀錄 logger.info('request received');,然後整個流程會在系統中的 middleware 中不斷流轉,會難以追蹤,且你會不知道是否有成功送出 response ,可能中途被 throw Error ,但是你仍然會看到「request received」等錯誤的語意。最後會變成自己寫「完整的 HTTP logging middleware」,監聽 res.on('finish') ,自己計算時間,自己規範欄位。本質上就是自己把整個 Morgan 的功能重寫一遍。
再來就是整個 header 的資訊是以純文字方式呈現且資訊量過大,有些要正規化,有些要做格式轉換,如果要自記處理會非常麻煩,變成要在每個 middleware 使用接收資料的 logger (紀錄系統 log 日誌的工具) 都要寫入要處理的 request 欄位資料,且每個 middleware 都有 request 和 response ,在不同流程要記錄不同欄位,不僅會讓程式碼變得複雜化,也很容易產生 copy-paste 汙染。
logger.info({
method: req.method,
url: req.url,
headers: req.headers,
});
使用 Morgan 之後
- Morgan Middleware 先攔截 request
- 記錄 req.method
- 記錄 req
- 記錄 req.url
- 記錄開始時間
- 設定 listener 監聽
res.on("finish")
或等價事件,然後取得:
- req.method
- req.url
- res.statusCode
- response time
- headers
Morgan 會在接收 request 後綁定 HTTP lifecycle
- request start
- response finish
- error / abort
再放行到下一個 middleware
當 response 準備送出時,Morgan 會:
- 計算 response time
- 讀取 res.statusCode
- 將所有資訊套入你設定的格式(如 combined)
保證 log 一定「一個 request 一筆紀錄」。
Morgan 會標準化欄位,例如:
- method
- url
- status
- response-time
- content-length
讓資料更容易可以被聚合,比較。
再來是 Morgan 可以使用標準化 token 來處理這些資料並標轉化欄位,變成容易處理的格式化資料,例如:
::method :url :status :response-time ms - :res[content-length]
每一個都是 token(Morgan 內建或自定義)
最後就是 Morgan 可插拔(stream)模式,在其函數可以接入 console ,或是其他 logger 工具來處理 Morgan 產生的資訊。
使用
基本使用方式
import express from "express";
import morgan from "morgan";
const PORT = 3000;
const app = express();
app.use(morgan("combined")); // 這種方式只會讓資訊以 console 的方式顯示
app.get("/", (req, res, next) => {
res.send("server is running.");
});
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
📦 內建格式(常用三種)
1. dev
開發最常用: app.use(morgan("dev"))
會顯示:
GET /api/users 200 12.4 ms - 217
2. combined
使用: app.use(morgan("comvined"))
最完整(與 Apache access log 同格式)
是 Apache access log 格式的複製版,它只包含
- remote-addr
- remote-user
- time
- request-line
- status
- content-length
- referrer
- user-agent
輸出:
127.0.0.1 - - [12/Feb/2025:10:29:03 +0000] "GET /api/login HTTP/1.1" 200 123 "-" "Mozilla/5.0..."
3. tiny
非常精簡
使用: app.use(morgan("tiny"))
GET / 200 3.439 ms - 13
4. 自定義資料格式
這部分細節較多,分開成幾個部分說明
首先要先了解一下 Morgan 的 toke 是什麼?
在這裡的 toke 其實是 Morgan 專為 HTTP request / response 中,取出某一個欄位值的『取值器』,如以下的例子:
morgan(':method :url :status :response-time ms')
這裡的
:method:url:status:response-time ms
每一個都是 token ,Morgan 在 request 結束時,會把它們「換成實際值」:
GET /test 200 12.3 ms
知道 token 是什麼了,接下來就是了解有那些 toke 可以使用
Request 基本資訊(最常用)
| Token | 說明 | 範例 |
| ----------------- | ----------------- | -------------------- |
| :method | HTTP method | `GET` |
| :url | 原始 URL | `/api/users?page=1` |
| :status | HTTP status code | `200` |
| :http-version | HTTP 版本 | `1.1` |
| :response-time | 回應時間(ms) | `12.345` |
📌 :response-time
只有 response 完成後才有
單位是 毫秒(ms)
Client / Network 相關
| Token | 說明 | 範例 |
| -------------- | -------------------------- | --------------------- |
| :remote-addr | client IP | 127.0.0.1 |
| :remote-user | HTTP auth user(幾乎不用) | - |
| :user-agent | User-Agent | Mozilla/5.0... |
| :referrer | Referrer | https://example.com |
📌 實務提醒
如果有使用 有
- reverse proxy(Nginx / Cloudflare / ELB)
- Docker
- Kubernetes
- CDN
真實 client IP 通常要搭配 x-forwarded-for(自訂 token)才能取的,不然的話很大可能會只取得上游的 Private IP ,如: 192.168.3.1 、 172.16.31.3 之類的私有 IP
經過多層代理,x-forwarded-for 結構可能會是這樣:
x-forwarded-for: 203.0.113.42, 10.0.0.1, 172.16.0.1
不過使用上需要對 node.js 程式入口做調整,讓 node.js 可以處理 x-forwarded-for 資料,我直接把說明用註解的方式放在範例程式碼:
// app.ts
// ✅ 第 1 步:告訴 Express「我有在代理後面」
app.set('trust proxy', true);
// 這一步讓 Express:
// 願意相信 x-forwarded-for
// 正確處理 req.ip
// ✅ 第 2 步:定義一個 Morgan 自訂 token
import morgan from 'morgan';
morgan.token('client-ip', (req) => {
const xff = req.headers['x-forwarded-for'];
if (typeof xff === 'string') {
// 多層代理時,取第一個
return xff.split(',')[0].trim();
}
// 沒有代理時,退回 socket IP
return req.socket.remoteAddress || '';
// ✅ 第 3 步:在 format 裡使用這個 token
app.use(
morgan(':client-ip :method :url :status :response-time ms')
);
// 可以在近一步把資料格式整理成 JSON 格式
morgan.format('http-json', (tokens, req, res) => {
return JSON.stringify({
ip: tokens['client-ip'](req, res),
method: tokens.method(req, res),
url: tokens.url(req, res),
status: Number(tokens.status(req, res)),
responseTimeMs: Number(tokens['response-time'](req, res)),
});
});
app.use(morgan('http-json'));
});
Response 相關
| Token | 說明 | 範例 |
| --------------- | ------------------- | ---------------------- |
| :res[header] | 讀 response header | :res[content-length] |
| :status | response status | 404 |
📌 Request Header 常用用法
| Token | 說明 |
| ------------------ | ---------------- |
| :req[user-agent] | 同 :user-agent |
| :req[host] | Host |
| :req[accept] | Accept |
⚠ 安全提醒
- authorization
- cookie
不建議直接記(需遮罩),有洩漏隱私風險
時間相關
| Token | 說明 | 範例 |
| ------------- | --------------------- | -------------------------------- |
| :date | 當下時間(預設格式) | Sat, 12 Dec 2025 10:20:30 GMT |
| :date[clf] | Common Log Format | 12/Dec/2025:10:20:30 +0000 |
| :date[iso] | ISO 8601 | 2025-12-12T10:20:30.123Z |
內建 format 專用(間接 token)
| Token | 用途 |
| ----------------------- | --------------- |
| :method | dev / combined |
| :url | dev / combined |
| :status | dev / combined |
| :response-time | dev |
| :res[content-length] | combined |
5. 將 Morgan 的 token 資料整理成 json 字串,讓 Winston 可以用 json 格式化資料紀錄
‼ 重要
因為 Morgan 只能輸出純字串資料,所以在最外面要到一層 JSON.stringify() 將 javascript object 資料轉換成字串資料,才能被正確輸出。
在知道 token 是什麼了之後如果不做處理直接輸出,會變成一堆只有數值的字串:
GET /test 200 12.3 ms
可以用 Morgan 的自定義輸出處理函式來將原始資料做 條件判斷、欄位清洗、遮罩時 等,記住:最終只能輸出字串資料,不然會變成 [object, obtect]
import morgan from 'morgan';
morgan.format('http-json', (tokens, req, res) => {
return JSON.stringify({
type: "http", // 用於表示 log 的來源
remoteAddr: tokens['remote-addr']?.(req, res) ?? "-",
method: tokens.method?.(req, res) ?? "-", // ?. ??
url: tokens.url?.(req, res) ?? "-",
status: Number(tokens.status?.(req, res) ?? 0),
contentLength: tokens.res?.(req, res, 'content-length') ?? "-",
responseTimeMs: Number(tokens["response-time"]?.(req, res) ?? 0),
timestamp: new Date().toISOString(),
});
});
// 使用
app.use(morgan('http-json'));
// 在傳入 winston 前轉成 object
import { logger } from './logger';
app.use(
morgan('http-json', {
stream: {
write: (message) => {
// message 本身就是 JSON 字串轉型成 object 給 logger 接收
logger.info(JSON.parse(message));
},
},
})
);











