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
9 changes: 6 additions & 3 deletions src/gitingest/utils/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,12 @@ def emit(self, record: logging.LogRecord) -> None:
except ValueError:
level = record.levelno

# Find caller from where originated the logged message
frame, depth = logging.currentframe(), 2
while frame.f_code.co_filename == logging.__file__:
# Find caller from where originated the logged message.
# Walk up from emit's caller through the stdlib logging frames
# until we reach the actual originating frame. depth starts at 1
# because .f_back already skips emit's own frame.
frame, depth = logging.currentframe().f_back, 1
while frame is not None and frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1

Expand Down
1 change: 1 addition & 0 deletions tests/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for gitingest.utils."""
79 changes: 79 additions & 0 deletions tests/utils/test_logging_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Regression tests for InterceptHandler's frame walker.

The positive test pins the fix: intercepted records are attributed to
the actual originating caller.

The negative test pins the original
bug: with the pre-fix frame walker, every record collapses to stdlib
Logger.callHandlers so a future regression reintroduces a
detectable failure mode.
"""

import logging

import pytest

# configure_logging() runs at module scope on import, installs
# gitingest's InterceptHandler globally.
from gitingest.utils import logging_config


def test_intercept_handler_attributes_to_caller() -> None:
"""Intercepted record is attributed to the actual caller."""
captured: dict = {}
sink_id = logging_config.logger.add(
lambda msg: captured.update(msg.record),
level=0,
format="{message}",
)
try:
logging.getLogger("demo").error("ping")
finally:
logging_config.logger.remove(sink_id)

# Bug renders this as function="callHandlers" (stdlib internal)
# or "<unknown>" (off-stack). Fix attributes to the test function.
assert captured.get("function") == "test_intercept_handler_attributes_to_caller"
assert captured.get("name") == __name__
assert captured.get("line") > 0


def test_buggy_frame_walker_attributes_to_callhandlers(monkeypatch: pytest.MonkeyPatch) -> None:
"""Pin the original bug so a future regression reintroduces a detectable failure mode.

With currentframe() (no .f_back) the while-loop's filename
check fails on the first iteration, frame is emit's own frame,
in gitingest's file, not in logging.__file__. So depth stays at
2 and loguru attributes every intercepted record to stdlib's
Logger.callHandlers.
"""

def buggy_emit(_self: logging.Handler, record: logging.LogRecord) -> None:
try:
level = logging_config.logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = logging.currentframe(), 2
while frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logging_config.logger.opt(depth=depth, exception=record.exc_info).log(
level,
record.getMessage(),
)

monkeypatch.setattr(logging_config.InterceptHandler, "emit", buggy_emit)

captured: dict = {}
sink_id = logging_config.logger.add(
lambda msg: captured.update(msg.record),
level=0,
format="{message}",
)
try:
logging.getLogger("demo").error("ping")
finally:
logging_config.logger.remove(sink_id)

assert captured.get("function") == "callHandlers"
assert captured.get("name") == "logging"