Skip to content

Commit e3285a6

Browse files
committed
Add async task name as option to callsite (#693)
1 parent 19d6987 commit e3285a6

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
@@ -127,6 +127,12 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
127127
[#684](https://github.com/hynek/structlog/pull/684)
128128

129129

130+
### Added
131+
132+
- `structlog.processors.CallsiteParameter.TASK_NAME` now available as callsite parameter.
133+
[#693](https://github.com/hynek/structlog/issues/693)
134+
135+
130136
### Changed
131137

132138
- `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
@@ -36,7 +36,7 @@
3636
_format_stack,
3737
)
3838
from ._log_levels import NAME_TO_LEVEL, add_log_level
39-
from ._utils import get_processname
39+
from ._utils import get_processname, get_taskname
4040
from .tracebacks import ExceptionDictTransformer
4141
from .typing import (
4242
EventDict,
@@ -757,6 +757,8 @@ class CallsiteParameter(enum.Enum):
757757
PROCESS = "process"
758758
#: The name of the process the callsite was executed in.
759759
PROCESS_NAME = "process_name"
760+
#: The name of the asynchronous task the callsite was executed in.
761+
TASK_NAME = "task_name"
760762

761763

762764
def _get_callsite_pathname(module: str, frame: FrameType) -> Any:
@@ -799,6 +801,10 @@ def _get_callsite_process_name(module: str, frame: FrameType) -> Any:
799801
return get_processname()
800802

801803

804+
def _get_callsite_task_name(module: str, frame: FrameType) -> Any:
805+
return get_taskname()
806+
807+
802808
class CallsiteParameterAdder:
803809
"""
804810
Adds parameters of the callsite that an event dictionary originated from to
@@ -853,6 +859,7 @@ class CallsiteParameterAdder:
853859
CallsiteParameter.THREAD_NAME: _get_callsite_thread_name,
854860
CallsiteParameter.PROCESS: _get_callsite_process,
855861
CallsiteParameter.PROCESS_NAME: _get_callsite_process_name,
862+
CallsiteParameter.TASK_NAME: _get_callsite_task_name,
856863
}
857864
_record_attribute_map: ClassVar[dict[CallsiteParameter, str]] = {
858865
CallsiteParameter.PATHNAME: "pathname",
@@ -864,6 +871,7 @@ class CallsiteParameterAdder:
864871
CallsiteParameter.THREAD_NAME: "threadName",
865872
CallsiteParameter.PROCESS: "process",
866873
CallsiteParameter.PROCESS_NAME: "processName",
874+
CallsiteParameter.TASK_NAME: "taskName",
867875
}
868876

869877
_all_parameters: ClassVar[set[CallsiteParameter]] = set(CallsiteParameter)
@@ -913,9 +921,12 @@ def __call__(
913921
# then the callsite parameters of the record will not be correct.
914922
if record is not None and not from_structlog:
915923
for mapping in self._record_mappings:
916-
event_dict[mapping.event_dict_key] = record.__dict__[
924+
# Careful since log record attribute taskName is only
925+
# supported as of python 3.12
926+
# https://docs.python.org/3.12/library/logging.html#logrecord-attributes
927+
event_dict[mapping.event_dict_key] = record.__dict__.get(
917928
mapping.record_attribute
918-
]
929+
)
919930

920931
return event_dict
921932

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,
@@ -301,6 +301,7 @@ class TestCallsiteParameterAdder:
301301
"thread_name",
302302
"process",
303303
"process_name",
304+
"task_name",
304305
}
305306

306307
# Exclude QUAL_NAME from the general set to keep parity with stdlib
@@ -400,7 +401,7 @@ def __init__(self):
400401
logger_params = json.loads(string_io.getvalue())
401402

402403
# These are different when running under async
403-
for key in ["thread", "thread_name"]:
404+
for key in ["thread", "thread_name", "task_name"]:
404405
callsite_params.pop(key)
405406
logger_params.pop(key)
406407

@@ -678,6 +679,7 @@ def get_callsite_parameters(cls, offset: int = 1) -> dict[str, object]:
678679
"thread_name": threading.current_thread().name,
679680
"process": os.getpid(),
680681
"process_name": get_processname(),
682+
"task_name": get_taskname(),
681683
}
682684

683685

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)