本文 AI 產出,尚未審核

ExpressJS (TypeScript) – Logging 與 Debug:使用 Morgan 或 Winston


簡介

Node.jsExpress 應用程式中,日誌(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 只會在開發環境輸出;infowarnerror 則會在所有環境中寫入檔案,確保關鍵資訊不會遺失。


5. 常見陷阱與最佳實踐

陷阱 可能的後果 解決方案/最佳實踐
把所有訊息都 console.log 生產環境日誌混雜、難以搜尋、無法分級 使用 Winston 處理所有應用層訊息,保留 console 只作為開發階段的即時輸出
未設定檔案旋轉 日誌檔案無限制增長,磁碟空間耗盡 透過 winston-daily-rotate-filelogrotate 設定 size / time 旋轉
在高併發環境直接寫入同步檔案 事件迴圈被阻塞,導致效能下降 Winston 內建非同步寫入;若自行使用 fs.appendFileSync 需改為 fs.appendFile
在錯誤處理中再次拋出錯誤 造成無限迴圈或雙重錯誤訊息 errorLogger只回傳一次,並確保不再呼叫 next(err)
日誌中洩露機密資訊(如密碼、金鑰) 安全風險、合規問題 使用 filterredact 功能,僅保留必要欄位;在 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. 實際應用場景

  1. 微服務間的追蹤

    • 為每個請求產生唯一的 requestId(可透過 cls-hookedasync-hooks),在 Morgan、Winston 皆加入此欄位。日後透過 requestId 可以在分散式追蹤系統(如 Jaeger)中快速定位。
  2. API 速率限制與監控

    • 使用 Winston 記錄每次 rate-limit 被觸發的 IP、路徑與時間,並在監控儀表板(Grafana)設定告警。
  3. 異常警報

    • 結合 Winstonwinston-mail(或自建 Slack webhook)在 error 級別時即時發送通知,讓 DevOps 團隊第一時間取得異常資訊。
  4. 符合 GDPR / PCI DSS 的日誌保留

    • 設定 Winston 的 maxFiles 為 90 天,並使用加密儲存(如 AWS KMS)確保敏感資訊被妥善保護。
  5. 容器化部署(Docker / Kubernetes)

    • 只保留 Console 輸出(Winston 的 Console transport),將日誌收集交給 fluentdfilebeat 等 side‑car,避免容器內部寫入硬碟。

總結

  • Morgan 為 Express 提供即時、輕量的 HTTP 請求日誌,適合快速了解流量與回應時間。
  • Winston 則是功能完整的通用日誌框架,支援多層級、各種 Transport(Console、File、Remote)以及格式化、檔案旋轉等高階需求。
  • TypeScript 專案中使用兩者,需要注意型別匯入、環境變數切換與非同步寫入的效能影響。
  • 避免常見的日誌洩漏、阻塞與未旋轉檔案等陷阱,並遵循「分層、結構化、可搜尋」的最佳實踐,才能在開發、測試與上線階段都保持可觀測性。

透過本篇教學,你已掌握:

  1. Morgan 的安裝、基本與自訂使用方式。
  2. Winston 的設定、日誌等級、每日旋轉檔案與在 Express 中的整合。
  3. 常見的坑與實務最佳做法,並能在真實專案中依需求選擇或同時使用兩套套件。

祝開發順利,日誌寫得漂亮、除錯更快 🎉!