[譯] Node.js 日誌記錄指南

當你開始使用 JavaScript 開始時,你應該學會的第一件事就是如何經過 console.log() 將事物記錄到控制檯。若是你搜索如何調試 JavaScript,你會發現數百篇博客文章和 StackOverflow 上的文章會告訴你很「簡單」的使用 console.log() 來完成調試。由於這是一種常見的作法,咱們甚至開始使用 linter 規則,好比 no-console,以確保咱們不會在生產代碼中留下意外的日誌記錄。可是若是咱們真的想記錄一些東西來提供更多的信息呢?html

在這篇博文中,我將會介紹一些你想要記錄信息的各類狀況,以及在 Node.js 中 console.logconsole.error 的區別,以及如何在不影響用戶控制檯的狀況下往庫裏面發送日誌記錄。前端

console.log(`Let's go!`);
複製代碼

理論第一:Node.js 的重要細節

雖然您能夠在瀏覽器和 Node.js 中使用 console.logconsole.error,但在使用 Node.js 時須要記住一件重要的事情。在一個叫作 index.js 的文件中寫下面的代碼:node

console.log('Hello there');
console.error('Bye bye');
複製代碼

而後在終端裏面使用 node index.js 來運行它,你會看到這兩個直接在下面輸出:android

Screenshot of Terminal running `node index.js`

然而,雖然這兩個看上去可能相同,但系統實際上對它們的處理方式並不相同。若是你去查看 Node.js 文檔中 console 部分,你會看到 console.log 是使用 stdout 來打印而 console.error 使用 stderr 來打印。ios

每一個進程均可以使用三個默認的 streams 來工做。它們分別是 stdinstdoutstderrstdin 流來處理和你的進程相關的輸出。例如按下按鈕或重定向輸出(咱們會在一秒鐘以內完成)。stdout 流則用於你的應用程序的輸出。最後 stderr 用於錯誤消息。若是你想了解 stderr 存在的緣由以及何時使用它,能夠查看本文git

簡而言之,這容許咱們使用 redirect(>)和 pipe(|)運算符來處理和應用程序實際結果分開的錯誤和診斷信息。雖然 > 容許咱們將命令的輸出重定向到文件中,2> 容許咱們將 stderr 的輸出重定向到文件中。例如,下面這個命令會將 「Hello there」 傳遞到一個叫作 hello.log 的文件中和將 「Bye bye」 傳遞到一個叫作 error.log 的文件中。github

node index.js > hello.log 2> error.log
複製代碼

Screenshot of terminal showing how error output is in different file

你何時想記錄?

既然咱們已經瞭解了日誌記錄的基礎記錄方面,讓咱們先談談你可能想要記錄某些內容的不一樣用例。一般這些用例屬於如下的類別之一:express

本篇博客將會跳過前面兩個類別,而後重點介紹基於 Node.js 的後三個類別npm

你的服務器應用程序的日誌

你可能須要在服務器上進行日誌記錄的緣由有不少。例如,記錄傳入的請求從而容許你從裏面提取信息,好比有多少用戶正在訪問 404,這些請求多是什麼,或者正在使用什麼 User-Agent。你也想知道何時出了問題以及爲何會出現問題。後端

若是你想在文章的這一部分中嘗試下面的內容,首先要確保建立一個文件夾。在項目目錄下建立一個叫作 index.js 的文件,而後使用下面的代碼來初始化整個項目而且安裝一下 express

npm init -y
npm install express
複製代碼

而後設置一個帶有中間件的服務器,只須要 console.log 爲來提供每次的請求。將下面的內容放在 index.js 文件裏面:

const express = require('express');

const PORT = process.env.PORT || 3000;
const app = express();

app.use((req, res, next) => {
 console.log('%O', req);
 next();
});

app.get('/', (req, res) => {
 res.send('Hello World');
});

app.listen(PORT, () => {
 console.log('Server running on port %d', PORT);
});
複製代碼

咱們用 console.log('%O', req) 來記錄整個對象。console.log 在引擎蓋下使用 util.format,它還支持 %O 等其餘佔位符。你能夠在 Node.js 文檔中閱讀它們

