-
Notifications
You must be signed in to change notification settings - Fork 917
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
270 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,5 +10,6 @@ space | |
datacollection | ||
batchrunner | ||
visualization | ||
logging | ||
experimental | ||
``` |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# logging | ||
|
||
```{eval-rst} | ||
.. automodule:: mesa.mesa_logging | ||
:members: | ||
:inherited-members: | ||
``` |
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 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
"""This provides logging functionality for MESA. | ||
It is modeled on the default `logging approach that comes with Python <https://docs.python.org/library/logging.html>`_. | ||
""" | ||
|
||
import inspect | ||
import logging | ||
from functools import wraps | ||
from logging import DEBUG, INFO | ||
|
||
__all__ = [ | ||
"get_rootlogger", | ||
"get_module_logger", | ||
"log_to_stderr", | ||
"DEBUG", | ||
"INFO", | ||
"DEFAULT_LEVEL", | ||
"LOGGER_NAME", | ||
"method_logger", | ||
"function_logger", | ||
] | ||
LOGGER_NAME = "MESA" | ||
DEFAULT_LEVEL = DEBUG | ||
|
||
|
||
def create_module_logger(name: str | None = None): | ||
"""Helper function for creating a module logger. | ||
Args: | ||
name (str): The name to be given to the logger. If the name is None, the name defaults to the name of the module. | ||
""" | ||
if name is None: | ||
frm = inspect.stack()[1] | ||
mod = inspect.getmodule(frm[0]) | ||
name = mod.__name__ | ||
logger = logging.getLogger(f"{LOGGER_NAME}.{name}") | ||
|
||
_module_loggers[name] = logger | ||
return logger | ||
|
||
|
||
def get_module_logger(name: str): | ||
"""Helper function for getting the module logger. | ||
Args: | ||
name (str): The name of the module in which the method being decorated is located | ||
""" | ||
try: | ||
logger = _module_loggers[name] | ||
except KeyError: | ||
logger = create_module_logger(name) | ||
|
||
return logger | ||
|
||
|
||
_rootlogger = None | ||
_module_loggers = {} | ||
_logger = get_module_logger(__name__) | ||
|
||
|
||
class MESAColorFormatter(logging.Formatter): | ||
"""Custom formatter for color based formatting.""" | ||
|
||
grey = "\x1b[38;20m" | ||
green = "\x1b[32m" | ||
yellow = "\x1b[33;20m" | ||
red = "\x1b[31;20m" | ||
bold_red = "\x1b[31;1m" | ||
reset = "\x1b[0m" | ||
format = ( | ||
"[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]" | ||
) | ||
|
||
FORMATS = { | ||
logging.DEBUG: grey + format + reset, | ||
logging.INFO: green + format + reset, | ||
logging.WARNING: yellow + format + reset, | ||
logging.ERROR: red + format + reset, | ||
logging.CRITICAL: bold_red + format + reset, | ||
} | ||
|
||
def format(self, record): | ||
"""Format record.""" | ||
log_fmt = self.FORMATS.get(record.levelno) | ||
formatter = logging.Formatter(log_fmt) | ||
return formatter.format(record) | ||
|
||
|
||
def method_logger(name: str): | ||
"""Decorator for adding logging to a method. | ||
Args: | ||
name (str): The name of the module in which the method being decorated is located | ||
""" | ||
logger = get_module_logger(name) | ||
classname = inspect.getouterframes(inspect.currentframe())[1][3] | ||
|
||
def real_decorator(func): | ||
@wraps(func) | ||
def wrapper(*args, **kwargs): | ||
# hack, because log is applied to methods, we can get | ||
# object instance as first arguments in args | ||
logger.debug( | ||
f"calling {classname}.{func.__name__} with {args[1::]} and {kwargs}" | ||
) | ||
res = func(*args, **kwargs) | ||
return res | ||
|
||
return wrapper | ||
|
||
return real_decorator | ||
|
||
|
||
def function_logger(name): | ||
"""Decorator for adding logging to a Function. | ||
Args: | ||
name (str): The name of the module in which the function being decorated is located | ||
""" | ||
logger = get_module_logger(name) | ||
|
||
def real_decorator(func): | ||
@wraps(func) | ||
def wrapper(*args, **kwargs): | ||
logger.debug(f"calling {func.__name__} with {args} and {kwargs}") | ||
res = func(*args, **kwargs) | ||
return res | ||
|
||
return wrapper | ||
|
||
return real_decorator | ||
|
||
|
||
def get_rootlogger(): | ||
"""Returns root logger used by MESA. | ||
Returns: | ||
the root logger of MESA | ||
""" | ||
global _rootlogger # noqa: PLW0603 | ||
|
||
if not _rootlogger: | ||
_rootlogger = logging.getLogger(LOGGER_NAME) | ||
_rootlogger.handlers = [] | ||
_rootlogger.addHandler(logging.NullHandler()) | ||
_rootlogger.setLevel(DEBUG) | ||
|
||
return _rootlogger | ||
|
||
|
||
def log_to_stderr(level: int | None = None, pass_root_logger_level: bool = False): | ||
"""Turn on logging and add a handler which prints to stderr. | ||
Args: | ||
level: minimum level of the messages that will be logged | ||
pass_root_logger_level: bool, optional. Default False | ||
if True, all module loggers will be set to the same logging level as the root logger. | ||
""" | ||
if not level: | ||
level = DEFAULT_LEVEL | ||
|
||
logger = get_rootlogger() | ||
|
||
# avoid creation of multiple stream handlers for logging to console | ||
for entry in logger.handlers: | ||
if (isinstance(entry, logging.StreamHandler)) and ( | ||
isinstance(entry.formatter, MESAColorFormatter) | ||
): | ||
return logger | ||
|
||
formatter = MESAColorFormatter() | ||
handler = logging.StreamHandler() | ||
handler.setLevel(level) | ||
handler.setFormatter(formatter) | ||
logger.addHandler(handler) | ||
logger.propagate = False | ||
|
||
if pass_root_logger_level: | ||
for _, mod_logger in _module_loggers.items(): | ||
mod_logger.setLevel(level) | ||
|
||
return logger |
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 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 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
"""Unit tests for mesa_logging.""" | ||
|
||
import logging | ||
|
||
import pytest | ||
|
||
from mesa import mesa_logging | ||
|
||
|
||
@pytest.fixture | ||
def tear_down(): | ||
"""Pytest fixture to ensure all logging is disabled after testing.""" | ||
yield None | ||
mesa_logging._logger = None | ||
ema_logger = logging.getLogger(mesa_logging.LOGGER_NAME) | ||
ema_logger.handlers = [] | ||
|
||
|
||
def test_get_logger(): | ||
"""Test get_logger.""" | ||
mesa_logging._rootlogger = None | ||
logger = mesa_logging.get_rootlogger() | ||
assert logger == logging.getLogger(mesa_logging.LOGGER_NAME) | ||
assert len(logger.handlers) == 1 | ||
assert isinstance(logger.handlers[0], logging.NullHandler) | ||
|
||
logger = mesa_logging.get_rootlogger() | ||
assert logger, logging.getLogger(mesa_logging.LOGGER_NAME) | ||
assert len(logger.handlers) == 1 | ||
assert isinstance(logger.handlers[0], logging.NullHandler) | ||
|
||
|
||
def test_log_to_stderr(): | ||
"""Test log_to_stderr.""" | ||
mesa_logging._rootlogger = None | ||
logger = mesa_logging.log_to_stderr(mesa_logging.DEBUG) | ||
assert len(logger.handlers) == 2 | ||
assert logger.level == mesa_logging.DEBUG | ||
|
||
mesa_logging._rootlogger = None | ||
logger = mesa_logging.log_to_stderr() | ||
assert len(logger.handlers) == 2 | ||
assert logger.level == mesa_logging.DEFAULT_LEVEL | ||
|
||
logger = mesa_logging.log_to_stderr() | ||
assert len(logger.handlers) == 2 | ||
assert logger.level == mesa_logging.DEFAULT_LEVEL |