コピーされたロガーが独立したスレッドセーフレベルキーを持つように変更
ActiveSupport の LoggerThreadSafeLevel が、ロガーを clone や dup した際に同一のスレッドローカルキーを共有していた問題を解消し、コピー間でログレベルが干渉しないようにしました。
背景
LoggerThreadSafeLevel は、各ロガーのスレッドローカルなログレベルをオブジェクト ID 由来のキー @local_level_key で管理しています。ロガーを clone または dup すると新しいオブジェクト ID が付与されますが、インスタンス変数はそのままコピーされるため、元ロガーと同一のキーを参照し続けます。結果として、二つのロガーが同じストレージを操作し、片方の log_at 呼び出しが他方のローカルレベルを上書きしてしまうリスクが生じました。
技術的な変更
LoggerThreadSafeLevel に initialize_copy を実装し、コピー時に @local_level_key を再生成するロジックが追加されました。これにより、コピーされたオブジェクトは自身の object_id に基づく新しいキーを保持します。実装は super 呼び出しの後にキー再生成を行うだけのシンプルなものです。
@@
def initialize(...)
@local_level_key = :"logger_thread_safe_level_#{object_id}"
end
+ def initialize_copy(other)
+ super
+ @local_level_key = :"logger_thread_safe_level_#{object_id}"
+ end
+
def local_level
IsolatedExecutionState[local_level_key]
end
同時にテスト test_copied_logger_has_an_independent_local_level が追加され、コピーロガーが log_at 内で独立したレベルを持ち、元ロガーのレベルが変わらないことを検証しています。
@@
def test_copied_logger_has_an_independent_local_level
copy = @logger.clone
copy.log_at(:fatal) do
assert_equal Logger::FATAL, copy.level
assert_equal Logger::DEBUG, @logger.level
end
end
この変更により、ロガーのコピーは互いに干渉せず、安全にスレッドローカルなログレベルを管理できるようになりました。
設計判断
コピーされたオブジェクトの状態を正しく分離するために、initialize_copy 内でキー再生成を行う方針が採用されました。initialize と同様のロジックを再利用することで、既存の初期化フローと整合性を保ちつつ、最小限のコード追加に留めています。新たな設定項目やフラグを導入せず、既存のインスタンス変数だけを更新する設計は、後方互換性を損なわない安全な選択と言えます。
まとめ
PR #57637 は、LoggerThreadSafeLevel がロガーのコピー間で独立したスレッドセーフレベルキーを持つようにし、clone/dup に伴うログレベル競合を防止しました。このシンプルな修正は、Rails のロギング機構の一貫性と信頼性を高め、開発者がロガーを複製しても安全に利用できる基盤を提供します。