當你運行 node index.js 執行服務器而且導航到 http://localhost:3000,你會注意到它將打印出許多咱們真正並不須要的信息。

Screenshot of terminal showing too much output of request object

若是將代碼改爲 console.log('%s', req) 爲不打印整個對象,咱們也不會得到太多的信息。

Screenshot of terminal printing

咱們能夠編寫咱們本身的打印函數,它只輸出咱們關心的東西,可是讓咱們先回退一步,討論一下咱們一般關心的事情。雖然這些信息常常成爲咱們關注的焦點,但實際上咱們可能還須要其餘信息。例如:

  • 時間戳 —— 用於得知事情什麼時候發生
  • 計算機/服務器名稱 —— 若是你運行的是分佈式系統
  • 進程 ID —— 若是你使用相似 pm2 的工具來運行多個 Node 進程
  • 消息 —— 包含一些內容的實際消息
  • 堆棧跟蹤 —— 以防咱們記錄錯誤
  • 也許還有一些額外的變量/信息

另外,既然咱們知道全部的東西都會轉到 stdoutstderr,那麼咱們可能須要不一樣的日誌級別,而且根據它們來配置和過濾日誌的能力。

咱們能夠經過訪問各部分的 process 而且寫一大堆 JavaScript 代碼來獲取這些,可是關於 Node.js 最好的事情是咱們獲得了 npm 生態系統,而且已經有各類各樣的庫供咱們使用。其中有一些是:

我我的很喜歡 pino 這個庫,由於它運行很快,而且生態系統比較好,讓咱們來看看如何使用 pino 來幫咱們記錄日誌。咱們同時也可使用 express-pino-logger 包來幫助咱們整潔的記錄請求。

同時安裝 pinoexpress-pino-logger

npm install pino express-pino-logger

複製代碼

而後更新 index.js 文件來使用記錄器和中間件:

const express = require('express');
const pino = require('pino');
const expressPino = require('express-pino-logger');

const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const expressLogger = expressPino({ logger });

const PORT = process.env.PORT || 3000;
const app = express();

app.use(expressLogger);

app.get('/', (req, res) => {
 logger.debug('Calling res.send');
 res.send('Hello World');
});

app.listen(PORT, () => {
 logger.info('Server running on port %d', PORT);
});
複製代碼

在這個代碼片斷中,咱們經過 pino 建立了一個 logger 實例並將其傳遞給 express-pino-logger 來建立一個新的中間件,而且經過 app.use 來調用它。此外,咱們在服務器啓動的位置用 logger.info 來替換 console.log,並在咱們的路由中添加一行 logger.debug 來顯示一個額外的日誌級別。

若是經過 node index.js 再次運行從新啓動服務器,你將會看到一個徹底不一樣的輸出,它會爲每一行打印一個 JSON。再次導航到 http://localhost:3000,你將會看到添加了另外一行 JSON。

Screenshot showing example pino logs from HTTP request

若是你檢查這個 JSON,你將看到它包含全部先前提到的信息,例如時間戳。您可能還會注意到咱們的 logger.debug 聲明沒有打印出來。那是由於咱們必須更改默認日誌級別才能使其可見。當咱們建立 logger 實例時,咱們將值設爲 process.env.LOG_LEVEL 意味着咱們能夠經過它更改值,或者接受默認值 info。咱們能夠經過運行 LOG_LEVEL=debug node index.js 來調整日誌的級別。

在咱們這樣作以前,讓咱們先認清這樣一個事實,即如今的輸出並非真正可讀的。這是故意的。pino 遵循一個理念,爲了提升性能,你應該經過管道(使用|)輸出將日誌任何處理移動到單獨的過程當中去。這包括使其可讀或將其上傳到雲主機上面去。咱們稱這些爲 傳輸。查看關於傳輸的文檔 去了解 pino 中的錯誤爲何沒有寫入 stderr

咱們將使用 pino-pretty 來查看更易讀的日誌版本。在終端運行:

npm install --save-dev pino-pretty
LOG_LEVEL=debug node index.js | ./node_modules/.bin/pino-pretty
複製代碼

