Skip to content

Instantly share code, notes, and snippets.

@wware
Created October 4, 2024 15:38
Show Gist options
  • Save wware/b3c5e2d1bc5f655ba7aac4621d067259 to your computer and use it in GitHub Desktop.
Save wware/b3c5e2d1bc5f655ba7aac4621d067259 to your computer and use it in GitHub Desktop.
I once fooled with a flavor of local loggers (actually I called them "topical loggers") that I ended up not liking. Here the name is automatically derived from the source filename so there's less to keep track of. Maybe I'll like this approach better. Environment variables are used to decide who gets DEBUG-level logging, otherwise it's INFO-level.
# This first piece can go into some global utils.py file.
# python foo.py
# LOGGER_FOO_PY=DEBUG python foo.py
# python -m pytest foo.py
import logging
import os
import inspect
import pytest
from unittest.mock import patch
def setup_local_logger(handler=None) -> logging.Logger:
"""
Sets up a logger for the calling module.
If an environment variable named `LOGGER_<FILENAME>` is set, it determines the logging level.
Otherwise, the default logging level is INFO. The logger will use a StreamHandler by default unless
a custom handler is provided.
Args:
handler (logging.Handler, optional): A custom logging handler. If not provided, a StreamHandler is used.
Returns:
logging.Logger: Configured logger instance.
"""
caller_frame = inspect.stack()[1]
caller_filename = os.path.basename(caller_frame.filename)
logger_key = ("LOGGER_" + os.path.basename(caller_filename)).upper().replace(".", "_")
logger = logging.getLogger(logger_key)
level = os.environ.get(logger_key, "INFO").upper()
numlevel = getattr(logging, level, logging.INFO)
if not logger.hasHandlers():
if handler is None:
handler = logging.StreamHandler()
formatter = logging.Formatter(
'%(asctime)-15s %(levelname)-6s %(filename)s:%(lineno)d %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(numlevel)
for handler in logger.handlers:
handler.setLevel(numlevel)
return logger
@pytest.mark.parametrize("log_level, log_method, expected_level, message", [
('INFO', 'info', logging.INFO, "This is an info message"),
('DEBUG', 'debug', logging.DEBUG, "This is a debug message"),
('ERROR', 'error', logging.ERROR, "This is an error message"),
])
@patch.object(logging.Logger, '_log')
def test_logger_levels(mock_log, log_level, log_method, expected_level, message):
"""
Tests that the logger logs messages at the correct level based on environment variable settings.
Args:
mock_log (unittest.mock.MagicMock): Mock object for the logger's _log method.
log_level (str): The log level to set in the environment variable.
log_method (str): The logger method to call (e.g., 'info', 'debug', 'error').
expected_level (int): The expected numeric log level.
message (str): The message to log.
"""
logger_key = ("LOGGER_" + os.path.basename(__file__)).upper().replace(".", "_")
os.environ[logger_key] = log_level
try:
logger = setup_local_logger()
getattr(logger, log_method)(message)
mock_log.assert_called_with(expected_level, message, ())
finally:
del os.environ[logger_key]
#################################################################
# This would go into one of many source files using this stuff.
mylogger = setup_local_logger()
def example_usage():
"""
Log a few messages at different logging levels.
"""
mylogger.error("This is an error message talking about catastrophic stuff")
mylogger.info("This is an info message talking about important stuff")
mylogger.debug("This is a debug message talking about nit-picky stuff")
if __name__ == "__main__":
example_usage()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment