Source code for cerf.logger
"""Logger for CERF model.
Copyright (c) 2018, Battelle Memorial Institute
Open source under license BSD 2-Clause - see LICENSE and DISCLAIMER
@author: Chris R. Vernon (chris.vernon@pnnl.gov)
"""
import logging
import sys
[docs]
class Logger:
"""Manage the package-wide ``cerf`` logger.
All CERF modules log through ``logging.getLogger(__name__)``, which makes them children of the ``cerf`` logger.
Handlers are attached to that named logger only, so CERF never modifies the root logger, never changes the level
of third-party libraries and never removes handlers it did not create. Attaching handlers is idempotent: creating
several models in one session does not duplicate log lines.
"""
# name of the package logger; module loggers (``cerf.stage`` etc.) propagate to it
LOGGER_NAME = 'cerf'
# handler names used to recognise handlers owned by CERF
CONSOLE_HANDLER_NAME = 'cerf-console'
FILE_HANDLER_NAME = 'cerf-file'
# output format for log string
LOG_FORMAT_STRING = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
@property
def log_format(self):
"""Generate log formatter."""
return logging.Formatter(self.LOG_FORMAT_STRING)
@property
def logger(self):
"""The ``cerf`` package logger."""
return logging.getLogger(self.LOGGER_NAME)
[docs]
@staticmethod
def resolve_level(log_level):
"""Translate the user-facing ``log_level`` string ('info' or 'debug') to a ``logging`` level."""
if str(log_level).lower() == 'debug':
return logging.DEBUG
return logging.INFO
[docs]
def initialize_logger(self, log_level='info', log_file=None):
"""Attach the console handler and, optionally, a file handler.
:param log_level: Log level. Options are 'info' and 'debug'. Default 'info'
:type log_level: str
:param log_file: Optional path to a log file. If given, log records are also written there.
:type log_file: str
"""
self.console_handler(log_level)
if log_file is not None:
self.file_handler(log_level, log_file)
def _get_handler(self, name):
"""Return the CERF-owned handler with ``name`` if it is attached, otherwise ``None``."""
for handler in self.logger.handlers:
if handler.get_name() == name:
return handler
return None
def _attach_handler(self, handler, name, log_level):
"""Configure ``handler`` and attach it to the ``cerf`` logger, replacing any previous handler of that name."""
level = self.resolve_level(log_level)
existing = self._get_handler(name)
if existing is not None:
existing.close()
self.logger.removeHandler(existing)
handler.set_name(name)
handler.setLevel(level)
handler.setFormatter(self.log_format)
self.logger.addHandler(handler)
# the logger level must be at least as permissive as the most verbose handler
if self.logger.level == logging.NOTSET or self.logger.level > level:
self.logger.setLevel(level)
return handler
[docs]
def console_handler(self, log_level='info'):
"""Attach a stdout handler to the ``cerf`` logger (idempotent)."""
return self._attach_handler(logging.StreamHandler(sys.stdout), self.CONSOLE_HANDLER_NAME, log_level)
[docs]
def file_handler(self, log_level='info', log_file=None):
"""Attach a file handler to the ``cerf`` logger (idempotent)."""
if log_file is None:
raise ValueError("`log_file` must be provided to attach a file handler.")
return self._attach_handler(logging.FileHandler(log_file), self.FILE_HANDLER_NAME, log_level)
[docs]
@classmethod
def close_logger(cls):
"""Detach and close the handlers CERF attached to the ``cerf`` logger.
Handlers owned by the application (on the root logger or elsewhere) are left untouched.
"""
logger = logging.getLogger(cls.LOGGER_NAME)
for handler in logger.handlers[:]:
if handler.get_name() in (cls.CONSOLE_HANDLER_NAME, cls.FILE_HANDLER_NAME):
handler.close()
logger.removeHandler(handler)