簡介
在學習 Node.js 的初期,許多開發者會習慣在程式中使用 console.log 來觀察程式流程與變數狀態。
這種方式在開發與除錯階段相當方便,但隨著專案進入正式環境,單純使用 console.log 來處理日誌便會逐漸顯得不足。
在實際的線上環境中,應用程式每天可能會產生大量的執行紀錄,如果缺乏日誌等級分類、結構化資料格式,以及可控的輸出方式,將會使問題排查與事後分析變得相當困難。
console.log 使用上雖然直覺且方便,但在正式環境(production)中,並不適合作為日誌系統的解決方案。
主要原因在於,console.log 在於 terminal 顯示訊息後,但是並不會保存於任何地方供日後分析,且缺乏日誌等級分類、結構化資料格式與輸出管道管理機制。再來就是這些訊息若只是以純文字形式輸出,不僅不會被妥善保存,也難以在事後進行搜尋、過濾與分析。此外,console.log 本身也無法與部署環境(例如 Docker、雲端平台)進行良好的整合。
因此,在實務上,Node.js 專案通常會導入專門的 logging library 來處理日誌需求。其中,Winston 是目前業界最主流、也最常被採用的日誌工具之一。
Winston 的主要特色包括:
- Log Level 分級:可依照嚴重程度區分訊息(如 error、warn、info),方便在大量日誌中快速定位問題。
- 多種 Transport 機制:可將日誌同時輸出至 console、檔案、HTTP,或是外部的日誌服務。
- 結構化日誌(Structured Logging):支援將日誌輸出為 JSON 格式,便於後續由程式或日誌系統進行分析。
- 高度客製化:可依需求自訂 format 與 transport,適合 Docker 或 serverless 等無狀態執行環境,將日誌集中送往外部系統統一蒐集與處理。
在紀錄 HTTP Request 日誌時,Winston 也常會搭配 Morgan 使用,透過 stream 將請求資訊轉為結構化資料,讓 HTTP 日誌更容易閱讀與分析。
Winston 的核心概念
Winston 的設計主要圍繞在以下幾個核心概念:
Log Level(日誌等級)
Winston 透過日誌等級來區分訊息的重要程度,例如 info 、 warn 、 error 等,使開發者能夠快速定位需要關注的問題。
Transport(輸出管道)
Transport 用來定義日誌的輸出位置,例如輸出到 console、檔案,或是傳送至外部的日誌系統。
透過不同的 Transport 組合,可以依照部署環境客製化日誌策略。
Structured Logging(結構化日誌)
Winston 支援將日誌輸出為結構化資料格式(例如 JSON),方便後續進行搜尋、過濾與分析。
Log Category(日誌來源分類)
在實務上,常會依照日誌的來源功能,例如 HTTP request、database query、應用程式邏輯等,透過欄位(如 type)對日誌進行分類。
Log Level(等級)
依據 官方文件 , Winston 的主要分類是依據 RFC5424 進行規劃,嚴重性都按重要性從高到低遞增的順序排列,可以分為如以下:
- error: 0
- warn: 1
- info: 2
- http: 3
- verbose: 4
- debug: 5
- silly: 6
這部分主要有兩種使用場合,分別是在建立 logger const logger = createLogger({ level: "info", ... }) 時候去規定在什麼等級以上才要紀錄日誌;再來就是在使用 logger 時候可以決定當時使用紀錄的 logger 分級等級 logger.error("API Error", { ... }) 。
Transport(輸出位置)
Transport 用來決定日誌要輸出到哪裡,例如 console、檔案或遠端服務。
實務上常見的 transport 類型如下。
Console Transport
將日誌輸出到標準輸出(stdout / stderr),將訊息輸出到 terminal ,在開發環境、Docker、雲端平台中非常常見。
new transports.Console(),
File Transport(輸出到檔案)
將日誌寫入檔案,常見於 VM / 傳統主機環境。
new transports.File({
filename: "./logs/app.log.json", // log file 儲存位置
level: "info", // 什麼等級以上才要紀錄日誌
}),
常見用途:
- app.log:所有資訊
- error.log:只存錯誤
HTTP Transport(送到遠端日誌服務)
將日誌透過 HTTP 傳送到外部系統。
new winston.transports.Http({
host: "log.example.com", // 遠端伺服器 URL
path: "/logs", // 遠端伺服器的檔案路徑
});
多半用於:
- 自建 log server
- 特定 legacy 系統
⚠ 實務上較常用現成服務(Datadog / CloudWatch)
Stream
搭配其他 library(如 morgan )來記錄由其他工具產生的其他紀錄
import morgan from 'morgan';
import { logger } from "./winston.logger.js"; // 已經建立的 Winston logger
// 建立 stream 物件,當 morgan 記錄完 http request 資料後,會呼叫該物件的 write 函式,將資料寫入函式的引數內
const stream = {
write: (message: string) => {
logger.info(JSON.parse(message.trim())); // 寫入 winston ; morgan 產生的 message 後有換行符號,要做 trim 處理
},
};
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),
});
});
const httpLogger = morgan("http-json", { stream }); // 建立 morgan 紀錄 http request middleware
app.use(httpLogger) // 放在網站 middleware 入口
Custom Transport(進階,自定義 Transport)
可以在 Winston 給的以上的 Transport 的基礎上,對於其內容及格式根據需求進行客製化設定
import { transports } from "winston";
const customFileTransport = new transports.File({
filename: "./logs/app.log.json",
level: "info",
format: winston.format.combine(
// 加上時間戳
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
// 如果 log 是 Error,自動把 stack 印出來
winston.format.errors({ stack: true }),
// 最終輸出成 JSON(機器友善)
winston.format.json()
);
})
第三方 Transport
Winston 本身只內建幾種 transport 種類,實務上還可以根據需求,如:要針對每天日誌獨立一個檔案,需要特殊格式的日誌紀錄等需求去使用第三方 transport。
第三方 transport 通常用於「檔案輪替」、「集中式日誌系統」或「雲端平台整合」,避免自行處理複雜的日誌保存與查詢問題。
- winston-daily-rotate-file
- winston-cloudwatch
- winston-elasticsearch
Format(格式)
大部分不管事開發人員,或是其應用程序,或是第三方工具產生的 log ,多是純文字,沒有結構化處理的訊息
ex:
Server started
Connected to database
GET /api/users 200 32ms
User 123 login success
Query failed: connection timeout
User 123 login success from 1.2.3.4
可以看出,以上的訊息並未做等級分類,且缺乏結構化資料,且每位開發人員寫的訊息格式各不相同,不論是未來在搜尋、分析、還是統計都會非常困難。
Winston 的 format 功能就是在訊息儲存前,將訊息做分類、結構化處理,統一資料格式,以利日後統計和分析。
先看 format 的基本用法
import winston from "winston";
// 建立 logger
const logger = winston.createLogger({
// 使用 format 功能,Winston 的 format 採用「管線(pipeline)設計」,
// 每一個 format 都會接收上一個 format 處理後的 log 物件,
// 並在輸出前依序加工、補充或轉換資料。
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
});
常見 Winston format 種類
format.timestamp() 在日誌加上時間戳記
可以自定義紀錄格式
format.timestamp({
format: "YYYY-MM-DD HH:mm:ss",
})
輸出結果
{
"timestamp": "2025-12-16 02:30:11"
}
format.json() 將訊息轉為容易處理的 JSON 格式結構化資料
format.printf() 主要是用在 console transport 上,將訊息依照期望的格式顯示在 terminal 上
⚠ format.printf 會將 log 轉回純文字 ,
因此不建議用在 File / Cloud / 分析用途的 Transport。
使用方式
// 在 callback 函式帶入 level, message, timestamp 等要顯示的資料欄位
format.printf(({ level, message, timestamp }) => {
// 在函式引用參數,回傳自定義格式的模版字符串(template literal)
return `${timestamp} [${level}]: ${message}`;
});
輸出結果
2025-12-16 02:30:11 [info]: User login
format.errors()
在 Winston 中,若直接將 Error 物件記錄到 log,預設只會輸出錯誤的 `message`,而不會包含 stack trace。
然而在實務中,錯誤往往來自多層套件或函式呼叫,若沒有 stack trace,實際排查問題時幾乎無法定位錯誤來源。
使用:
format.errors({ stack: true })
啟用後,當 log payload 中包含 Error 物件時,Winston 會自動將該 Error 的 stack 一併輸出到日誌中。
搭配 logger
logger.error("DB failed", {
type: "db",
error: err,
});
輸出結果
{
"message": "DB failed",
"error": {
"message": "connection refused",
"stack": "Error: ..."
}
}
format.colorize() 主要用在 console transport ,讓不同 level 以不同顏色顯示
format.label() 可以讓 logger 依據不同來源,或是目的,給日誌加上標籤
format.label({ label: "api-server" })
format.metadata() 解決「log 欄位太亂」,把非核心欄位集中起來。
大型專案比較常用
使用:
format.metadata({
fillExcept: ["message", "level", "timestamp"],
})
結果:
{
"message": "User login",
"metadata": {
"userId": 123,
"ip": "1.2.3.4"
}
}
接下來會根據 transport 功能採用不同的 format 策略分別說明:
format 策略
Console Transport format 策略
主要是應用在開發階段,要實時透過 terminal 隨時監視程式運行的各種狀態,需要有對不同 level 訊息有不同顏色顯示,實時的時間標示,且為了讓人眼可以一眼看出重要訊系,會有需要將資訊以特定格式顯示的需求。
const consoleTransport = new transports.Console({
format: format.combine(
format.colorize(), // 對不同 level 訊息有不同顏色顯示
format.timestamp(), // 實時的時間標示
// 將資訊以特定格式顯示
format.printf(({ level, message, timestamp }) => {
return `[${timestamp}] ${level}: ${message}`;
})
),
});
File Transport format 策略
儲存在檔案的日誌,需要有每一筆資訊的時間戳, JSON 等格式化資料,方便未來的數據分析、搜索和統計。
檔案型日誌的主要讀者不是人,而是系統與工具,因此 format 的設計應以「可搜尋、可分析、可統計」為優先。
const fileTransport = new transports.File({
filename: "./logs/app.log.json", // 日誌儲存路徑
level: "info",
format: winston.format.combine(
// 加上時間戳
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
// 如果 log 是 Error,自動把 stack 印出來
winston.format.errors({ stack: true }),
// 最終輸出成 JSON 容易被工具分析、索引
winston.format.json()
);
});
HTTP Transport format 策略
如果是在 docker 或是 serverless 環境無法儲存資料,或是希望可以把各系統產生的日誌統一傳送到專門 server 來儲存處理,把結構化日誌送到遠端日誌服務(log server / log collector)。,就會使用到 http transport 。基本資料結構格式和File Transport 相同。
不過在實務上,傳送 http request 時候可能會因為 網路問題 或是log server 掛了等狀況導致傳出失敗,所以在傳輸上基本採用 best-effort ,傳輸失敗、log server 沒有回應等狀況並不會重傳,或是有暫存等行為,失敗就失敗了。
範例:
const HttpTransport = new transports.Http({
host: "log.example.com", // 日誌接收伺服器的 api
port: 443, // 通常是 80 / 443
path: "/logs", // log server 提供的 API endpoint
ssl: true, // https
format: winston.format.combine(
// 加上時間戳
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
// 如果 log 是 Error,自動把 stack 印出來
winston.format.errors({ stack: true }),
// 最終輸出成 JSON 容易被工具分析、索引
winston.format.json()
);
});
補充:
在高流量高並發的場景,並不會採用 winston 來傳輸日誌資訊,應用程式本身不會直接同步把每一筆 log 用 HTTP 傳送到遠端伺服器,否則日誌系統會反過來拖垮應用程式。
在高流量與高併發的場景下,同步將每一筆日誌透過 HTTP 傳送到遠端伺服器,這樣的設計容易造成效能瓶頸,甚至使日誌系統反過來影響應用程式的穩定性。
實務上,應用程式只負責將日誌快速輸出至標準輸出或本地檔案,後續的傳送、批次處理、重試與流量控制,則交由專門的日誌收集工具(如 Fluent Bit、Filebeat)處理。
這種設計可以確保即使日誌系統短暫異常,也不會影響核心服務的正常運作。
應用程式只負責「快速輸出 log」,傳送、重試、聚合、壓縮交給專門的 log pipeline。
日誌分類
一般會在呼叫 logger 時候帶入 type 欄位來表示該筆日誌的「邏輯來源 / 功能領域」
範例:
logger.error("Request validation failed", { type: "api", ... })
等方式標示該筆日誌是來自何種程序,然後通一蒐集集中於一個位置(檔案資料夾,或是 log server)。
在實務環境中,日誌通常會被集中蒐集到同一個 log server 中,若僅依賴訊息內容(message)來判斷來源,在搜尋、統計或告警時會非常困難。
因此,會在呼叫 logger 時,主動加入 type 欄位,將日誌依其功能來源進行分類,而非依檔案或目錄區分。
以下列出常見的 type 分類:
- http / api → HTTP request / response
- app → 商業邏輯
- db → Database 操作
- auth / security → 認證、授權、可疑行為
- system → 啟動、關閉、系統狀態
基礎使用
以下為 Winston 使用示範程式碼
import express from "express";
import { createLogger, format, transports } from "winston";
const PORT = 3000;
const app = express();
/**
* Winston Logger
*/
const logger = createLogger({
level: "info",
// 預設 format(會被 transport 的 format 覆蓋)
format: format.combine(
format.timestamp(),
format.json()
),
transports: [
// =========================
// File Transport
// =========================
new transports.File({
filename: "./logs/app.log.json",
level: "info",
format: format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
format.errors({ stack: true }),
format.json()
),
}),
// =========================
// Console Transport
// =========================
new transports.Console({
format: format.combine(
format.colorize(),
format.timestamp(),
format.printf(({ level, message, timestamp }) => {
return `[${timestamp}] ${level}: ${message}`;
})
),
}),
],
});
/**
* Routes
*/
app.get("/", (req, res) => {
logger.info("Some request log info message", {
type: "api",
method: req.method,
path: req.path,
});
res.send("server is running.");
});
/**
* Server start
*/
app.listen(PORT, () => {
logger.info("Server started", {
type: "server",
port: PORT,
url: `http://localhost:${PORT}`,
});
});











