Skip to content

Commit

Permalink
Add async task name as option to callsite (#693)
Browse files Browse the repository at this point in the history
  • Loading branch information
PrieJos committed Feb 3, 2025
1 parent 66e22d2 commit 34a6864
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 7 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
[#684](https://github.com/hynek/structlog/pull/684)


### Added

- `structlog.processors.CallsiteParameter.TASK_NAME` now available as callsite parameter.
[#693](https://github.com/hynek/structlog/issues/693)


### Changed

- `structlog.stdlib.BoundLogger`'s binding-related methods now also return `Self`.
Expand Down
18 changes: 17 additions & 1 deletion src/structlog/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@

from __future__ import annotations

import asyncio
import sys

from contextlib import suppress
from typing import Any
from typing import Any, Optional


def get_processname() -> str:
Expand All @@ -28,3 +29,18 @@ def get_processname() -> str:
processname = mp.current_process().name

return processname


def get_taskname() -> Optional[str]: # noqa: UP007
"""
Get the current asynchronous task if applicable.
Returns:
Optional[str]: asynchronous task name.
"""
task_name = None
with suppress(Exception):
task = asyncio.current_task()
if task:
task_name = task.get_name()
return task_name
17 changes: 14 additions & 3 deletions src/structlog/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
_format_stack,
)
from ._log_levels import NAME_TO_LEVEL, add_log_level
from ._utils import get_processname
from ._utils import get_processname, get_taskname
from .tracebacks import ExceptionDictTransformer
from .typing import (
EventDict,
Expand Down Expand Up @@ -735,6 +735,8 @@ class CallsiteParameter(enum.Enum):
PROCESS = "process"
#: The name of the process the callsite was executed in.
PROCESS_NAME = "process_name"
#: The name of the asynchronous task the callsite was executed in.
TASK_NAME = "task_name"


def _get_callsite_pathname(module: str, frame: FrameType) -> Any:
Expand Down Expand Up @@ -773,6 +775,10 @@ def _get_callsite_process_name(module: str, frame: FrameType) -> Any:
return get_processname()


def _get_callsite_task_name(module: str, frame: FrameType) -> Any:
return get_taskname()


class CallsiteParameterAdder:
"""
Adds parameters of the callsite that an event dictionary originated from to
Expand Down Expand Up @@ -825,6 +831,7 @@ class CallsiteParameterAdder:
CallsiteParameter.THREAD_NAME: _get_callsite_thread_name,
CallsiteParameter.PROCESS: _get_callsite_process,
CallsiteParameter.PROCESS_NAME: _get_callsite_process_name,
CallsiteParameter.TASK_NAME: _get_callsite_task_name,
}
_record_attribute_map: ClassVar[dict[CallsiteParameter, str]] = {
CallsiteParameter.PATHNAME: "pathname",
Expand All @@ -836,6 +843,7 @@ class CallsiteParameterAdder:
CallsiteParameter.THREAD_NAME: "threadName",
CallsiteParameter.PROCESS: "process",
CallsiteParameter.PROCESS_NAME: "processName",
CallsiteParameter.TASK_NAME: "taskName",
}

_all_parameters: ClassVar[set[CallsiteParameter]] = set(CallsiteParameter)
Expand Down Expand Up @@ -881,9 +889,12 @@ def __call__(
# then the callsite parameters of the record will not be correct.
if record is not None and not from_structlog:
for mapping in self._record_mappings:
event_dict[mapping.event_dict_key] = record.__dict__[
# Careful since log record attribute taskName is only
# supported as of python 3.12
# https://docs.python.org/3.12/library/logging.html#logrecord-attributes
event_dict[mapping.event_dict_key] = record.__dict__.get(
mapping.record_attribute
]
)
else:
frame, module = _find_first_app_frame_and_name(
additional_ignores=self._additional_ignores
Expand Down
6 changes: 4 additions & 2 deletions tests/processors/test_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import structlog

from structlog import BoundLogger
from structlog._utils import get_processname
from structlog._utils import get_processname, get_taskname
from structlog.processors import (
CallsiteParameter,
CallsiteParameterAdder,
Expand Down Expand Up @@ -281,6 +281,7 @@ class TestCallsiteParameterAdder:
"thread_name",
"process",
"process_name",
"task_name",
}

_all_parameters = set(CallsiteParameter)
Expand Down Expand Up @@ -330,7 +331,7 @@ def __init__(self):
logger_params = json.loads(string_io.getvalue())

# These are different when running under async
for key in ["thread", "thread_name"]:
for key in ["thread", "thread_name", "task_name"]:
callsite_params.pop(key)
logger_params.pop(key)

Expand Down Expand Up @@ -607,6 +608,7 @@ def get_callsite_parameters(cls, offset: int = 1) -> dict[str, object]:
"thread_name": threading.current_thread().name,
"process": os.getpid(),
"process_name": get_processname(),
"task_name": get_taskname(),
}


Expand Down
30 changes: 29 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
# 2.0, and the MIT License. See the LICENSE file in the root of this
# repository for complete details.

import asyncio
import multiprocessing
import sys

import pytest

from structlog._utils import get_processname
from structlog._utils import get_processname, get_taskname


class TestGetProcessname:
Expand Down Expand Up @@ -69,3 +70,30 @@ def _current_process() -> None:
)

assert get_processname() == "n/a"


class TestGetTaskname:
def test_event_loop_running(self) -> None:
"""
Test returned task name when executed within an event loop.
"""

async def aroutine() -> None:
assert get_taskname() == "AsyncTask"

async def run() -> None:
task = asyncio.create_task(aroutine(), name="AsyncTask")
await asyncio.gather(task)

asyncio.run(run())

def test_no_event_loop_running(self) -> None:
"""
Test returned task name when executed asynchronously without an event
loop.
"""

def routine() -> None:
assert get_taskname() is None

routine()

0 comments on commit 34a6864

Please sign in to comment.