ログレベルを「次のアクション」で決めたら、運用でのアラート疲れが減った

運用ログのレベルは、開発者の「困った度」やHTTPステータスの重大度ではなく、運用者が次に何をするかで決めるものだ——そう気づいてから、日々の運用が変わった、という話です。

以前関わったシステムでは、Slack通知をログのメッセージ本文で切っていました。log.level がERRORではないログでも通知が飛ぶことがあり、開いてみれば対応不要な記録だった——そんな経験がありました。これはレベルの付け方が悪かったというより、通知が構造化フィールドではなく本文を見ていたのが原因でした。

本プロダクトでは、Software Design 2026年3月号のログ設計特集などを参考に、Canonical Log(リクエスト1本1行のJSON)で log.level を毎回出し、Slackはそのフィールドで切るようにしました。そのうえで、レベルの中身は、HTTPステータスや例外の重大度ではなく、運用者が次に何をするかという基準で決めました。たとえば本プロダクトはACL付きのアセット共有システムで、権限のないアクセスに対する403はバグではなく想定内の拒否なのでINFO、DBとS3の不整合は当日中に手を動かす必要があるのでERROR——といった具合です。

この記事では、なぜその設計にしたかと、運用が変わった理由を整理します。

1. まず前提をそろえる — 通知は本文ではなくフィールドで切る

Slackに通知が来たとき、運用者が最初に知りたいのは「今すぐ動くべきか」です。以前関わったシステムでは、これをログのメッセージ本文で判定していました。結果、log.level はINFOのまま通知が飛ぶことがあり、開いてみれば対応不要——という空振りが積み重なります。原因はレベルの付け方ではなく、何を見て通知するか(検知条件)でした。

そこで本プロダクトでは、Canonical Log Line の考え方に沿い、リクエスト終了時に運用サマリをJSON 1行で出すようにしました。trace.idevent.action に加え、log.levelerror.codeなどを載せ、Slackのアラートは本文ではなく log.level フィールドの値で切るようにしました。これで「ERRORなら必ず通知される・INFOなら通知されない」という対応関係が保証されるようになりました。

権限のないファイルをダウンロードしようとしたときのサマリは、たとえばこうなります(値はマスク・簡略化した例です)。

{
  "@timestamp": "2026-08-12T10:14:32+09:00",
  "trace.id": "5f2a1e3d-9c7b-4e21-8a3f-1b6d2c9e0a44",
  "event.action": "asset.download",
  "event.outcome": "failure",
  "http.response.status_code": 403,
  "user.id": "***",
  "target.id": "a1b2c3d4-****-****-****-************",
  "error.code": "APP.FORBIDDEN",
  "log.level": "INFO",
  "event.duration": 42
}

http.response.status_code は403ですが、log.level はINFOです。この2つの値が一致していない理由こそが、次章の本題です。

ただし、これはあくまで前提条件です。フィールドで切れるようになっただけでは、まだ何も解決していません。本題は次の問いです。log.level に、何を書くか。

2. ログレベルは運用アクションで決める

2.1 レベルは「次に何をするか」で定義する

私たちは、ログレベルを次のように定義しました。

レベルいつ使うか
ERRORデータ不整合、インフラ障害など、当日中に手を動かす
WARNING操作は失敗したが整合性は保たれている。翌営業日までに状況確認
INFO正常な拒絶、想定内の失敗。記録はするが通知しない

HTTPレスポンスが4xxだからERROR、例外が出たからERROR——ではありません。通知を受けた人が次に何をするかで決めます。

2.2 具体例 — ステータスコードではなく操作の結果で判断

本プロダクトはACL付きのアセット共有システムです。権限のないフォルダへのアクセスは、バグではなくありうる結果です。403 Forbidden を運用ERRORにすると、アラートチャネルが権限チェックのたびに鳴ってしまいます。リンク切れの404や入力不備の400も、同様にINFOに寄せています。これらに共通するのは、「403は常にINFOであるべき」という一般論ではなく、いずれも想定内の失敗で、運用者が次にやることがないという点です。

逆に、ステータスコードだけでは判断できないケースもあります。一括削除のような操作はHTTP 200を返しつつ一部だけ失敗することがありますが、この場合は「操作としては部分的に失敗した」という事実を別途レベルに反映させ、WARNING寄りにしています。403も一括削除の部分失敗も、見ているのはステータスコードではなく操作の結果です。

2.3 判断がぶれないよう、先に取り決める

「403はINFO」のような判断は、担当者の感覚に任せるとぶれます。
そこで本プロダクトでは、発生しうる事象とログレベルの対応をあらかじめ一覧として整理し、以降の実装ではその一覧を参照しながらレベルを設定するようにしました。明確な指針がないまま実装のたびにレベルを判断していくと、似た事象なのにレベルが揃わない、ということが起きやすいからです。

この取り決めは、夜間・休日の運用とも噛み合っています。本システムは平日日中の保守を前提とし、夜間・休日は即時対応しません。ERRORは #alert に届きますが、時間外はメンションなしで蓄積し、翌朝出勤時にトリアージします。だからこそ、鳴ってもよい通知の質があらかじめ揃っていることが重要で、403や想定内のINFOが混ざらない設計が、そのまま翌朝の負荷を下げてくれます。

3. Canonical Logのもう一つの効果

Canonical Logに event.action やリソースIDを揃えて載せるようにしてから、問い合わせ調査も早くなりました。ログ1行で「どのAPIの、どのリソースに対する処理か」がわかるからです。実際、一括削除の部分失敗(WARNING)が起きたときは、該当のJSON 1行を元に「このログから何が起きたか説明して」とプロンプトを投げただけで、event.actionerror.code の組み合わせから「対象件数のうち一部だけ権限エラーで失敗している」と当たりがつき、DBを開く前に原因の見当がついたことが何度かありました。

4. おわりに

まとめると、次の2段です。

  1. Canonical Logで log.level をフィールドとして出すようにし、そのフィールドを元にSlack通知を行う(検知の仕組みを見直す)
  2. log.level の中身は、運用アクションを元に定義する(403はINFO、不整合はERRORなど)

ログレベルはHTTPステータスや例外の重大度ではなく、運用者の「次のアクション」で決めるものだ——そう気づいてから、通知の判断が変わり、運用全体が変わりました。

今回、Canonical Log Lineや構造化ログといった土台の部分は、Software Design 2026年3月号「再考・ログ設計」 を参考にしました。

同じような課題を抱えている方の参考に少しでもなれば嬉しいです。

新規CTA