Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow usage of regex in LOG_FILTER setting #3108

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Release type: minor

Allow regular expressions in `LOG_FILTER` setting

1 change: 1 addition & 0 deletions THANKS
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Julian Berman
Justin Mayer
Kevin Deldycke
Kevin Yap
Koen Martens
Kyle Fuller
Laureline Guerin
Leonard Huang
Expand Down
31 changes: 21 additions & 10 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,18 @@ Basic settings

.. data:: LOG_FILTER = []

A list of tuples containing the logging level (up to ``warning``) and the
message to be ignored.
A list of tuples containing the type (either ``string`` or ``regex``),
the logging level (up to ``warning``) and a string. If the type is ``string``
messages that are equal to the third argument are not shown. If the type is
``regex``, the third argument is interpreted as a regular expression and any
message matching that will not be shown.

Example::

LOG_FILTER = [(logging.WARN, 'TAG_SAVE_AS is set to False')]
LOG_FILTER = [
('string', logging.WARN, 'Empty theme folder. Using `basic` theme.'),
('regex', logging.WARN, r'Cannot get modification stamp for /foo/.*'),
]

.. data:: READERS = {}

Expand Down Expand Up @@ -1304,29 +1310,34 @@ the **meaningful** error message in the middle of tons of annoying log output
can be quite tricky. In order to filter out redundant log messages, Pelican
comes with the ``LOG_FILTER`` setting.

``LOG_FILTER`` should be a list of tuples ``(level, msg)``, each of them being
composed of the logging level (up to ``warning``) and the message to be
``LOG_FILTER`` should be a list of tuples ``(type, level, msg_or_regexp)``, each
of them being composed of the type (``string`` or ``regex``), the logging level
(up to ``warning``) and the message or regular expression to be
ignored. Simply populate the list with the log messages you want to hide, and
they will be filtered out.

For example::

import logging
LOG_FILTER = [(logging.WARN, 'TAG_SAVE_AS is set to False')]
LOG_FILTER = [
('string', logging.WARN, 'TAG_SAVE_AS is set to False'),
('regex', logging.WARN, r'Cannot get modification stamp for /foo/.*'),
]

It is possible to filter out messages by a template. Check out source code to
obtain a template.

For example::

import logging
LOG_FILTER = [(logging.WARN, 'Empty alt attribute for image %s in %s')]
LOG_FILTER = [('string', logging.WARN, 'Empty alt attribute for image %s in %s')]

.. Warning::

Silencing messages by templates is a dangerous feature. It is possible to
unintentionally filter out multiple message types with the same template
(including messages from future Pelican versions). Proceed with caution.
Silencing messages by templates or regular expressons is a dangerous
feature. It is possible to unintentionally filter out multiple message
types with the same template (including messages from future Pelican
versions). Proceed with caution.

.. note::

Expand Down
39 changes: 36 additions & 3 deletions pelican/log.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import logging
import re
import warnings
from collections import defaultdict

from rich.console import Console
from rich.logging import RichHandler

__all__ = [
'init'
'init',
'LimitFilter',
]

console = Console()
Expand All @@ -23,11 +26,37 @@ class LimitFilter(logging.Filter):

LOGS_DEDUP_MIN_LEVEL = logging.WARNING

_ignore = set()
ignore = set()
ignore_regexp = set()
_raised_messages = set()
_threshold = 5
_group_count = defaultdict(int)

@classmethod
def add_ignore_rule(cls, rule_specification):
if len(rule_specification) == 2: # old-style string or template
LimitFilter.ignore.add(rule_specification)
warnings.warn(
'2-tuple specification of LOG_FILTER item is deprecated,' +
'replace with 3-tuple starting with \'string\' (see' +
'documentation of LOG_FILTER for more details)',
FutureWarning
)
elif len(rule_specification) == 3: # new-style string/template/regexp
if rule_specification[0] == "string":
LimitFilter.ignore.add(rule_specification[1:])
elif rule_specification[0] == "regex":
regex = re.compile(rule_specification[2])
LimitFilter.ignore_regexp.add((rule_specification[1], regex))
else:
raise ValueError(
f"Invalid LOG_FILTER type '{rule_specification[0]}'"
)
else:
raise ValueError(
f"Invalid item '{str(rule_specification)}' in LOG_FILTER"
)

def filter(self, record):
# don't limit log messages for anything above "warning"
if record.levelno > self.LOGS_DEDUP_MIN_LEVEL:
Expand All @@ -50,7 +79,11 @@ def filter(self, record):
if logger_level > logging.DEBUG:
template_key = (record.levelno, record.msg)
message_key = (record.levelno, record.getMessage())
if (template_key in self._ignore or message_key in self._ignore):
if template_key in self.ignore or message_key in self.ignore:
return False
if any(regexp[1].match(record.getMessage())
for regexp in self.ignore_regexp
if regexp[0] == record.levelno):
return False

# check if we went over threshold
Expand Down
3 changes: 2 additions & 1 deletion pelican/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,8 @@ def configure_settings(settings):

