ExpressJS (TypeScript) – Logging 與 Debug:使用 Morgan 或 Winston
簡介
在 Node.js 與 Express 應用程式中,日誌(logging)是除錯、效能監控與問題追蹤的關鍵工具。沒有完整的日誌,開發者在本機除錯或在生產環境排錯時往往只能靠 console.log(),這不僅資訊量有限,也不易於後續分析與保存。
本單元將說明在 TypeScript 專案裡,如何使用兩套主流的日誌套件——Morgan(輕量的 HTTP 請求日誌)與 Winston(功能完整的通用日誌框架),讓你的 Express 應用在開發、測試與上線階段都能得到清晰、結構化且可擴充的日誌。
核心概念
1. 為什麼同時需要 Morgan 與 Winston?
| 功能 | Morgan | Winston |
|---|---|---|
| HTTP 請求紀錄(method、url、status、response time) | ✅ | ❌(需自行實作) |
| 多層級 Log(error、warn、info、debug) | ❌ | ✅ |
| 多種輸出目標(console、檔案、遠端服務) | ❌ | ✅ |
| 格式化、旋轉檔案、JSON 輸出 | ❌ | ✅ |
| 在 TypeScript 中提供型別安全 | ✅(有 @types/morgan) | ✅(有 @types/winston) |
結論:在大多數專案裡,Morgan 用於快速捕捉 HTTP 請求資訊,Winston 則負責應用層面的錯誤、業務資訊與系統事件。兩者結合,可達到「請求層 + 業務層」的完整日誌策略。
2. 在 TypeScript 中安裝與設定
# 安裝核心套件
npm i express morgan winston
# 安裝 TypeScript 型別定義(開發依賴)
npm i -D @types/express @types/morgan @types/winston ts-node typescript
Tip:若想使用每日旋轉檔案(log rotation),可以再安裝
winston-daily-rotate-file。
npm i winston-daily-rotate-file
npm i -D @types/winston-daily-rotate-file
3. Morgan 基本使用
3.1. 最簡單的範例
// src/middleware/morgan.ts
import morgan, { StreamOptions } from 'morgan';
import { Request, Response, NextFunction } from 'express';
// 直接輸出到 console
const morganMiddleware = morgan('combined');
export default morganMiddleware;
// src/app.ts
import express from 'express';
import morganMiddleware from './middleware/morgan';
const app = express();
// 先掛載 Morgan,確保每個請求都被紀錄
app.use(morganMiddleware);
combined 是 Morgan 內建的 Apache 風格 請求日誌,包含了遠端 IP、使用者代理、回應時間等資訊。
3.2. 自訂格式與寫入檔案
// src/middleware/morgan.ts
import morgan from 'morgan';
import fs from 'fs';
import path from 'path';
// 建立 logs 目錄(若不存在)
const logDirectory = path.join(__dirname, '../../logs');
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory);
// 建立寫入串流(每日一檔)
const accessLogStream = fs.createWriteStream(
path.join(logDirectory, 'access.log'),
{ flags: 'a' } // 以附加模式寫入
);
// 自訂格式:method url status response-time ms - user-agent
const format = ':method :url :status :response-time ms - :user-agent';
export const morganMiddleware = morgan(format, {
stream: accessLogStream,
});
重點:使用
fs.createWriteStream時,務必設定flags: 'a',避免每次啟動伺服器時覆寫舊日誌。
3.3. 依環境切換日誌等級
// src/middleware/morgan.ts
import morgan from 'morgan';
import { Request, Response } from 'express';
const devFormat = morgan('dev'); // 彩色、簡潔,適合開發
const prodFormat = morgan('combined'); // 完整資訊,適合上線
export const morganMiddleware = (req: Request, res: Response, next: Function) => {
if (process.env.NODE_ENV === 'production') {
return prodFormat(req, res, next);
}
return devFormat(req, res, next);
};
4. Winston 基本使用
4.1. 建立共用 logger
// src/logger.ts
import { createLogger, format, transports } from 'winston';
import 'winston-daily-rotate-file';
// 取得執行環境
const env = process.env.NODE_ENV || 'development';
// 共用的 Log 格式
const logFormat = format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.errors({ stack: true }), // 錯誤堆疊
format.splat(), // 支援 %s、%d 佔位符
format.json() // 輸出 JSON,方便 Log 分析平台
);
// 日誌傳輸(Console + DailyRotateFile)
const logger = createLogger({
level: env === 'production' ? 'info' : 'debug',
format: logFormat,
transports: [
// Console
new transports.Console({
format: format.combine(
format.colorize(),
format.printf(({ timestamp, level, message }) => {
return `${timestamp} ${level}: ${message}`;
})
),
}),
// 檔案(每日旋轉)
new transports.DailyRotateFile({
dirname: 'logs',
filename: 'app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
}),
],
});
export default logger;
說明:
winston-daily-rotate-file會自動在logs目錄下產生app-2024-11-25.log之類的檔案,並在 14 天後自動刪除。這對於長期運營的服務非常實用。
4.2. 在 Express 中使用 Winston
// src/middleware/logger.ts
import { Request, Response, NextFunction } from 'express';
import logger from '../logger';
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
// 記錄每一次請求的基本資訊
logger.info('HTTP %s %s - %s', req.method, req.originalUrl, req.ip);
next();
};
// 捕獲未處理的例外
export const errorLogger = (err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error('Error on %s %s: %s', req.method, req.originalUrl, err.stack);
res.status(500).json({ message: 'Internal Server Error' });
};
// src/app.ts
import express from 'express';
import morganMiddleware from './middleware/morgan';
import { requestLogger, errorLogger } from './middleware/logger';
const app = express();
// 先掛載 Morgan(HTTP 請求層)
app.use(morganMiddleware);
// 再掛載自訂的 Winston 請求 logger
app.use(requestLogger);
// ... 你的路由
app.get('/api/hello', (req, res) => {
res.json({ message: 'Hello World' });
});
// 捕獲錯誤
app.use(errorLogger);
export default app;
4.3. 使用不同 Log Level
// src/services/userService.ts
import logger from '../logger';
export async function getUser(id: string) {
try {
logger.debug('Fetching user with id=%s', id);
// 假設有 DB 操作
const user = await db.findUserById(id);
if (!user) {
logger.warn('User not found, id=%s', id);
return null;
}
logger.info('User retrieved, id=%s', id);
return user;
} catch (error) {
logger.error('Failed to get user, id=%s, error=%o', id, error);
throw error;
}
}
debug 只會在開發環境輸出;info、warn、error 則會在所有環境中寫入檔案,確保關鍵資訊不會遺失。
5. 常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方案/最佳實踐 |
|---|---|---|
把所有訊息都 console.log |
生產環境日誌混雜、難以搜尋、無法分級 | 使用 Winston 處理所有應用層訊息,保留 console 只作為開發階段的即時輸出 |
| 未設定檔案旋轉 | 日誌檔案無限制增長,磁碟空間耗盡 | 透過 winston-daily-rotate-file 或 logrotate 設定 size / time 旋轉 |
| 在高併發環境直接寫入同步檔案 | 事件迴圈被阻塞,導致效能下降 | Winston 內建非同步寫入;若自行使用 fs.appendFileSync 需改為 fs.appendFile |
| 在錯誤處理中再次拋出錯誤 | 造成無限迴圈或雙重錯誤訊息 | 在 errorLogger 中 只回傳一次,並確保不再呼叫 next(err) |
| 日誌中洩露機密資訊(如密碼、金鑰) | 安全風險、合規問題 | 使用 filter 或 redact 功能,僅保留必要欄位;在 format 中自行過濾 |
忘記在 package.json 加入 NODE_ENV=production |
開發環境設定流入生產,過度輸出 debug | 在部署腳本或容器設定中明確設定 NODE_ENV,並在程式碼中檢查 |
5.1. 建議的 Log 結構
{
"timestamp": "2024-11-25 14:32:10",
"level": "error",
"message": "Failed to get user",
"service": "user-service",
"requestId": "c1a2b3d4-5678-90ef-ghij-klmnopqrstuv",
"metadata": {
"userId": "12345",
"errorStack": "... stack trace ..."
}
}
使用 metadata(或 meta)欄位,可讓日誌平台(如 ELK、Graylog)自動索引,提升搜尋與分析效率。
6. 實際應用場景
微服務間的追蹤
- 為每個請求產生唯一的
requestId(可透過cls-hooked或async-hooks),在 Morgan、Winston 皆加入此欄位。日後透過requestId可以在分散式追蹤系統(如 Jaeger)中快速定位。
- 為每個請求產生唯一的
API 速率限制與監控
- 使用 Winston 記錄每次
rate-limit被觸發的 IP、路徑與時間,並在監控儀表板(Grafana)設定告警。
- 使用 Winston 記錄每次
異常警報
- 結合 Winston 與 winston-mail(或自建 Slack webhook)在
error級別時即時發送通知,讓 DevOps 團隊第一時間取得異常資訊。
- 結合 Winston 與 winston-mail(或自建 Slack webhook)在
符合 GDPR / PCI DSS 的日誌保留
- 設定 Winston 的
maxFiles為 90 天,並使用加密儲存(如 AWS KMS)確保敏感資訊被妥善保護。
- 設定 Winston 的
容器化部署(Docker / Kubernetes)
- 只保留 Console 輸出(Winston 的
Consoletransport),將日誌收集交給fluentd、filebeat等 side‑car,避免容器內部寫入硬碟。
- 只保留 Console 輸出(Winston 的
總結
- Morgan 為 Express 提供即時、輕量的 HTTP 請求日誌,適合快速了解流量與回應時間。
- Winston 則是功能完整的通用日誌框架,支援多層級、各種 Transport(Console、File、Remote)以及格式化、檔案旋轉等高階需求。
- 在 TypeScript 專案中使用兩者,需要注意型別匯入、環境變數切換與非同步寫入的效能影響。
- 避免常見的日誌洩漏、阻塞與未旋轉檔案等陷阱,並遵循「分層、結構化、可搜尋」的最佳實踐,才能在開發、測試與上線階段都保持可觀測性。
透過本篇教學,你已掌握:
- Morgan 的安裝、基本與自訂使用方式。
- Winston 的設定、日誌等級、每日旋轉檔案與在 Express 中的整合。
- 常見的坑與實務最佳做法,並能在真實專案中依需求選擇或同時使用兩套套件。
祝開發順利,日誌寫得漂亮、除錯更快 🎉!