Denoの@std/logが便利だった
はじめに
Node.js では logger として pino や winston を採用することが多いのではないでしょうか。
Deno でも利用することは可能ですが、標準ライブラリには @std/log が用意されています。
なぜ@std/log を選んだか
私は Cloud Run をよく利用しますので Cloud Run にデプロイする想定です。
デプロイ先では stdout にログを出力し、Cloud Logging によって自動収集させたいです。
さらに、出力するログは JSON オブジェクトとしてフォーマット(構造化ログ)して運用時の検索性を向上させたいです。
Node.js ではこのような時、前述したライブラリを採用すると思いますが、Deno では@std/log
だけでログのフォーマットやログの書き出し方法の設定、環境によってのロガーの切り替えを行うことができます。@std/log
は標準ライブラリです。最高ですね!
JSR のドキュメントにはたくさんのレシピがあります。
どうやって使っているか
セットアップ
@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