Thread name CallsiteParameterAdder parameter doesn't work in async methods
This is based on @PrieJos's helpful analysis in https://github.com/hynek/structlog/issues/693#issuecomment-2629117069
In async log methods, callsite collection happens in the async logger thread which makes its thread-related information worthless:
# /// script
# dependencies = [
# "structlog",
# ]
# ///
import asyncio
import logging
import sys
import structlog
sl = structlog.get_logger()
logger = logging.getLogger()
logging.basicConfig(
stream=sys.stdout,
format=(
"%(process)d %(processName)s %(module)s"
" %(funcName)s:%(lineno)d %(threadName)s %(taskName)s"
" %(message)s"
),
datefmt=r"%Y-%m-%dT%H:%M:%S",
level=logging.INFO,
encoding="utf-8",
)
structlog.configure(
processors=[
structlog.processors.CallsiteParameterAdder(
[
structlog.processors.CallsiteParameter.PROCESS,
structlog.processors.CallsiteParameter.PROCESS_NAME,
structlog.processors.CallsiteParameter.MODULE,
structlog.processors.CallsiteParameter.FILENAME,
structlog.processors.CallsiteParameter.FUNC_NAME,
structlog.processors.CallsiteParameter.LINENO,
structlog.processors.CallsiteParameter.THREAD_NAME,
]
),
structlog.processors.LogfmtRenderer(),
],
)
async def afunc():
await sl.ainfo("async")
logger.info("stdlib async")
def func():
sl.info("sync")
logger.info("stdlib sync")
asyncio.run(afunc())
func()
Notably, thread name is wrong and so would be a task name as shown in #698:
event=async process=55347 process_name=n/a module=t filename=t.py func_name=afunc lineno=47 thread_name=asyncio_0
55347 MainProcess t afunc:48 MainThread Task-1 stdlib async
event=sync process=55347 process_name=n/a module=t filename=t.py func_name=func lineno=51 thread_name=MainThread
55347 MainProcess t func:52 MainThread None stdlib sync
Not sure how to deal with this most elegantly – I feel like we'll have to pass thread/task objects into the event_dict or something?
I think the process_name is in fact correct, since logging just sets it unconditionally and we get n/a in sync code too. Happy to be corrected, though.
@hynek Just thinking quickly aloud, maybe a possible solution will be to pass some context about the origin of the respective log method calls using contextvars when the "async-to-sync" proxy is called? What I mean in short is to try to use/simulate the function asyncio.to_thread() included in the standard lib as of Python 3.9 which (I'm quoting the official documentation):
Also, the current contextvars.Context is propagated, allowing context variables from the event loop thread to be accessed in the separate thread.
The main advantage of this is that it'll be totally thread-safe/async task-safe.
What do you think? Does it sound feasible to you?
By the way, regarding this ...
I think the process_name is in fact correct, since logging just sets it unconditionally and we get n/a in sync code too. Happy to be corrected, though.
Yes, you're right! All ok from my point of view as well. Anyway, the solution above should also be able to cover the original process_name indeed.
Cheers Jose M. Prieto