📝

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

2024/09/27に公開

はじめに

弊社のとある新規プロジェクトではバックエンドに Deno を採用しました。(誠意開発中です)

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

弊社のとある新規プロジェクトではこの標準ライブラリを採用しました。

なぜ@std/log を選んだか

弊社のとある新規プロジェクトではデプロイ先に 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;
};

おわりに

Deno でバックエンド開発を頑張っているので興味がある方はモニクルへ!カジュアル面談もあります!

株式会社モニクル

Discussion