aboutsummaryrefslogtreecommitdiff
path: root/absl
diff options
context:
space:
mode:
Diffstat (limited to 'absl')
-rw-r--r--absl/CHANGELOG.md1
-rw-r--r--absl/flags/tests/flags_test.py2
-rw-r--r--absl/logging/BUILD1
-rw-r--r--absl/logging/__init__.py74
-rwxr-xr-xabsl/logging/tests/logging_functional_test.py25
-rw-r--r--absl/logging/tests/logging_test.py74
6 files changed, 176 insertions, 1 deletions
diff --git a/absl/CHANGELOG.md b/absl/CHANGELOG.md
index c5479f4..49a2c28 100644
--- a/absl/CHANGELOG.md
+++ b/absl/CHANGELOG.md
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com).
### Added
+* (logging) `--logger_levels`: allows specifying the log levels of loggers.
* (flags) `FLAGS.validate_all_flags`: a new method that validates all flags
and raises an exception if one fails.
diff --git a/absl/flags/tests/flags_test.py b/absl/flags/tests/flags_test.py
index 0a54cd3..575164d 100644
--- a/absl/flags/tests/flags_test.py
+++ b/absl/flags/tests/flags_test.py
@@ -604,6 +604,7 @@ class FlagsUnitTest(absltest.TestCase):
'--kwery None',
'--l 9223372032559808512',
"--letters ['a', 'b', 'c']",
+ '--logger_levels {}',
"--m ['str1', 'str2']",
"--m_str ['str1', 'str2']",
'--name giants',
@@ -673,6 +674,7 @@ class FlagsUnitTest(absltest.TestCase):
'--kwery None',
'--l 9223372032559808512',
"--letters ['a', 'b', 'c']",
+ '--logger_levels {}',
"--m ['str1', 'str2', 'upd1']",
"--m_str ['str1', 'str2', 'upd1']",
'--name giants',
diff --git a/absl/logging/BUILD b/absl/logging/BUILD
index 61e1861..90a90ed 100644
--- a/absl/logging/BUILD
+++ b/absl/logging/BUILD
@@ -11,6 +11,7 @@ py_library(
visibility = ["//visibility:public"],
deps = [
":converter",
+ "//absl:_collections_abc",
"//absl/flags",
"@six_archive//:six",
],
diff --git a/absl/logging/__init__.py b/absl/logging/__init__.py
index c4ce102..bd06cdd 100644
--- a/absl/logging/__init__.py
+++ b/absl/logging/__init__.py
@@ -75,6 +75,7 @@ from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
+import collections
import getpass
import io
import itertools
@@ -86,9 +87,11 @@ import sys
import time
import timeit
import traceback
+import types
import warnings
from absl import flags
+from absl._collections_abc import abc
from absl.logging import converter
import six
@@ -174,7 +177,10 @@ class _VerbosityFlag(flags.Flag):
self._update_logging_levels()
def _update_logging_levels(self):
- """Updates absl logging levels to the current verbosity."""
+ """Updates absl logging levels to the current verbosity.
+
+ Visibility: module-private
+ """
if not _absl_logger:
return
@@ -194,6 +200,64 @@ class _VerbosityFlag(flags.Flag):
_absl_logger.setLevel(standard_verbosity)
+class _LoggerLevelsFlag(flags.Flag):
+ """Flag class for --logger_levels."""
+
+ def __init__(self, *args, **kwargs):
+ super(_LoggerLevelsFlag, self).__init__(
+ _LoggerLevelsParser(),
+ _LoggerLevelsSerializer(),
+ *args, **kwargs)
+
+ @property
+ def value(self):
+ # For lack of an immutable type, be defensive and return a copy.
+ # Modifications to the dict aren't supported and won't have any affect.
+ # While Py3 could use MappingProxyType, that isn't deepcopy friendly, so
+ # just return a copy.
+ return self._value.copy()
+
+ @value.setter
+ def value(self, v):
+ self._value = {} if v is None else v
+ self._update_logger_levels()
+
+ def _update_logger_levels(self):
+ # Visibility: module-private.
+ # This is called by absl.app.run() during initialization.
+ for name, level in self._value.items():
+ logging.getLogger(name).setLevel(level)
+
+
+class _LoggerLevelsParser(flags.ArgumentParser):
+ """Parser for --logger_levels flag."""
+
+ def parse(self, value):
+ if isinstance(value, abc.Mapping):
+ return value
+
+ pairs = [pair.strip() for pair in value.split(',') if pair.strip()]
+
+ # Preserve the order so that serialization is deterministic.
+ levels = collections.OrderedDict()
+ for name_level in pairs:
+ name, level = name_level.split(':', 1)
+ name = name.strip()
+ level = level.strip()
+ levels[name] = level
+ return levels
+
+
+class _LoggerLevelsSerializer(object):
+ """Serializer for --logger_levels flag."""
+
+ def serialize(self, value):
+ if isinstance(value, six.string_types):
+ return value
+ return ','.join(
+ '{}:{}'.format(name, level) for name, level in value.items())
+
+
class _StderrthresholdFlag(flags.Flag):
"""Flag class for --stderrthreshold."""
@@ -245,6 +309,13 @@ flags.DEFINE_flag(_VerbosityFlag(
'supplied, the value will be changed from the default of -1 (warning) to '
'0 (info) after flags are parsed.',
short_name='v', allow_hide_cpp=True))
+flags.DEFINE_flag(
+ _LoggerLevelsFlag(
+ 'logger_levels', {},
+ 'Specify log level of loggers. The format is a CSV list of '
+ '`name:level`. Where `name` is the logger name used with '
+ '`logging.getLogger()`, and `level` is a level name (INFO, DEBUG, '
+ 'etc). e.g. `myapp.foo:INFO,other.logger:DEBUG`'))
flags.DEFINE_flag(_StderrthresholdFlag(
'stderrthreshold', 'fatal',
'log messages at this level, or more severe, to stderr in '
@@ -1146,6 +1217,7 @@ def use_absl_handler():
if absl_handler not in logging.root.handlers:
logging.root.addHandler(absl_handler)
FLAGS['verbosity']._update_logging_levels() # pylint: disable=protected-access
+ FLAGS['logger_levels']._update_logger_levels() # pylint: disable=protected-access
def _initialize():
diff --git a/absl/logging/tests/logging_functional_test.py b/absl/logging/tests/logging_functional_test.py
index dd7e711..b8c79d3 100755
--- a/absl/logging/tests/logging_functional_test.py
+++ b/absl/logging/tests/logging_functional_test.py
@@ -647,6 +647,31 @@ E0000 00:00:00.000000 12345 logging_functional_test_helper.py:123] std error log
test_name='bad_exc_info',
use_absl_log_file=True)
+ def test_verbosity_logger_levels_flag_ordering(self):
+ """Make sure last-specified flag wins."""
+
+ def assert_error_level_logged(stderr):
+ lines = stderr.splitlines()
+ for line in lines:
+ self.assertIn('std error log', line)
+
+ self._exec_test(
+ _verify_ok,
+ test_name='std_logging',
+ expected_logs=[('stderr', None, assert_error_level_logged)],
+ extra_args=['-v=1', '--logger_levels=:ERROR'])
+
+ def assert_debug_level_logged(stderr):
+ lines = stderr.splitlines()
+ for line in lines:
+ self.assertRegex(line, 'std (debug|info|warning|error) log')
+
+ self._exec_test(
+ _verify_ok,
+ test_name='std_logging',
+ expected_logs=[('stderr', None, assert_debug_level_logged)],
+ extra_args=['--logger_levels=:ERROR', '-v=1'])
+
def test_none_exc_info_py_logging(self):
if six.PY2:
diff --git a/absl/logging/tests/logging_test.py b/absl/logging/tests/logging_test.py
index ddae7a1..290b2f3 100644
--- a/absl/logging/tests/logging_test.py
+++ b/absl/logging/tests/logging_test.py
@@ -59,6 +59,80 @@ class ConfigurationTest(absltest.TestCase):
logging.PythonFormatter))
+class LoggerLevelsTest(parameterized.TestCase):
+
+ def setUp(self):
+ super(LoggerLevelsTest, self).setUp()
+ # Since these tests muck with the flag, always save/restore in case the
+ # tests forget to clean up properly.
+ # enter_context() is py3-only, but manually enter/exit should suffice.
+ cm = self.set_logger_levels({})
+ cm.__enter__()
+ self.addCleanup(lambda: cm.__exit__(None, None, None))
+
+ @contextlib.contextmanager
+ def set_logger_levels(self, levels):
+ original_levels = {
+ name: std_logging.getLogger(name).level for name in levels
+ }
+
+ try:
+ with flagsaver.flagsaver(logger_levels=levels):
+ yield
+ finally:
+ for name, level in original_levels.items():
+ std_logging.getLogger(name).setLevel(level)
+
+ def assert_logger_level(self, name, expected_level):
+ logger = std_logging.getLogger(name)
+ self.assertEqual(logger.level, expected_level)
+
+ def assert_logged(self, logger_name, expected_msgs):
+ logger = std_logging.getLogger(logger_name)
+ # NOTE: assertLogs() sets the logger to INFO if not specified.
+ with self.assertLogs(logger, logger.level) as cm:
+ logger.debug('debug')
+ logger.info('info')
+ logger.warning('warning')
+ logger.error('error')
+ logger.critical('critical')
+
+ actual = {r.getMessage() for r in cm.records}
+ self.assertEqual(set(expected_msgs), actual)
+
+ @unittest.skipIf(six.PY2, 'Py2 is missing assertLogs')
+ def test_setting_levels(self):
+ # Other tests change the root logging level, so we can't
+ # assume it's the default.
+ orig_root_level = std_logging.root.getEffectiveLevel()
+ with self.set_logger_levels({'foo': 'ERROR', 'bar': 'DEBUG'}):
+
+ self.assert_logger_level('foo', std_logging.ERROR)
+ self.assert_logger_level('bar', std_logging.DEBUG)
+ self.assert_logger_level('', orig_root_level)
+
+ self.assert_logged('foo', {'error', 'critical'})
+ self.assert_logged('bar',
+ {'debug', 'info', 'warning', 'error', 'critical'})
+
+ @parameterized.named_parameters(
+ ('empty', ''),
+ ('one_value', 'one:INFO'),
+ ('two_values', 'one.a:INFO,two.b:ERROR'),
+ ('whitespace_ignored', ' one : DEBUG , two : INFO'),
+ )
+ def test_serialize_parse(self, levels_str):
+ fl = FLAGS['logger_levels']
+ fl.parse(levels_str)
+ expected = levels_str.replace(' ', '')
+ actual = fl.serialize()
+ self.assertEqual('--logger_levels={}'.format(expected), actual)
+
+ def test_invalid_value(self):
+ with self.assertRaisesRegex(ValueError, 'Unknown level.*10'):
+ FLAGS['logger_levels'].parse('foo:10')
+
+
class PythonHandlerTest(absltest.TestCase):
"""Tests the PythonHandler class."""