Skip to content

Commit

Permalink
Extended functionality of util.logging:
Browse files Browse the repository at this point in the history
 * Add run_cli
 * Add remove_log_handler
 * Add set_configure_callback
 * Add FileLoggerContext

Added new optional dependencies to environment.yml (for run_cli):
 * jsonargparse
 * docstring_parser
  • Loading branch information
opcode81 committed Oct 27, 2023
1 parent c56d533 commit 1a2cb71
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 2 deletions.
2 changes: 2 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ dependencies:
- clearml
- tensorflow-cpu
- pytorch-lightning
- jsonargparse
- docstring_parser
# required for locally running the tox build (which will work on Linux only at this time)
- tox
- virtualenv
73 changes: 71 additions & 2 deletions src/sensai/util/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@
log = getLogger(__name__)

LOG_DEFAULT_FORMAT = '%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s - %(message)s'

# Holds the log format that is configured by the user (using function `configure`), such
# that it can be reused in other places
_logFormat = LOG_DEFAULT_FORMAT

# User-configured callback which is called after logging is configured via function `configure`
_configureCallback: Optional[Callable[[], None]] = None


def remove_log_handlers():
"""
Expand All @@ -24,10 +30,27 @@ def remove_log_handlers():
logger.removeHandler(logger.handlers[0])


def remove_log_handler(handler):
getLogger().removeHandler(handler)


def is_log_handler_active(handler):
return handler in getLogger().handlers


def set_configure_callback(callback: Callable[[], None]):
"""
Configures a function to be called when logging is configured, e.g. through :func:`configure, :func:`run_main` or
:func:`run_cli`.
A typical use for the callback is to configure the logging behaviour of packages, setting appropriate log levels.
:param callback: the function to cal
:return:
"""
global _configureCallback
_configureCallback = callback


# noinspection PyShadowingBuiltins
def configure(format=LOG_DEFAULT_FORMAT, level=lg.DEBUG):
"""
Expand All @@ -45,6 +68,8 @@ def configure(format=LOG_DEFAULT_FORMAT, level=lg.DEBUG):
getLogger("urllib3").setLevel(lg.INFO)
getLogger("msal").setLevel(lg.INFO)
pd.set_option('display.max_colwidth', 255)
if _configureCallback:
_configureCallback()


# noinspection PyShadowingBuiltins
Expand All @@ -69,6 +94,26 @@ def run_main(main_fn: Callable[[], Any], format=LOG_DEFAULT_FORMAT, level=lg.DEB
log.error("Exception during script execution", exc_info=e)


def run_cli(main_fn: Callable[[], Any], format=LOG_DEFAULT_FORMAT, level=lg.DEBUG):
"""
Configures logging with the given parameters and runs the given main function as a
CLI using `jsonargparse` (which is configured to also parse attribute docstrings, such
that dataclasses can be used as function arguments).
Using this function requires that `jsonargparse` and `docstring_parser` be available.
Like `run_main`, two additional log messages will be logged (at the beginning and end
of the execution), and it is ensured that all exceptions will be logged.
:param main_fn: the function to be executed
:param format: the log message format
:param level: the minimum log level
:return: the result of `main_fn`
"""
from jsonargparse import set_docstring_parse_options, CLI

set_docstring_parse_options(attribute_docstrings=True)
return run_main(lambda: CLI(main_fn), format=format, level=level)


def datetime_tag() -> str:
"""
:return: a string tag for use in log file names which contains the current date and time (compact but readable)
Expand All @@ -86,16 +131,17 @@ def _at_exit_report_file_logger():
print(f"A log file was saved to {path}")


def add_file_logger(path):
def add_file_logger(path, register_atexit=True):
global _isAtExitReportFileLoggerRegistered
log.info(f"Logging to {path} ...")
handler = FileHandler(path)
handler.setFormatter(Formatter(_logFormat))
Logger.root.addHandler(handler)
_fileLoggerPaths.append(path)
if not _isAtExitReportFileLoggerRegistered:
if not _isAtExitReportFileLoggerRegistered and register_atexit:
atexit.register(_at_exit_report_file_logger)
_isAtExitReportFileLoggerRegistered = True
return handler


def add_memory_logger() -> None:
Expand Down Expand Up @@ -263,3 +309,26 @@ def __exit__(self, exc_type, exc_value, traceback):
def __enter__(self):
self.start()
return self


class FileLoggerContext:
"""
A context handler to be used in conjunction with Python's `with` statement which enables file-based logging.
"""
def __init__(self, path: str, enabled=True):
"""
:param path: the path to the log file
:param enabled: whether to actually perform any logging.
This switch allows the with statement to be applied regardless of whether logging shall be enabled.
"""
self.enabled = enabled
self.path = path
self._log_handler = None

def __enter__(self):
if self.enabled:
self._log_handler = add_file_logger(self.path, register_atexit=False)

def __exit__(self, exc_type, exc_value, traceback):
if self._log_handler is not None:
remove_log_handler(self._log_handler)

0 comments on commit 1a2cb71

Please sign in to comment.