Node.JS 應用最佳實踐:日誌

做者:Mahesh Haldarjavascript

翻譯:瘋狂的技術宅前端

原文:blog.bitsrc.io/logging-bes…java

未經容許嚴禁轉載node

img

日誌記錄是每一個開發人員從第一天編寫代碼時就要作的事情,但不多有人知道它能夠產生的價值和最佳實踐。git

在本文中,咱們將討論如下主題:github

  • 什麼是日誌,爲何很重要性?
  • 記錄日誌的最佳作法
  • 日誌的重要部分
  • 正確使用日誌級別
  • 爲何選擇 Winston?

什麼是日誌,爲何很重要?

日誌是反映程序各個方面的事件,若是可以正確編寫,那麼它就是最簡單的故障排除和診斷程序的模式。數據庫

當你啓動 Node.js 服務器時,若是數據庫因爲某些問題而沒有運行,或服務器端口已經被佔用時,若是沒有日誌,你將永遠不知道服務器失敗的緣由。npm

做爲開發人員,你常常須要調試一些問題,咱們很喜歡用調試器和斷點來定位故障的位置和內容。json

當你的程序在生產環境中運行時,你會作些什麼?你能在那裏附加調試器並重現 bug 嗎?顯然沒有。所以,這是日誌記錄可以幫助你的地方。前端工程化

在不使用調試器的狀況下,你能夠經過瀏覽日誌找到問題並瞭解出現問題的緣由和位置。

最佳實踐

1)日誌的三個重要部分

程序日誌既適用於人類,也適用於機器。人類參考日誌來調試問題,機器用日誌生成各類圖表,並經過數據分析來產生關於客戶使用的各類結論。

每一個日誌都應包含三個最重要的部分:

  • 日誌源

    當咱們有一個微服務架構時,這對於瞭解日誌的來源、服務名稱、區域、主機名等信息很是重要(有關管理微服務中的公共代碼的更多信息請在此處閱讀

    有關源的詳細元數據主要由日誌 agent 進行處理,日誌 agent 將日誌從全部微服務推送到集中式日誌系統。 ELK 棧的 Filebeat 是日誌 agent 的最佳選擇之一。

  • 時間戳

    事件發生或生成日誌的時間很是重要。因此要確保每一個日誌都有時間戳,以便咱們進行排序和篩選。

  • 級別和上下文

    在經過查看日誌查找錯誤時,若是日誌沒有提供足夠的信息,你就必須回到代碼中,那將很是使人沮喪。所以在記錄時咱們應該傳遞足夠的上下文

    例如。沒有上下文的日誌將以下所示:

    The operation failed!

    有意義的上下文應該是是:

    Failed to create user, as the user id already exist

2)日誌的使用方法

  • 日誌方法和輸入:

    在調試的同時,若是咱們知道調用了哪一個函數以及傳遞了哪些參數,它就能發揮真正的做用。

