@@ -138,6 +138,7 @@ def __init__(
138138 * ,
139139 prefix : str = "" ,
140140 rename_fields : Optional [Dict [str , str ]] = None ,
141+ rename_fields_keep_missing : bool = False ,
141142 static_fields : Optional [Dict [str , Any ]] = None ,
142143 reserved_attrs : Optional [Sequence [str ]] = None ,
143144 timestamp : Union [bool , str ] = False ,
@@ -154,7 +155,8 @@ def __init__(
154155 prefix: an optional string prefix added at the beginning of
155156 the formatted string
156157 rename_fields: an optional dict, used to rename field names in the output.
157- Rename message to @message: {'message': '@message'}
158+ Rename `message` to `@message`: `{'message': '@message'}`
159+ rename_fields_keep_missing: When renaming fields, include missing fields in the output.
158160 static_fields: an optional dict, used to add fields with static values to all logs
159161 reserved_attrs: an optional list of fields that will be skipped when
160162 outputting json log record. Defaults to all log record attributes:
@@ -164,14 +166,18 @@ def __init__(
164166 to log record using string as key. If True boolean is passed, timestamp key
165167 will be "timestamp". Defaults to False/off.
166168
167- *Changed in 3.1*: you can now use custom values for style by setting validate to `False`.
168- The value is stored in `self._style` as a string. The `parse` method will need to be
169- overridden in order to support the new style.
169+ *Changed in 3.1*:
170+
171+ - you can now use custom values for style by setting validate to `False`.
172+ The value is stored in `self._style` as a string. The `parse` method will need to be
173+ overridden in order to support the new style.
174+ - Renaming fields now preserves the order that fields were added in and avoids adding
175+ missing fields. The original behaviour, missing fields have a value of `None`, is still
176+ available by setting `rename_fields_keep_missing` to `True`.
170177 """
171178 ## logging.Formatter compatibility
172179 ## ---------------------------------------------------------------------
173- # Note: validate added in 3.8
174- # Note: defaults added in 3.10
180+ # Note: validate added in 3.8, defaults added in 3.10
175181 if style in logging ._STYLES :
176182 _style = logging ._STYLES [style ][0 ](fmt ) # type: ignore[operator]
177183 if validate :
@@ -192,6 +198,7 @@ def __init__(
192198 ## ---------------------------------------------------------------------
193199 self .prefix = prefix
194200 self .rename_fields = rename_fields if rename_fields is not None else {}
201+ self .rename_fields_keep_missing = rename_fields_keep_missing
195202 self .static_fields = static_fields if static_fields is not None else {}
196203 self .reserved_attrs = set (reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS )
197204 self .timestamp = timestamp
@@ -215,6 +222,7 @@ def format(self, record: logging.LogRecord) -> str:
215222 record .message = ""
216223 else :
217224 record .message = record .getMessage ()
225+
218226 # only format time if needed
219227 if "asctime" in self ._required_fields :
220228 record .asctime = self .formatTime (record , self .datefmt )
@@ -225,6 +233,7 @@ def format(self, record: logging.LogRecord) -> str:
225233 message_dict ["exc_info" ] = self .formatException (record .exc_info )
226234 if not message_dict .get ("exc_info" ) and record .exc_text :
227235 message_dict ["exc_info" ] = record .exc_text
236+
228237 # Display formatted record of stack frames
229238 # default format is a string returned from :func:`traceback.print_stack`
230239 if record .stack_info and not message_dict .get ("stack_info" ):
@@ -289,13 +298,16 @@ def add_fields(
289298 Args:
290299 log_record: data that will be logged
291300 record: the record to extract data from
292- message_dict: ???
301+ message_dict: dictionary that was logged instead of a message. e.g
302+ `logger.info({"is_this_message_dict": True})`
293303 """
294304 for field in self ._required_fields :
295- log_record [field ] = record .__dict__ .get (field )
305+ log_record [self ._get_rename (field )] = record .__dict__ .get (field )
306+
307+ for data_dict in [self .static_fields , message_dict ]:
308+ for key , value in data_dict .items ():
309+ log_record [self ._get_rename (key )] = value
296310
297- log_record .update (self .static_fields )
298- log_record .update (message_dict )
299311 merge_record_extra (
300312 record ,
301313 log_record ,
@@ -304,19 +316,19 @@ def add_fields(
304316 )
305317
306318 if self .timestamp :
307- # TODO: Can this use isinstance instead?
308- # pylint: disable=unidiomatic-typecheck
309- key = self .timestamp if type (self .timestamp ) == str else "timestamp"
310- log_record [key ] = datetime .fromtimestamp (record .created , tz = timezone .utc )
311-
312- self ._perform_rename_log_fields (log_record )
319+ key = self .timestamp if isinstance (self .timestamp , str ) else "timestamp"
320+ log_record [self ._get_rename (key )] = datetime .fromtimestamp (
321+ record .created , tz = timezone .utc
322+ )
323+
324+ if self .rename_fields_keep_missing :
325+ for field in self .rename_fields .values ():
326+ if field not in log_record :
327+ log_record [field ] = None
313328 return
314329
315- def _perform_rename_log_fields (self , log_record : Dict [str , Any ]) -> None :
316- for old_field_name , new_field_name in self .rename_fields .items ():
317- log_record [new_field_name ] = log_record [old_field_name ]
318- del log_record [old_field_name ]
319- return
330+ def _get_rename (self , key : str ) -> str :
331+ return self .rename_fields .get (key , key )
320332
321333 # Child Methods
322334 # ..........................................................................
0 commit comments