- 設計
ログ設計の出力項目一覧|ログ設計書テンプレートとマスク
ログ設計とは、「どの項目を・どのタイミングで・どのレベルで出し、何を隠すか」を実装の前に決めて文書にすることです。 決めておくと、調査のときに必要な情報がそろい、個人情報がログに残ることも防げます。
この記事はレベルの定義やJSONの形式ではなく、ログ設計書に書く項目の定義に絞りました。共通出力項目の一覧、出力タイミング表、マスク規則表をコピーして使えるテンプレートとして載せ、Python の logging.Filter でマスクするコードを実行結果つきで示します。
確認環境:Ubuntu 24.04、Python 3.11.15(標準ライブラリの logging のみ)。Python 公式ドキュメント(logging、3.14版のページ)と OWASP Logging Cheat Sheet を開いて確認しました(確認日 2026-10-05)。
この記事の結論
- ログ設計書の中心は3つの表です。共通出力項目の一覧、出力タイミング表、マスク規則表。
- 共通出力項目は「いつ・どこで・誰が・何を・結果」がそろう16個前後にし、全ログで同じ名前を使います。
- 出力タイミングは処理の開始・終了・外部呼び出し・例外の4か所を基本にし、それぞれのレベルを表で決めます。
- マスクは出力の直前に1か所でかけます。Python では Filter をハンドラに付け、例外の文面まで隠すには Formatter でもマスクします。
ログ設計書の早見表
| 決めること | 設計書に書く形 | この記事の該当箇所 |
|---|---|---|
| ログの種類と目的 | 種類・目的・読み手・保管期間の表 | アプリログ・アクセスログ・監査ログの違い |
| 共通出力項目 | 項目名・型・例・必須の表 | 共通出力項目の一覧 |
| 出力タイミングとレベル | タイミング・レベル・メッセージ・追加項目の表 | 出力タイミング表 |
| 隠す情報 | 対象・規則・出力例の表 | マスク規則表 |
| 出力先と保管 | 出力先・切り替え・保管期間・閲覧できる人の表 | テンプレートの5章 |
| メッセージの書き方 | 文の型と例 | テンプレートの6章 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「ログ設計の出力項目一覧|ログ設計書テンプレートとマスク」
共通出力項目の一覧(16項目)
すべてのアプリログに付ける項目です。項目名は全サービスで同じにすると、検索や集計の式を使い回せます。
| No. | 項目名 | 型 | 例 | 必須 | 説明 |
|---|---|---|---|---|---|
| 1 | timestamp | 文字列(ISO 8601、タイムゾーンつき) | 2026-10-05T10:15:30.123+09:00 | 必須 | 出来事が起きた日時。ミリ秒まで |
| 2 | level | 文字列 | INFO | 必須 | ERROR/WARN/INFO/DEBUG のいずれか |
| 3 | service | 文字列 | reserve-api | 必須 | サービスやアプリの名前 |
| 4 | env | 文字列 | prod | 必須 | 動いている環境(prod/stg/dev) |
| 5 | version | 文字列 | 1.4.2 | 必須 | アプリの版。リリースの前後を見分ける |
| 6 | host | 文字列 | app-01 | 必須 | サーバー名やコンテナ名 |
| 7 | logger | 文字列 | app.reservation | 必須 | 出力したモジュールやクラスの名前 |
| 8 | event | 文字列(英小文字と下線) | reservation_created | 必須 | 出来事の種類。検索の鍵にする固定の語 |
| 9 | message | 文字列 | 予約を登録しました | 必須 | 人が読む説明文 |
| 10 | request_id | 文字列 | 7f3a9c2e1b4d | 必須 | 1回のリクエストやジョブの中で共通のID |
| 11 | user_id | 文字列 | U000123 | 条件つき | 操作した人の内部ID(ログイン後のみ)。氏名やメールアドレスは入れない |
| 12 | client_ip | 文字列 | 203.0.113.10 | 条件つき | 接続元のアドレス(リクエストを受ける処理のみ) |
| 13 | result | 文字列 | success | 条件つき | 処理の結果(success/failure)。処理の終了時に出す |
| 14 | duration_ms | 整数 | 182 | 条件つき | かかった時間(ミリ秒)。処理の終了時と外部呼び出しの後に出す |
| 15 | error_code | 文字列 | E-RSV-409 | 条件つき | 設計書で定めたエラーコード。WARN/ERROR のとき |
| 16 | exception | 文字列 | ValueError: … | 条件つき | 例外の種類と呼び出しの経路。ERROR のとき |
No. 項目名 型 例 必須 説明
1 timestamp 文字列(ISO 8601、タイムゾーンつき) 2026-10-05T10:15:30.123+09:00 必須 出来事が起きた日時。ミリ秒まで
2 level 文字列 INFO 必須 ERROR/WARN/INFO/DEBUG のいずれか
3 service 文字列 reserve-api 必須 サービスやアプリの名前
4 env 文字列 prod 必須 動いている環境(prod/stg/dev)
5 version 文字列 1.4.2 必須 アプリの版。リリースの前後を見分ける
6 host 文字列 app-01 必須 サーバー名やコンテナ名
7 logger 文字列 app.reservation 必須 出力したモジュールやクラスの名前
8 event 文字列(英小文字と下線) reservation_created 必須 出来事の種類。検索の鍵にする固定の語
9 message 文字列 予約を登録しました 必須 人が読む説明文
10 request_id 文字列 7f3a9c2e1b4d 必須 1回のリクエストやジョブの中で共通のID
11 user_id 文字列 U000123 条件つき 操作した人の内部ID(ログイン後のみ)。氏名やメールアドレスは入れない
12 client_ip 文字列 203.0.113.10 条件つき 接続元のアドレス(リクエストを受ける処理のみ)
13 result 文字列 success 条件つき 処理の結果(success/failure)。処理の終了時に出す
14 duration_ms 整数 182 条件つき かかった時間(ミリ秒)。処理の終了時と外部呼び出しの後に出す
15 error_code 文字列 E-RSV-409 条件つき 設計書で定めたエラーコード。WARN/ERROR のとき
16 exception 文字列 ValueError: … 条件つき 例外の種類と呼び出しの経路。ERROR のとき
OWASP Logging Cheat Sheet は、記録する属性を「いつ(When)・どこで(Where)・誰が(Who)・何を(What)」で整理しています。上の表との対応は次のとおりです。
| 観点 | この表の項目 |
|---|---|
| いつ | timestamp、request_id |
| どこで | service、env、version、host、logger |
| 誰が | user_id、client_ip |
| 何を | event、level、message、result、duration_ms、error_code、exception |
機能ごとに足す項目(予約ID、件数など)は、「追加項目」として出力タイミング表の側に書きます。共通項目を増やしすぎないのがこつです。
出力タイミング表(どこで何レベルを出すか)
| No. | タイミング | レベル | メッセージの型 | 追加で出す項目 | 補足 |
|---|---|---|---|---|---|
| 1 | アプリの起動・停止 | INFO | 起動しました/停止します | version、設定の要点(秘密の値は除く) | リリースの境目が分かる |
| 2 | リクエスト・ジョブの開始 | INFO | 〜を開始します | 入力の鍵になるID、件数 | 入力の全文は出さない |
| 3 | リクエスト・ジョブの終了 | INFO | 〜を終了しました | result、duration_ms、処理件数 | 開始と対で出す |
| 4 | 業務上の節目(登録・更新・削除・承認) | INFO | 〜を登録しました | 対象のID、変更後の状態 | event を固定の語にする |
| 5 | 外部呼び出しの前 | DEBUG | 〜を呼び出します | 呼び出し先、鍵になるID | 本番では通常出さない |
| 6 | 外部呼び出しの後(正常) | INFO | 〜の応答を受け取りました | 呼び出し先、状態コード、duration_ms | 遅延の調査に使う |
| 7 | 外部呼び出しの再試行 | WARN | 〜を再試行します(n回目) | 回数、待ち時間、理由 | 回復する見込みがある |
| 8 | 外部呼び出しが回復しなかった | ERROR | 〜を呼び出せませんでした | error_code、exception、回数 | 対応が必要 |
| 9 | 入力チェックに合わなかった | INFO | 入力内容を確認してください(項目名) | 項目名、チェックの種類 | 入力値そのものは出さない |
| 10 | 権限が無い操作 | WARN | 権限の無い操作です | user_id、対象、操作 | 監査ログにも記録する |
| 11 | 想定した業務エラー(在庫なし、重複など) | WARN | 〜できませんでした(理由) | error_code、対象のID | 利用者に案内して続行できる |
| 12 | 想定していない例外 | ERROR | 〜の処理でエラーが発生しました | error_code、exception | 最も外側で1回だけ出す |
| 13 | しきい値の超過(遅い処理、件数の急増) | WARN | 〜に時間がかかっています | duration_ms、しきい値 | 早めに気づく |
| 14 | 分岐や途中の値 | DEBUG | 〜を判定しました | 判定に使った値(マスク済み) | 開発・調査時だけ出す |
No. タイミング レベル メッセージの型 追加で出す項目 補足
1 アプリの起動・停止 INFO 起動しました/停止します version、設定の要点(秘密の値は除く) リリースの境目が分かる
2 リクエスト・ジョブの開始 INFO 〜を開始します 入力の鍵になるID、件数 入力の全文は出さない
3 リクエスト・ジョブの終了 INFO 〜を終了しました result、duration_ms、処理件数 開始と対で出す
4 業務上の節目(登録・更新・削除・承認) INFO 〜を登録しました 対象のID、変更後の状態 event を固定の語にする
5 外部呼び出しの前 DEBUG 〜を呼び出します 呼び出し先、鍵になるID 本番では通常出さない
6 外部呼び出しの後(正常) INFO 〜の応答を受け取りました 呼び出し先、状態コード、duration_ms 遅延の調査に使う
7 外部呼び出しの再試行 WARN 〜を再試行します(n回目) 回数、待ち時間、理由 回復する見込みがある
8 外部呼び出しが回復しなかった ERROR 〜を呼び出せませんでした error_code、exception、回数 対応が必要
9 入力チェックに合わなかった INFO 入力内容を確認してください(項目名) 項目名、チェックの種類 入力値そのものは出さない
10 権限が無い操作 WARN 権限の無い操作です user_id、対象、操作 監査ログにも記録する
11 想定した業務エラー(在庫なし、重複など) WARN 〜できませんでした(理由) error_code、対象のID 利用者に案内して続行できる
12 想定していない例外 ERROR 〜の処理でエラーが発生しました error_code、exception 最も外側で1回だけ出す
13 しきい値の超過(遅い処理、件数の急増) WARN 〜に時間がかかっています duration_ms、しきい値 早めに気づく
14 分岐や途中の値 DEBUG 〜を判定しました 判定に使った値(マスク済み) 開発・調査時だけ出す
例外は、捕まえた各層で出さず、最も外側で1回だけ ERROR にすると、同じ出来事が何行にも重ならず、件数を数えやすくなります。途中の層では、情報を足して上へ渡します。
マスク規則表
OWASP Logging Cheat Sheet は、ログにそのまま記録しないものとして、セッションID、アクセストークン、パスワード、データベースの接続文字列、暗号鍵、個人を特定できる情報などを挙げ、取り除く・マスクする・ハッシュ化するなどの扱いを求めています。設計書では、対象ごとに「出さない」「一部を残す」「置き換える」のどれにするかを決めます。
| No. | 対象 | 扱い | 規則 | 元の値の例 | 出力例 |
|---|---|---|---|---|---|
| 1 | メールアドレス | 一部を残す | 先頭1文字とドメインを残し、間を *** にする |
taro.yamada@example.com | t***@example.com |
| 2 | 電話番号(ハイフンあり) | 一部を残す | 下4桁だけ残す | 090-1234-5678 | ***-****-5678 |
| 3 | 電話番号(携帯、11桁の連続) | 一部を残す | 下4桁だけ残す | 09012345678 | *******5678 |
| 4 | パスワード・トークン・APIキー | 置き換える | password= などの名前の後ろを *** にする |
password=Secret123! | password=*** |
| 5 | Authorization ヘッダー | 置き換える | Bearer の後ろを *** にする |
Bearer eyJhbGciOi.abc.def | Bearer *** |
| 6 | 氏名 | 出さない | 内部の user_id で代える | 山田 太郎 | U000123 |
| 7 | 住所 | 出さない | 必要なら都道府県まで | 東京都〇〇区… | 東京都 |
| 8 | 生年月日 | 出さない | 必要なら年代に丸める | 1990-04-01 | 30代 |
| 9 | セッションID | 出さない | 追跡には request_id を使う | (セッションの値) | (出力なし) |
| 10 | データベースの接続文字列・暗号鍵 | 出さない | 設定のログには名前だけ出す | (接続文字列) | DB_URL=(設定あり) |
| 11 | リクエストとレスポンスの本文 | 出さない | 鍵になるIDと件数だけ出す | (本文全体) | reservation_id=R-1001 |
No. 対象 扱い 規則 元の値の例 出力例
1 メールアドレス 一部を残す 先頭1文字とドメインを残し、間を * にする taro.yamada@example.com t*@example.com
2 電話番号(ハイフンあり) 一部を残す 下4桁だけ残す 090-1234-5678 *--5678
3 電話番号(携帯、11桁の連続) 一部を残す 下4桁だけ残す 09012345678 *5678
4 パスワード・トークン・APIキー 置き換える password= などの名前の後ろを * にする password=Secret123! password=*
5 Authorization ヘッダー 置き換える Bearer の後ろを * にする Bearer eyJhbGciOi.abc.def Bearer *
6 氏名 出さない 内部の user_id で代える 山田 太郎 U000123
7 住所 出さない 必要なら都道府県まで 東京都〇〇区… 東京都
8 生年月日 出さない 必要なら年代に丸める 1990-04-01 30代
9 セッションID 出さない 追跡には request_id を使う (セッションの値) (出力なし)
10 データベースの接続文字列・暗号鍵 出さない 設定のログには名前だけ出す (接続文字列) DB_URL=(設定あり)
11 リクエストとレスポンスの本文 出さない 鍵になるIDと件数だけ出す (本文全体) reservation_id=R-1001
1〜5 は文字列の形で見分けられるので、次のコードで機械的にマスクします。6〜11 は形で見分けにくいため、そもそもログの引数に渡さないことを設計書とレビューの観点に書きます。
Python の logging.Filter でマスクするコードと実行結果
標準ライブラリだけで動く完成形です。mask_log.py として保存し、python3 mask_log.py で実行します。
import logging
import re
import sys
# マスク規則:(名前, パターン, 置き換え)。上から順に当てる
RULES = [
("email", re.compile(r"([A-Za-z0-9])[A-Za-z0-9._%+-]*@([A-Za-z0-9.-]+\.[A-Za-z]{2,})"), r"\1***@\2"),
("bearer", re.compile(r"(?i)\b(bearer)\s+[A-Za-z0-9._~+/=-]+"), r"\1 ***"),
("secret", re.compile(r"(?i)\b(password|passwd|token|api_key|secret)(\s*[=:]\s*)\S+"), r"\1\2***"),
("phone", re.compile(r"(?<![\d-])0\d{1,4}-\d{1,4}-(\d{4})(?![\d-])"), r"***-****-\1"),
("phone11", re.compile(r"(?<!\d)0[789]0\d{4}(\d{4})(?!\d)"), r"*******\1"),
]
def mask(text: str) -> str:
for _name, pattern, repl in RULES:
text = pattern.sub(repl, text)
return text
class MaskFilter(logging.Filter):
"""メッセージを組み立ててからマスクし、レコードを書き換える"""
def filter(self, record: logging.LogRecord) -> bool:
record.msg = mask(record.getMessage()) # msg % args を済ませた文字列
record.args = None # 二重に % 展開されないよう空にする
return True # True を返すと出力される
class MaskFormatter(logging.Formatter):
"""例外のトレースバックまで含めた最終の1件をマスクする"""
def format(self, record: logging.LogRecord) -> str:
return mask(super().format(record))
def build(name: str, *, use_filter: bool, use_formatter: bool) -> logging.Logger:
handler = logging.StreamHandler(sys.stdout)
fmt = "%(levelname)s %(name)s %(message)s"
handler.setFormatter(MaskFormatter(fmt) if use_formatter else logging.Formatter(fmt))
if use_filter:
handler.addFilter(MaskFilter()) # ハンドラに付ける
logger = logging.getLogger(name)
logger.handlers[:] = [handler]
logger.setLevel(logging.INFO)
logger.propagate = False
return logger
def main() -> None:
print("--- 1. マスクなし")
log = build("app", use_filter=False, use_formatter=False)
log.info("会員登録 email=%s tel=%s", "taro.yamada@example.com", "090-1234-5678")
print("--- 2. ハンドラに MaskFilter")
log = build("app", use_filter=True, use_formatter=False)
log.info("会員登録 email=%s tel=%s", "taro.yamada@example.com", "090-1234-5678")
log.info("外部呼び出し Authorization: Bearer eyJhbGciOi.abc.def password=Secret123!")
log.info("連絡先 tel=09012345678 代表=03-1234-5678 注文番号=2026100500012345")
logging.getLogger("app.order").info("子ロガーから email=%s", "hanako@example.co.jp")
print("--- 3. ロガーに付けた場合(子ロガーには効かない)")
log = build("app", use_filter=False, use_formatter=False)
log.addFilter(MaskFilter())
log.info("親ロガーから email=%s", "taro.yamada@example.com")
logging.getLogger("app.order").info("子ロガーから email=%s", "hanako@example.co.jp")
log.filters.clear()
print("--- 4. 例外のメッセージは Filter だけでは残る")
log = build("app", use_filter=True, use_formatter=False)
try:
raise ValueError("送信できません: taro.yamada@example.com")
except ValueError:
log.exception("通知メールの送信でエラー")
print("--- 5. MaskFormatter を足すと例外の行もマスクされる")
log = build("app", use_filter=True, use_formatter=True)
try:
raise ValueError("送信できません: taro.yamada@example.com")
except ValueError:
log.exception("通知メールの送信でエラー")
if __name__ == "__main__":
main()
実行結果(Python 3.11.15。ファイルのパスは環境に合わせて置き換えています):
--- 1. マスクなし
INFO app 会員登録 email=taro.yamada@example.com tel=090-1234-5678
--- 2. ハンドラに MaskFilter
INFO app 会員登録 email=t***@example.com tel=***-****-5678
INFO app 外部呼び出し Authorization: Bearer *** password=***
INFO app 連絡先 tel=*******5678 代表=***-****-5678 注文番号=2026100500012345
INFO app.order 子ロガーから email=h***@example.co.jp
--- 3. ロガーに付けた場合(子ロガーには効かない)
INFO app 親ロガーから email=t***@example.com
INFO app.order 子ロガーから email=hanako@example.co.jp
--- 4. 例外のメッセージは Filter だけでは残る
ERROR app 通知メールの送信でエラー
Traceback (most recent call last):
File "/home/taro/logdemo/mask_log.py", line 72, in main
raise ValueError("送信できません: taro.yamada@example.com")
ValueError: 送信できません: taro.yamada@example.com
--- 5. MaskFormatter を足すと例外の行もマスクされる
ERROR app 通知メールの送信でエラー
Traceback (most recent call last):
File "/home/taro/logdemo/mask_log.py", line 79, in main
raise ValueError("送信できません: t***@example.com")
ValueError: 送信できません: t***@example.com
結果の読み取り方
| 番号 | 分かること |
|---|---|
| 1 | 何もしなければ、引数に渡した値がそのまま出る |
| 2 | ハンドラに Filter を付けると、app と子の app.order の両方でマスクされる。16桁の注文番号は電話番号の規則に当たらず、そのまま残る |
| 3 | ロガーに Filter を付けると、子ロガーから出したログにはかからない |
| 4 | 例外のメッセージとトレースバックは Filter の後で組み立てられるので、Filter だけでは残る |
| 5 | Formatter の format() の結果にもマスクをかけると、例外の行まで隠せる |
Python 公式ドキュメントは、ロガーに付けた Filter について、子孫のロガーで発生したイベントには、その子孫にも Filter を付けない限り適用されないと説明しています。3番の結果はこの説明どおりです。マスクのように全ログへ必ずかけたい処理は、ハンドラ側に付けます。
Filter の中で record.getMessage() を使っているのは、log.info("email=%s", addr) のように引数で渡した値は record.msg には入っておらず、record.args にあるためです。msg だけを置き換えると引数の値が残るので、いったん組み立ててからマスクし、args を空にしています。
規則が意図どおりかを確かめるテスト
マスクの正規表現は、隠したいものが隠れることと、隠さなくてよいものが残ることの両方を確かめます。
from mask_log import mask
cases = [
("taro.yamada@example.com","t***@example.com"),
("a@example.jp","a***@example.jp"),
("090-1234-5678","***-****-5678"),
("03-1234-5678","***-****-5678"),
("09012345678","*******5678"),
("注文番号=2026100500012345","注文番号=2026100500012345"),
("日付 2026-10-05","日付 2026-10-05"),
("password=abc123","password=***"),
("token: xyz","token: ***"),
("Bearer abc.def","Bearer ***"),
("郵便番号 100-0001","郵便番号 100-0001"),
]
ok=0
for src,exp in cases:
got=mask(src); ok+=got==exp
print("OK " if got==exp else "NG ", src, "->", got)
print(f"{ok}/{len(cases)} 件が期待どおり")
OK taro.yamada@example.com -> t***@example.com
OK a@example.jp -> a***@example.jp
OK 090-1234-5678 -> ***-****-5678
OK 03-1234-5678 -> ***-****-5678
OK 09012345678 -> *******5678
OK 注文番号=2026100500012345 -> 注文番号=2026100500012345
OK 日付 2026-10-05 -> 日付 2026-10-05
OK password=abc123 -> password=***
OK token: xyz -> token: ***
OK Bearer abc.def -> Bearer ***
OK 郵便番号 100-0001 -> 郵便番号 100-0001
11/11 件が期待どおり
電話番号の規則には、前後に数字やハイフンが続かないことを条件に入れています。この条件が無いと、長い番号や日付の一部まで置き換えてしまい、調査に使う値が読めなくなります。
アプリログ・アクセスログ・監査ログの違いと出し分け
| 種類 | 目的 | 主な読み手 | 出すタイミング | 主な項目 | 保管期間の例 |
|---|---|---|---|---|---|
| アプリログ | 動作の確認、原因の調査 | 開発者、運用担当 | 処理の開始・終了・節目・例外 | 共通出力項目16個と追加項目 | 例:90日 |
| アクセスログ | 利用状況と応答の把握 | 運用担当、開発者 | リクエスト1件につき1行 | 日時、メソッド、パス、状態コード、応答時間、接続元、request_id | 例:1年 |
| 監査ログ | 誰が何をしたかの証跡 | 管理者、監査の担当 | ログイン、権限の変更、重要なデータの参照・更新・削除・出力 | 日時、操作した人、操作、対象、変更前後、結果、接続元 | 例:3〜7年(規程に合わせる) |
保管期間は例です。自社の規程や契約で定めがあれば、それに合わせます。
どのログに出すかの判断基準
| 出したい内容 | 出す先 |
|---|---|
| 処理がどこまで進んだか、なぜエラーになったか | アプリログ |
| どのURLに何件来て、何ミリ秒で返したか | アクセスログ |
| 誰がいつログインしたか、権限を変えたか | 監査ログ(アプリログには event だけ) |
| 誰がどのデータを見た・書き出したか | 監査ログ |
| 入力チェックに合わなかった | アプリログ(INFO) |
| 権限の無い操作があった | 監査ログとアプリログ(WARN)の両方 |
| 後から「消されていない・書き換えられていない」ことを示す必要がある | 監査ログ(追記のみ、閲覧できる人を限定) |
| 開発中だけ見たい途中の値 | アプリログ(DEBUG) |
判断の軸は「調査のためか、証跡のためか」です。証跡として残すものは、アプリログとは別の出力先にして、保管期間と閲覧できる人を分けます。request_id を3種類すべてに入れておくと、1件の操作をまたいでたどれます。
ログ設計書テンプレート(Markdown)
章立てと表の枠です。2〜4章には、この記事の表をそのまま貼って、自分のシステムに合わせて直します。
# ログ設計書(システム名)
- 版: 1.0
- 作成日: YYYY-MM-DD
- 作成者:
- 対象: (サービス名、バッチ名)
## 1. ログの種類と目的
| 種類 | 目的 | 読み手 | 出力先 | 形式 | 保管期間 |
|---|---|---|---|---|---|
| アプリログ | | | | | |
| アクセスログ | | | | | |
| 監査ログ | | | | | |
## 2. 共通出力項目
| No. | 項目名 | 型 | 例 | 必須 | 説明 |
|---|---|---|---|---|---|
| 1 | timestamp | | | | |
## 3. 出力タイミングとレベル
| No. | タイミング | レベル | event | メッセージ | 追加で出す項目 |
|---|---|---|---|---|---|
| 1 | | | | | |
## 4. マスク規則
| No. | 対象 | 扱い(出さない/一部を残す/置き換える) | 規則 | 出力例 |
|---|---|---|---|---|
| 1 | | | | |
- マスクをかける場所: (例:ログのハンドラ。フォーマッターでも最終の文字列にかける)
- 確かめ方: (例:規則ごとのテストを自動テストに入れる)
## 5. 出力先と保管
| 種類 | 出力先 | 切り替え(日次・サイズ) | 保管期間 | 閲覧できる人 | 消し方 |
|---|---|---|---|---|---|
| | | | | | |
## 6. メッセージの書き方
- 文の型: 「(何を)(どうした)」+ 鍵になるIDは項目として付ける
- event は英小文字と下線で、過去形にそろえる(例: reservation_created)
- 利用者に見せる文言とログの文言は分ける
## 7. 環境ごとの出力レベル
| 環境 | アプリログ | 補足 |
|---|---|---|
| 本番 | | |
| 検証 | | |
| 開発 | | |
## 8. 変更履歴
| 版 | 日付 | 変更内容 | 変更者 |
|---|---|---|---|
| 1.0 | | 初版 | |
記入例(架空の備品予約システム、3章の一部)
| No. | タイミング | レベル | event | メッセージ | 追加で出す項目 |
|---|---|---|---|---|---|
| 1 | 予約登録の開始 | INFO | reservation_create_started | 予約登録を開始します | item_id、利用日 |
| 2 | 空き状況の確認(外部の在庫サービス)の後 | INFO | stock_checked | 空き状況の応答を受け取りました | 状態コード、duration_ms |
| 3 | 同じ時間帯にすでに予約がある | WARN | reservation_conflicted | 予約できませんでした(時間帯の重複) | error_code=E-RSV-409、item_id |
| 4 | 予約登録の終了 | INFO | reservation_created | 予約を登録しました | reservation_id、result、duration_ms |
| 5 | 通知メールの送信で例外 | ERROR | notification_failed | 通知メールの送信でエラーが発生しました | error_code=E-NTF-500、exception(宛先はマスク) |
| 6 | 管理者が予約を取り消した | INFO | reservation_cancelled_by_admin | 予約を取り消しました | reservation_id、user_id(監査ログにも記録) |
No. タイミング レベル event メッセージ 追加で出す項目
1 予約登録の開始 INFO reservation_create_started 予約登録を開始します item_id、利用日
2 空き状況の確認(外部の在庫サービス)の後 INFO stock_checked 空き状況の応答を受け取りました 状態コード、duration_ms
3 同じ時間帯にすでに予約がある WARN reservation_conflicted 予約できませんでした(時間帯の重複) error_code=E-RSV-409、item_id
4 予約登録の終了 INFO reservation_created 予約を登録しました reservation_id、result、duration_ms
5 通知メールの送信で例外 ERROR notification_failed 通知メールの送信でエラーが発生しました error_code=E-NTF-500、exception(宛先はマスク)
6 管理者が予約を取り消した INFO reservation_cancelled_by_admin 予約を取り消しました reservation_id、user_id(監査ログにも記録)
確かめて分かったこと
- ロガーに付けた Filter は子ロガーのログにかからないことを実行で確認しました(結果の3番)。
appに付けてもapp.orderから出したメールアドレスはそのまま出ます。ハンドラに付けると両方にかかります。 - 例外のメッセージは Filter を通り抜けることを確認しました(結果の4番)。
log.exception()のトレースバックは Formatter が組み立てるため、Formatter の出力にもマスクをかけると隠せます(5番)。 record.msgだけを置き換えても、%sの引数に渡した値は残ります。getMessage()で組み立ててからマスクし、argsを空にする形で対応できました。- 電話番号の規則は、前後の数字とハイフンを除外する条件を入れると、16桁の注文番号・日付・郵便番号に当たらないことをテストで確認しました(11件すべて期待どおり)。
- Python 公式ドキュメント(3.14版)には、3.12 以降は Filter が置き換え用の LogRecord を返せると書かれています。今回の 3.11 では、レコードをその場で書き換える方法で動かしています。
つまずきやすい点と直し方
| 起きやすいこと | こうなる | こうするとよい |
|---|---|---|
| 項目名がサービスごとに違う(userId/user_id/uid) | 横断の検索が書けない | 共通出力項目の表で名前を1つに決める |
| message に値を埋め込むだけで event が無い | 件数を数えにくい | 固定の event を付け、値は項目に分ける |
| 開始のログだけで終了が無い | どこで止まったか分からない | 開始と終了を対で出し、duration_ms を付ける |
| 例外を各層で ERROR にする | 1件の出来事が何行にも増える | 最も外側で1回だけ出す |
| マスクを呼び出し側の各所で行う | かけ忘れが出る | ハンドラと Formatter の1か所に集める |
| 規則が広すぎる | 調査に使う番号まで隠れる | 残したい値のテストも用意する |
よくある質問
Q. ログ設計書には何を書きますか? A. ログの種類と目的、共通出力項目、出力タイミングとレベル、マスク規則、出力先と保管の5つを書きます。この記事のテンプレートの1〜5章がそれに当たります。
Q. ログの出力項目は何個くらい必要ですか? A. 共通項目は16個前後が目安です。日時・レベル・サービス名・環境・リクエストID・出来事の種類・メッセージを必須にし、利用者IDや処理時間は出すタイミングを決めて付けます。
Q. ログに個人情報を出してよいですか? A. そのままは出しません。氏名や住所は内部のIDで代え、メールアドレスや電話番号は一部だけ残す規則を決めて、出力の直前に機械的にマスクします。
Q. アプリログと監査ログの違いは何ですか? A. 目的が違います。アプリログは動作の確認と原因の調査のため、監査ログは誰が何をしたかの証跡のために残し、出力先・保管期間・閲覧できる人を分けます。
Q. マスクが効かないときはどこを見ますか? A. Filter を付けた場所と、例外の文面の2か所を確かめます。ロガーに付けた Filter は子ロガーのログにかからず、例外のトレースバックは Formatter 側でマスクする必要があります。
まとめ
- ログ設計書は共通出力項目・出力タイミング・マスク規則の3つの表が中心です。
- 共通出力項目は16個を目安に、全サービスで同じ名前にします。
- 出力タイミングは開始・終了・外部呼び出し・例外を基本に、レベルを表で決めます。
- マスクはハンドラの Filter と Formatter の1か所に集め、残す値のテストも用意します。
- アプリログ・アクセスログ・監査ログは「調査のためか、証跡のためか」で出し分け、request_id でつなぎます。
- テンプレートに自分のシステムの値を入れ、実装の前にレビューします。
参考・出典
- Python 公式ドキュメント「logging — Logging facility for Python」(Filter Objects、LogRecord attributes), docs.python.org
- OWASP Cheat Sheet Series「Logging Cheat Sheet」(Event attributes、Data to exclude), cheatsheetseries.owasp.org
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「ログ設計の出力項目一覧|ログ設計書テンプレートとマスク」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。