Logging

Logging is often an efficient way to start analyzing where an issue comes from. If most actions and errors can be logged easily in the application, then this can become a powerful tool to detect when errors occurred, and understand what happened.

However, for such a mechanism to be really useful, it has to rely on a real logging system, and not only debug traces that must be decommented manually. Fortunately, once again, Python comes with a complete logging system in the standard library. The logging module contains all the features needed to efficiently use logs:

  • Configurable log levels
  • Several logging namespaces
  • The possibility to add new backends to process the logs

So, a good way to help find issues, both during testing and debugging, is to add logs to all important actions and all possible failures. The natural way to implement this is with a logging driver. Such a driver takes an observable of a log request as a sink, and uses the Python logging system to do the actual logging. Other drivers can use this logging driver by exposing another source observable that contains all the log requests. The following figure shows a reactivity diagram of this principle on the transcode driver:

Figure 10.5: Logging via a dedicated driver

The log driver is a sink-only driver: it takes log requests as input, and does not return a source observable. The log driver also accepts configuration items as input: these items configure the level of each logger. The transcode driver now returns two source observables: the existing response observable, and a new log observable.

Using the Python logging module in this way has several benefits:

  • The actual logging is independent of the configuration: each component that has to log some information just emits log requests.
  • There is no overhead if nobody subscribed to the log sources (in this case the component does not emit any log request). This allows us to completely disable logging by not using the log driver.
  • The configuration can be dynamic. Just like the transcode driver configuration is dynamic, it is possible to make the logging configuration dynamic. This allows us to change the debug level on-demand during production.

In practice, a log driver would accept items that look like this:

Log = namedtuple('Log', ['logger', 'level', 'message'])

In the previous example, logger is the name of the logger, level the level of the provided message, and message the information to the log. If components and drivers are written in dedicated modules, as was done in the audio transcode application, then the provided logger name can be just __name__, that is, the name of the module.

Emitting logs in the encode driver requires a few changes. First, a second Source observable is declared, as can be seen in the following example:

Source = namedtuple('Source', ['response', 'log'])

And this new Source is returned with two observables when the driver is called, as can be seen in the following code:

return Source(
response=Observable.create(on_subscribe),
log=Observable.create(on_log_subscribe)
)

The following code demonstrates how the on_log_subscribe function just stores the logs observer:

    def encoder(sink):
log_observer = None

def on_log_subscribe(observer):
nonlocal log_observer
log_observer = observer

Then logs are created in the following way:

if log_observer is not None:
log_observer.on_next(logging.Log(
message="some information",
level=DEBUG,
logger=__name__
))

Not all parameters are needed (level and logger should be optional with default values), and this code block can be wrapped in a helper function so that it fits on one line. With these simple changes, an application can have dynamically configurable logs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.107.25