何をしたいユーティリティか:「ログ用文字列生成」
ここで作りたいのは、「人間が読んで状況をすぐ理解できるログ文字列」を、毎回バラバラに書かず、共通のフォーマットで生成するユーティリティです。
業務システムでは、ログは「あとから原因を追うための唯一の手がかり」になることが多いです。
だからこそ、ログは
- いつ
- どの処理で
- 何が起きて
- どんな値だったか
が、一目で分かる形で出ていてほしい。
それを毎回手書きではなく、関数にしてしまおうという話です。
ログ用文字列に最低限ほしい情報
時刻・レベル・コンテキスト・メッセージ・詳細
ログ 1 行に、最低限こういう情報があると、かなり読みやすくなります。
- 時刻(いつ)
- レベル(INFO / WARN / ERROR など)
- コンテキスト(どの処理・どの画面・どの API か)
- メッセージ(何が起きたかの要約)
- 詳細(パラメータやエラー内容などの追加情報)
例えば、こんな形です。
2024-01-05T10:23:45.123Z INFO LoginService Login succeeded userId=123
2024-01-05T10:23:46.001Z ERROR OrderApi Failed to create order detail={"orderId":999,"reason":"stock short"}
この「形」をユーティリティで統一してしまうと、
ログが一気に読みやすくなります。
ログ用文字列生成の設計方針
フォーマットを 1 箇所に閉じ込める
やりたいことはシンプルで、
formatLog("INFO", "LoginService", "Login succeeded", { userId: 123 });
JavaScriptのように呼ぶと、決まったフォーマットの 1 行文字列が返ってくる、という形にします。
ポイントは、「ログの見た目のルール」を 1 箇所に閉じ込めることです。
そうすれば、あとから「時刻のフォーマットを変えたい」「JSON 部分を整形したい」となっても、そこだけ直せば全ログに反映されます。
日時フォーマット用の小さな関数を用意する
ISO 風のタイムスタンプを作る
まずは、ログの先頭に付ける「時刻文字列」を作る関数を用意します。
function formatLogTimestamp(date = new Date()) {
const d = date instanceof Date ? date : new Date(date);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const h = String(d.getHours()).padStart(2, "0");
const min = String(d.getMinutes()).padStart(2, "0");
const s = String(d.getSeconds()).padStart(2, "0");
const ms = String(d.getMilliseconds()).padStart(3, "0");
return `${y}-${m}-${day}T${h}:${min}:${s}.${ms}`;
}
JavaScriptこの関数は、例えばこう動きます。
formatLogTimestamp(new Date("2024-01-05T10:23:45.123Z"));
// "2024-01-05T10:23:45.123"
JavaScriptここではタイムゾーンまでは付けていませんが、必要なら "Z" を付けたり、+09:00 を付けたりすることもできます。
ログ用文字列生成関数の実装
formatLog の実装
次に、ログ 1 行を組み立てる本体を作ります。
function formatLog(level, context, message, detail) {
const ts = formatLogTimestamp();
const lvl = String(level || "").toUpperCase().padEnd(5, " ");
const ctx = String(context || "").padEnd(12, " ");
let detailPart = "";
if (detail !== undefined && detail !== null) {
if (typeof detail === "string") {
detailPart = " " + detail;
} else {
try {
detailPart = " " + JSON.stringify(detail);
} catch (e) {
detailPart = " [detail: JSON stringify failed]";
}
}
}
return `${ts} ${lvl} ${ctx} ${message}${detailPart}`;
}
JavaScript重要なポイントを深掘りして説明する
レベルとコンテキストを「桁揃え」する
const lvl = String(level || "").toUpperCase().padEnd(5, " ");
const ctx = String(context || "").padEnd(12, " ");
JavaScriptここでは、レベルとコンテキストを「固定幅」にしています。
padEnd(5, " ") は、「長さ 5 になるまで右側をスペースで埋める」という意味です。
例えば、
"INFO".padEnd(5, " "); // "INFO "
"ERROR".padEnd(5, " "); // "ERROR"
"DBG".padEnd(5, " "); // "DBG "
JavaScriptこうしておくと、ログを縦に並べたときに、
レベルやコンテキストの位置が揃って、とても読みやすくなります。
詳細情報の扱いを柔らかくする
let detailPart = "";
if (detail !== undefined && detail !== null) {
if (typeof detail === "string") {
detailPart = " " + detail;
} else {
try {
detailPart = " " + JSON.stringify(detail);
} catch (e) {
detailPart = " [detail: JSON stringify failed]";
}
}
}
JavaScriptここでは、detail に何が来てもそれなりに扱えるようにしています。
文字列ならそのまま付ける。
オブジェクトや配列なら JSON.stringify で 1 行の JSON にする。JSON.stringify が失敗したら、その旨をメッセージとして出す。
こうしておくと、呼び出し側は
formatLog("INFO", "OrderApi", "Created order", { orderId: 123, amount: 9999 });
JavaScriptのように、素直にオブジェクトを渡すだけで、
ログには
... INFO OrderApi Created order {"orderId":123,"amount":9999}
のように出てくれます。
実際の動きを例で確認する
シンプルなログ
formatLog("INFO", "LoginService", "Login succeeded", { userId: 123 });
JavaScript出力イメージ:
2024-01-05T10:23:45.123 INFO LoginService Login succeeded {"userId":123}
エラーログ
formatLog(
"ERROR",
"OrderApi",
"Failed to create order",
{ orderId: 999, reason: "stock short" }
);
JavaScript出力イメージ:
2024-01-05T10:23:46.001 ERROR OrderApi Failed to create order {"orderId":999,"reason":"stock short"}
詳細なしのログ
formatLog("WARN", "BatchJob", "Retrying connection");
JavaScript出力イメージ:
2024-01-05T10:23:47.500 WARN BatchJob Retrying connection
ロガーっぽいラッパーを用意する
logger オブジェクトを作る
毎回 formatLog("INFO", ...) と書くのは少し面倒なので、
レベルごとのメソッドを持つ「ロガー」を作ると、さらに使いやすくなります。
const logger = {
info(context, message, detail) {
console.log(formatLog("INFO", context, message, detail));
},
warn(context, message, detail) {
console.warn(formatLog("WARN", context, message, detail));
},
error(context, message, detail) {
console.error(formatLog("ERROR", context, message, detail));
},
};
JavaScript使い方はこうです。
logger.info("LoginService", "Login succeeded", { userId: 123 });
logger.warn("BatchJob", "Retrying connection", { retryCount: 2 });
logger.error("OrderApi", "Failed to create order", { orderId: 999, reason: "stock short" });
JavaScriptこれで、ログのフォーマットは formatLog に閉じ込めつつ、
呼び出し側は「レベル+コンテキスト+メッセージ+詳細」だけを意識すればよくなります。
実務で意識してほしい設計のポイント
「ログはあとから読む自分への手紙」
ログは、エラーが起きたときに「過去の自分が残してくれた手がかり」です。
だからこそ、
- 何が起きたか(メッセージ)
- どこで起きたか(コンテキスト)
- どんな値だったか(詳細)
を、未来の自分が読んで分かるように書くことが大事です。
"error" だけのログは、あとから見ても何も分かりません。"Failed to create order" だけでも足りません。orderId や userId など、「特定に必要な情報」を一緒に出しておくと、調査が一気に楽になります。
フォーマットを変えたくなったときのために
今回の formatLog はあくまで一例です。
「JSON 形式で出したい」「タイムゾーンを付けたい」「スレッド ID を入れたい」など、
プロジェクトごとに要件は変わります。
大事なのは、「ログの見た目を決める場所が 1 箇所にある」ことです。formatLog を中心にしておけば、
あとからフォーマットを変えたくなっても、そこだけ直せば全体に反映されます。
機密情報を出しすぎない
ログには何でもかんでも出していいわけではありません。
パスワード、クレジットカード番号、個人情報などは、
マスクするか、そもそも出さないようにする必要があります。
formatLog の前段で、「ログに出してよい情報だけを detail に渡す」
という意識を持っておくと、安全側に倒せます。
少し手を動かして感覚をつかむ
コンソールで、次のようなコードを実際に打ってみてください。
formatLog("INFO", "LoginService", "Login succeeded", { userId: 123 });
formatLog("WARN", "BatchJob", "Retrying connection", { retryCount: 2 });
formatLog("ERROR", "OrderApi", "Failed to create order", { orderId: 999, reason: "stock short" });
logger.info("LoginService", "Login succeeded", { userId: 123 });
logger.error("OrderApi", "Failed to create order", { orderId: 999, reason: "stock short" });
JavaScriptコンソールに出てきたログを眺めて、
- 時刻
- レベル
- コンテキスト
- メッセージ
- 詳細
が、一目で分かるかどうかを感じてみてください。
そのうえで、自分のプロジェクトに
export function formatLog(...) { ... }
export const logger = { info, warn, error, ... };
JavaScriptを置き、
「ログを出したくなったら必ずこのロガーを使う」
というルールを作ってみてください。
それだけで、あなたのログは、場当たり的な console.log の寄せ集めから、
意図と一貫性を備えた“業務レベルのログ用文字列生成”に変わっていきます。