# specify the log messages to be ignored
log_filter = settings.get('LOG_FILTER', DEFAULT_CONFIG['LOG_FILTER'])
LimitFilter._ignore.update(set(log_filter))
for item in log_filter:
LimitFilter.add_ignore_rule(item)

# lookup the theme in "pelican/themes" if the given one doesn't exist
if not os.path.isdir(settings['THEME']):
Expand Down
30 changes: 24 additions & 6 deletions pelican/tests/test_log.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import re
import unittest
from collections import defaultdict
from contextlib import contextmanager
Expand All @@ -19,7 +20,8 @@ def tearDown(self):
super().tearDown()

def _reset_limit_filter(self):
log.LimitFilter._ignore = set()
log.LimitFilter.ignore = set()
log.LimitFilter.ignore_regexp = set()
log.LimitFilter._raised_messages = set()
log.LimitFilter._threshold = 5
log.LimitFilter._group_count = defaultdict(int)
Expand Down Expand Up @@ -49,7 +51,7 @@ def do_logging():

# filter by template
with self.reset_logger():
log.LimitFilter._ignore.add((logging.WARNING, 'Log %s'))
log.LimitFilter.ignore.add((logging.WARNING, 'Log %s'))
do_logging()
self.assertEqual(
self.handler.count_logs('Log \\d', logging.WARNING),
Expand All @@ -60,7 +62,7 @@ def do_logging():

# filter by exact message
with self.reset_logger():
log.LimitFilter._ignore.add((logging.WARNING, 'Log 3'))
log.LimitFilter.ignore.add((logging.WARNING, 'Log 3'))
do_logging()
self.assertEqual(
self.handler.count_logs('Log \\d', logging.WARNING),
Expand All @@ -69,14 +71,30 @@ def do_logging():
self.handler.count_logs('Another log \\d', logging.WARNING),
5)

# filter by both
# filter by regular expression
with self.reset_logger():
log.LimitFilter._ignore.add((logging.WARNING, 'Log 3'))
log.LimitFilter._ignore.add((logging.WARNING, 'Another log %s'))
log.LimitFilter.ignore_regexp.add((logging.WARNING,
re.compile(r'Log.*')))
log.LimitFilter.ignore_regexp.add((logging.WARNING,
re.compile(r'.*log 4')))
do_logging()
self.assertEqual(
self.handler.count_logs('Log \\d', logging.WARNING),
0)
self.assertEqual(
self.handler.count_logs('Another log \\d', logging.WARNING),
4)

# filter by all
with self.reset_logger():
log.LimitFilter.ignore.add((logging.WARNING, 'Log 3'))
log.LimitFilter.ignore.add((logging.WARNING, 'Another log %s'))
log.LimitFilter.ignore_regexp.add((logging.WARNING,
re.compile(r'Lo.*4$')))
do_logging()
self.assertEqual(
self.handler.count_logs('Log \\d', logging.WARNING),
3)
self.assertEqual(
self.handler.count_logs('Another log \\d', logging.WARNING),
0)
44 changes: 43 additions & 1 deletion pelican/tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import copy
import locale
import logging
import os
import re
from os.path import abspath, dirname, join


from pelican.log import LimitFilter
from pelican.settings import (DEFAULT_CONFIG, DEFAULT_THEME,
_printf_s_to_format_field,
configure_settings,
Expand Down Expand Up @@ -108,6 +110,46 @@ def test_configure_settings(self):
configure_settings(settings)
self.assertEqual(settings['FEED_DOMAIN'], 'http://feeds.example.com')

def test_configure_log_filter_settings(self):
# Various forms of filter settings should be applied correctly.
settings = {
'LOG_FILTER': [
(logging.WARNING, 'foo'),
('string', logging.ERROR, 'bar'),
('regex', logging.INFO, r'baz.*boo'),
],
'PATH': os.curdir,
'THEME': DEFAULT_THEME,
}
with self.assertWarns(
FutureWarning,
msg='2-tuple specification of LOG_FILTER item is deprecated,' +
'replace with 3-tuple starting with \'string\' (see' +
'documentation of LOG_FILTER for more details)'):
configure_settings(settings)

self.assertEqual(LimitFilter.ignore, {
(logging.WARNING, 'foo'),
(logging.ERROR, 'bar'),
})
self.assertEqual(LimitFilter.ignore_regexp, {
(logging.INFO, re.compile(r'baz.*boo'))
})

settings['LOG_FILTER'] = [(1, 2, 3, 4)]
with self.assertRaisesRegex(
ValueError,
r"Invalid item '\(1, 2, 3, 4\)' in LOG_FILTER"
):
configure_settings(settings)

settings['LOG_FILTER'] = [('foo', 'bar', 'baz')]
with self.assertRaisesRegex(
ValueError,
r"Invalid LOG_FILTER type 'foo'"
):
configure_settings(settings)

def test_theme_settings_exceptions(self):
settings = self.settings

Expand Down