Created
October 4, 2024 15:38
-
-
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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