pydrex.logger

PyDRex: logger settings and boilerplate.

Python's logging module is weird and its methods don't allow us to specify which logger to use, so just using logging.debug for example always uses the "root" logger, which spams a bunch of messages from other imports/modules. Instead, the methods in this module are thin wrappers that use custom logging objects (LOGGER and CONSOLE_LOGGER). The method quiet_aliens can be invoked to suppress most messages from third-party modules, except critical errors and warnings from Numba.

By default, PyDRex emits INFO level messages to the console. This can be changed globally by setting the new level with CONSOLE_LOGGER.setLevel:

from pydrex import logger as _log
_log.info("this message will be printed to the console")

_log.CONSOLE_LOGGER.setLevel("ERROR")
_log.info("this message will NOT be printed to the console")
_log.error("this message will be printed to the console")

To change the console logging level for a particular local context, use the handler_level context manager:

_log.CONSOLE_LOGGER.setLevel("INFO")
_log.info("this message will be printed to the console")

with handler_level("ERROR"):
    _log.info("this message will NOT be printed to the console")

_log.info("this message will be printed to the console")

To save logs to a file, the pydrex.io.logfile_enable context manager is recommended. Always use the old printf style formatting for log messages, not fstrings, otherwise compute time will be wasted on string conversions when logging is disabled:

from pydrex import io as _io
_log.quiet_aliens()  # Suppress third-party log messages except CRITICAL from Numba.
with _io.logfile_enable("my_log_file.log"):  # Overwrite existing file unless mode="a".
    value = 42
    _log.critical("critical error with value: %s", value)
    _log.error("runtime error with value: %s", value)
    _log.warning("warning with value: %s", value)
    _log.info("information message with value: %s", value)
    _log.debug("verbose debugging message with value: %s", value)
    ... # Construct Minerals, update orientations, etc.
  1"""> PyDRex: logger settings and boilerplate.
  2
  3Python's logging module is weird and its methods don't allow us to specify
  4which logger to use, so just using `logging.debug` for example always uses
  5the "root" logger, which spams a bunch of messages from other imports/modules.
  6Instead, the methods in this module are thin wrappers that use custom
  7logging objects (`pydrex.logger.LOGGER` and `pydrex.logger.CONSOLE_LOGGER`).
  8The method `quiet_aliens` can be invoked to suppress most messages
  9from third-party modules, except critical errors and warnings from Numba.
 10
 11By default, PyDRex emits INFO level messages to the console.
 12This can be changed globally by setting the new level with `CONSOLE_LOGGER.setLevel`:
 13
 14```python
 15from pydrex import logger as _log
 16_log.info("this message will be printed to the console")
 17
 18_log.CONSOLE_LOGGER.setLevel("ERROR")
 19_log.info("this message will NOT be printed to the console")
 20_log.error("this message will be printed to the console")
 21```
 22
 23To change the console logging level for a particular local context,
 24use the `handler_level` context manager:
 25
 26```python
 27_log.CONSOLE_LOGGER.setLevel("INFO")
 28_log.info("this message will be printed to the console")
 29
 30with handler_level("ERROR"):
 31    _log.info("this message will NOT be printed to the console")
 32
 33_log.info("this message will be printed to the console")
 34```
 35
 36To save logs to a file, the `pydrex.io.logfile_enable` context manager is recommended.
 37Always use the old printf style formatting for log messages, not fstrings,
 38otherwise compute time will be wasted on string conversions when logging is disabled:
 39
 40```python
 41from pydrex import io as _io
 42_log.quiet_aliens()  # Suppress third-party log messages except CRITICAL from Numba.
 43with _io.logfile_enable("my_log_file.log"):  # Overwrite existing file unless mode="a".
 44    value = 42
 45    _log.critical("critical error with value: %s", value)
 46    _log.error("runtime error with value: %s", value)
 47    _log.warning("warning with value: %s", value)
 48    _log.info("information message with value: %s", value)
 49    _log.debug("verbose debugging message with value: %s", value)
 50    ... # Construct Minerals, update orientations, etc.
 51
 52```
 53
 54"""
 55
 56import contextlib as cl
 57import functools as ft
 58import logging
 59import sys
 60
 61import numpy as np
 62
 63# NOTE: Do NOT import any pydrex submodules here to avoid cyclical imports.
 64
 65np.set_printoptions(
 66    formatter={
 67        "float_kind": np.format_float_scientific,
 68        "object": ft.partial(np.array2string, separator=", "),
 69    },
 70    linewidth=1000,
 71)
 72
 73
 74class ConsoleFormatter(logging.Formatter):
 75    """Log formatter that uses terminal color codes."""
 76
 77    def colorfmt(self, code):
 78        return (
 79            f"\033[{code}m%(levelname)s [%(asctime)s]\033[m"
 80            + " \033[1m%(name)s:\033[m %(message)s"
 81        )
 82
 83    def format(self, record):
 84        format_specs = {
 85            logging.CRITICAL: self.colorfmt("1;31"),
 86            logging.ERROR: self.colorfmt("31"),
 87            logging.INFO: self.colorfmt("32"),
 88            logging.WARNING: self.colorfmt("33"),
 89            logging.DEBUG: self.colorfmt("34"),
 90        }
 91        self._style._fmt = format_specs.get(record.levelno)
 92        return super().format(record)
 93
 94
 95# To create a new logger we use getLogger as recommended by the logging docs.
 96LOGGER = logging.getLogger("pydrex")
 97# To allow for multiple handlers at different levels, default level must be DEBUG.
 98LOGGER.setLevel(logging.DEBUG)
 99# Set up console handler.
