ログ設計とは、「どの項目を・どのタイミングで・どのレベルで出し、何を隠すか」を実装の前に決めて文書にすることです。 決めておくと、調査のときに必要な情報がそろい、個人情報がログに残ることも防げます。

この記事はレベルの定義や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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?