如今,你的全部日誌信息都會使用 | 操做符輸出到 pino-pretty 中去。若是你再次去請求 http://localhost:3000。你應該還能看到你的 debug 信息。

Screenshot of pretty printed pino logs

有許多現有的傳輸工具能夠美化或轉換你的日誌。你甚至能夠經過 pino-colada 來顯示 emojis。這會對你的本地開發頗有用。在生產環境中運行服務器後,你可能但願將日誌輸出到到另一個傳輸中,使用 > 將其寫入磁盤以待稍後處理,或者使用相似於 tee 的命令來進行同時的處理。

文檔 還將包含有關諸如輪換日誌文件,過濾和將日誌寫入不一樣文件等內容的信息。

你的庫的日誌

既然咱們研究瞭如何有效地爲服務器應用程序編寫日誌,爲何不對咱們編寫的庫使用相同的技術呢?

問題是,你的庫可能但願記錄用於調試的內容,但實際上不該該讓使用者的應用程序變得混亂。相反,若是須要調試某些東西,使用者應該可以啓用日誌。你的庫在默認狀況下應該是不會處理這些的,並將寫入輸出的操做留給用戶。

express 就是一個很好的例子。在 express 框架下有不少的事情要作,在調試應用程序時,你可能但願瞭解一下框架的內容。若是咱們查詢 express 文檔,你會注意到你能夠在你的命令前面加上 DEBUG=express:* 這樣一行代碼:

DEBUG=express:* node index.js
複製代碼

若是你使用如今的應用程序運行這個命令,你將看到許多其餘輸出,可幫助你調試問題。

Screenshot of express debug logs

若是你沒有啓用調試日誌記錄,則不會看到任何這樣的日誌。這是經過調用一個叫作 debug 的包來完成的。它容許咱們在「命名空間」下編寫消息,若是庫的用戶包含命名空間或者在其 DEBUG 環境變量 中匹配它的通配符,它將輸出這些。使用 debug 庫,首先要先安裝它:

npm install debug
複製代碼

讓咱們經過建立一個模擬咱們的庫調用的新文件 random-id.js 來嘗試它,並在裏面寫上這樣的代碼:

const debug = require('debug');

const log = debug('mylib:randomid');

log('Library loaded');

function getRandomId() {
 log('Computing random ID');
 const outcome = Math.random()
   .toString(36)
   .substr(2);
 log('Random ID is "%s"', outcome);
 return outcome;
}

module.exports = { getRandomId };
複製代碼

這裏會建立一個帶有命名空間 mylib:randomiddebug 記錄器,而後會將兩種消息記錄上去。而後咱們在前一節的 index.js 文件中使用它:

const express = require('express');
const pino = require('pino');
const expressPino = require('express-pino-logger');

const randomId = require('./random-id');

const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const expressLogger = expressPino({ logger });

const PORT = process.env.PORT || 3000;
const app = express();

app.use(expressLogger);

app.get('/', (req, res) => {
 logger.debug('Calling res.send');
 const id = randomId.getRandomId();
 res.send(`Hello World [${id}]`);
});

app.listen(PORT, () => {
 logger.info('Server running on port %d', PORT);
});
複製代碼

若是你此次使用 DEBUG=mylib:randomid node index.js 來從新啓動服務器,它會打印咱們「庫」的調式日誌。

Screenshot of custom debug logs

有意思的是,若是使用你的庫的用戶想把這些調試信息方法到本身的 pino 日誌中去,他們可使用一個由 pino 團隊出的一個叫作 pino-debug 庫來正確的格式化這些日誌。

使用下面的命令來安裝這個庫:

npm install pino-debug
複製代碼

pino-debug 在咱們第一次使用以前須要初始化一次 debug。最簡單的方法是在啓動腳本以前使用 Node.js 的 -r--require 標識符 來初始化。使用下面的命令來重啓你的服務器(假設你已經安裝了 pino-colada):

DEBUG=mylib:randomid node -r pino-debug index.js | ./node_modules/.bin/pino-colada
複製代碼

你如今就能夠用和應用程序日誌相同的格式來查看庫的的調試日誌。

Screenshot of debug logs working with pino and pino-colada

你的 CLI 輸出