import logger from '../logSetup';
getInstallment(month: number, count: number ): number {
    logger.debug(`>>>> Entering getInstallment(month = ${month}, count= ${count}"); // process const installment: number = 3; log.debug("<<<< Exiting getIntallment()"); return installment; } 複製代碼

經過日誌 >>>><<<< 將給出函數輸入和退出的信息。這是受到了 git merge 衝突的啓發。

  • 日誌不該該評估拋出異常

    在第7行中,userService.getUser() 能夠返回 null,且 .getId() 能夠拋出異常,因此要避免這些狀況。

import logger from '../logSetup';
processLoan(...) {
    logger.debug(">>>> Entering processLoan()");
    
    // ... process

    logger.debug(`Processing user loan with id ${userService.getUser().getId()}`);
    // this might throw error, when getUser returns undefined

    logger.debug("<<<< Exiting processLoan()");
    return true;
}
複製代碼

你應該用 Aspect js 自動執行函數級日誌。

  • 日誌不該產生反作用

    日誌應該是無狀態的,不該產生任何反作用。例如,下面第 7 行的日誌將在數據庫中建立新資源。

import logger from '../logSetup';
createUser() {
  logger.debug(">>>> Entering createUser");

  // ... process

  logger.debug("Saving user loan {}", userInfoRepository.save(userInfo)) // don't do this

  return true;
}
複製代碼
  • 記錄錯誤和詳細信息

當描述錯誤時,請說起嘗試的內容及其失敗的緣由。

記錄哪些是失敗的你接下來作什麼

import logger from '../logSetup';
processLoan(id: number, userId: number) {
    try {
        getLoanDeatilsById()
    } catch(error) {
        log.error(`Failed to do getLoanDetails with id ${id}, ignoring it and trying to getLoanDetailsByUserId`, error);
        // good example: provide what failed, and how you are handling. 
        // e.g here on fail I am trying to call other function
        getLoanDetailsByUserId();
    }
}
複製代碼

若是你在 catch 部分中丟棄錯誤,請記錄哪一個操做失敗並說起你正在拋出錯誤。

import logger from '../logSetup';
processLoan(id: number, userId: number) {
    try {
        getLoanDeatilsById()
    } catch(error) {
        log.error(`Failed to do getLoanDetails with id ${id} hence throwing error`, error);
        // good example: provide what failed, and how you are handling. 
        // e.g here on fail I am throwing
        throw error;
    }
}
複製代碼

3)敏感信息

該系列日誌應該反映用戶在程序中的活動以便調試更容易,而且應該記錄錯誤以便儘快採起措施。日誌包含一些信息,例如調用哪些函數,輸入的內容,發生的位置和錯誤等。

記錄時咱們必須確保不去記錄用戶名和密碼等敏感信息,例如信用卡號、CVV 號碼等財務信息。

做爲開發人員,咱們應該經過與產品團隊溝通,來準備敏感信息的列表並在記錄以前將其屏蔽。

4)正確使用日誌級別

若是生產環境下的程序具備至關多的用戶事務,那麼理想的日誌設置可能天天會生成 GB 級別的日誌,所以咱們須要將日誌分組爲多個組。根據受衆,咱們能夠在運行時切換日誌級別,並僅獲取適當的日誌。

例如,若是產品經理但願在咱們的日誌記錄儀錶板中查看有多少客戶交易成功或失敗,則不該向他展現各類功能調用的雜亂信息,這些信息僅供開發人員使用。當生產環境中存在錯誤時,開發人員應該看到各類函數成功執行和失敗的詳細日誌。這樣就能夠儘快發現並修復問題。

要實現這種設置,咱們須要更好地瞭解每一個日誌級別。

讓咱們討論最重要的級別及其用法:

  • **INFO:**一些重要的消息,描述一個任務完成時的事件消息。例如:New User created with id xxx

這表示僅記錄進度信息

  • **DEBUG:**此級別適用於開發人員,這相似於記錄你在使用調試器或斷點時看到的信息,例如調用了哪一個函數以及傳遞了哪些參數等。它應該記錄當前狀態,這樣在調試和查找確切問題時會頗有用。

  • **WARN:**這些日誌是警告而且不阻止應用程序繼續運行,這些日誌會在出現問題並使用變通方法時發出警報。例如錯誤的用戶輸入、重試等。管理員未來應該修復這些警告。

  • **ERROR:**發生了錯誤時,應在優先在這裏進行調查。例如數據庫與其餘微服務的通訊失敗,或所須要的輸入未定義。

主要受衆是系統操做員或監控系統。

理想狀況下,生產環境下的程序應該具備接近零的錯誤日誌。

5)不要使用console.log

大多數開發人員使用控制檯模塊做爲獲取日誌或調試代碼的第一個工具,由於它簡單容易且全局可用,無需設置。在 Node.Js 中,控制檯的實現方式與瀏覽器不一樣,控制檯模塊在使用 console.log 時會在 stdout 中打印消息,若是使用 console.error 它將打印到 stderr。

console.logconsole.debugconsole.info 都在 stdout 中打印,所以咱們將沒法關閉或打開調試和及信息。一樣,``console.warnconsole.error` 都在 stderr 中打印。

生產環境程序很難切換各類級別。

咱們還須要不一樣類型的配置,如標準格式、把JSON 輸出格式發送到 ELK 棧,這些在開箱即用的控制檯中不可用。

要克服全部這些問題,可使用 Winston 日誌框架,還有其餘一些選項,如BunyanPino等。

爲何須要像 Winston 這樣的日誌庫?

在上一節中咱們討論了控制檯的一些缺陷,讓咱們列出 Winston 提供的一些重要功能:

  • 級別: Winston 提供了幾組日誌級別,而且還將級別打印爲日誌的一部分,這可使咱們可以在集中式儀表板中過濾日誌。 例如 {message: 「something wrong」, level: 「error"}

    若是須要,你也能夠建立自定義級別。

  • 格式: Winston 有一些高級配置,好比給日誌着色,輸出 JSON 格式等等。

  • **動態更改日誌級別:**咱們將在生產環境程序中啓用警告和錯誤,並能夠根據須要將日誌級別更改成調試並返回錯誤,而無需從新啓動程序。 Winston 具備這種開箱即用的功能。

// log setup
import winston from 'winston';
const transports = {
  console: new winston.transports.Console({ level: 'warn' }),
};

const logger = winston.createLogger({
  transports: [transports.console, transports.file]
});

logger.info('This will not be logged in console transport because warn is set!');

transports.console.level = 'info'; // changed the level

logger.info('This will be logged in now!');

export default {logger, transport}
複製代碼

咱們還能夠公開 API 動態更改級別,公開 REST API 並在處理程序中執行第 13 行以更改級別。

  • **傳輸:**對於生產環境,咱們但願有一個集中式日誌記錄系統,全部的微服務都會推送日誌,咱們將經過儀表板過濾和搜索日誌。這是標準的 ELK 設置或等效設置。
import winston from 'winston';
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    //
    new winston.transports.File({ filename: 'stdout.log' })
  ]
});

export default logger;
複製代碼

經過配置 Winston 將咱們的日誌寫入文件,以便任何日誌託運代理均可以將日誌推送到集中式系統。可是,這超出了本文的範圍,咱們會在另外一篇文章中詳細討論。

6)性能影響

若是程序寫日誌的頻率很高,則可能直接影響程序性能。

DEBUG 和 INFO 級別的日誌可佔到總體的 95% 以上,這就是爲何應該只啓用 ERROR 和 WARN 級別,並在想要找出問題時將級別更改成DEBUG,以後再將其切換回 ERROR 。

當應用程序出現問題時,日誌就是救星。若是你當前尚未很好的使用日誌,請實施日誌記錄實踐並將日誌添加到代碼審查覈對錶中。

歡迎關注前端公衆號:前端先鋒,領取前端工程化實用工具包。

相關文章
相關標籤/搜索