Note: this was written before support for multiple handlers per logger was added to CircuitPython logging.
In CPython, there is a hierarchy of loggers.
Each logger except the root logger has a parent. When a log record is handled for particular logger,
the hierarchy is traversed until either the propagate
flag of given logger is found to be False
,
or root logger is encountered.
During this traversal, for each logger, the log record is passed to all its handlers.
This schema is useful for module hierarchy, where a function in given module calls:
logger = logging.getLogger(__name__)
where __name__
can be something in the form of a.b.c
,
which makes it possible to set loggers for various points in the module hierarchy.
CircuitPython does not have this hierarchy.
When getLogger()
is called with no arguments, the logger name in CircuitPython will be empty string,
while in CPython it will be root
.
In CircuitPython, getLogger()
automatically assigns the newly created logger the StreamHandler
handler.
In CPython, the created logger does not have any handler. There is a concept of global last resort handler,
that is set to _StderrHandler(WARNING)
. If there is no handler found after traversing the hierarchy of loggers,
this last resort handler is used. If the last resort handler is disabled, there are no handlers to be used,
and a warning message is emitted to stderr
:
> import logging
> logger = logging.getLogger("foo")
> logger.hasHandlers()
False
> logger.handlers
[]
> logger.parent
<RootLogger root (WARNING)>
> logger.parent.handlers
[]
> logger.warning("foo")
foo
> logging.lastResort
<_StderrHandler <stderr> (WARNING)>
> logging.lastResort = None
> logger.warning("foo")
No handlers could be found for logger "foo"
Note: this 'No handlers could be found...' message is emitted just once.
Currently in CircuitPython it is not possible to remove the handler from the logger
(at least using the public functions of the Logger
class), so no special handling
of non-existing handler is necessary.
CPython logging has a concept of per handler filters, that allow to perform matching on log records.
If the record is not matched, it will not be passed to a logger. This filtering action
happens in the handle()
function before the emit()
function,
that performs the action in given handler, is called.
CircuitPython logging lacks the concept of filters, hence emit()
is used directly.
In fact, the Handler
class in CPython extends the Filterer
class. In CicruitPython
the Handler
class is standalone.
In CPython, the logging related objects are released upon logging.shutdown()
. It also uses weak references.
CircuitPython logging has no concept of object lifecycle.
Log handlers CPython have a log level that is checked in handle()
. When logging a message, 2 level checks are performed - once for the logger level and another one for each handler in that logger.
In CicruitPython logging, the handle()
function is not part of the Handler
API and the handlers do not have log levels.
In CPython, Handler
subclasses should call handleError()
defined in the base class, if they hit an exception
during emit()
. The sub-classes can obvisouly override the handleError()
function. By default it will print some
detailed information about the exception (by walking the exception stack) to stderr
. In some rare circumstanes it will
rethrow the exception (recursion). There is a global tunable raiseExceptions
that controls the behavior of exception
handling; also it is checked by handleError()
, however there it merely controls whether to print the warning or not
(with the recursion corner case caveat). The default value of the tunable is True
, which in the case of emit()
means
that the warning will be printed. Most of the handlers in logging.handlers
heed the recommendation of calling
handleError
on exception in emit()
.
CircuitPython logging does not have this sort of support for handlers, even though individual handlers can surely implement exception handling of their own.