100CONSOLE_LOGGER = logging.StreamHandler()
101CONSOLE_LOGGER.setFormatter(ConsoleFormatter(datefmt="%H:%M"))
102CONSOLE_LOGGER.setLevel(logging.INFO)
103# Turn on console logger by default.
104LOGGER.addHandler(CONSOLE_LOGGER)
105
106
107def handle_exception(exec_type, exec_value, exec_traceback):
108    # Ignore KeyboardInterrupt so ^C (ctrl + C) works as expected.
109    if issubclass(exec_type, KeyboardInterrupt):
110        sys.__excepthook__(exec_type, exec_value, exec_traceback)
111        return
112    # Send other exceptions to the logger.
113    LOGGER.exception(
114        "uncaught exception", exc_info=(exec_type, exec_value, exec_traceback)
115    )
116
117
118# Make our logger handle uncaught exceptions.
119sys.excepthook = handle_exception
120
121
122@cl.contextmanager
123def handler_level(level: str, handler: logging.Handler = CONSOLE_LOGGER):
124    """Set logging handler level for current context.
125
126    - `level` — logging level name e.g. "DEBUG", "ERROR", etc. See Python's logging
127      module for details.
128    - `handler` (optional) — alternative handler to control instead of the default,
129      `CONSOLE_LOGGER`.
130
131    """
132    default_level = handler.level
133    handler.setLevel(level)
134    yield
135    handler.setLevel(default_level)
136
137
138def critical(msg, *args, **kwargs):
139    """Log a CRITICAL message in PyDRex."""
140    LOGGER.critical(msg, *args, **kwargs)
141
142
143def error(msg, *args, **kwargs):
144    """Log an ERROR message in PyDRex."""
145    LOGGER.error(msg, *args, **kwargs)
146
147
148def warning(msg, *args, **kwargs):
149    """Log a WARNING message in PyDRex."""
150    LOGGER.warning(msg, *args, **kwargs)
151
152
153def info(msg, *args, **kwargs):
154    """Log an INFO message in PyDRex."""
155    LOGGER.info(msg, *args, **kwargs)
156
157
158def debug(msg, *args, **kwargs):
159    """Log a DEBUG message in PyDRex."""
160    LOGGER.debug(msg, *args, **kwargs)
161
162
163def exception(msg, *args, **kwargs):
164    """Log a message with level ERROR but retain exception information.
165
166    This function should only be called from an exception handler.
167
168    """
169    LOGGER.exception(msg, *args, **kwargs)
170
171
172def quiet_aliens():
173    """Restrict alien loggers 👽 because I'm trying to find MY bugs, thanks."""
174    # Only allow warnings or above from root logger.
175    logging.getLogger().setLevel(logging.WARNING)
176    # Only allow critical stuff from other things.
177    for name in logging.Logger.manager.loggerDict.keys():
178        if name != "pydrex":
179            logging.getLogger(name).setLevel(logging.CRITICAL)
180    # Numba is not in the list for some reason, I guess we can leave warnings.
181    logging.getLogger("numba").setLevel(logging.WARNING)
class ConsoleFormatter(logging.Formatter):
75class ConsoleFormatter(logging.Formatter):
76    """Log formatter that uses terminal color codes."""
77
78    def colorfmt(self, code):
79        return (
80            f"\033[{code}m%(levelname)s [%(asctime)s]\033[m"
81            + " \033[1m%(name)s:\033[m %(message)s"
82        )
83
84    def format(self, record):
85        format_specs = {
86            logging.CRITICAL: self.colorfmt("1;31"),
87            logging.ERROR: self.colorfmt("31"),
88            logging.INFO: self.colorfmt("32"),
89            logging.WARNING: self.colorfmt("33"),
90            logging.DEBUG: self.colorfmt("34"),
91        }
92        self._style._fmt = format_specs.get(record.levelno)
93        return super().format(record)

Log formatter that uses terminal color codes.

