Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/build-docset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- run: tox run -e docset
- run: tar --exclude='.DS_Store' -cvzf structlog.tgz structlog.docset

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: docset
path: structlog.tgz
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:

steps:
- name: Download pre-built packages
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: Packages
path: dist
Expand All @@ -69,7 +69,7 @@ jobs:
-f py${PYTHON//./}-tests

- name: Upload coverage data
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: coverage-data-${{ matrix.python-version }}
path: .coverage.*
Expand Down Expand Up @@ -98,7 +98,7 @@ jobs:
- uses: hynek/setup-cached-uv@757bedc3f972eb7227a1aa657651f15a8527c817 # v2.3.0

- name: Download coverage data
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
pattern: coverage-data-*
merge-multiple: true
Expand All @@ -117,7 +117,7 @@ jobs:
coverage report --fail-under=100

- name: Upload HTML report if check failed.
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: html-report
path: htmlcov
Expand All @@ -130,7 +130,7 @@ jobs:

steps:
- name: Download pre-built packages
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: Packages
path: dist
Expand All @@ -150,7 +150,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download pre-built packages
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: Packages
path: dist
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ jobs:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
languages: ${{ matrix.language }}

- name: Autobuild
uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
4 changes: 2 additions & 2 deletions .github/workflows/pypi-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:

steps:
- name: Download packages built by build-and-inspect-python-package
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: Packages
path: dist
Expand All @@ -66,7 +66,7 @@ jobs:

steps:
- name: Download packages built by build-and-inspect-python-package
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: Packages
path: dist
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
# Path to SARIF file relative to the root of the repository
sarif_file: results.sarif
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ ci:

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.1
rev: v0.14.2
hooks:
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
- On Python 3.11+, `structlog.processors.CallsiteParameterAdder` now supports `CallsiteParameter.QUAL_NAME` that adds the qualified name of the callsite, including scope and class names.
This is only available for *structlog*-originated events since the standard library has no equivalent.

- `structlog.stdlib.LoggerFactory` now supports the *stacklevel* parameter.
[#763](https://github.com/hynek/structlog/pull/763)


### Changed

Expand Down
15 changes: 14 additions & 1 deletion src/structlog/_frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def _format_exception(exc_info: ExcInfo) -> str:
def _find_first_app_frame_and_name(
additional_ignores: list[str] | None = None,
*,
stacklevel: int | None = None,
_getframe: Callable[[], FrameType] = sys._getframe,
) -> tuple[FrameType, str]:
"""
Expand All @@ -45,22 +46,34 @@ def _find_first_app_frame_and_name(
additional_ignores:
Additional names with which the first frame must not start.

stacklevel:
After getting out of structlog, skip this many frames.

_getframe:
Callable to find current frame. Only for testing to avoid
monkeypatching of sys._getframe.

Returns:
tuple of (frame, name)
"""
ignores = tuple(["structlog"] + (additional_ignores or []))
ignores = ("structlog", *tuple(additional_ignores or ()))
f = _ASYNC_CALLING_STACK.get(_getframe())
name = f.f_globals.get("__name__") or "?"

while name.startswith(ignores):
if f.f_back is None:
name = "?"
break
f = f.f_back
name = f.f_globals.get("__name__") or "?"

if stacklevel is not None:
for _ in range(stacklevel):
if f.f_back is None:
break
f = f.f_back
name = f.f_globals.get("__name__") or "?"

return f, name


Expand Down
18 changes: 15 additions & 3 deletions src/structlog/stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,15 @@ def findCaller(
This logger gets set as the default one when using LoggerFactory.
"""
sinfo: str | None
f, _name = _find_first_app_frame_and_name(["logging"])
# stdlib logging passes stacklevel=1 from log methods like .warning(),
# but we've already skipped those frames by ignoring "logging", so we
# need to adjust stacklevel down by 1. We need to manually drop
# logging frames, because there's cases where we call logging methods
# from within structlog and the stacklevel offsets don't work anymore.
adjusted_stacklevel = max(0, stacklevel - 1) if stacklevel else None
f, _name = _find_first_app_frame_and_name(
["logging"], stacklevel=adjusted_stacklevel
)
sinfo = _format_stack(f) if stack_info else None

return f.f_code.co_filename, f.f_lineno, f.f_code.co_name, sinfo
Expand Down Expand Up @@ -323,12 +331,16 @@ def setLevel(self, level: int) -> None:
self._logger.setLevel(level)

def findCaller(
self, stack_info: bool = False
self, stack_info: bool = False, stacklevel: int = 1
) -> tuple[str, int, str, str | None]:
"""
Calls :meth:`logging.Logger.findCaller` with unmodified arguments.
"""
return self._logger.findCaller(stack_info=stack_info)
# No need for stacklevel-adjustments since we're within structlog and
# our frames are ignored unconditionally.
return self._logger.findCaller(
stack_info=stack_info, stacklevel=stacklevel
)

def makeRecord(
self,
Expand Down
34 changes: 34 additions & 0 deletions tests/test_frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ def test_ignoring_of_additional_frame_names_works(self):

assert (f1, "test") == (f, n)

def test_stacklevel(self):
"""
stacklevel is respected.
"""
f0 = stub(
f_globals={"__name__": "test"},
f_back=stub(f_globals={"__name__": "too far"}, f_back=None),
)
f1 = stub(f_globals={"__name__": "skipped"}, f_back=f0)
f2 = stub(f_globals={"__name__": "ignored.bar"}, f_back=f1)
f3 = stub(f_globals={"__name__": "structlog.blubb"}, f_back=f2)

f, n = _find_first_app_frame_and_name(
additional_ignores=["ignored"], stacklevel=1, _getframe=lambda: f3
)

assert (f0, "test") == (f, n)

def test_stacklevel_capped(self):
"""
stacklevel is capped at the number of frames.
"""
f0 = stub(f_globals={"__name__": "test"}, f_back=None)
f1 = stub(f_globals={"__name__": "skipped"}, f_back=f0)
f2 = stub(f_globals={"__name__": "ignored.bar"}, f_back=f1)
f3 = stub(f_globals={"__name__": "structlog.blubb"}, f_back=f2)

f, n = _find_first_app_frame_and_name(
additional_ignores=["ignored"],
stacklevel=100,
_getframe=lambda: f3,
)
assert (f0, "test") == (f, n)

def test_tolerates_missing_name(self):
"""
Use ``?`` if `f_globals` lacks a `__name__` key
Expand Down
Loading