我將在這篇博文中介紹的最後一個案例是針對 CLI 而不是庫去進行日誌記錄的特殊狀況。個人理念是將邏輯日誌和你的 CLI 輸出 「logs」 分開。對於任何邏輯日誌,你應該使用相似 debug 的庫。這樣你或其餘人就能夠從新使用邏輯,而不受 CLI 的特定用例約束。

當你使用 Node.js 構建 CLI 時,你可能但願經過特定的視覺吸引力方式來添加顏色、旋轉器或格式化內容來使事物看起來很漂亮。可是,在構建 CLI 時,應該記住幾種狀況。

一種狀況是,你的 CLI 可能在持續繼承(CI)系統的上下文中使用,所以你可能但願刪除顏色或任何花哨的裝飾輸出。一些 CI 系統設置了一個稱爲 「CI」 的環境標誌。若是你想更安全的檢查本身是否在 CI 中,可使用已經支持多個 CI 系統的包,例如is-ci

有些庫例如 chalk 已經爲你檢測了 CI 並幫你刪除顏色。讓咱們來看看這是什麼樣子。

使用 npm install chalk 來安裝 chalk,並建立一個叫作 cli.js 的文件。將下面的內容放在裏面:

const chalk = require('chalk');

console.log('%s Hi there', chalk.cyan('INFO'));
複製代碼

如今,若是你使用 node cli.js 運行這個腳本,你將會看到對應的顏色輸出。

Screenshot showing colored CLI output

可是你使用 CI=true node cli.js 來運行它,你會看到顏色被刪除了:

Screenshot showing CLI output without colors and enabled CI mode

你要記住另一個場景就是 stdout 可否在終端模式下運行。意思是將內容寫入終端。若是是這種狀況,咱們可使用相似 boxen 的東西來顯示全部漂亮的輸出。若是不是,則可能會將輸出重定向到文件或傳輸到其餘地方。

你能夠檢查 isTTY 相應的流屬性來檢查 stdinstdoutstderr 是否處於終端模式。例如:process.stdout.isTTY. 在這種狀況下特別用於終端,TTY 表明「電傳打字機」。

根據 Node.js 進程的啓動方式,三個流中的每一個流的值可能不一樣。你能夠在 Node.js 文檔的「進程 I/O」 部分瞭解到更多關於它的信息。

讓咱們看看 process.stdout.isTTY 在不一樣狀況下價值的變化狀況。更新你的 cli.js 文件以檢查它:

const chalk = require('chalk');

console.log(process.stdout.isTTY);
console.log('%s Hi there', chalk.cyan('INFO'));
複製代碼

而後使用 node cli.js 在你的終端你面運行,你會看到 true 打印後會跟着咱們的彩色消息。

Screenshot of output saying

以後運行相同的東西,但將輸出重定向到一個文件,而後經過運行檢查內容:

node cli.js > output.log
cat output.log
複製代碼

此次你會看到它會打印 undefined 後面跟着一個簡單的無色消息。由於 stdout 關閉了終端模式下 stdout 的重定向。由於 chalk 使用了 supports-color,因此在引擎蓋下會檢查各個流上的 isTTY

Screenshot saying

可是,像 chalk 這樣的工具已經爲你處理了這種行爲,當你開發 CLI 時,你應該始終注意你的 CLI 可能在 CI 模式下運行或輸出被重定向的狀況。它也能夠幫助你把你的 CLI 的經驗更進一步。例如,你能夠在終端以一種漂亮的方式排列數據,若是 isTTYundefined 的話,則切換到更容易解析的方式。

總結

開始使用 JavaScript 並使用 console.log 記錄你的第一行是很快的,可是當你將代碼帶到生產環境時,你應該考慮更多關於記錄的內容。本文僅介紹各類方法和可用的日誌記錄解決方案。它不包含你須要知道的一切。我建議你檢查一些你最喜歡的開源項目,看看它們如何解決日誌記錄問題以及它們使用的工具。如今去記錄全部的事情,不要打印你的日誌😉

GIF of endless printing of a document

若是你知道或找到任何我應該明確說起的工具,或者若是你有任何問題,請隨時聯繫我。我等不及想看看你作了什麼。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索