Node.js精進(7)——紀錄檔

2022-07-04 09:01:00

  在 Node.js 中,提供了console模組,這是一個簡單的偵錯控制檯,其功能類似於瀏覽器提供的 JavaScript 控制檯。

  本系列所有的範例原始碼都已上傳至Github,點選此處獲取。 

一、原理

  與瀏覽器一樣,Node.js 也提供了一個全域性變數 console(範例化 Console 類),可呼叫 log()、error() 等方法。

1)同步還是非同步

  console 的方法既不像瀏覽器中那樣始終同步,也不像 Node.js 中的流那樣始終非同步。

  是否為同步取決於連結的是什麼流以及作業系統是 Windows 還是 POSIX:

  • 檔案:在 Windows 和 POSIX 中都是同步。
  • TTY(終端):在 Windows 上是非同步,在 POSIX 上是同步。
  • 管道和通訊端:在 Windows 上是同步,在 POSIX 上是非同步。

  可移植作業系統介面(Portable Operating System Interface,縮寫為POSIX)是 IEEE 為要在各種 UNIX 作業系統上執行軟體,而定義API的一系列互相關聯的標準的總稱。

  console 的這些行為部分是出於歷史原因,因為一旦將它們更改,那麼會導致向後不相容(即不相容舊版本)。

  注意,同步寫入會阻塞事件迴圈,直至寫入完成。

  在輸出到檔案的情況下,這可能幾乎是瞬時的。

  但在系統負載高、接收端未讀取管道或終端或檔案系統速度較慢的情況下,事件迴圈可能經常被阻塞足夠長,足以對效能產生嚴重的負面影響。

2)log() 和 error()

  在src/lib/internal/console/constructor.js中,儲存著 console.log() 和 console.error() 兩個方法的原始碼。

  下面是刪減過的原始碼,可以看到在輸出之前會自動加換行符,並且 log() 和 error() 使用的輸出方法還不同。

function(streamSymbol, string) {
  const groupIndent = this[kGroupIndent];
  const useStdout = streamSymbol === kUseStdout;
  
  // 若是普通輸出,則用 process.stdout 輸出,若是錯誤,則用 process.stderr 輸出
  const stream = useStdout ? this._stdout : this._stderr;
  const errorHandler = useStdout
    ? this._stdoutErrorHandler
    : this._stderrErrorHandler;

  if (groupIndent.length !== 0) {
    if (StringPrototypeIncludes(string, "\n")) {
      string = StringPrototypeReplace(string, /\n/g, `\n${groupIndent}`);
    }
    string = groupIndent + string;
  }
  // 末尾加換行
  string += "\n";

  try {
    // 控制檯輸出
    stream.write(string, errorHandler);
  } catch (e) {
    // Console is a debugging utility, so it swallowing errors is not
    // desirable even in edge cases such as low stack space.
    if (isStackOverflowError(e)) throw e;
    // Sorry, there's no proper way to pass along the error here.
  } finally {
    stream.removeListener("error", noop);
  }
}

  若是 log() 輸出,則用 process.stdout.write() 方法;若是 error() 輸出,則用 process.stderr.write() 方法。

  process.stdout 返回的是一個流,配合 util 模組的inspect()方法,可將物件解析成字串,並在控制檯著色,如下所示。

const util = require('util');
const str = util.inspect({ name: "strick" }, { colors: true });
process.stdout.write(str +'\n');

  

二、bunyan.js

  目前在我們的專案中,使用的紀錄檔庫是bunyan.js,這個庫不僅支援終端和 Node.js 環境,還支援瀏覽器環境。

1)原理

  從 Github 上下載原始碼後,可以看到 bin 和 lib 兩個目錄,終端的程式碼存於前者,專案中參照的程式碼存於後者。

  下面是一個簡單的範例,引入後也不用範例化,可直接呼叫方法,log.info() 相當於 console.log(),不過前者會自動將物件轉換成字串。

const bunyan = require('bunyan');
const log = bunyan.createLogger({name: "example"});
log.info("strick");

  在紀錄檔內部,會為每條紀錄檔維護一個物件,像上面的 strick 字串,在內部生成的物件格式如下。

