Asyncio/Trio Coroutine Support

As of Eliot 1.8, asyncio and trio coroutines have appropriate context propogation for Eliot, automatically.

Asyncio

On Python 3.7 or later, no particular care is needed. For Python 3.5 and 3.6 you will need to import either eliot (or the backport package aiocontextvars) before you create your first event loop.

Here’s an example using aiohttp:

import asyncio
import aiohttp
from eliot import start_action, to_file
to_file(open("linkcheck.log", "w"))


async def check_links(urls):
    session = aiohttp.ClientSession()
    with start_action(action_type="check_links", urls=urls):
        for url in urls:
            try:
                with start_action(action_type="download", url=url):
                    async with session.get(url) as response:
                        response.raise_for_status()
            except Exception as e:
                raise ValueError(str(e))

try:
    loop = asyncio.get_event_loop()
    loop.run_until_complete(
        check_links(["http://eliot.readthedocs.io", "http://nosuchurl"])
    )
except ValueError:
    print("Not all links were valid.")

And the resulting logs:

$ eliot-tree linkcheck.log
0a9a5e1b-330c-4251-b7db-fd3161403443
└── check_links/1 ⇒ started 2019-04-06 19:49:16 ⧖ 0.535s
    ├── urls:
    │   ├── 0: http://eliot.readthedocs.io
    │   └── 1: http://nosuchurl
    ├── download/2/1 ⇒ started 2019-04-06 19:49:16 ⧖ 0.527s
    │   ├── url: http://eliot.readthedocs.io
    │   └── download/2/2 ⇒ succeeded 2019-04-06 19:49:16
    ├── download/3/1 ⇒ started 2019-04-06 19:49:16 ⧖ 0.007s
    │   ├── url: http://nosuchurl
    │   └── download/3/2 ⇒ failed 2019-04-06 19:49:16
    │       ├── errno: -2
    │       ├── exception: aiohttp.client_exceptions.ClientConnectorError
    │       └── reason: Cannot connect to host nosuchurl:80 ssl:None [Name or service not known]
    └── check_links/4 ⇒ failed 2019-04-06 19:49:16
        ├── exception: builtins.ValueError
        └── reason: Cannot connect to host nosuchurl:80 ssl:None [Name or service not known]

Trio

Here’s an example of using Trio—we put the action outside the nursery so that it finishes only when the nursery shuts down.

from eliot import start_action, to_file
import trio

to_file(open("trio.log", "w"))


async def say(message, delay):
    with start_action(action_type="say", message=message):
        await trio.sleep(delay)

async def main():
    with start_action(action_type="main"):
        async with trio.open_nursery() as nursery:
            nursery.start_soon(say, "hello", 1)
            nursery.start_soon(say, "world", 2)

trio.run(main)

And the resulting logs:

$ eliot-tree trio.log
93a4de27-8c95-498b-a188-f0e91482ad10
└── main/1 ⇒ started 2019-04-10 21:07:20 ⧖ 2.003s
    ├── say/2/1 ⇒ started 2019-04-10 21:07:20 ⧖ 2.002s
    │   ├── message: world
    │   └── say/2/2 ⇒ succeeded 2019-04-10 21:07:22
    ├── say/3/1 ⇒ started 2019-04-10 21:07:20 ⧖ 1.001s
    │   ├── message: hello
    │   └── say/3/2 ⇒ succeeded 2019-04-10 21:07:21
    └── main/4 ⇒ succeeded 2019-04-10 21:07:22

If you put the start_action inside the nursery context manager:

  1. The two say calls will be scheduled, but not started.
  2. The parent action will end.
  3. Only then will the child actions be created.

The result is somewhat confusing output. Trying to improve this situation is covered in issue #401.