📝

Denoの@std/logが便利だった

2024/09/27に公開

はじめに

Node.js では logger として pinowinston を採用することが多いのではないでしょうか。
Deno でも利用することは可能ですが、標準ライブラリには @std/log が用意されています。

なぜ@std/log を選んだか

私は Cloud Run をよく利用しますので Cloud Run にデプロイする想定です。
デプロイ先では stdout にログを出力し、Cloud Logging によって自動収集させたいです。
さらに、出力するログは JSON オブジェクトとしてフォーマット(構造化ログ)して運用時の検索性を向上させたいです。
Node.js ではこのような時、前述したライブラリを採用すると思いますが、Deno では@std/log だけでログのフォーマットやログの書き出し方法の設定、環境によってのロガーの切り替えを行うことができます。@std/log は標準ライブラリです。最高ですね!

JSR のドキュメントにはたくさんのレシピがあります。

https://jsr.io/@std/log

どうやって使っているか

セットアップ

@std/log から setup という関数が提供されています。基本的にこの関数内で logger 用の設定をしていくのみでシンプルです。
loggers には本番(Cloud Run)とローカルで切り替え用の logger を定義し、logger には handlers を割り当て、handler には書き出し方法を定義しています。
プロジェクトでは ConsoleHandler のみ利用していますが、FileHandler を使って特定のファイルに書き出すこともできるみたいです。
handler ではログレベルやフォーマッターを定義できるのでこの中で環境別のフォーマッターや設定をしています。

log.setup({
  handlers: {
    dev: new log.ConsoleHandler("DEBUG", {
      formatter: defaultConsoleFormatter,
      useColors: true,
    }),
    prod: new log.ConsoleHandler("INFO", {
      formatter: gcpFormatter,
      useColors: false,
    }),
  },
  loggers: {
    production: {
      level: "INFO",
      handlers: ["prod"],
    },
    development: {
      level: "DEBUG",
      handlers: ["dev"],
    },
  },
});

実際に使うとこのように出力されます。

logger.info(`Listening on :4000`);

development

production

フォーマッター込みの全貌

Google Cloud の Log Entry 形式にフォーマットするものも入れてこのような感じになるでしょうか。
setupGlobalLogger から得られる logger はサーバーフレームワークの context などに入れてあらゆる箇所で取り出して使っています。

import * as log from "@std/log";
import { formatInTimeZone } from "date-fns-tz";

export function setupGlobalLogger(env: "development" | "production") {
  log.setup({
    handlers: {
      dev: new log.ConsoleHandler("DEBUG", {
        formatter: defaultConsoleFormatter,
        useColors: true,
      }),
      prod: new log.ConsoleHandler("INFO", {
        formatter: gcpFormatter,
        useColors: false,
      }),
    },
    loggers: {
      production: {
        level: "INFO",
        handlers: ["prod"],
      },
      development: {
        level: "DEBUG",
        handlers: ["dev"],
      },
    },
  });

  return log.getLogger(env);
}

const defaultConsoleFormatter: log.FormatterFunction = (record) => {
  const serializedArgs = record.args.map((arg) => Deno.inspect(arg)).join(" ");

  return `${formatInTimeZone(
    new Date(record.datetime),
    "Asia/Tokyo",
    "yyyy-MM-dd HH:mm:ss.SSS"
  )} [${record.levelName}] ${record.msg} ${serializedArgs}`;
};

const gcpFormatter: log.FormatterFunction = (record) => {
  const levelName = log.getLevelName(record.level as log.LogLevel);
  const [info, ...args] = record.args;
  const payload: LogPayload = {
    // RFC3339(ISO8601)
    time: record.datetime.toISOString(),
    severity: levelName,
    message: record.msg,
    name: "server",
    args: args,
    /**
     * NOTE: Error Reportingで通知するため、捕捉可能な形式に変換する
     * sevetiryがERROR以上の場合は通知されます
     * https://cloud.google.com/error-reporting/docs/formatting-error-messages?hl=ja#format-log-entry
     */
    ...(info instanceof Error
      ? {
          info: JSON.parse(
            JSON.stringify(info, Object.getOwnPropertyNames(info))
          ),
          stack_tace: info.stack,
        }
      : {
          info,
        }),
  };

  return JSON.stringify(payload);
};

type LogPayload = {
  message: string;
  severity: log.LevelName;
  time: string;
  name: string;
  info: any;
  args: any[];
  stack_trace?: string;
};
株式会社モニクル

Discussion