Conditionally Logging Expensive Tasks
Sat 13 June 2020 by Moshe Zadka(I have shown this technique in my mailing list. If this kind of thing seems interesting, why not subscribe?)
Imagine you want to log something that is,
potentially,
expensive to calculate.
For example,
in DEBUG mode,
you would like to count the classes of the objects in
gc.get_objects()
and log those counts:
this is often a useful technique for diagnosing reference leaks.
This is pretty heavy to calculate,
and logging it always sounds wasteful.
The logging module has lazy interpolation.
A line like
logging.debug("total number: %r", 2500)
will not bother calculating the string interpolation total number: 2500 unless the logging level is set to output it.
However,
calling
logging.debug("total number: %r", get_object_counts())
will still call
get_object_counts()
regardless.
It is not the cost of calculating the interpolation,
but that of calculating the counts themselves
that you would like to avoid.
One way to piggy-back on the lazy interpolation to do lazy evaluation
is to write a custom class with a lazy
__repr__()
:
class OnDemand: def __init__(self, callable): self.callable = callable def __repr__(self): return repr(self.callable())
This defines a class,
OnDemand
,
which will only call the function when it needs its
repr.
This allows us to write code like:
logging.debug("total number: %r", OnDemand(get_object_counts))`
Now,
get_object_counts()
will not be called at all:
notice that
the logging line does not call it.
This makes the lazy evaluation explicit:
we explicitly delay evaluation with
OnDemand
.
Here is a simple example of how it would work:
>>> def get_object_counts(): ... print("doing something expensive") ... return 5 ... >>> debug_logging.debug("result is %r", OnDemand(get_object_counts)) doing something expensive DEBUG:debug:result is 5 >>> warnonly_logging.debug("result is %r", OnDemand(get_object_counts)) >>> warnonly_logging.debug("result is %r", get_object_counts()) doing something expensive
Note that when the
warnonly_logging
is called with a
.debug()
method,
the expensive calculation is not done:
not only is the log message ignored,
but the function that calculates the value is not done.
The last line shows that without the careful usage of
OnDemand
,
the expensive calculation is done.
This should make it easy to sprinkle heavy calculations in
logging.debug
statements.
Coupled with easy ways to trigger logging level changes
(which are beyond the scope of this article)
this gives a powerful way to get insight into your program's innards.
(Thanks to Adi Stav, Avy Faingezicht, Chris Withers, Dave Briccetti, and Lucas Wiman on their feedback on an early draft of this article. Any mistakes or issues that remain are my responsibility.)