深入解析 Node.js 的 console.log[每日前端夜話0x73]

深入解析 Node.js 的 console.log[每日前端夜話0x73]

前端先鋒 前端先鋒

每日前端夜話0x73
每日前端夜話,陪你聊前端。
每天晚上18:00準時推送。
正文共:4477 字
預計閱讀時間: 12 分鐘
翻譯:瘋狂的技術宅
來源:twilio





深入解析 Node.js 的 console.log[每日前端夜話0x73]

當你開始用 JavaScript 進行開發時,可能學到的第一件事就是如何用 console.log 將內容記錄到控制檯。如果你去搜索如何調試 JavaScript,會發現數百篇博文和 StackOverflow 文章都會簡單的告訴你用 console.log。因爲這是一種很常見的做法,我們甚至會在代碼中使用像 no-console 這樣的 linter 規則來確保不會留下意外的日誌信息。但是如果我們真的想要去記錄某些內容呢?

在本文中,我們將梳理各種情況下要記錄的日誌信息,Node.js 中 console.log 和console.error之間的區別是什麼,以及如何在不發生混亂的情況下把你庫中的日誌記錄輸出到用戶控制檯。


1console.log(`Let's go!`);

理論先行:Node.js 的重要細節

雖然你可以在瀏覽器和 Node.js 中使用 console.log 或 console.error,但在使用 Node.js 時要記住一件重要的事。當你在 Node.js 中將以下代碼寫入名爲 index.js 的文件中時:


1console.log('Hello there');
2console.error('Bye bye');

並用 node index.js 在終端中執行它,你會直接看到兩者的輸出:

深入解析 Node.js 的 console.log[每日前端夜話0x73]
在終端中執行 node index.js 的結果
雖然它們看起來可能一樣,但實際上系統對它們的處理方式是不同的。如果你查閱 Node.js 文檔的 console部分,會看到 console.log 是輸出到 stdout 而 console .error 用的是 stderr。