{
  name: 'example',
  hostname: '192.168.0.101',
  pid: 94371,
  level: 30,
  msg: 'strick',
  time: 2022-05-11T07:21:51.310Z,
  v: 0
}

  下面是紀錄檔輸出的核心程式碼(已做刪減),其中 s.stream 使用的也是 process.stdout 類。

  fastAndSafeJsonStringify() 函數用於將物件轉換成字串,它的工作原理會在後文分析。

Logger.prototype._emit = function (rec, noemit) {
  var i;
  // 將物件轉換成字串
  var str;
  if (noemit || this.haveNonRawStreams) {
    str = fastAndSafeJsonStringify(rec) + os.EOL;
  }
  if (noemit) return str;
  var level = rec.level;
  for (i = 0; i < this.streams.length; i++) {
    var s = this.streams[i];
    if (s.level <= level) {
      // 輸出原始物件或字串
      s.stream.write(s.raw ? rec : str);
    }
  }
  return str;
};

  預設執行 node src.js,在控制檯輸出時是沒有著色的,不便於閱讀。

  在本地偵錯 bunyan 庫時,可以加 node src.js | ../bin/bunyan,執行此命令後,不僅能著色,還會格式化成可讀性更高的字串,如下所示。

  

  注意,在專案中載入此庫後,命令中就不需要加路徑了,例如 node index.js | bunyan。

2)fastAndSafeJsonStringify()

  此函數會在 JSON 物件序列化時,處理物件中的迴圈和 getter 異常,如下所示。

  第一組 try-catch 用於處理JSON.stringify()的異常,第二組 try-catch 用於處理 JSON.stringify(rec, safeCycles()) 的異常。

  最後會判斷是否安裝了safe-json-stringify庫,若安裝就用該庫處理,否則就輸出報錯資訊。

function fastAndSafeJsonStringify(rec) {
  try {
    return JSON.stringify(rec);
  } catch (ex) {
    try {
      return JSON.stringify(rec, safeCycles());
    } catch (e) {
      // 安裝了 safe-json-stringify 庫
      if (safeJsonStringify) {
        return safeJsonStringify(rec);
      } else {
        var dedupKey = e.stack.split(/\n/g, 3).join("\n");
        _warn(
          "bunyan: ERROR: Exception in " +
            "`JSON.stringify(rec)`. You can install the " +
            '"safe-json-stringify" module to have Bunyan fallback ' +
            "to safer stringification. Record:\n" +
            _indent(format("%s\n%s", util.inspect(rec), e.stack)),
          dedupKey
        );
        return format(
          "(Exception in JSON.stringify(rec): %j. " +
            "See stderr for details.)",
          e.message
        );
      }
    }
  }
}

  接下來看一個迴圈參照的例子,如下所示,在呼叫 JSON.stringify() 方法時會報錯:TypeError: Converting circular structure to JSON。

  一般自己寫的 JSON 物件很少會出現迴圈參照,但是在一些比較複雜的物件內部有可能會出現。

const man = {
  child: {}
}
man.child = man;
JSON.stringify(man);

  在 bunyan.js 的內部給出了兩種解決方案,一種是用Set資料型別處理,另一種是用陣列處理。

  解決思路其實差不多,都是依次將屬性值加到資料結構中,當判斷到已存在時,就返回 [Circular],終止其後面屬性的序列化。

var safeCycles = typeof (Set) !== 'undefined' ? safeCyclesSet : safeCyclesArray;
// Set 方式
function safeCyclesSet() {
  var seen = new Set();
  return function (key, val) {
    // 若 value 不存在或不是物件型別,則返回該值
    if (!val || typeof val !== "object") {
      return val;
    }
    // 若 seen 中包含該值,則返回 [Circular]
    if (seen.has(val)) {
      return "[Circular]";
    }
    seen.add(val);
    return val;
  };
}
// 陣列方式
function safeCyclesArray() {
  var seen = [];
  return function (key, val) {
    if (!val || typeof val !== "object") {
      return val;
    }
    if (seen.indexOf(val) !== -1) {
      return "[Circular]";
    }
    seen.push(val);
    return val;
  };
}

  JSON.stringify() 方法的第二個引數是一個回撥,被序列化的值的每個屬性都會經過該函數的處理。

  在呼叫 safeCyclesSet() 函數後,就能輸出 {"child":"[Circular]"}。

JSON.stringify(man, safeCyclesSet());  // {"child":"[Circular]"}

 

 

參考資料:

紀錄檔模組

process物件

官網 process console