record = _old_factory(*args, **kwargs)
if isinstance(record.msg, str):
record.msg = mask_text(record.msg)
if record.args:
record.args = tuple(mask_text(str(a)) for a in record.args)
return record
logging.setLogRecordFactory(masked_record_factory)
URLクエリの key、api_key、token や、HTTPヘッダーの Bearer トークンを *** に置き換えます。monkey= のような無関係なクエリを巻き込まないよう、境界を考慮した正規表現を設計し、単体テストで固定しました。
### ② 例外トレースバックの文字化の瞬間
エラー発生時に出力されるスタックトレースの中にも、URLやクエリパラメータが露出する可能性があります。カスタムの logging.Formatter または formatException をオーバーライドし、トレースバック文字列全体に対しても同様のマスキングを通します。
### ③ データベースに保存する例外メッセージ
見落としがちだったのが、**「バッチ処理が失敗した際のエラーメッセージをデータベースに保存していた」** 経路です。ログファイルを塞いでも、DBテーブルに生のキーが残っては意味がありません。DB保存の直前にも同じ mask_text 関数を噛ませることで、すべての流出経路を塞ぎました。
## 3. 実稼働中のプロセスを止めるタイミング

*発見から27分でコミット*
修正コードができても、稼働中のワーカープロセスがログファイルをロックしており、古いプロセスのままではログにキーが書き足され続けます。
AIは「今すぐワーカーを止める(推奨)」を提案しましたが、人間は「実行中の学習バッチの終了(約15分後)を待ってから停止する」判断を選びました。
バッチ完了直後にプロセスを停止し、該当ログファイルを削除、修正版コードを適用して再起動。新しく出力されたログを確認し、生キーが0件、マスクされた key=*** が出力されていることを確かめてからコミットしました。発見からコミットまで27分でした。
## 4. 鍵をログに出さないためのチェックリスト

*鍵をログに出さないための5つの確認*
:::note info
- [ ] APIキーをURLのクエリパラメータで渡すSDK/サービスを使っていないか
- [ ] HTTPライブラリがリクエストURLを記録するログレベル(INFOなど)になっていないか
- [ ] マスキングはプロセス個別ではなく、共通の出口(LogRecordFactory等)に置いたか
- [ ] 失敗時の例外メッセージをDBや外部ストレージに平文保存していないか
- [ ] AIエージェントにログを読ませる際、機密行を除外・停止するガードがあるか
:::
AIに自律的なタスクを任せる際は、「秘密情報やセキュリティ懸念を見つけたら、ログを表示して書き写す前に作業を中断して報告する」というハーネス側のルールが不可欠です。