Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thread name CallsiteParameterAdder parameter doesn't work in async methods #710

Open
hynek opened this issue Mar 8, 2025 · 1 comment
Open

Comments

@hynek
Copy link
Owner

hynek commented Mar 8, 2025

This is based on @PrieJos's helpful analysis in #693 (comment)

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.

@PrieJos
Copy link

PrieJos commented Mar 9, 2025

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants