Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
allow logging filters to return a LogRecord
  • Loading branch information
adriangb committed May 10, 2022
commit 3aeeba6ed13eda4b895d8a531cf320768f5dc030
38 changes: 30 additions & 8 deletions Lib/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,9 @@ def __repr__(self):
return '<LogRecord: %s, %s, %s, %s, "%s">'%(self.name, self.levelno,
self.pathname, self.lineno, self.msg)

def __bool__(self):
return True

def getMessage(self):
"""
Return the message for this LogRecord.
Expand Down Expand Up @@ -811,23 +814,34 @@ def filter(self, record):
Determine if a record is loggable by consulting all the filters.

The default is to allow the record to be logged; any filter can veto
this and the record is then dropped. Returns a zero value if a record
is to be dropped, else non-zero.
this by returning a falsy value and the record is then dropped and
this method returns a falsy value.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this now repetition which isn't needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth differentiating between the return values of filters and the return value of the Filterer.filter function. The main difference is that filters may return a truthy value that is not a log record but Filterer.filter will now always return a log record (or falsy). I reworded it a bit to make these two different paragraphs.

Filters can return a log record, which case that log record
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change sentences to If a filter attached to a handler returns a log record instance, then that instance is used in place of the original log record in any further processing of the event by that handler. If a filter returns any other truthy value, the original log record is used in any further processing of the event by that handler.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you that does sound better

is used to call the next filter.
If filters return a truthy value that is not a log record the
next filter is called with the existing log record.

If none of the filters return falsy values, this method returns
a log record.

.. versionchanged:: 3.2

Allow filters to be just callables.

.. versionchanged:: 3.12
Allow filters to return a LogRecord instead of
modifying it in place.
"""
rv = True
for f in self.filters:
if hasattr(f, 'filter'):
result = f.filter(record)
else:
result = f(record) # assume callable - will raise if not
if not result:
rv = False
break
return rv
return False
if isinstance(result, LogRecord):
record = result
return record

#---------------------------------------------------------------------------
# Handler classes and functions
Expand Down Expand Up @@ -966,6 +980,8 @@ def handle(self, record):
emission.
"""
rv = self.filter(record)
if isinstance(rv, LogRecord):
record = rv
if rv:
self.acquire()
try:
Expand Down Expand Up @@ -1634,8 +1650,14 @@ def handle(self, record):
This method is used for unpickled records received from a socket, as
well as those created locally. Logger-level filtering is applied.
"""
if (not self.disabled) and self.filter(record):
self.callHandlers(record)
if self.disabled:
return
maybe_record = self.filter(record)
if not maybe_record:
return
if isinstance(maybe_record, LogRecord):
record = maybe_record
self.callHandlers(record)

def addHandler(self, hdlr):
"""
Expand Down
34 changes: 34 additions & 0 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,40 @@ def log_at_all_levels(self, logger):
for lvl in LEVEL_RANGE:
logger.log(lvl, self.next_message())

def test_handler_filter_replaces_record(self):
def replace_message(record: logging.LogRecord):
return logging.LogRecord(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use copy.copy() here and elsewhere instead of this.

name=record.name,
level=record.levelno,
pathname=record.pathname,
lineno=record.lineno,
msg="new message!",
exc_info=record.exc_info,
args=(),
)

# Set up a logging hierarchy such that "child" and it's handler
# (and thus `replace_message()`) always get called before
# propagating up to "parent".
# Then we can confirm that `replace_message()` was able to
# replace the log record without having a side effect on
# other loggers or handlers.
parent = logging.getLogger("parent")
child = logging.getLogger("parent.child")
stream_1 = io.StringIO()
stream_2 = io.StringIO()
handler_1 = logging.StreamHandler(stream_1)
handler_2 = logging.StreamHandler(stream_2)
handler_2.addFilter(replace_message)
parent.addHandler(handler_1)
child.addHandler(handler_2)

child.info("original message")
handler_1.flush()
handler_2.flush()
self.assertEqual(stream_1.getvalue(), "original message\n")
self.assertEqual(stream_2.getvalue(), "new message!\n")

def test_logger_filter(self):
# Filter at logger level.
self.root_logger.setLevel(VERBOSE)
Expand Down