每個進程都有三個可用的默認 stream。那些是 stdin,stdout 和 stderr。 stdin 流用來在處理進程的輸入。例如按下按鈕或重定向輸出。 stdout 流用於程序的輸出。最後 stderr 用於錯誤消息。如果你想了解爲什麼會有 stderr 存在,以及應該在什麼時候使用它,可以查看這篇文章(https://www.jstorimer.com/blogs/workingwithcode/7766119-when-to-use-stderr-instead-of -stdout)。

簡而言之,這允許我們在 shell 中使用重定向(>)和管道(|)來處理錯誤和診斷信息,它們是與程序的實際輸出結果是分開的。雖然 > 允許我們將命令的輸出重定向到文件中,但是 2> 允許我們將 stderr 的輸出重定向到文件中。例如,下面這個命令會將 “Hello there” 傳給一個名爲 hello.log 的文件並把 “Bye bye” 傳到一個名爲 error.log 的文件中。


1node index.js > hello.log 2> error.log

深入解析 Node.js 的 console.log[每日前端夜話0x73]

錯誤輸出被重定向到不同的文件

應該在什麼時候記錄日誌?

現在我們已經瞭解了日誌記錄的底層技術,接下來讓我們談談應該在什麼情況下記錄日誌內容。通常應該是以下情況之一:

  • 在開發過程中快速調試意外行爲
  • 基於瀏覽器的分析或診斷日誌記錄
  • 記錄你服務器上傳入的請求,以及所有可能發生的故障
  • 使用庫的日誌調試選項來幫助用戶解決問題
  • 在 CLI 輸出進度、確認消息或錯誤信息
    我們將跳過前兩種情況,並重點介紹基於 Node.js 的後三點。

服務器程序日誌

可能你在服務器上記錄日誌的原因有多種。例如記錄傳入的請求並允許你從中提取諸如統計信息之類的內容,比如有多少用戶在點擊時發生了 404 錯誤,或者用戶瀏覽器的 User-Agent。你也想知道在什麼時候因爲什麼出錯了。

如果你想編碼嘗試下面的內容,請先創建一個新的項目目錄。在目錄中創建一個 index.js 並運行以下命令來初始化項目並安裝 express:


1npm init -y
2npm install express

讓我們設置一個帶有中間件的服務器,每個請求只需用 console.log進行輸出。將以下內容複製到 index.js 文件中:


 1const express = require('express');
 2
 3const PORT = process.env.PORT || 3000;
 4const app = express();
 5
 6app.use((req, res, next) => {
 7 console.log('%O', req);
 8 next();
 9});
10
11app.get('/', (req, res) => {
12 res.send('Hello World');
13});
14
15app.listen(PORT, () => {
16 console.log('Server running on port %d', PORT);
17});

在這裏用 console.log('%O', req) 來記錄整個對象的信息。 console.log 在底層使用了 util.format 來支持 %O 佔位符。你可以在 Node.js 文檔中查閱它們的細節。

當你運行 node index.js 來啓動你的服務器並導航到 http://localhost:3000 時,會發現它會打印出很多我們確實需要但不知道的信息。
深入解析 Node.js 的 console.log[每日前端夜話0x73]

在終端中輸出的 request 對象信息
如果將其更改爲 console.log('%s', req) 不打印整個對象,我們就不會獲得更多信息。
深入解析 Node.js 的 console.log[每日前端夜話0x73]

在終端中輸出的 request 對象信息
可以通過編寫自己的日誌函數只輸出我們關心的東西,但是先等等,談談我們通常關心的東西。雖然這些信息經常成爲我們關注的焦點,但實際上可能還需要其他信息:

  • 時間戳 - 知道事情何時發生
  • 計算機/服務器名稱 - 如果你運行的是分佈式系統
  • 進程ID - 如果你用了 pm2 來運行多個Node進程
  • 消息 - 包含某些內容的實際消息
  • 可能會需要的其它變量或信息
    既然一切都會被轉到 stdout 和 stderr,那麼我們可能會想要不同的日誌級別,還有配置和過濾日誌的能力。

我們可以通過依賴 process 的各個部分並編寫一堆 JavaScript 來獲得所有這些,但關於 Node.js 的好消息是有 npm 這個生態系統,裏面已經有了各種各樣的庫供我們使用。其中一些是:

  • pino
  • winston
  • roarr
  • bunyan(請注意,這個已經 2 年沒有更新了)
    我更喜歡pino,因爲它速度很快。接下來看看怎樣使用 pino 來幫助我們記錄日誌。同時我們可以用 express-pino-logger 包來記錄請求。

安裝 pino 和 express-pino-logger:


1npm install pino express-pino-logger

用下面的代碼更新你的 index.js文件以使用 logger 和中間件:


 1const express = require('express');
 2const pino = require('pino');
 3const expressPino = require('express-pino-logger');
 4
 5const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
 6const expressLogger = expressPino({ logger });
 7
 8const PORT = process.env.PORT || 3000;
 9const app = express();
10
11app.use(expressLogger);
12
13app.get('/', (req, res) => {
14 logger.debug('Calling res.send');
15 res.send('Hello World');
16});
17
18app.listen(PORT, () => {
19 logger.info('Server running on port %d', PORT);
20});

在這段代碼中,我們創建了一個 pino 的實例 logger,並將其傳給 express-pino-logger 創建一個新的 logger中間件來調用 app.use。另外,我們用 logger.info 替換了服務器啓動時的 console.log,並在路由中添加了一個額外的 logger.debug 來顯示不同的日誌級別。

再次運行 node index.js 重新啓動服務器,你會看到一個完全不同的輸出,它每一行打印一個 JSON。再次導航到 http://localhost:3000 ,你會看到添加了另一行JSON。
深入解析 Node.js 的 console.log[每日前端夜話0x73]

如果你檢查這些 JSON,將看到它包含所有前面所提到的信息,例如時間戳等。你可能還會注意到 logger.debug 語句沒有打印出來。那是因爲我們必須修改默認日誌級別才能看到。當我們創建 logger 實例時,將值設置爲 process.env.LOG_LEVEL,這意味着我們可以通過它修改值,或接受默認的 info。通過執行 LOG_LEVEL = debug node index.js,就可以調整日誌級別。

在這之前要先解決一個問題,即現在的輸出不適合人類閱讀。pino 遵循一種理念,爲了提高性能,你應該通過管道(使用 |)將輸出的任何處理移動到一個單獨的進程中。這包括使其可讀或將其上傳到雲主機。這些被稱爲 transports。可以通過查看 transports 文檔瞭解爲什麼 pino 中的錯誤不會寫入 stderr。

讓我們用工具 pino-pretty 來查看更易閱讀的日誌版本。在你的終端中運行:


1npm install --save-dev pino-pretty
2LOG_LEVEL=debug node index.js | ./node_modules/.bin/pino-pretty

現在所有的日誌都被用 | 運算符輸入給 pino-pretty 命令,你的輸出應該會經過美化,並且還會包含一些關鍵信息,而且應該是彩色的。如果再次請求 http://localhost:3000 ,你還應該看到debug消息。
深入解析 Node.js 的 console.log[每日前端夜話0x73]

被美化過的pino日誌輸出
有各種各樣的 transports 來美化或轉換你的日誌。你甚至可以用 pino-colada 顯示 emoji。這些對你的本地開發很有用。在生產中運行服務器之後,你可能希望將日誌傳輸到另一個 transports,再用 > 或者用像 tee 這樣的命令將它們寫入磁盤以便稍後處理。

這個文檔 中還將包含有關輪換日誌文件、過濾和把日誌寫入不同文件等內容的信息。

庫的日誌

現在討論一下怎樣有效地爲我們的服務器程序編寫日誌,爲什麼不對我們的庫使用相同的技術呢?

問題是你的庫可能希望通過記錄日誌來進行調試,但是不應該與使用者的程序相混淆。如果需要調試某些內容,使用者應該能夠啓用日誌。默認情況下,你的庫應該是靜默的,並將是否輸出日誌的決策權留給用戶。

一個很好的例子是 express。 express 的底層有很多東西,你可能想在調試自己的程序時偷看它。如果我們查閱 express 文檔,就會注意到你可以在自己的命令之前添加 DEBUG=express:*,如下所示:


1DEBUG=express:* node index.js

如果你運行這個命令,將看到許多其他的輸出,這些可幫助你調試程序中的問題。
深入解析 Node.js 的 console.log[每日前端夜話0x73]

express debug logs
如果你沒有啓用調試日誌記錄,則不會看到任何此類日誌。這是通過一個稱爲 debug 的包來完成的。它允許我們在“命名空間”下編寫日誌消息,如果庫的用戶包含該命名空間或在 DEBUG 環境變量 中匹配了它的通配符,就會輸出這些。要使用 debug 庫,首先要安裝它:


1npm install debug

讓我們通過創建一個名爲 random-id.js 的新文件來模擬我們的庫,並將以下代碼複製到其中:


 1const debug = require('debug');
 2
 3const log = debug('mylib:randomid');
 4
 5log('Library loaded');
 6
 7function getRandomId() {
 8 log('Computing random ID');
 9 const outcome = Math.random()
10   .toString(36)
11   .substr(2);
12 log('Random ID is "%s"', outcome);
13 return outcome;
14}
15
16module.exports = { getRandomId };

這將創建一個帶有命名空間 mylib:randomid 的新 debug 記錄器,然後將兩條消息輸出到日誌。讓我們在前面的 index.js 中使用它:


 1const express = require('express');
 2const pino = require('pino');
 3const expressPino = require('express-pino-logger');
 4
 5const randomId = require('./random-id');
 6
 7const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
 8const expressLogger = expressPino({ logger });
 9
10const PORT = process.env.PORT || 3000;
11const app = express();
12
13app.use(expressLogger);
14
15app.get('/', (req, res) => {
16 logger.debug('Calling res.send');
17 const id = randomId.getRandomId();
18 res.send(`Hello World [${id}]`);
19});
20
21app.listen(PORT, () => {
22 logger.info('Server running on port %d', PORT);
23});

如果用 DEBUG=mylib:randomid node index.js 重新運行我們的服務器,它會打印前面“庫”的調試日誌。
深入解析 Node.js 的 console.log[每日前端夜話0x73]

自定義調試日誌
如果你的庫的用戶想要將這個調試信息放到他們的 pino 日誌中,他們可以用 pino 團隊開發的名爲 pino-debug 的庫來正確的格式化這些日誌。

用以下命令安裝庫:


1npm install pino-debug

在我們第一次使用debug之前,需要初始化pino-debug。最簡單的方法是在啓動 javascript 腳本的命令之前使用 Node.js 的 -r 或 --require 標誌來 require 模塊。使用如下命令重新運行你的服務器(假設你安裝了pino-colada):


1DEBUG=mylib:randomid node -r pino-debug index.js | ./node_modules/.bin/pino-colada

你現在將用與程序日誌相同的格式查看庫的調試日誌。

深入解析 Node.js 的 console.log[每日前端夜話0x73]
使用pino和pino-colada的調試日誌

CLI 輸出

本文介紹的最後一個案例是針對 CLI 進行日誌記錄的特殊情況。我的理念是將“邏輯日誌”與 CLI 的輸出 “日誌” 分離。對於所有的邏輯日誌,你應該用像 debug 這樣的庫。這樣你或其他人就可以重新使用該邏輯,而不受 CLI 的特定用例的約束。

當你用 Node.js 構建 CLI 時,可能希望添加一些看上去很漂亮顏色,或者用有視覺吸引力的方式格式化信息。但是,在構建 CLI 時,應該記住以下這幾種情況。

一種情況是你的 CLI 可能會在持續集成(CI)系統的上下文中使用,因此你可能希望刪除顏色和花哨的裝飾輸出。一些 CI 系統設置了一個名爲 CI 的環境標誌。如果你想更安全地檢查自己是否在 CI 中,那就是使用像 is-ci 這樣的包去支持一堆 CI 系統。

像 chalk 這樣的庫已經爲你檢測了CI 併爲你刪除了顏色。我們來看看它的樣子。

使用 npm install chalk 安裝 chalk 並創建一個名爲 cli.js 的文件。將以下內容複製到其中:


1const chalk = require('chalk');
2
3console.log('%s Hi there', chalk.cyan('INFO'));
Now if you would run this script using node cli.js you'll see colored output.

現在如果你用 node cli.js 運行這個腳本,將會看到彩色輸出。
深入解析 Node.js 的 console.log[每日前端夜話0x73]

顯示彩色CLI輸出
但是如果你用 CI=true node cli.js 運行它,你會看到顏色被消除了:
深入解析 Node.js 的 console.log[每日前端夜話0x73]

啓用CI模式顯示沒有顏色的CLI輸出
你要記住的另一個場景是 stdout 是否以終端模式運行,也就是將內容寫入終端。如果是這種情況,我們可以使用 boxen 之類的東西顯示所有漂亮的輸出。如果不是,則可能會將輸出重定向到文件或用管道傳輸到某處。

你可以通過檢查相應流上的 isTTY 屬性來檢查 stdin、stdout 或 stderr 是否處於終端模式。例如:process.stdout.isTTY。 TTY 的意思是 “電傳打印機(teletypewriter)”,在這種情況下專門用於終端。

根據 Node.js 進程的啓動方式,這三個流每個流的值可能不同。你可以在 Node.js 文檔的"process I/O" 這一部分中詳細瞭解它。

讓我們來看看 process.stdout.isTTY 的值在不同情況下是如何變化的。先更新你的 cli.js :


1const chalk = require('chalk');
2
3console.log(process.stdout.isTTY);
4console.log('%s Hi there', chalk.cyan('INFO'));

在終端中運行 node cli.js,你會看到輸出的 true 被着色了。
深入解析 Node.js 的 console.log[每日前端夜話0x73]

在控制檯輸出被上色的 "true"
之後運行相同的內容,但是將輸出重定向到一個文件,然後檢查內容:


1node cli.js > output.log
2cat output.log

你會看到這次它打印了 undefined 後面跟着一個簡單的無色消息,因爲 stdout 的重定向關閉了它的終端模式。因爲 chalk 用了 supports-color,它們會在相應的流上檢查 isTTY。
深入解析 Node.js 的 console.log[每日前端夜話0x73]

單色CLI輸出
像 chalk這樣的工具已經爲你處理了這種行爲,但是在開發 CLI 時,你應該始終了解 CLI 可能在 CI 模式下運行或重定向輸出的情況。它還可以幫助你進一步獲得 CLI 的體驗。例如你可以在終端中以漂亮的方式排列數據,如果isTTY 是 undefined ,你可以切換到更容易解析的方式。

總結

剛開始用 JavaScript 開發時用 console.log 記錄你的第一行日誌確實很快,但是當你將代碼投入生產環境時,應該考慮更多關於日誌記錄的內容。本文純粹是對各種方式和可用的日誌記錄解決方案的介紹。我建議你去看一些自己喜歡的開源項目,看看它們是怎樣解決日誌記錄問題的,還有它們所用到的工具。

如果你知道或找到了我沒有提及的工具,或者有什麼疑問,請留言。

原文:https://www.twilio.com/blog/guide-node-js-logging

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章