GCP Cloud Run で構造化ロギングする - Node.js

GCP Cloud Run で構造化ロギングする - Node.js

December 4, 2022

本記事は Node.js 環境を想定して記載しています。

GCP の Firebase Functions では {structuredData: true} を指定することで簡単に構造化ログを出力することができます。

import * as functions from 'firebase-functions';

functions.logger.error('Something happened!', { structuredData: true });

しかしこれは firebase-functions ライブラリの機能であるため、残念ながら GCP エコシステムのほかのサービスでは利用できません。

Firebase 以外の環境では @google-cloud/logging ライブラリを使用することで構造化ログを実現可能です。これによって Cloud Logging(旧称 Stackdriver)に構造化ログを書き込むことができます。

参考

しかしこの場合は Cloud Logging の API にログを送信して書き込む形になり、非同期通信を待つ必要があります。そのため firebase-functions とは使い勝手が若干変わります。

JSON.stringify() + console.log() するだけで OK #

実は @google-cloud/logging を使用せずとも console.log() に渡す値を JSON.stringify() するだけで構造化ログを記録することができます。

const entry = {
  name: 'Alice',
  age: 18,
  address: 'U.K.',
};

console.log(JSON.stringify(entry));

次の方法で、Logging に構造化ログを書き込むことができます。

  • Logging エージェントに対してシリアル化された JSON オブジェクトを指定する

https://cloud.google.com/logging/docs/structured-logging

構造化ログの値のなかには Cloud Logging として特別な意味を持つものがあります。例えば severity がその1つです。

こういった値を渡すことで Cloud Logging コンソール上でログのフィルタリングがしやすくなりますので、設定しておくと良いでしょう。

上記を踏まえると最低限のログ関数は次のようなものになります。

function log(message: any) {
  const severity = 'ERROR';

  const entry = {
    severity,
    message,
  };

  console.log(JSON.stringify(entry));
}

JSON.stringify() でのシリアライズの弱点を対策する #

JSON.stringify() ではシリアライズできないオブジェクトもあります。一例が Set や Map といったものです。これらを JSON.stringify() すると {} になってしまいます。

const names = new Set(['Alice', 'Bob', 'Charlie']);

console.log(JSON.stringify(names)); // -> {}

この理由は JSON.stringify() が列挙可能なプロパティのみをシリアライズするためです。

JSON.stringify() は値をそれを表す JSON 表記に変換します。

  • その他のすべての Object のインスタンスは (Map, Set, WeakMap, WeakSet を含め)、列挙可能なプロパティのみがシリアライズされます。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

ほかにも BigInt をシリアライズしようとすると例外が発生するといった問題もあります。

// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

// 列挙可能でないプロパティ:
JSON.stringify(
  Object.create(null, { x: { value: 'x', enumerable: false }, y: { value: 'y', enumerable: true } })
);
// '{"y":"y"}'

// BigInt の値は例外が発生
JSON.stringify({ x: 2n });
// TypeError: BigInt value can't be serialized in JSON

これらは JSON.stringify() の第二引数に変換用関数を渡すことで対応できます。

こういったように自作関数を渡すことでも対応可能なものの、良いライブラリが存在するのでそれらを使用するのが確実で手っ取り早いでしょう。

serialize-javascript でほぼすべてのオブジェクトをシリアライズ可能になります。ただし Error オブジェクトだけはシリアライズ対象となっていません。ISSUE & プルリクがあがっていますが、取り込まれていないのは残念ですね。

https://github.com/yahoo/serialize-javascript/issues/135

ということでエラーのシリアライズ化には serialize-error を使用するのが良いでしょう。

両方のライブラリを使用した log 関数の一例は次の通りです。

import { serializeError } from 'serialize-error';
import serialize from 'serialize-javascript';

function log(severity: 'INFO' | 'ERROR', message: any) {
  if (message instanceof Error) {
    message = serializeError(message);
  } else {
    message = serialize(message);
  }

  const entry = {
    severity,
    message,
  };

  console.log(JSON.stringify(entry));
}

const names = new Set(['Alice', 'Bob', 'Charlie']);
const error = new Error('Something happened!');

log('INFO', names); // -> {"severity":"INFO","message":"new Set([\"Alice\",\"Bob\",\"Charlie\"])"}
log('ERROR', error); // -> {"severity":"ERROR","message":{"name":"Error","message":"Something happened!","stack":"Error: Something happened!\n at .........