# Licensed under a 3-clause BSD style license - see LICENSE.rst
# -*- coding: utf-8 -*-
"""
============
desiutil.log
============
DESI-specific utility functions that wrap the standard :mod:`logging`
module.
This module is intended to support three different logging use patterns:
1. Just get an easy-to-use, pre-configured logging object.
2. Easily change the log level temporarily within a function. This is
provided by a context manager.
3. Change the default log level on the command-line. This can actually be
accomplished in two ways: the command-line interpreter can call
:func:`~desiutil.log.get_logger` with the appropriate level, or
the environment variable :envvar:`DESI_LOGLEVEL` can be set.
In addition, it is possible to add timestamps and change the delimiter of
log messages as needed. See the optional arguments to
:func:`~desiutil.log.get_logger`.
Examples
--------
Simplest possible use:
>>> from desiutil.log import log
>>> log.info('This is some information.')
This is exactly equivalent to:
>>> from desiutil.log import get_logger
>>> log = get_logger()
>>> log.info('This is some information.')
Temporarily change the log level with a context manager:
>>> from desiutil.log import get_logger, DesiLogContext, DEBUG
>>> log = get_logger() # defaults to INFO
>>> log.info('This is some information.')
>>> log.debug("This won't be logged.")
>>> with DesiLogContext(log, DEBUG):
... log.debug("This will be logged.")
>>> log.debug("This won't be logged.")
Create the logger with a different log level:
>>> from desiutil.log import get_logger, DEBUG
>>> if options.debug:
... log = get_logger(DEBUG)
>>> else:
... log = get_logger()
"""
import os
import sys
import logging
from warnings import warn
_desiutil_log_root = dict()
_good_levels = {'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARNING': logging.WARNING,
'ERROR': logging.ERROR,
'CRITICAL': logging.CRITICAL,
logging.DEBUG: logging.DEBUG,
logging.INFO: logging.INFO,
logging.WARNING: logging.WARNING,
logging.ERROR: logging.ERROR,
logging.CRITICAL: logging.CRITICAL,
}
_level_children = {logging.DEBUG: 'debug',
logging.INFO: 'info',
logging.WARNING: 'warning',
logging.ERROR: 'error',
logging.CRITICAL: 'critical',
}
# Just for convenience to avoid importing logging, we duplicate the logging levels
# Detailed information, typically of interest only when diagnosing problems.
DEBUG = logging.DEBUG
# Confirmation that things are working as expected.
INFO = logging.INFO
# An indication that something unexpected happened, or indicative of some problem
# in the near future (e.g. "disk space low"). The software is still working as expected.
WARNING = logging.WARNING
# Due to a more serious problem, the software has not been able to perform some function.
ERROR = logging.ERROR
# A serious error, indicating that the program itself may be unable to continue running.
CRITICAL = logging.CRITICAL
# see example of usage in test/test_log.py
[docs]class DesiLogWarning(UserWarning):
"""Warnings related to misconfiguration of the DESI logging object.
"""
pass
[docs]class DesiLogContext(object):
"""Provides a context manager to temporarily change the log level of
an existing logging object.
Parameters
----------
logger : :class:`logging.Logger`
Logging object.
level : :class:`int`, optional
The logging level to set. If it is not set, this whole class
does nothing.
"""
def __init__(self, logger, level=None): # , handler=None, close=True):
self.logger = logger
self.level = level
# self.handler = handler
# self.close = close
def __enter__(self):
if self.level is None:
warn("This context manager will not actually do anything!",
DesiLogWarning)
else:
self.old_level = self.logger.level
self.logger.setLevel(self.level)
# if self.handler:
# self.logger.addHandler(self.handler)
def __exit__(self, et, ev, tb):
if self.level is not None:
self.logger.setLevel(self.old_level)
# if self.handler:
# self.logger.removeHandler(self.handler)
# if self.handler and self.close:
# self.handler.close()
[docs]def get_logger(level=None, timestamp=False, delimiter=':'):
"""Returns a default DESI logger.
Parameters
----------
level : :class:`int` or :class:`str`, optional
Set the logging level (default ``INFO``).
timestamp : :class:`bool`, optional
If ``True``, add a timestamp to the log message.
delimiter : :class:`str`, optional
Use `delimiter` to separate fields in the log messages (default ``:``).
Returns
-------
:class:`logging.Logger`
A logging object configured with the DESI defaults.
Notes
-----
* If `level` is not ``None``, that sets the log level, overriding anything
else.
* If `level` is not set, and if the environment variable
:envvar:`DESI_LOGLEVEL` exists and has value
DEBUG, INFO, WARNING, ERROR or CRITICAL (upper or lower case),
that is used to set the log level.
* If :envvar:`DESI_LOGLEVEL` is not set and `level` is ``None``,
the default level is set to INFO.
"""
root_name = _configure_root_logger(timestamp=timestamp, delimiter=delimiter)
if level is None:
try:
ul = os.environ["DESI_LOGLEVEL"].upper()
except KeyError:
ul = logging.INFO
else:
try:
ul = level.upper()
except AttributeError:
# level should be an integer in this case.
ul = level
try:
gl = _good_levels[ul]
except KeyError:
message = "Invalid level='{0}' ignored. Setting INFO.".format(str(ul))
warn(message, DesiLogWarning)
gl = logging.INFO
log = logging.getLogger(root_name + '.' + _level_children[gl])
log.setLevel(gl)
return log
log = get_logger()