diff options
Diffstat (limited to 'absl')
-rw-r--r-- | absl/CHANGELOG.md | 1 | ||||
-rw-r--r-- | absl/flags/tests/flags_test.py | 2 | ||||
-rw-r--r-- | absl/logging/BUILD | 1 | ||||
-rw-r--r-- | absl/logging/__init__.py | 74 | ||||
-rwxr-xr-x | absl/logging/tests/logging_functional_test.py | 25 | ||||
-rw-r--r-- | absl/logging/tests/logging_test.py | 74 |
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.""" |