diff --git a/README.md b/README.md index e15c9cc17ca10b1294694991596bab2f4aac83f2..52cca4ebd89e05a9a50951838f3df1e19a833e43 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,25 @@ def main(): configure_logging(overrides=ADDITIONAL_LOGGING_CONFIG) ``` +Custom handlers that use the standard logging format may be useful. In this case, the function +`get_default_formatter` is available. The example below is contrived, but shows the approach. +A more practical use case is adding and removing handlers at runtime. + +```python +import logging +import logging.handlers +from ska_logging import configure_logging, get_default_formatter + + +def main(): + configure_logging() + logger = logging.getLogger("ska.example") + handler = logging.handlers.MemoryHandler(capacity=10) + handler.setFormatter(get_default_formatter()) + logger.addHandler(handler) + logger.info("Logging started for Example application") +``` + By default, calls to `configure_logging` do not disable existing non-root loggers. This allows multiple calls to the function, although that will generally not be required. This behaviour can be overridden using the `"disable_existing_loggers"` key. diff --git a/ska_logging/__init__.py b/ska_logging/__init__.py index f90d3da80dc28123a54e35ab31136c7c4564a33e..1dcf20a50f0e7f7a37b878a0ba0ca69424513b50 100644 --- a/ska_logging/__init__.py +++ b/ska_logging/__init__.py @@ -2,12 +2,22 @@ """Module init code.""" -__all__ = ("configure_logging",) +__all__ = ( + "configure_logging", + "get_default_formatter", + "SkaLoggingError", + "SkaLoggingTagsFormatError", +) __author__ = "Anton Joubert" __email__ = "ajoubert+ska@ska.ac.za" -from .configuration import configure_logging +from .configuration import ( + configure_logging, + get_default_formatter, + SkaLoggingError, + SkaLoggingTagsFormatError, +) # BEGIN VERSION CHECK diff --git a/ska_logging/configuration.py b/ska_logging/configuration.py index f68d4e54cc5fba913f3181d6d6a6c53f505f3736..11135f31b1cb2b14562ec76bdc33dc49cd6abb48 100644 --- a/ska_logging/configuration.py +++ b/ska_logging/configuration.py @@ -7,6 +7,14 @@ import logging.config import time +class SkaLoggingError(Exception): + """Base class for all SKA Logger exceptions.""" + + +class SkaLoggingTagsFormatError(SkaLoggingError): + """Invalid format for the 'tags' field string.""" + + class _UTCFormatter(logging.Formatter): converter = time.gmtime @@ -38,6 +46,8 @@ _FORMAT_STR_WITH_TAGS = ( "%(message)s" ) +_INVALID_TAG_CHARS = ("|", "%") + _LOGGING_CONFIG = { "version": 1, "disable_existing_loggers": False, @@ -107,6 +117,45 @@ def configure_logging(level=None, tags_filter=None, overrides=None): logging.config.dictConfig(config) +def get_default_formatter(tags=False): + """Return a formatter configured with the standard logging format. + + Parameters + ---------- + + tags : bool or str, optional + If boolean, then treated as a toggle: + - True: include the "tags" field in the format string. This requires + a tags filter to be linked to the corresponding handler. + - False: exclude the "tags" field from the format string. + If string, then it is a static tag. Instead of using a logging filter, the + formatter will just use this static string for the "tags" field directly. + + Returns + ------- + logging.Formatter + A new default formatter. + + Raises + ------ + SkaLoggingTagsFormatError: + If the static tags string has an invalid format. + + """ + if isinstance(tags, str): + invalid_chars = [c for c in _INVALID_TAG_CHARS if c in tags] + if invalid_chars: + raise SkaLoggingTagsFormatError( + "Invalid char(s) {} in tags: {!r}".format(invalid_chars, tags) + ) + format_str = _FORMAT_STR_WITH_TAGS.replace("%(tags)s", tags) + elif tags: + format_str = _FORMAT_STR_WITH_TAGS + else: + format_str = _FORMAT_STR_NO_TAGS + return _UTCFormatter(fmt=format_str) + + def _override(config, overrides): """Update a config dictionary with overrides, merging dictionaries. diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 2956de636eda149f7a03a6743fef1c16032158d0..3e72a36a1bb83fbdae61416e8b3fea67f0c7f3da 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -9,7 +9,7 @@ import pytest import ska_logging.configuration -from ska_logging import configure_logging +from ska_logging import configure_logging, get_default_formatter, SkaLoggingTagsFormatError @pytest.fixture @@ -103,31 +103,31 @@ class TestConfigureLogging: configure_logging(tags_filter=MyFilter, overrides=RECORDER_OVERRIDES) yield logging.getLogger("ska.logger") - def test_configure_logging_includes_console_handler(self, default_logger): + def test_includes_console_handler(self, default_logger): assert get_named_handler(default_logger, "console") - def test_configure_logging_multiple_calls_non_root_logger_still_enabled(self, default_logger): + def test_multiple_calls_non_root_logger_still_enabled(self, default_logger): logger = logging.getLogger("ska.logger.test") assert not logger.disabled configure_logging() assert not logger.disabled - def test_configure_logging_default_log_level_info(self, default_logger): + def test_default_log_level_info(self, default_logger): logger = logging.getLogger("ska.logger.test") assert logger.getEffectiveLevel() == logging.INFO assert default_logger.getEffectiveLevel() == logging.INFO - def test_configure_logging_set_log_level_int(self): + def test_set_log_level_int(self): configure_logging(level=logging.DEBUG) logger = logging.getLogger("ska.logger.test") assert logger.getEffectiveLevel() == logging.DEBUG - def test_configure_logging_set_log_level_string(self): + def test_set_log_level_string(self): configure_logging(level="WARNING") logger = logging.getLogger("ska.logger.test") assert logger.getEffectiveLevel() == logging.WARNING - def test_configure_logging_default_uses_utc_time(self, recording_logger): + def test_default_uses_utc_time(self, recording_logger): recording_logger.info("UTC message") recorder = get_named_handler(recording_logger, "recorder") record = recorder.records[0] @@ -136,17 +136,17 @@ class TestConfigureLogging: assert "UTC message" in log_message assert expected_time in log_message # just testing UTC, so ignore the milliseconds part - def test_configure_logging_default_no_tags(self, default_logger): + def test_default_no_tags(self, default_logger): handler = get_named_handler(default_logger, "console") formatter = handler.formatter assert "%(tag)s" not in formatter._fmt - def test_configure_logging_tags_filter_adds_tags_field(self, recording_tags_logger): + def test_tags_filter_adds_tags_field(self, recording_tags_logger): handler = get_named_handler(recording_tags_logger, "console") formatter = handler.formatter assert "%(tags)s" in formatter._fmt - def test_configure_logging_tags_filter_emits_tags_value(self, recording_tags_logger): + def test_tags_filter_emits_tags_value(self, recording_tags_logger): recording_tags_logger.info("Tags message") recorder = get_named_handler(recording_tags_logger, "recorder") record = recorder.records[0] @@ -154,7 +154,7 @@ class TestConfigureLogging: assert record.tags == "key1:value1,key2:value2" assert log_message.endswith("|key1:value1,key2:value2|Tags message") - def test_configure_logging_override(self): + def test_override(self): overrides = { "handlers": {"test": {"class": "logging.StreamHandler", "formatter": "default"}}, "root": {"handlers": ["console", "test"]}, @@ -165,6 +165,38 @@ class TestConfigureLogging: assert get_named_handler(logger, "test") +class TestGetDefaultFormatter: + """Tests for :func:`~ska_logging.configuration.get_default_formatter`.""" + + def test_default_no_tags(self): + formatter = get_default_formatter() + assert isinstance(formatter, ska_logging.configuration._UTCFormatter) + assert formatter._fmt == ska_logging.configuration._FORMAT_STR_NO_TAGS + + def test_get_tags_disabled(self): + formatter = get_default_formatter(tags=False) + assert isinstance(formatter, ska_logging.configuration._UTCFormatter) + assert formatter._fmt == ska_logging.configuration._FORMAT_STR_NO_TAGS + + def test_get_tags_enabled(self): + formatter = get_default_formatter(tags=True) + assert isinstance(formatter, ska_logging.configuration._UTCFormatter) + assert formatter._fmt == ska_logging.configuration._FORMAT_STR_WITH_TAGS + + def test_get_tags_static_string(self): + formatter = get_default_formatter(tags="test-key:test-value") + assert isinstance(formatter, ska_logging.configuration._UTCFormatter) + tags_format = ska_logging.configuration._FORMAT_STR_WITH_TAGS + expected_format = tags_format.replace("%(tags)s", "test-key:test-value") + assert formatter._fmt == expected_format + + def test_get_tags_invalid_static_string(self): + with pytest.raises(SkaLoggingTagsFormatError): + get_default_formatter(tags="no|pipes|allowed") + with pytest.raises(SkaLoggingTagsFormatError): + get_default_formatter(tags="no%percentage%symbols%allowed") + + class TestOverride: """Tests for :func:`~ska_logging.configuration._override`.