Skip to content

Commit e93f31b

Browse files
committed
Add async task name as option to callsite (#693)
1 parent 768931a commit e93f31b

File tree

5 files changed

+69
-7
lines changed

5 files changed

+69
-7
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
2121
[#684](https://github.com/hynek/structlog/pull/684)
2222

2323

24+
### Added
25+
26+
- `structlog.processors.CallsiteParameter.TASK_NAME` now available as callsite parameter.
27+
[#693](https://github.com/hynek/structlog/issues/693)
28+
29+
2430
### Changed
2531

2632
- `structlog.stdlib.BoundLogger`'s binding-related methods now also return `Self`.

src/structlog/_utils.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99

1010
from __future__ import annotations
1111

12+
import asyncio
1213
import sys
1314

1415
from contextlib import suppress
15-
from typing import Any
16+
from typing import Any, Optional
1617

1718

1819
def get_processname() -> str:
@@ -28,3 +29,17 @@ def get_processname() -> str:
2829
processname = mp.current_process().name
2930

3031
return processname
32+
33+
34+
def get_taskname() -> Optional[str]: # noqa: UP007
35+
"""
36+
Get the current asynchronous task if applicable.
37+
38+
Returns:
39+
Optional[str]: asynchronous task name.
40+
"""
41+
task_name = None
42+
with suppress(Exception):
43+
task = asyncio.current_task()
44+
task_name = task.get_name() if task else None
45+
return task_name

src/structlog/processors.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
_format_stack,
3838
)
3939
from ._log_levels import NAME_TO_LEVEL, add_log_level
40-
from ._utils import get_processname
40+
from ._utils import get_processname, get_taskname
4141
from .tracebacks import ExceptionDictTransformer
4242
from .typing import (
4343
EventDict,
@@ -736,6 +736,8 @@ class CallsiteParameter(enum.Enum):
736736
PROCESS = "process"
737737
#: The name of the process the callsite was executed in.
738738
PROCESS_NAME = "process_name"
739+
#: The name of the asynchronous task the callsite was executed in.
740+
TASK_NAME = "task_name"
739741

740742

741743
def _get_callsite_pathname(module: str, frame: FrameType) -> Any:
@@ -774,6 +776,10 @@ def _get_callsite_process_name(module: str, frame: FrameType) -> Any:
774776
return get_processname()
775777

776778

779+
def _get_callsite_task_name(module: str, frame: FrameType) -> Any:
780+
return get_taskname()
781+
782+
777783
class CallsiteParameterAdder:
778784
"""
779785
Adds parameters of the callsite that an event dictionary originated from to
@@ -826,6 +832,7 @@ class CallsiteParameterAdder:
826832
CallsiteParameter.THREAD_NAME: _get_callsite_thread_name,
827833
CallsiteParameter.PROCESS: _get_callsite_process,
828834
CallsiteParameter.PROCESS_NAME: _get_callsite_process_name,
835+
CallsiteParameter.TASK_NAME: _get_callsite_task_name,
829836
}
830837
_record_attribute_map: ClassVar[dict[CallsiteParameter, str]] = {
831838
CallsiteParameter.PATHNAME: "pathname",
@@ -837,6 +844,7 @@ class CallsiteParameterAdder:
837844
CallsiteParameter.THREAD_NAME: "threadName",
838845
CallsiteParameter.PROCESS: "process",
839846
CallsiteParameter.PROCESS_NAME: "processName",
847+
CallsiteParameter.TASK_NAME: "taskName",
840848
}
841849

842850
_all_parameters: ClassVar[set[CallsiteParameter]] = set(CallsiteParameter)
@@ -882,9 +890,12 @@ def __call__(
882890
# then the callsite parameters of the record will not be correct.
883891
if record is not None and not from_structlog:
884892
for mapping in self._record_mappings:
885-
event_dict[mapping.event_dict_key] = record.__dict__[
893+
# Careful since log record attribute taskName is only
894+
# supported as of python 3.12
895+
# https://docs.python.org/3.12/library/logging.html#logrecord-attributes
896+
event_dict[mapping.event_dict_key] = record.__dict__.get(
886897
mapping.record_attribute
887-
]
898+
)
888899
else:
889900
frame, module = _find_first_app_frame_and_name(
890901
additional_ignores=self._additional_ignores

tests/processors/test_processors.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import structlog
2323

2424
from structlog import BoundLogger
25-
from structlog._utils import get_processname
25+
from structlog._utils import get_processname, get_taskname
2626
from structlog.processors import (
2727
CallsiteParameter,
2828
CallsiteParameterAdder,
@@ -281,6 +281,7 @@ class TestCallsiteParameterAdder:
281281
"thread_name",
282282
"process",
283283
"process_name",
284+
"task_name",
284285
}
285286

286287
_all_parameters = set(CallsiteParameter)
@@ -330,7 +331,7 @@ def __init__(self):
330331
logger_params = json.loads(string_io.getvalue())
331332

332333
# These are different when running under async
333-
for key in ["thread", "thread_name"]:
334+
for key in ["thread", "thread_name", "task_name"]:
334335
callsite_params.pop(key)
335336
logger_params.pop(key)
336337

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

612614

tests/test_utils.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
# 2.0, and the MIT License. See the LICENSE file in the root of this
44
# repository for complete details.
55

6+
import asyncio
67
import multiprocessing
78
import sys
89

910
import pytest
1011

11-
from structlog._utils import get_processname
12+
from structlog._utils import get_processname, get_taskname
1213

1314

1415
class TestGetProcessname:
@@ -69,3 +70,30 @@ def _current_process() -> None:
6970
)
7071

7172
assert get_processname() == "n/a"
73+
74+
75+
class TestGetTaskname:
76+
def test_event_loop_running(self) -> None:
77+
"""
78+
Test returned task name when executed within an event loop.
79+
"""
80+
81+
async def aroutine() -> None:
82+
assert get_taskname() == "AsyncTask"
83+
84+
async def run() -> None:
85+
task = asyncio.create_task(aroutine(), name="AsyncTask")
86+
await asyncio.gather(task)
87+
88+
asyncio.run(run())
89+
90+
def test_no_event_loop_running(self) -> None:
91+
"""
92+
Test returned task name when executed asynchronously without an event
93+
loop.
94+
"""
95+
96+
def routine() -> None:
97+
assert get_taskname() is None
98+
99+
routine()

0 commit comments

Comments
 (0)