# -*- coding: utf-8 -*-
"""Logging System
====================
.. module:: pcapkit.utilities.logging
:mod:`pcapkit.utilities.logging` integrates :mod:`pcapkit` with the standard
:mod:`logging` system. It owns the package-wide logger hierarchy rooted at
:data:`~pcapkit.utilities.logging.logger` (the logger named ``pcapkit``), and
the :func:`~pcapkit.utilities.logging.configure` and
:func:`~pcapkit.utilities.logging.reset` pair through which an application
decides what, if anything, :mod:`pcapkit` emits.
:mod:`pcapkit` is a library, so importing it configures **no** logging output:
the only handler attached to :data:`logger` is a :class:`logging.NullHandler`,
and no level is set, which leaves both the destination and the verbosity to the
embedding application. Every module logs through its own child logger, obtained
via :func:`get_logger` with ``__name__``, so that a single subtree may be
silenced or amplified independently::
import logging
logging.getLogger('pcapkit.foundation.registry').setLevel(logging.WARNING)
For applications that do not configure :mod:`logging` themselves,
:func:`configure` attaches a handler of :mod:`pcapkit`'s own::
import sys
from pcapkit.utilities.logging import configure
configure('DEBUG', stream=sys.stderr)
and :func:`reset` puts everything back the way importing :mod:`pcapkit` left it.
.. seealso::
:envvar:`PCAPKIT_DEVMODE` still bootstraps the historical :obj:`sys.stderr`
handler at :data:`logging.DEBUG`, so a development session needs no explicit
:func:`configure` call.
"""
import logging
import os
import sys
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import IO, Optional, Union
__all__ = [
'logger', 'get_logger', 'configure', 'reset', 'ensure_output',
'ROOT_LOGGER_NAME', 'DEFAULT_FORMAT', 'DEFAULT_DATE_FORMAT',
]
###############################################################################
# Dev Mode
###############################################################################
# boolean mappings
BOOLEAN_STATES = {'1': True, '0': False,
'yes': True, 'no': False,
'true': True, 'false': False,
'on': True, 'off': False}
#: bool: Development mode flag.
DEVMODE = BOOLEAN_STATES.get(os.environ.get('PCAPKIT_DEVMODE', 'false').casefold(), False)
#: bool: Verbose output flag.
VERBOSE = BOOLEAN_STATES.get(os.environ.get('PCAPKIT_VERBOSE', 'false').casefold(), False)
###############################################################################
# Sphinx Mode
###############################################################################
#: bool: This is a workaround for :data:`typing.TYPE_CHECKING` in Sphinx.
SPHINX_TYPE_CHECKING = BOOLEAN_STATES.get(os.environ.get('PCAPKIT_SPHINX', 'false').casefold(), False)
###############################################################################
# Logger Setup
###############################################################################
#: str: Name of the logger at the root of :mod:`pcapkit`'s hierarchy.
ROOT_LOGGER_NAME = 'pcapkit'
#: str: Default :class:`logging.Formatter` format string.
DEFAULT_FORMAT = '[%(levelname)s] %(asctime)s - %(message)s'
#: str: Default :class:`logging.Formatter` date format string.
DEFAULT_DATE_FORMAT = '%m/%d/%Y %I:%M:%S %p'
#: logging.Logger: :class:`~logging.Logger` instance named after ``pcapkit``,
#: at the root of the package's logger hierarchy. Per-module loggers are its
#: children, so configuring this one configures all of :mod:`pcapkit`.
logger = logging.getLogger(ROOT_LOGGER_NAME)
#: logging.Formatter: Default formatter, used by any handler that
#: :func:`configure` creates and by the :envvar:`PCAPKIT_DEVMODE` bootstrap.
formatter = logging.Formatter(fmt=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT)
#: logging.StreamHandler: The historical :obj:`sys.stderr` handler. It is only
#: *attached* under :envvar:`PCAPKIT_DEVMODE`; it is constructed unconditionally
#: so that ``logger.addHandler(handler)`` remains a one-line way back to the
#: pre-1.4 default output.
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(formatter)
[docs]
def get_logger(name: 'Optional[str]' = None) -> 'logging.Logger':
"""Retrieve a logger inside :mod:`pcapkit`'s hierarchy.
This is the accessor every module in :mod:`pcapkit` uses, as
``logger = get_logger(__name__)``, so that records carry the emitting
module's name and an application can address one subtree at a time.
Args:
name: Dotted logger name, normally the caller's :data:`__name__`.
:data:`None` or ``'pcapkit'`` yields the root :data:`logger`.
A name outside the ``pcapkit`` hierarchy -- notably ``'__main__'``,
which is what :data:`__name__` reports for a module run as a
script -- is placed *under* the root rather than beside it, since a
sibling of ``pcapkit`` would escape every :mod:`pcapkit`-level
configuration.
Returns:
The requested logger.
"""
if not name or name == ROOT_LOGGER_NAME:
return logger
if name.startswith(f'{ROOT_LOGGER_NAME}.'):
return logging.getLogger(name)
return logger.getChild(name)
def _detach(target: 'logging.Logger') -> 'None':
"""Remove every handler attached to ``target``.
Handlers are detached but deliberately never closed: a handler reached here
was either built from a stream the caller still owns, or handed to
:func:`configure` ready-made, and closing someone else's file or socket on
their behalf is not this module's business. Whoever created a handler closes
it.
Args:
target: Logger to strip.
"""
for entry in list(target.handlers):
target.removeHandler(entry)
def _has_output(target: 'logging.Logger') -> 'bool':
"""Whether a record from ``target`` would reach a real handler.
Walks the ancestry the way :meth:`logging.Logger.callHandlers` does,
stopping where propagation stops, and ignores
:class:`~logging.NullHandler` since its whole purpose is to swallow records.
Args:
target: Logger to inspect.
Returns:
:data:`True` if some handler would receive the record.
"""
current = target # type: Optional[logging.Logger]
while current is not None:
for entry in current.handlers:
if not isinstance(entry, logging.NullHandler):
return True
if not current.propagate:
break
current = current.parent
return False
[docs]
def ensure_output(level: 'Union[int, str]' = logging.DEBUG, *,
stream: 'Optional[IO[str]]' = None) -> 'bool':
"""Guarantee that :mod:`pcapkit`'s records have somewhere to go.
This is for a caller that has switched something on precisely *because* it
wants to see the output, and for which staying silent merely because the
application never configured :mod:`logging` would be unhelpful.
Note that ``Extractor(verbose=True)`` and the CLI's ``-v`` do **not** go
through here: their frame chains are user-facing output and are written to
:data:`sys.stdout` with :func:`print`, so they are visible with no logging
configuration at all. Nothing in :mod:`pcapkit` calls this function itself;
it exists for consumers.
An application that has configured its own handlers has already answered the
question, so nothing is changed in that case.
Args:
level: Level to configure if, and only if, a handler has to be added.
stream: Where to write, defaulting to :obj:`sys.stderr`.
Returns:
:data:`True` if a handler was added, :data:`False` if output was already
going somewhere and the existing configuration was left alone.
"""
if _has_output(logger):
return False
configure(level, stream=stream or sys.stderr, replace=False)
return True
[docs]
def reset(name: 'Optional[str]' = None) -> 'logging.Logger':
"""Restore a :mod:`pcapkit` logger to its pristine, library-neutral state.
That is: no handlers other than the :class:`logging.NullHandler` on the
root, no level of its own (:data:`logging.NOTSET`, so the level is inherited
from the application's configuration), and propagation enabled.
Args:
name: Logger to reset, as accepted by :func:`get_logger`. Defaults to
the root :data:`logger`, which also resets nothing else -- children
keep any level explicitly set on them.
Returns:
The logger that was reset.
Note:
This discards the :envvar:`PCAPKIT_DEVMODE` bootstrap along with
everything else. To reinstate it, call
``configure(logging.DEBUG, stream=sys.stderr)``.
"""
target = get_logger(name)
_detach(target)
target.setLevel(logging.NOTSET)
target.propagate = True
if target is logger:
# the null handler is what keeps a library quiet rather than noisy:
# without it, ``logging`` prints its own "no handlers could be found"
# complaint the first time an unconfigured application triggers a record
target.addHandler(logging.NullHandler())
return target
# NOTE: Import must not disturb configuration the application has already
# made -- that is the whole point of the NullHandler convention -- so this only
# guarantees the logger has *a* handler, and leaves level and propagation
# alone. Calling reset() here instead would detach a host application's
# handlers merely because it imported pcapkit after configuring logging.
#
# Re-execution has to be idempotent too, which the test suite relies on when it
# exercises import-time behaviour. That rules out an identity test for the
# devmode handler: ``handler`` is built when this module runs, so re-executing
# it yields a *new* object every time and ``handler not in logger.handlers``
# would be true on each pass, stacking one stderr handler per reload. The test
# is therefore on what would actually duplicate -- a stream handler already
# writing to the same stream.
if not logger.handlers:
logger.addHandler(logging.NullHandler())
def _writes_to(candidate: 'logging.Handler', stream: 'Any') -> 'bool':
"""Whether ``candidate`` is a stream handler already writing to ``stream``."""
return (
isinstance(candidate, logging.StreamHandler) and
getattr(candidate, 'stream', None) is stream
)
if DEVMODE:
# development mode keeps the historical behaviour: everything, on stderr
logger.setLevel(logging.DEBUG)
if not any(_writes_to(installed, handler.stream) for installed in logger.handlers):
logger.addHandler(handler)