Skip to content

Commit 73ba675

Browse files
authored
More lenient handling of loguru message mismatch and better warnings (#338)
1 parent b07b3cf commit 73ba675

File tree

4 files changed

+50
-40
lines changed

4 files changed

+50
-40
lines changed

logfire/_internal/config.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
from .integrations.executors import instrument_executors
7777
from .metrics import ProxyMeterProvider, configure_metrics
7878
from .scrubbing import NOOP_SCRUBBER, BaseScrubber, Scrubber, ScrubbingOptions, ScrubCallback
79-
from .stack_info import get_user_frame_and_stacklevel
79+
from .stack_info import warn_at_user_stacklevel
8080
from .tracer import PendingSpanProcessor, ProxyTracerProvider
8181
from .utils import UnexpectedResponse, ensure_data_dir_exists, get_version, read_toml_file, suppress_instrumentation
8282

@@ -801,12 +801,10 @@ def get_meter_provider(self) -> ProxyMeterProvider:
801801

802802
def warn_if_not_initialized(self, message: str):
803803
if not self._initialized and not self.ignore_no_config:
804-
_frame, stacklevel = get_user_frame_and_stacklevel()
805-
warnings.warn(
804+
warn_at_user_stacklevel(
806805
f'{message} until `logfire.configure()` has been called. '
807806
f'Set the environment variable LOGFIRE_IGNORE_NO_CONFIG=1 or add ignore_no_config=true in pyproject.toml to suppress this warning.',
808807
category=LogfireNotConfiguredWarning,
809-
stacklevel=stacklevel,
810808
)
811809

812810
@cached_property

logfire/_internal/formatter.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
from typing_extensions import NotRequired, TypedDict
1515

1616
import logfire
17-
from logfire._internal.stack_info import get_user_frame_and_stacklevel
1817

1918
from .constants import ATTRIBUTES_SCRUBBED_KEY, MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT
2019
from .scrubbing import NOOP_SCRUBBER, BaseScrubber, ScrubbedNote
20+
from .stack_info import warn_at_user_stacklevel
2121
from .utils import log_internal_error, truncate_string
2222

2323

@@ -466,14 +466,12 @@ class FormattingFailedWarning(UserWarning):
466466

467467

468468
def warn_formatting(msg: str):
469-
_frame, stacklevel = get_user_frame_and_stacklevel()
470-
warnings.warn(
469+
warn_at_user_stacklevel(
471470
f'\n'
472471
f' Ensure you are either:\n'
473472
' (1) passing an f-string directly, with inspect_arguments enabled and working, or\n'
474473
' (2) passing a literal `str.format`-style template, not a preformatted string.\n'
475474
' See https://docs.pydantic.dev/logfire/guides/onboarding_checklist/add_manual_tracing/#messages-and-span-names.\n'
476475
f' The problem was: {msg}',
477-
stacklevel=stacklevel,
478476
category=FormattingFailedWarning,
479477
)

logfire/_internal/stack_info.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import inspect
44
import sys
5+
import warnings
56
from functools import lru_cache
67
from pathlib import Path
78
from types import CodeType, FrameType
@@ -97,3 +98,8 @@ def is_user_code(code: CodeType) -> bool:
9798
str(Path(code.co_filename).absolute()).startswith(PREFIXES)
9899
or code.co_name in ('<listcomp>', '<dictcomp>', '<setcomp>')
99100
)
101+
102+
103+
def warn_at_user_stacklevel(msg: str, category: type[Warning]):
104+
_frame, stacklevel = get_user_frame_and_stacklevel()
105+
warnings.warn(msg, stacklevel=stacklevel, category=category)

logfire/integrations/loguru.py

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,25 @@
33
from __future__ import annotations
44

55
import inspect
6-
import warnings
76
from logging import LogRecord
7+
from pathlib import Path
88
from typing import Any
99

10-
from loguru import logger
10+
import loguru
1111

1212
from .._internal.constants import ATTRIBUTES_LOGGING_ARGS_KEY, ATTRIBUTES_MESSAGE_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY
13+
from .._internal.stack_info import warn_at_user_stacklevel
1314
from .logging import LogfireLoggingHandler
1415

16+
LOGURU_PATH = Path(loguru.__file__).parent
17+
18+
19+
class LoguruInspectionFailed(RuntimeWarning):
20+
"""Warning raised when magic introspection of loguru stack frames fails.
21+
22+
This may happen if the loguru library changes in a way that breaks the introspection.
23+
"""
24+
1525

1626
class LogfireHandler(LogfireLoggingHandler):
1727
"""A loguru handler that sends logs to **Logfire**."""
@@ -38,51 +48,49 @@ def fill_attributes(self, record: LogRecord) -> dict[str, Any]:
3848
while frame: # pragma: no branch
3949
if frame.f_code is _LOG_METHOD_CODE:
4050
frame_locals = frame.f_locals
41-
if 'message' in frame_locals:
51+
if 'message' in frame_locals: # pragma: no branch
4252
attributes[ATTRIBUTES_MESSAGE_TEMPLATE_KEY] = frame_locals['message']
4353
else: # pragma: no cover
44-
_warn_inspection_failure()
54+
warn_at_user_stacklevel(
55+
'Failed to extract message template (span name) for loguru log.', LoguruInspectionFailed
56+
)
4557

4658
args = frame_locals.get('args')
47-
if isinstance(args, (tuple, list)):
59+
if isinstance(args, (tuple, list)): # pragma: no branch
4860
if args:
4961
attributes[ATTRIBUTES_LOGGING_ARGS_KEY] = args
5062
else: # pragma: no cover
51-
_warn_inspection_failure()
52-
53-
if record.exc_info:
54-
original_record = frame_locals.get('log_record')
55-
if isinstance(original_record, dict):
56-
message = original_record.get('message') # type: ignore
57-
if isinstance(message, str) and record.msg.startswith(
58-
message + '\nTraceback (most recent call last):'
59-
):
60-
# `record.msg` includes a traceback added by Loguru,
61-
# replace it with the original message.
62-
attributes[ATTRIBUTES_MESSAGE_KEY] = message
63-
else: # pragma: no cover
64-
_warn_inspection_failure()
65-
else: # pragma: no cover
66-
_warn_inspection_failure()
63+
warn_at_user_stacklevel('Failed to extract args for loguru log.', LoguruInspectionFailed)
64+
65+
original_record: dict[str, Any] | None = frame_locals.get('log_record')
66+
if (
67+
isinstance(original_record, dict)
68+
and isinstance(message := original_record.get('message'), str)
69+
and message in record.msg
70+
): # pragma: no branch
71+
# `record.msg` may include a traceback added by Loguru,
72+
# replace it with the original message.
73+
attributes[ATTRIBUTES_MESSAGE_KEY] = message
74+
else: # pragma: no cover
75+
warn_at_user_stacklevel(
76+
'Failed to extract original message for loguru log.', LoguruInspectionFailed
77+
)
6778

6879
break
6980

7081
frame = frame.f_back
82+
else: # pragma: no cover
83+
warn_at_user_stacklevel(
84+
'Failed to find loguru log frame to extract detailed information', LoguruInspectionFailed
85+
)
7186

7287
return attributes
7388

7489

75-
def _warn_inspection_failure() -> None: # pragma: no cover
76-
warnings.warn(
77-
'Failed to extract info from loguru logger. '
78-
'This may affect span names and/or positional arguments. '
79-
'Please report an issue to logfire.',
80-
RuntimeWarning,
81-
)
82-
83-
8490
try:
85-
_LOG_METHOD_CODE = inspect.unwrap(type(logger)._log).__code__ # type: ignore
91+
_LOG_METHOD_CODE = inspect.unwrap(type(loguru.logger)._log).__code__ # type: ignore
8692
except Exception: # pragma: no cover
8793
_LOG_METHOD_CODE = None # type: ignore
88-
_warn_inspection_failure()
94+
warn_at_user_stacklevel(
95+
'Failed to find loguru log method code to extract detailed information', LoguruInspectionFailed
96+
)

0 commit comments

Comments
 (0)