Why Eliot?

Suppose we turn from outside estimates of a man, to wonder, with keener interest, what is the report of his own consciousness about his doings or capacity: with what hindrances he is carrying on his daily labors; what fading of hopes, or what deeper fixity of self-delusion the years are marking off within him; and with what spirit he wrestles against universal pressure, which will one day be too heavy for him, and bring his heart to its final pause.

— George Eliot, Middlemarch

The log messages generated by a piece of software ought tell a story: what, where, when, even why and how if you’re lucky. But most logging systems omit the all-important why. You know that some things happened, but not how they relate to each other.

The problem: What caused this to happen?

Most log messages in your program are going to involve actions:

Not long after that dinner-party she had become Mrs. Casaubon, and was on her way to Rome.

A marriage has a beginning and eventually an end. The end may be successful, presuming “until death do us part” is a form of success, or a failure. The same is true of all actions, much like function calls in Python are started and eventually return a result or throw an exception. Actions may of course span multiple function calls or extended periods of time.

Actions also generate other actions: a marriage leads to a trip to Rome, the trip to Rome might lead to a visit to the Vatican Museum, and so on. Other unrelated actions are occurring at the same time, resulting in a forest of actions, with root actions that grow a tree of child actions.

You might want to trace an action from beginning to end, e.g. to measure how long it took to run. You might want to know what high-level action caused a particular unexpected low-level action. You might want to know what actions a specific entity was involved with.

None of these are possible in most logging systems since they have no concept of actions.

The solution: Eliot

Eliot is designed to solve these problems: the basic logging abstraction is the action.

An “action” is something with a start and an end; the end can be successful or it can fail due to an exception. Log messages, as well as log actions, know the log action whose context they are running in. The result is a tree of actions.

In the following example we have one top-level action (the honeymoon), which leads to other action (travel):

from sys import stdout
from eliot import start_action, to_file
to_file(stdout)


class Place(object):
    def __init__(self, name, contained=()):
        self.name = name
        self.contained = contained

    def visited(self, people):
        # No need to repetitively log people, since caller will:
        with start_action(action_type="visited", place=self.name):
            for thing in self.contained:
                thing.visited(people)


def honeymoon(family, destination):
    with start_action(action_type="honeymoon", people=family):
        destination.visited(family)


honeymoon(["Mrs. Casaubon", "Mr. Casaubon"],
          Place("Rome, Italy",
                [Place("Vatican Museum",
                       [Place("Statue #1"), Place("Statue #2")])]))

Actions provide a Python context manager. When the action starts, a start message is logged. If the block finishes successfully a success message is logged for the action; if an exception is thrown a failure message is logged for the action with the exception type and contents.

By default the messages are machine-parseable JSON, but for human consumption a visualization is better. Here’s how the log messages generated by the new code look, as summarized by the eliot-tree tool:

f9dcc74f-ecda-4543-9e9a-1bb062d199f0
+-- honeymoon@1/started
    |-- people: ['Mrs. Casaubon', 'Mr. Casaubon']
    +-- visited@2,1/started
        |-- place: Rome, Italy
        +-- visited@2,2,1/started
            |-- place: Vatican Museum
            +-- visited@2,2,2,1/started
                |-- place: Statue #1
                +-- visited@2,2,2,2/succeeded
            +-- visited@2,2,3,1/started
                |-- place: Statue #2
                +-- visited@2,2,3,2/succeeded
            +-- visited@2,2,4/succeeded
        +-- visited@2,3/succeeded
    +-- honeymoon@3/succeeded

No longer isolated fragments of meaning, our log messages are now a story. Log events have context, you can tell where they came from and what they led to without guesswork.

Was looking at a statue the result of the honeymoon? It most definitely was.