- 原文地址:A Guide to Node.js Logging
- 原文做者:dkundel
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:fireairforce
當你開始使用 JavaScript 開始時,你應該學會的第一件事就是如何經過 console.log()
將事物記錄到控制檯。若是你搜索如何調試 JavaScript
,你會發現數百篇博客文章和 StackOverflow 上的文章會告訴你很「簡單」的使用 console.log()
來完成調試。由於這是一種常見的作法,咱們甚至開始使用 linter
規則,好比 no-console
,以確保咱們不會在生產代碼中留下意外的日誌記錄。可是若是咱們真的想記錄一些東西來提供更多的信息呢?html
在這篇博文中,我將會介紹一些你想要記錄信息的各類狀況,以及在 Node.js 中 console.log
和 console.error
的區別,以及如何在不影響用戶控制檯的狀況下往庫裏面發送日誌記錄。前端
console.log(`Let's go!`);
複製代碼
雖然您能夠在瀏覽器和 Node.js 中使用 console.log
或 console.error
,但在使用 Node.js 時須要記住一件重要的事情。在一個叫作 index.js
的文件中寫下面的代碼:node
console.log('Hello there');
console.error('Bye bye');
複製代碼
而後在終端裏面使用 node index.js
來運行它,你會看到這兩個直接在下面輸出:android
然而,雖然這兩個看上去可能相同,但系統實際上對它們的處理方式並不相同。若是你去查看 Node.js 文檔中 console
部分,你會看到 console.log
是使用 stdout
來打印而 console.error
使用 stderr
來打印。ios
每一個進程均可以使用三個默認的 streams
來工做。它們分別是 stdin
、stdout
和 stderr
。stdin
流來處理和你的進程相關的輸出。例如按下按鈕或重定向輸出(咱們會在一秒鐘以內完成)。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
複製代碼
既然咱們已經瞭解了日誌記錄的基礎記錄方面,讓咱們先談談你可能想要記錄某些內容的不一樣用例。一般這些用例屬於如下的類別之一: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,你會注意到它將打印出許多咱們真正並不須要的信息。
若是將代碼改爲 console.log('%s', req)
爲不打印整個對象,咱們也不會得到太多的信息。
咱們能夠編寫咱們本身的打印函數,它只輸出咱們關心的東西,可是讓咱們先回退一步,討論一下咱們一般關心的事情。雖然這些信息常常成爲咱們關注的焦點,但實際上咱們可能還須要其餘信息。例如:
pm2
的工具來運行多個 Node 進程另外,既然咱們知道全部的東西都會轉到 stdout
和 stderr
,那麼咱們可能須要不一樣的日誌級別,而且根據它們來配置和過濾日誌的能力。
咱們能夠經過訪問各部分的 process
而且寫一大堆 JavaScript 代碼來獲取這些,可是關於 Node.js 最好的事情是咱們獲得了 npm
生態系統,而且已經有各類各樣的庫供咱們使用。其中有一些是:
我我的很喜歡 pino
這個庫,由於它運行很快,而且生態系統比較好,讓咱們來看看如何使用 pino
來幫咱們記錄日誌。咱們同時也可使用 express-pino-logger
包來幫助咱們整潔的記錄請求。
同時安裝 pino
和 express-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。
若是你檢查這個 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
信息。
有許多現有的傳輸工具能夠美化或轉換你的日誌。你甚至能夠經過 pino-colada
來顯示 emojis。這會對你的本地開發頗有用。在生產環境中運行服務器後,你可能但願將日誌輸出到到另一個傳輸中,使用 >
將其寫入磁盤以待稍後處理,或者使用相似於 tee
的命令來進行同時的處理。
該 文檔 還將包含有關諸如輪換日誌文件,過濾和將日誌寫入不一樣文件等內容的信息。
既然咱們研究瞭如何有效地爲服務器應用程序編寫日誌,爲何不對咱們編寫的庫使用相同的技術呢?
問題是,你的庫可能但願記錄用於調試的內容,但實際上不該該讓使用者的應用程序變得混亂。相反,若是須要調試某些東西,使用者應該可以啓用日誌。你的庫在默認狀況下應該是不會處理這些的,並將寫入輸出的操做留給用戶。
express
就是一個很好的例子。在 express
框架下有不少的事情要作,在調試應用程序時,你可能但願瞭解一下框架的內容。若是咱們查詢 express
文檔,你會注意到你能夠在你的命令前面加上 DEBUG=express:*
這樣一行代碼:
DEBUG=express:* node index.js
複製代碼
若是你使用如今的應用程序運行這個命令,你將看到許多其餘輸出,可幫助你調試問題。
若是你沒有啓用調試日誌記錄,則不會看到任何這樣的日誌。這是經過調用一個叫作 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:randomid
的 debug
記錄器,而後會將兩種消息記錄上去。而後咱們在前一節的 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
來從新啓動服務器,它會打印咱們「庫」的調式日誌。
有意思的是,若是使用你的庫的用戶想把這些調試信息方法到本身的 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
複製代碼
你如今就能夠用和應用程序日誌相同的格式來查看庫的的調試日誌。
我將在這篇博文中介紹的最後一個案例是針對 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
運行這個腳本,你將會看到對應的顏色輸出。
可是你使用 CI=true node cli.js
來運行它,你會看到顏色被刪除了:
你要記住另一個場景就是 stdout
可否在終端模式下運行。意思是將內容寫入終端。若是是這種狀況,咱們可使用相似 boxen
的東西來顯示全部漂亮的輸出。若是不是,則可能會將輸出重定向到文件或傳輸到其餘地方。
你能夠檢查 isTTY
相應的流屬性來檢查 stdin
、stdout
或 stderr
是否處於終端模式。例如: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
打印後會跟着咱們的彩色消息。
以後運行相同的東西,但將輸出重定向到一個文件,而後經過運行檢查內容:
node cli.js > output.log
cat output.log
複製代碼
此次你會看到它會打印 undefined
後面跟着一個簡單的無色消息。由於 stdout
關閉了終端模式下 stdout
的重定向。由於 chalk
使用了 supports-color
,因此在引擎蓋下會檢查各個流上的 isTTY
。
可是,像 chalk
這樣的工具已經爲你處理了這種行爲,當你開發 CLI 時,你應該始終注意你的 CLI 可能在 CI 模式下運行或輸出被重定向的狀況。它也能夠幫助你把你的 CLI 的經驗更進一步。例如,你能夠在終端以一種漂亮的方式排列數據,若是 isTTY
是 undefined
的話,則切換到更容易解析的方式。
開始使用 JavaScript 並使用 console.log
記錄你的第一行是很快的,可是當你將代碼帶到生產環境時,你應該考慮更多關於記錄的內容。本文僅介紹各類方法和可用的日誌記錄解決方案。它不包含你須要知道的一切。我建議你檢查一些你最喜歡的開源項目,看看它們如何解決日誌記錄問題以及它們使用的工具。如今去記錄全部的事情,不要打印你的日誌😉
若是你知道或找到任何我應該明確說起的工具,或者若是你有任何問題,請隨時聯繫我。我等不及想看看你作了什麼。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。