JavaScript Tips | 文字列ユーティリティ:業務用 - ログ用文字列生成

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「ログ用文字列生成」

ここで作りたいのは、「人間が読んで状況をすぐ理解できるログ文字列」を、毎回バラバラに書かず、共通のフォーマットで生成するユーティリティです。

業務システムでは、ログは「あとから原因を追うための唯一の手がかり」になることが多いです。
だからこそ、ログは

  • いつ
  • どの処理で
  • 何が起きて
  • どんな値だったか

が、一目で分かる形で出ていてほしい。
それを毎回手書きではなく、関数にしてしまおうという話です。


ログ用文字列に最低限ほしい情報

時刻・レベル・コンテキスト・メッセージ・詳細

ログ 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" だけでも足りません。
orderIduserId など、「特定に必要な情報」を一緒に出しておくと、調査が一気に楽になります。

フォーマットを変えたくなったときのために

今回の 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 の寄せ集めから、
意図と一貫性を備えた“業務レベルのログ用文字列生成”に変わっていきます。

タイトルとURLをコピーしました