Node.js 日誌管理:從 console.log 到 Winston 的進化

更新 發佈閱讀 26 分鐘

簡介

在學習 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}`,
  });
});

參考資料

入门系列之 使用 Winston 记录 Node.js 应用程序

留言
avatar-img
weijie 的各種記事
0會員
6內容數
原本是打算用來當作技術部落格的,不過以後也會把各種雜記紀錄於此
weijie 的各種記事的其他內容
2025/12/13
本文將深入探討 Node.js 中常用的 Morgan HTTP request middleware。我們將從 Morgan 的核心功能、未使用 Morgan 的潛在問題,以及使用 Morgan 後的優勢等方面進行說明。
Thumbnail
2025/12/13
本文將深入探討 Node.js 中常用的 Morgan HTTP request middleware。我們將從 Morgan 的核心功能、未使用 Morgan 的潛在問題,以及使用 Morgan 後的優勢等方面進行說明。
Thumbnail
2025/12/07
步驟 一般在 git 預設 commit 訊息編輯器會是 vim ,但是如果不習慣使用 vim 命令列,且有安裝 visual studio code 的環境下,可以使用以下命令將 commit 訊息編輯器改為 visual studio code : git config --global c
Thumbnail
2025/12/07
步驟 一般在 git 預設 commit 訊息編輯器會是 vim ,但是如果不習慣使用 vim 命令列,且有安裝 visual studio code 的環境下,可以使用以下命令將 commit 訊息編輯器改為 visual studio code : git config --global c
Thumbnail
2025/12/03
前言 這裡主要是記錄我想把學習在 node.js 使用 prisma 中的踩坑紀錄,把整個過程整理成筆記 環境建立 1. 建立專案資料夾 mkdir node-prisma-pg cd ./node-prisma-pg 2. 設定 mpn 環境 npm init -y 3. 修改 pa
Thumbnail
2025/12/03
前言 這裡主要是記錄我想把學習在 node.js 使用 prisma 中的踩坑紀錄,把整個過程整理成筆記 環境建立 1. 建立專案資料夾 mkdir node-prisma-pg cd ./node-prisma-pg 2. 設定 mpn 環境 npm init -y 3. 修改 pa
Thumbnail
看更多
你可能也想看
Thumbnail
安裝完成 nodejs 後選用一個工作目錄執行 npm init,npm 會產生一個 package.json 檔案,之後為此專案安裝套件時都會記錄在此,讓專案可以很容易的重建和移植,也可設定 npm start 執行時以哪一個 js 檔當作系統入口。直接開寫了,以下我用 app.js 當作系統入口
Thumbnail
安裝完成 nodejs 後選用一個工作目錄執行 npm init,npm 會產生一個 package.json 檔案,之後為此專案安裝套件時都會記錄在此,讓專案可以很容易的重建和移植,也可設定 npm start 執行時以哪一個 js 檔當作系統入口。直接開寫了,以下我用 app.js 當作系統入口
Thumbnail
NodeJS 學習來到 file systems 操作,在文檔操作上有分為同步跟異步的處理,接下來分階段介紹操作函數
Thumbnail
NodeJS 學習來到 file systems 操作,在文檔操作上有分為同步跟異步的處理,接下來分階段介紹操作函數
Thumbnail
最近跟著影片學習NodeJS,第一部分先學習對Buffer的處理跟理解,以下是對於NodeJS Buffer的理解筆記
Thumbnail
最近跟著影片學習NodeJS,第一部分先學習對Buffer的處理跟理解,以下是對於NodeJS Buffer的理解筆記
Thumbnail
NodeJS作用? NodeJS 作為一個後端程式語言,與伺服器交互,能夠開發服務器應用、開發工具類應用,例如:Webback、Vite 這些好用工具,或是透過NodeJS的electron框架開發桌面端應用,而常見的桌面端應用如 VScode
Thumbnail
NodeJS作用? NodeJS 作為一個後端程式語言,與伺服器交互,能夠開發服務器應用、開發工具類應用,例如:Webback、Vite 這些好用工具,或是透過NodeJS的electron框架開發桌面端應用,而常見的桌面端應用如 VScode
Thumbnail
這篇想來寫,剛碰到js得時候,為了讓程式可以運作而安裝Node.js 。Node.js 是能夠在伺服器上面運行 JavaScript 的應用平台環境,透過 Node.js 提供的函式庫與執行環境能完成伺服器端服務。此篇幅就直接從純後端的角度切入摟(對不起拉我寫來寫去還是不知道怎麼順順的寫好文章開頭Q
Thumbnail
這篇想來寫,剛碰到js得時候,為了讓程式可以運作而安裝Node.js 。Node.js 是能夠在伺服器上面運行 JavaScript 的應用平台環境,透過 Node.js 提供的函式庫與執行環境能完成伺服器端服務。此篇幅就直接從純後端的角度切入摟(對不起拉我寫來寫去還是不知道怎麼順順的寫好文章開頭Q
Thumbnail
這一篇文章,我想來談談模板語言(template language/engine)。而其中比較有名的為handlebar、pug、ejs。那我會的事後兩著,因此拿這兩個出來寫一篇文章。 Pug 指令:npm install pug 比起 HTML 的語法,pug 語法可以說簡潔很多。 那下面
Thumbnail
這一篇文章,我想來談談模板語言(template language/engine)。而其中比較有名的為handlebar、pug、ejs。那我會的事後兩著,因此拿這兩個出來寫一篇文章。 Pug 指令:npm install pug 比起 HTML 的語法,pug 語法可以說簡潔很多。 那下面
Thumbnail
由於Javascript本身設計就適合於單線程的應用, 但一般後端應用程式都會支援多個服務來處理client的請求, nodejs中也提供了cluster模組來達成此功能。 Cluster的原理很簡單,由於每個Process都只能用單核心的CPU來運行,那麼就多開幾個來幫忙處理吧! 而這個Clust
Thumbnail
由於Javascript本身設計就適合於單線程的應用, 但一般後端應用程式都會支援多個服務來處理client的請求, nodejs中也提供了cluster模組來達成此功能。 Cluster的原理很簡單,由於每個Process都只能用單核心的CPU來運行,那麼就多開幾個來幫忙處理吧! 而這個Clust
Thumbnail
在第一次建立rails專案時,通常都會遇到不少問題,像我是使用windows系統,所以也經歷了一些麻煩。如果各位第一次建立rails專案就可以看到下圖綠色字體的成功字樣,那真的如天選之人般幸運阿~ 如果你跟我一樣注定要經歷windows系統的苦痛,就請繼續看下去吧QQ
Thumbnail
在第一次建立rails專案時,通常都會遇到不少問題,像我是使用windows系統,所以也經歷了一些麻煩。如果各位第一次建立rails專案就可以看到下圖綠色字體的成功字樣,那真的如天選之人般幸運阿~ 如果你跟我一樣注定要經歷windows系統的苦痛,就請繼續看下去吧QQ
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News