-
reivilibre authoredreivilibre authored
Log Contexts
To help track the processing of individual requests, synapse uses a
'log context
' to track which request it is handling at any given
moment. This is done via a thread-local variable; a logging.Filter
is
then used to fish the information back out of the thread-local variable
and add it to each log record.
Logcontexts are also used for CPU and database accounting, so that we can track which requests were responsible for high CPU use or database activity.
The synapse.logging.context
module provides facilities for managing
the current log context (as well as providing the LoggingContextFilter
class).
Asynchronous functions make the whole thing complicated, so this document describes how it all works, and how to write code which follows the rules.
In this document, "awaitable" refers to any object which can be await
ed. In the context of
Synapse, that normally means either a coroutine or a Twisted
Deferred
.
Logcontexts without asynchronous code
In the absence of any asynchronous voodoo, things are simple enough. As with any code of this nature, the rule is that our function should leave things as it found them:
from synapse.logging import context # omitted from future snippets
def handle_request(request_id):
request_context = context.LoggingContext()
calling_context = context.set_current_context(request_context)
try:
request_context.request = request_id
do_request_handling()
logger.debug("finished")
finally:
context.set_current_context(calling_context)
def do_request_handling():
logger.debug("phew") # this will be logged against request_id
LoggingContext implements the context management methods, so the above can be written much more succinctly as:
def handle_request(request_id):
with context.LoggingContext() as request_context:
request_context.request = request_id
do_request_handling()
logger.debug("finished")
def do_request_handling():
logger.debug("phew")
Using logcontexts with awaitables
Awaitables break the linear flow of code so that there is no longer a single entry point where we should set the logcontext and a single exit point where we should remove it.
Consider the example above, where do_request_handling
needs to do some
blocking operation, and returns an awaitable:
async def handle_request(request_id):
with context.LoggingContext() as request_context:
request_context.request = request_id
await do_request_handling()
logger.debug("finished")
In the above flow:
- The logcontext is set
-
do_request_handling
is called, and returns an awaitable -
handle_request
awaits the awaitable - Execution of
handle_request
is suspended
So we have stopped processing the request (and will probably go on to start processing the next), without clearing the logcontext.
To circumvent this problem, synapse code assumes that, wherever you have
an awaitable, you will want to await
it. To that end, whereever
functions return awaitables, we adopt the following conventions:
Rules for functions returning awaitables:
- If the awaitable is already complete, the function returns with the same logcontext it started with.
- If the awaitable is incomplete, the function clears the logcontext before returning; when the awaitable completes, it restores the logcontext before running any callbacks.
That sounds complicated, but actually it means a lot of code (including the example above) "just works". There are two cases:
-
If
do_request_handling
returns a completed awaitable, then the logcontext will still be in place. In this case, execution will continue immediately after theawait
; the "finished" line will be logged against the right context, and thewith
block restores the original context before we return to the caller. -
If the returned awaitable is incomplete,
do_request_handling
clears the logcontext before returning. The logcontext is therefore clear whenhandle_request
await
s the awaitable.Once
do_request_handling
's awaitable completes, it will reinstate the logcontext, before running the second half ofhandle_request
, so again the "finished" line will be logged against the right context, and thewith
block restores the original context.
As an aside, it's worth noting that handle_request
follows our rules
- though that only matters if the caller has its own logcontext which it cares about.
The following sections describe pitfalls and helpful patterns when implementing these rules.
Always await your awaitables
Whenever you get an awaitable back from a function, you should await
on
it as soon as possible. Do not pass go; do not do any logging; do not
call any other functions.
async def fun():
logger.debug("starting")
await do_some_stuff() # just like this
coro = more_stuff()
result = await coro # also fine, of course
return result
Provided this pattern is followed all the way back up to the callchain
to where the logcontext was set, this will make things work out ok:
provided do_some_stuff
and more_stuff
follow the rules above, then
so will fun
.