fix: prevent mutation of original dict when record.msg is a dict - #66
Conversation
|
Hi @gaoflow, thanks for putting this together. Overall it makes sense what it's trying to achieve. I would however like to take some time to think about if this is a change I want to make. In particular, if my understanding of Assuming I decide to go ahead with this change, it would also need a unit test to ensure there are no regressions (the sample provided in the description could form the basis of it). We'd also need consider what documentation / doc-strings need to be updated to make this API contract clear. Finally, it looks like the failing CI is unrelated to your change. |
|
The shallow copy is sufficient here, though not for a comforting reason: the only writes into What does remain is that nested values are shared, so the output is a live view of the caller's nested data at serialization time. A deep copy wouldn't close that either — with a buffering handler like So I documented the contract rather than deepening the copy. Both tests were mutation-checked: reverting |
nhairs
left a comment
There was a problem hiding this comment.
After re-reading your comments I agree that just doing a shallow copy is the correct approach here 👌
The only time I can think of something getting mutated is if you write a custom object with a custom __str__ method that mutates the object, in which case that's kind of on you and not something that I want to try support in the library.
Thanks for your patience and work on this. There's a couple of minor inline comments.
When record.msg is used as a dict log message, the format() method assigned it by reference to message_dict. Subsequent mutations to message_dict (adding exc_info, stack_info) would leak into the caller's original dict. Fix by copying the dict immediately so the original is never modified.
94497f4 to
dab53fe
Compare
Covers both directions of the copy behaviour: - test_log_dict_not_modified pins that exc_info/stack_info are not added to the caller's dict. - test_log_dict_shallow_copied pins that nested values stay shared, so swapping in a deep copy is a deliberate change rather than a silent one. Documents the contract in the format() docstring and the quickstart, and adds the changelog entry.
dab53fe to
79922e8
Compare
|
Addressed the inline requests in 79922e8: bumped the pending release to 4.2.0.dev1, kept the documentation and test focused on the non-mutation guarantee, and added the changelog thanks. The formatter tests pass (125). |
nhairs
left a comment
There was a problem hiding this comment.
Nice, thanks for working on this :)
Summary
When
record.msgis a dict (used as a structured log message), theformat()method assigns it tomessage_dictby reference. Subsequent mutations tomessage_dict-- such as injectingexc_infoorstack_info-- leak into the caller's original dict, causing unexpected side effects.Reproduction
Fix
Use
record.msg.copy()instead of a direct reference, so the original dict remains untouched.Tests
All 135 existing tests pass.
(This contribution was made under my direction and I take full responsibility for its correctness.)