Overview of Logging¶
Logging is an important part of a program’s internal operations, an essential
tool for development, debugging, troubleshooting, performance-tuning and
general maintenance. A program logs messages in order to record
its successive states, and to report any anomalies, unexpected situations or
errors, together with enough context to aid diagnosis. Messages can be logged
to multiple destinations at once — stderr
in a terminal, a local file,
the system log, email, or a Unix log server over TCP, to cite common choices.
At the end of this chapter we provide several logging documentation links, for reference and general culture. It’s not our purpose to rehash or repeat the extensive (and generally quite good) documentation for Python’s logging package; in fact, we presuppose that you’re familiar with basic concepts and standard use cases. Nevertheless, it will be helpful to review several topics, and in the process clarify some obscure features of logging.
Using logging¶
A program logs messages using the log
method of objects called loggers,
which are implemented in logging by the Logger
class. You can think of
the log
method as a pumped-up print
statement. It writes a message,
tagged with a level of severity, to zero or more destinations.
In logging, a handler — a Handler
object — represents a single
destination, together with a specified output format.
A handler implements abstract methods which format message data into structured
text and write or transmit that text to the output.
A logger contains zero or more handlers.
When a program logs a message by calling a logger’s log
method (or a
shorthand method such as debug
or warning
), the logger dispatches the
message data to its handlers.
All messages have a logging level, or loglevel, indicating their severity
or importance. The predefined levels in logging
are DEBUG
, INFO
,
WARNING
, ERROR
, CRITICAL
, listed in order of increasing severity.
Both loggers and handlers have an associated loglevel, indicating a
severity threshold: a logger or a handler will filter out any message whose
loglevel is less than its own. In order for a message to actually be sent
to a particular destination, its loglevel must equal or exceed the loglevels
of both the logger and the handler representing the destination.
Note: This last statement is basically true, but glosses over details. We’ll sharpen it below, in the subsection How a message is logged.
This elegant system allows developers to easily dial in different amounts
of logging verbosity. When developing a module or package, you can use a
dedicated logger to log internal messages at thoughtfully chosen loglevels.
In development, set the logger’s loglevel to DEBUG
or INFO
as needed;
once the module/package is in good condition, raise that to WARNING
; in
production, use ERROR
. There’s no need to delete or comment out the lines
of code that log messages, or to precede each such block with a conditional guard.
The logging facility is a very sophisticated version of using the print
statement for debugging.
logging classes that can be configured¶
logging defines a few types of entities, culminating in the Logger
class. Typically, a program or library will set up, or configure, logging
only once, at startup. This entails specifying message formats, destinations,
loggers, and containment relations between those things. Once a program has
configured logging as desired, use of loggers is very straightforward.
Configuration, then, is the only barrier to entry.
The following diagram displays the types that can be configured statically, and their dependencies:
Symbol | Meaning |
---|---|
has zero or more | |
m: 0/1 | many-to-(zero-or-one) |
m: n | many-to-many |
In words:
- a
Logger
can have one or moreHandler
s, and aHandler
can be used by multipleLogger
s;- a
Handler
has at most oneFormatter
, but aFormatter
can be shared by multipleHandler
s;Handler
s andLogger
s can each have zero or moreFilter
s; aFilter
can be used by multipleHandler
s and/orLogger
s.
What these objects do¶
A Formatter
is basically just a format string that uses keywords
defined by the logging module — for example, '%(message)s'
and
'%(name)-20s: %(levelname)-8s: %(message)s'
.
A Handler
formats and writes logged messages to a particular
destination — a stream (e.g. sys.stderr
, sys.stdout
, or an in-memory
stream such as an io.StringIO
), a file, a rotating set of files, a socket,
etc.
A Logger
sends logged messages to its associated handlers. Various
criteria filter out which messages are actually written, notably loglevel
thresholding as described above and in greater detail below.
Filter
s provide still more fine-grained control over which messages are
written. They can also be used to modify messages or supplement them with
additional context.
Loggers are identified by name¶
A logger is uniquely identified by name (except for the name 'root'
: see the
Warning below). For example, the expression logging.getLogger('mylogger')
always denotes the same object, no matter where in a program it occurs or when
it’s evaluated. The logging package always creates a special logger, the root
logger, which we, as users of logging, identify by the name ''
(the
empty string); it’s accessed by the expression logging.getLogger('')
, or
equivalently by logging.getLogger()
.
Warning
- The root logger’s name is set to, and reported as,
'root'
: >>> logging.getLogger('').name 'root'
- Confusingly, however, you cannot access the root logger by that name:
>>> logging.getLogger('') is logging.getLogger('root') False
- It’s most unfortunate that these two distinct loggers share the same name:
>>> logging.getLogger('root').name 'root'
Do not use the logger name 'root'
.
Logger names are dotted names, and behave in a way that’s analogous to package
and module names. The analogy is intentional, to facilitate a style of logging
in which each package, and/or each module, uses its own logger, with names
__package__
and __name__
respectively. The basic idioms are, for example:
logging.getLogger(__name__).debug("About to do that thing")
and:
logging.getLogger(__package__).warning("dict of defaults is empty")
Broadly speaking, a logger corresponds to an “area” of your program; you’re free to construe that in whatever way suits your needs and situation.
The parent-child and ancestor relationships between loggers¶
A parent-child relation obtains among loggers: the parent of a logger 'a.b.c'
is the logger 'a.b'
, whose parent is 'a'
; the parent of logger 'a'
is the root logger, identified by ''
. The logger 'a'
is an ancestor of
both 'a.b'
and 'a.b.c'
; 'a.b'
is an ancestor of 'a.b.c'
;
the root logger is an ancestor of every other logger. (Note, though, that aa
is not a parent or ancestor of a
, nor is a.b
a parent or ancestor
of a.bxyz
: the relation isn’t just “startswith” between strings.)
How a message is logged¶
In order to explain what happens when a logger logs a message,
logging.getLogger('L').log(level, message)
we first have to introduce a few more concepts:
- the ‘NOTSET’ loglevel
- the “effective level” of a logger
- the
propagate
flag of a logger.
The special loglevel NOTSET¶
There’s actually a sixth predefined loglevel, NOTSET
, whose numeric
value is 0, lower than the “real” loglevels (DEBUG
= 10, …, CRITICAL
= 50),
which are all non-zero. The root logger by default has loglevel WARNING
, but
all created loggers and handlers have default loglevel NOTSET
.
NOTSET
is useless as a loglevel of individual messages. You can’t successfully
log a message at level NOTSET
— nothing happens (unless you do something unusual.
If you call logging.disable(neg)
with some negative integer neg
, you can get
logger.log(0, message)
to emit message
; but ordinarily, you wouldn’t, and
it won’t.)
A handler with loglevel NOTSET
rejects no messages; it’s the most inclusive
level.
When a logger has loglevel NOTSET
, the loglevels of its ancestors
are examined to compute its effective level — the level that logging uses
to determine whether a message that the logger logs will be sent to handlers or not.
The “effective level” of a logger¶
The effective level of a logger is its own level if that is non-zero;
otherwise, it’s the level of its nearest ancestor whose level is non-zero;
otherwise, if there’s no such ancestor, it’s NOTSET
(0). The Logger
method
getEffectiveLevel()
returns this value. Its docstring explains that it “[loops]
through [the] logger and its parents in the logger hierarchy, looking for a non-zero
logging level[, returning] the first one found.” (getEffectiveLevel()
is in the
__init__.py
module of logging.)
Now we can make good on an earlier promise – the following statement isn’t just “basically true” but really is the case:
In order for a message to actually be written to a particular destination,its level must equal or exceed the effective level of the logger thatlogged it, as well as the level of the handler representing the destination.
In the next subsection we’ll explain just which handlers a message is sent to when its level clears the effective level threshold.
Propagation¶
Loggers have a propagate
attribute, a flag, which by default is True.
propagate
determines which handlers a message
is sent to when a logger
logs it at a particular level
via a call such as logger.log(level, message)
.
If logger
has handlers, the message
is sent to them. If logger
isn’t
the root and logger.propagate
is True, the message is also sent to any
handlers of the logger’s parent; if the parent isn’t the root and its propagate
flag is True, the message is sent to the handlers of the parent’s parent; and so
on, until this process reaches either the root or an ancestor whose propagate
flag is False. The loglevels of ancestor loggers are not consulted when they
are ascended through; the message is sent directly to their handlers.
If no handlers are encountered in this procedure, in Python 3.2+ the message is sent
to the “handler of last resort”, logging.LastResort
, whose loglevel is ‘WARNING’,
and which simply writes the message to stderr
. (In earlier versions of Python,
or if you set logging.LastResort = None
in 3.2+, an error message is written
to stderr
that no handlers could be found for the logger.)
In many cases, to configure logging it’s sufficient just to add a handler or few and attach them to the root.
Note
The logging documentation contains a pair of flowcharts, “Logging flow” and “Handler flow”, which summarize what this section, How a message is logged, has described; however, they seem to predate Python 3.2, so “Handler flow” doesn’t mention the “last resort” handler.
logging defaults¶
logging supplies reasonable out-of-the-box defaults and shorthands so that you can easily start to use its capabilities.
When accessed for the first time, the Logger
named 'mylogger'
is created
“just in time” if it hasn’t been explicitly configured. You don’t have to
attach handlers to 'mylogger'
; logging a message with that logger will “just
work”. Suppose this is a complete program:
import logging
logging.getLogger('mylogger').warning("Uh oh.")
When run, it writes Uh oh.
to stderr
. In light of the last section,
we can now understand why. The effective level of 'mylogger'
is the level of
its parent, the root logger, which is WARNING
, and the level of the message clears
that threshold. Thus, the message is sent to 'mylogger'
’s handlers (none). Because
'mylogger'
has propagate
set to True, the message is also sent to
the handlers of the root. The root has no handlers, so the message is sent to the
last resort handler, whose loglevel is WARNING
, which lets the message through,
writing it to stderr
.
The warning(...)
logger method shown above is a shorthand for
log(logging.WARNING, ...)
. Similarly, there are convenience methods debug
,
info
, error
and critical
.
The logging convenience functions log()
, debug()
, … critical()
have a side-effect¶
logging provides six functions, logging.log()
, logging.debug()
, …
logging.critical()
, which let you instantly use logging out of the box,
with no configuration or even any calls to getLogger
. You can just call:
logging.error("Something went wrong")
and something plausible will happen. This works because logging.error(...)
is (almost always) a shorthand for logging.getLogger().error(...)
.
However, in one circumstance these six functions all have a side effect which can
make them not mere shorthands for expressions that explicitly access the root
logger with getLogger
.
Specifically, if the root logger has no handlers when any of them is called, these
functions call logging.basicConfig()
(with no arguments), which creates a stderr
stream handler that has a formatter with format string
BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s"
and attaches it to the root. One might expect that these functions under such circumstances would use the LastResort handler, as described above; but they don’t.
Consider this complete program:
import logging
from importlib import reload
import sys
# logging.error installs a root handler because none exist,
# so order of these three calls matters.
logging.getLogger('').error('Trouble!')
logging.getLogger('newlogger').critical('Big trouble!')
logging.error('Trouble!')
print('-----------------', file=sys.stderr)
reload(logging)
# Clear everything, and do the three calls again, with logging.error first.
logging.error('Trouble!')
logging.getLogger('newlogger').critical('Big trouble!')
logging.getLogger('').error('Trouble!')
When run, it prints these messages to stderr
:
Trouble!
Big trouble!
ERROR:root:Trouble!
-----------------
ERROR:root:Trouble!
CRITICAL:newlogger:Big trouble!
ERROR:root:Trouble!
The call to logging.error
attaches a new handler to the root. Subsequently,
all loggers that propagate to the root have the format of their messages changed
(albeit for the better).
logging.basicConfig()
The logging.basicConfig() function lets you configure the root logger (up to a point), using a monolithic function that’s somewhat complex yet of limited capabilities. When used to quickly configure logging with a single call, the function can create a stream handler, or a file handler (but not both!), and attaches it to the root.
In the next chapter, we’ll examine the approaches to configuration offered by logging, and then see how prelogging simplifies the process.
logging documentation links¶
See the logging docs for the official explanation of how Python logging works.
For the definitive account of static configuration, see the documentation of logging.config, in particular the documentation for the format of a logging configuration dictionary.
Here’s a useful reference: the complete list of keywords that can appear in formatters.
The logging HOWTO contains tutorials that show typical setups and uses of logging, configured in code at runtime. The logging Cookbook contains a wealth of techniques, several of which exceed the scope of prelogging because they involve logging capabilities that can’t be configured statically (e.g. the use of LoggerAdapters, or QueueListeners ). A few of the examples contained in the prelogging distribution are examples from the Cookbook and HOWTO, reworked to use prelogging.
The logging package supports multithreaded operation, but does not directly support logging to a single file from multiple processes. Happily, prelogging does, in a couple of ways, both illustrated by examples.
One additional resource merits mention: the documentation for logging in Django provides another, excellent overview of logging and configuration, with examples. Its first few sections aren’t at all Django-specific.