def colorfmt(self, code):
78    def colorfmt(self, code):
79        return (
80            f"\033[{code}m%(levelname)s [%(asctime)s]\033[m"
81            + " \033[1m%(name)s:\033[m %(message)s"
82        )
def format(self, record):
84    def format(self, record):
85        format_specs = {
86            logging.CRITICAL: self.colorfmt("1;31"),
87            logging.ERROR: self.colorfmt("31"),
88            logging.INFO: self.colorfmt("32"),
89            logging.WARNING: self.colorfmt("33"),
90            logging.DEBUG: self.colorfmt("34"),
91        }
92        self._style._fmt = format_specs.get(record.levelno)
93        return super().format(record)

Format the specified record as text.

The record's attribute dictionary is used as the operand to a string formatting operation which yields the returned string. Before formatting the dictionary, a couple of preparatory steps are carried out. The message attribute of the record is computed using LogRecord.getMessage(). If the formatting string uses the time (as determined by a call to usesTime(), formatTime() is called to format the event time. If there is exception information, it is formatted using formatException() and appended to the message.

Inherited Members
logging.Formatter
Formatter
converter
datefmt
default_time_format
default_msec_format
formatTime
formatException
usesTime
formatMessage
formatStack
LOGGER = <Logger pydrex (DEBUG)>
CONSOLE_LOGGER = <StreamHandler (INFO)>
def handle_exception(exec_type, exec_value, exec_traceback):
108def handle_exception(exec_type, exec_value, exec_traceback):
109    # Ignore KeyboardInterrupt so ^C (ctrl + C) works as expected.
110    if issubclass(exec_type, KeyboardInterrupt):
111        sys.__excepthook__(exec_type, exec_value, exec_traceback)
112        return
113    # Send other exceptions to the logger.
114    LOGGER.exception(
115        "uncaught exception", exc_info=(exec_type, exec_value, exec_traceback)
116    )
@cl.contextmanager
def handler_level(level: str, handler: logging.Handler = <StreamHandler (INFO)>):
123@cl.contextmanager
124def handler_level(level: str, handler: logging.Handler = CONSOLE_LOGGER):
125    """Set logging handler level for current context.
126
127    - `level` — logging level name e.g. "DEBUG", "ERROR", etc. See Python's logging
128      module for details.
129    - `handler` (optional) — alternative handler to control instead of the default,
130      `CONSOLE_LOGGER`.
131
132    """
133    default_level = handler.level
134    handler.setLevel(level)
135    yield
136    handler.setLevel(default_level)

Set logging handler level for current context.

  • level — logging level name e.g. "DEBUG", "ERROR", etc. See Python's logging module for details.
  • handler (optional) — alternative handler to control instead of the default, CONSOLE_LOGGER.
def critical(msg, *args, **kwargs):
139def critical(msg, *args, **kwargs):
140    """Log a CRITICAL message in PyDRex."""
141    LOGGER.critical(msg, *args, **kwargs)

Log a CRITICAL message in PyDRex.

def error(msg, *args, **kwargs):
144def error(msg, *args, **kwargs):
145    """Log an ERROR message in PyDRex."""
146    LOGGER.error(msg, *args, **kwargs)

Log an ERROR message in PyDRex.

def warning(msg, *args, **kwargs):
149def warning(msg, *args, **kwargs):
150    """Log a WARNING message in PyDRex."""
151    LOGGER.warning(msg, *args, **kwargs)

Log a WARNING message in PyDRex.

def info(msg, *args, **kwargs):
154def info(msg, *args, **kwargs):
155    """Log an INFO message in PyDRex."""
156    LOGGER.info(msg, *args, **kwargs)

Log an INFO message in PyDRex.

def debug(msg, *args, **kwargs):
159def debug(msg, *args, **kwargs):
160    """Log a DEBUG message in PyDRex."""
161    LOGGER.debug(msg, *args, **kwargs)

Log a DEBUG message in PyDRex.

def exception(msg, *args, **kwargs):
164def exception(msg, *args, **kwargs):
165    """Log a message with level ERROR but retain exception information.
166
167    This function should only be called from an exception handler.
168
169    """
170    LOGGER.exception(msg, *args, **kwargs)

Log a message with level ERROR but retain exception information.

This function should only be called from an exception handler.

def quiet_aliens():
173def quiet_aliens():
174    """Restrict alien loggers 👽 because I'm trying to find MY bugs, thanks."""
175    # Only allow warnings or above from root logger.
176    logging.getLogger().setLevel(logging.WARNING)
177    # Only allow critical stuff from other things.
178    for name in logging.Logger.manager.loggerDict.keys():
179        if name != "pydrex":
180            logging.getLogger(name).setLevel(logging.CRITICAL)
181    # Numba is not in the list for some reason, I guess we can leave warnings.
182    logging.getLogger("numba").setLevel(logging.WARNING)

Restrict alien loggers 👽 because I'm trying to find MY bugs, thanks.