Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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
17 changes: 16 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,17 @@ def get_processname() -> str:
processname = mp.current_process().name

return processname


def get_taskname() -> Optional[str]:
"""
Get the current asynchronous task if applicable.

Returns:
Optional[str]: asynchronous task name.
"""
task_name = None
with suppress(Exception):
task = asyncio.current_task()
task_name = task.get_name() if task else None
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 @@ -36,7 +36,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 @@ -757,6 +757,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 @@ -799,6 +801,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 @@ -853,6 +859,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 @@ -864,6 +871,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 @@ -913,9 +921,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
]
)

return event_dict

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 @@ -301,6 +301,7 @@ class TestCallsiteParameterAdder:
"thread_name",
"process",
"process_name",
"task_name",
}

# Exclude QUAL_NAME from the general set to keep parity with stdlib
Expand Down Expand Up @@ -400,7 +401,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 @@ -678,6 +679,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()