エラーレスポンスとログの設計
調査に使う識別子を残しながら、例外の詳細や秘密値を外へ出さない方法を確認できます。
説明
処理が失敗したとき、利用者への応答と運用者の調査ログには異なる情報が必要です。内部の例外文をそのまま返すと、接続情報や入力値が漏れる場合があります。
注文更新を途中で失敗させ、応答とログを同じrequest_idで照合します。改行入りの利用者名と公開の秘密値試料を渡し、ログの行構造とロールバック後の保存額を調べます。
調査に使う識別子を残しながら、例外の詳細や秘密値を外へ出さない方法を確認できます。
1元の注文と失敗条件を用意する
注文7の金額は100です。失敗時は内部例外を発生させる固定処理を使います。
from sd13 import *
lab = Errors()
secret = "PUBLIC-DEMO-SECRET"
print(lab.db.execute("SELECT * FROM orders失敗前の金額を読む").fetchall())注文は1件です。次の操作では更新後に失敗を起こし、応答だけでなく保存値を追います。
2失敗応答へ内部情報を含めない
例外文には公開秘密値が含まれますが、応答には種別とrequest_idだけを返します。
status,body = lab.request固定の更新失敗を発生させる("r-7","alice",secret,True)
print(status,body)
print(secret in応答への秘密値混入を調べる json.dumps(body))500、processing_failed、r-7です。秘密値の包含はFalse。利用者は照会用のIDを伝えられます。
3同じIDでログと応答を結び付ける
ログは例外全文ではなく、調査用の項目を選んで記録します。
event = json.loads構造化ログを解析する(lab.lines[0])
print(event)
print(event["request_id"]==body["request_id"]応答との対応を確認する)同じr-7を持ち、例外の型はRuntimeErrorです。原因調査に追加情報が必要なら、アクセス制限した記録へ必要な範囲で残します。
4改行を含む値でログの行数を調べる
利用者名に改行を含む固定文字列を渡します。
lab.request("r-8","alice"+chr(10)値の内部に改行を含める+"forged success",secret,True)
line = lab.lines[-1]
print(line)
print(len(line.splitlines())偽の独立ログ行が生まれていないか調べる)JSON中の改行はエスケープされ、物理行は1行です。後から解析した文字列には改行が戻るため、表示先でもデータとして扱います。
5調査ログにも秘密値を残さない
ログ全体を検索し、内部の例外詳細が混ざっていないか調べます。
print(any(secret in line記録全体の秘密値混入を調べる for line in lab.lines))
print([json.loads(line)["error_type"] for line in lab.lines])秘密値はなく、例外型だけが2件あります。何でも記録して後でマスクする方法に頼らず、出力項目を選択しています。
6失敗後の保存値を確認する
二回とも更新途中で失敗しました。
print(lab.db.execute("SELECT amount失敗後に残った金額を読む FROM orders WHERE id=7").fetchone())100のままです。例外を捕捉して一般的な応答を返すだけでなく、途中更新がロールバックされることも確かめます。
7成功した処理と失敗記録を分ける
同じ処理を失敗なしで実行します。
print(lab.request("r-9","alice",secret,False失敗を起こさず確定させる))
print(lab.db.execute("SELECT amount FROM orders").fetchone(),len(lab.lines))200で金額90へ更新し、失敗ログは2件のままです。成功・失敗を同じラベルで記録して原因を曖昧にしません。
まとめ
応答とログを関連IDで結び付けます。
構造化と出力項目の選択でログを保護します。
エラー処理後の保存状態も照合します。
操作履歴と解説を振り返れます。
Python 3.15 / 固定の公開試料
from sd13 import *
lab = Errors()
secret = "PUBLIC-DEMO-SECRET"
print(lab.db.execute("SELECT * FROM orders").fetchall())[(7, 100)]
status,body = lab.request("r-7","alice",secret,True)
print(status,body)
print(secret in json.dumps(body))500 {'request_id': 'r-7', 'error': 'processing_failed'}
Falseevent = json.loads(lab.lines[0])
print(event)
print(event["request_id"]==body["request_id"]){'request_id': 'r-7', 'event': 'order_failed', 'user': 'alice', 'error_type': 'RuntimeError'}
Truelab.request("r-8","alice"+chr(10)+"forged success",secret,True)
line = lab.lines[-1]
print(line)
print(len(line.splitlines())){"request_id": "r-8", "event": "order_failed", "user": "alice\nforged success", "error_type": "RuntimeError"}
1print(any(secret in line for line in lab.lines))
print([json.loads(line)["error_type"] for line in lab.lines])False ['RuntimeError', 'RuntimeError']
print(lab.db.execute("SELECT amount FROM orders WHERE id=7").fetchone())(100,)
print(lab.request("r-9","alice",secret,False))
print(lab.db.execute("SELECT amount FROM orders").fetchone(),len(lab.lines))(200, {'request_id': 'r-9', 'amount': 90})
(90,) 2演習完了