Skip to content

Commit f5983eb

Browse files
authored
Release v4.4.0 (#1377)
1 parent c66eb8c commit f5983eb

File tree

12 files changed

+148
-24
lines changed

12 files changed

+148
-24
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Release Notes
22

3+
## [v4.4.0] (2025-09-05)
4+
5+
* Add `logfire.instrument_print()` by @alexmojaki in [#1368](https://github.com/pydantic/logfire/pull/1368)
6+
* Record response on 'MCP server handle request' span by @alexmojaki in [#1362](https://github.com/pydantic/logfire/pull/1362)
7+
* Handle MCP request metadata being a dict by @alexmojaki in [#1360](https://github.com/pydantic/logfire/pull/1360)
8+
* fix: handle optional args in `logfire.instrument` by @stevenh in [#1337](https://github.com/pydantic/logfire/pull/1337)
9+
* Add `logfire_token` to scrubbing patterns by @alexmojaki in [#1367](https://github.com/pydantic/logfire/pull/1367)
10+
311
## [v4.3.6] (2025-08-26)
412

513
* Add specific code agent options for `logfire prompt` by @Kludex in [#1350](https://github.com/pydantic/logfire/pull/1350)
@@ -871,3 +879,4 @@ First release from new repo!
871879
[v4.3.4]: https://github.com/pydantic/logfire/compare/v4.3.3...v4.3.4
872880
[v4.3.5]: https://github.com/pydantic/logfire/compare/v4.3.4...v4.3.5
873881
[v4.3.6]: https://github.com/pydantic/logfire/compare/v4.3.5...v4.3.6
882+
[v4.4.0]: https://github.com/pydantic/logfire/compare/v4.3.6...v4.4.0

logfire-api/logfire_api/__init__.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ from .version import VERSION as VERSION
1414
from logfire.sampling import SamplingOptions as SamplingOptions
1515
from typing import Any
1616

17-
__all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'AdvancedOptions', 'ConsoleOptions', 'CodeSource', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'warning', 'error', 'exception', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_asgi', 'instrument_wsgi', 'instrument_pydantic', 'instrument_pydantic_ai', 'instrument_fastapi', 'instrument_openai', 'instrument_openai_agents', 'instrument_anthropic', 'instrument_google_genai', 'instrument_litellm', 'instrument_asyncpg', 'instrument_httpx', 'instrument_celery', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_aiohttp_server', 'instrument_sqlalchemy', 'instrument_sqlite3', 'instrument_aws_lambda', 'instrument_redis', 'instrument_pymongo', 'instrument_mysql', 'instrument_system_metrics', 'instrument_mcp', 'AutoTraceModule', 'with_tags', 'with_settings', 'suppress_scopes', 'shutdown', 'no_auto_trace', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'add_non_user_code_prefix', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'loguru_handler', 'SamplingOptions', 'MetricsOptions', 'logfire_info', 'get_baggage', 'set_baggage']
17+
__all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'AdvancedOptions', 'ConsoleOptions', 'CodeSource', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'warning', 'error', 'exception', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_asgi', 'instrument_wsgi', 'instrument_pydantic', 'instrument_pydantic_ai', 'instrument_fastapi', 'instrument_openai', 'instrument_openai_agents', 'instrument_anthropic', 'instrument_google_genai', 'instrument_litellm', 'instrument_print', 'instrument_asyncpg', 'instrument_httpx', 'instrument_celery', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_aiohttp_server', 'instrument_sqlalchemy', 'instrument_sqlite3', 'instrument_aws_lambda', 'instrument_redis', 'instrument_pymongo', 'instrument_mysql', 'instrument_system_metrics', 'instrument_mcp', 'AutoTraceModule', 'with_tags', 'with_settings', 'suppress_scopes', 'shutdown', 'no_auto_trace', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'add_non_user_code_prefix', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'loguru_handler', 'SamplingOptions', 'MetricsOptions', 'logfire_info', 'get_baggage', 'set_baggage']
1818

1919
DEFAULT_LOGFIRE_INSTANCE = Logfire()
2020
span = DEFAULT_LOGFIRE_INSTANCE.span
@@ -32,6 +32,7 @@ instrument_openai_agents = DEFAULT_LOGFIRE_INSTANCE.instrument_openai_agents
3232
instrument_anthropic = DEFAULT_LOGFIRE_INSTANCE.instrument_anthropic
3333
instrument_google_genai = DEFAULT_LOGFIRE_INSTANCE.instrument_google_genai
3434
instrument_litellm = DEFAULT_LOGFIRE_INSTANCE.instrument_litellm
35+
instrument_print = DEFAULT_LOGFIRE_INSTANCE.instrument_print
3536
instrument_asyncpg = DEFAULT_LOGFIRE_INSTANCE.instrument_asyncpg
3637
instrument_httpx = DEFAULT_LOGFIRE_INSTANCE.instrument_httpx
3738
instrument_celery = DEFAULT_LOGFIRE_INSTANCE.instrument_celery

logfire-api/logfire_api/_internal/ast_utils.pyi

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import ast
2+
import executing
3+
import types
24
from .constants import ATTRIBUTES_MESSAGE_TEMPLATE_KEY as ATTRIBUTES_MESSAGE_TEMPLATE_KEY, ATTRIBUTES_SAMPLE_RATE_KEY as ATTRIBUTES_SAMPLE_RATE_KEY, ATTRIBUTES_TAGS_KEY as ATTRIBUTES_TAGS_KEY
35
from .stack_info import StackInfo as StackInfo, get_filepath_attribute as get_filepath_attribute
46
from .utils import uniquify_sequence as uniquify_sequence
7+
from _typeshed import Incomplete
8+
from abc import ABC, abstractmethod
9+
from collections.abc import Iterator
510
from dataclasses import dataclass
611
from opentelemetry.util import types as otel_types
712

@@ -31,3 +36,43 @@ class BaseTransformer(ast.NodeTransformer):
3136
def rewrite_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef, qualname: str) -> ast.AST: ...
3237
def logfire_method_call_node(self, node: ast.FunctionDef | ast.AsyncFunctionDef, qualname: str) -> ast.Call: ...
3338
def logfire_method_arg_values(self, qualname: str, lineno: int) -> tuple[str, dict[str, otel_types.AttributeValue]]: ...
39+
40+
class CallNodeFinder(ABC):
41+
"""Base class for finding the `ast.Call` node corresponding to a function call in a given frame.
42+
43+
Uses `executing`, then falls back to a heuristic if that fails.
44+
The heuristic is defined by subclasses which depends on what we're looking for,
45+
but in general `executing` is expected to work.
46+
Warns appropriately when things don't work.
47+
Only used when `inspect_arguments=True` in `logfire.configure()`.
48+
"""
49+
frame: Incomplete
50+
ex: Incomplete
51+
source: Incomplete
52+
node: Incomplete
53+
def __init__(self, frame: types.FrameType) -> None: ...
54+
@abstractmethod
55+
def heuristic_main_nodes(self) -> Iterator[ast.AST]:
56+
"""AST nodes (e.g. statements) to search for potential call nodes inside."""
57+
@abstractmethod
58+
def heuristic_call_node_filter(self, node: ast.Call) -> bool:
59+
"""Condition that a potential call node must satisfy to be considered a match."""
60+
@abstractmethod
61+
def warn_inspect_arguments_middle(self) -> str:
62+
"""Middle part of the warning message for `warn_inspect_arguments`.
63+
64+
Should describe the consequences of the failure.
65+
"""
66+
def warn_inspect_arguments(self, msg: str): ...
67+
def get_stacklevel(self): ...
68+
69+
class InspectArgumentsFailedWarning(Warning): ...
70+
71+
def get_node_source_text(node: ast.AST, ex_source: executing.Source):
72+
"""Returns some Python source code representing `node`.
73+
74+
Preferably the actual original code given by `ast.get_source_segment`,
75+
but falling back to `ast.unparse(node)` if the former is incorrect.
76+
This happens sometimes due to Python bugs (especially for older Python versions)
77+
in the source positions of AST nodes inside f-strings.
78+
"""

logfire-api/logfire_api/_internal/config.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ def configure(*, local: bool = False, send_to_logfire: bool | Literal['if-token-
135135
136136
Defaults to `True` if and only if the Python version is at least 3.11.
137137
138+
Also enables magic argument inspection in [`logfire.instrument_print()`][logfire.Logfire.instrument_print].
139+
138140
min_level:
139141
Minimum log level for logs and spans to be created. By default, all logs and spans are created.
140142
For example, set to 'info' to only create logs with level 'info' or higher, thus filtering out debug logs.

logfire-api/logfire_api/_internal/formatter.pyi

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import ast
22
import executing
33
import types
4-
from .constants import ATTRIBUTES_SCRUBBED_KEY as ATTRIBUTES_SCRUBBED_KEY, MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT as MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT
5-
from .scrubbing import BaseScrubber as BaseScrubber, NOOP_SCRUBBER as NOOP_SCRUBBER, ScrubbedNote as ScrubbedNote
4+
from .ast_utils import CallNodeFinder as CallNodeFinder, get_node_source_text as get_node_source_text
5+
from .scrubbing import BaseScrubber as BaseScrubber, MessageValueCleaner as MessageValueCleaner, NOOP_SCRUBBER as NOOP_SCRUBBER
66
from .stack_info import warn_at_user_stacklevel as warn_at_user_stacklevel
7-
from .utils import log_internal_error as log_internal_error, truncate_string as truncate_string
7+
from .utils import log_internal_error as log_internal_error
88
from _typeshed import Incomplete
9+
from collections.abc import Iterator
910
from functools import lru_cache
1011
from string import Formatter
1112
from types import CodeType as CodeType
@@ -36,19 +37,6 @@ def compile_formatted_value(node: ast.FormattedValue, ex_source: executing.Sourc
3637
2. A compiled code object which can be evaluated to calculate the value.
3738
3. Another code object which formats the value.
3839
"""
39-
def get_node_source_text(node: ast.AST, ex_source: executing.Source):
40-
"""Returns some Python source code representing `node`.
41-
42-
Preferably the actual original code given by `ast.get_source_segment`,
43-
but falling back to `ast.unparse(node)` if the former is incorrect.
44-
This happens sometimes due to Python bugs (especially for older Python versions)
45-
in the source positions of AST nodes inside f-strings.
46-
"""
47-
def get_stacklevel(frame: types.FrameType): ...
48-
49-
class InspectArgumentsFailedWarning(Warning): ...
50-
51-
def warn_inspect_arguments(msg: str, stacklevel: int): ...
5240

5341
class KnownFormattingError(Exception):
5442
"""An error raised when there's something wrong with a format string or the field values.
@@ -66,3 +54,9 @@ class FormattingFailedWarning(UserWarning): ...
6654

6755
def warn_formatting(msg: str): ...
6856
def warn_fstring_await(msg: str): ...
57+
58+
class FormattingCallNodeFinder(CallNodeFinder):
59+
"""Finds the call node corresponding to a call like `logfire.span` or `logfire.info`."""
60+
def heuristic_main_nodes(self) -> Iterator[ast.AST]: ...
61+
def heuristic_call_node_filter(self, node: ast.Call) -> bool: ...
62+
def warn_inspect_arguments_middle(self): ...
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from logfire import LevelName as LevelName, Logfire as Logfire
22
from logfire._internal.utils import handle_internal_errors as handle_internal_errors
33
from logfire.propagate import attach_context as attach_context, get_context as get_context
4+
from mcp.shared.session import ReceiveRequestT as ReceiveRequestT
5+
from mcp.types import ClientRequest as ClientRequest, ClientResult as ClientResult, ServerRequest as ServerRequest, ServerResult as ServerResult
46

57
def instrument_mcp(logfire_instance: Logfire, propagate_otel_context: bool): ...
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import ast
2+
from collections.abc import Iterator
3+
from contextlib import AbstractContextManager
4+
from logfire import Logfire as Logfire
5+
from logfire._internal.ast_utils import CallNodeFinder as CallNodeFinder, get_node_source_text as get_node_source_text
6+
from logfire._internal.constants import ATTRIBUTES_MESSAGE_KEY as ATTRIBUTES_MESSAGE_KEY
7+
from logfire._internal.scrubbing import MessageValueCleaner as MessageValueCleaner
8+
from logfire._internal.utils import handle_internal_errors as handle_internal_errors
9+
from typing import Any
10+
11+
FALLBACK_ATTRIBUTE_KEY: str
12+
13+
def instrument_print(logfire_instance: Logfire) -> AbstractContextManager[None]:
14+
"""Instruments the built-in `print` function to send logs to **Logfire**.
15+
16+
See Logfire.instrument_print for full documentation.
17+
"""
18+
19+
class PrintCallNodeFinder(CallNodeFinder):
20+
def heuristic_main_nodes(self) -> Iterator[ast.AST]: ...
21+
def heuristic_call_node_filter(self, node: ast.Call) -> bool: ...
22+
def warn_inspect_arguments_middle(self): ...
23+
def get_magic_attributes(self, args: tuple[Any, ...], value_cleaner: MessageValueCleaner) -> tuple[dict[str, Any], list[str]]:
24+
"""Inspects argument expression AST nodes.
25+
26+
Returns:
27+
- An attributes dict mapping non-literal argument expressions sources to their runtime values.
28+
- A list of strings to construct the log message.
29+
"""

logfire-api/logfire_api/_internal/main.pyi

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,24 @@ class Logfire:
582582
"""
583583
def instrument_google_genai(self) -> None: ...
584584
def instrument_litellm(self, **kwargs: Any): ...
585+
def instrument_print(self) -> AbstractContextManager[None]:
586+
"""Instrument the built-in `print` function so that calls to it are logged.
587+
588+
If Logfire is configured with [`inspect_arguments=True`][logfire.configure(inspect_arguments)],
589+
the names of the arguments passed to `print` will be included in the log attributes
590+
and will be used for scrubbing.
591+
592+
The fallback attribute name `logfire.print_args` will be used if:
593+
594+
- `inspect_arguments` is `False`
595+
- Inspection fails for any reason
596+
- Multiple starred arguments are used (e.g. `print(*args1, *args2)`)
597+
in which case names can't be unambiguously determined.
598+
599+
Returns:
600+
A context manager that will revert the instrumentation when exited.
601+
Use of this context manager is optional.
602+
"""
585603
def instrument_asyncpg(self, **kwargs: Any) -> None:
586604
"""Instrument the `asyncpg` module so that spans are automatically created for each query."""
587605
@overload

logfire-api/logfire_api/_internal/scrubbing.pyi

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import re
22
import typing_extensions
3-
from .constants import ATTRIBUTES_JSON_SCHEMA_KEY as ATTRIBUTES_JSON_SCHEMA_KEY, ATTRIBUTES_LOGGING_NAME as ATTRIBUTES_LOGGING_NAME, ATTRIBUTES_LOG_LEVEL_NAME_KEY as ATTRIBUTES_LOG_LEVEL_NAME_KEY, ATTRIBUTES_LOG_LEVEL_NUM_KEY as ATTRIBUTES_LOG_LEVEL_NUM_KEY, ATTRIBUTES_MESSAGE_KEY as ATTRIBUTES_MESSAGE_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY as ATTRIBUTES_MESSAGE_TEMPLATE_KEY, ATTRIBUTES_PENDING_SPAN_REAL_PARENT_KEY as ATTRIBUTES_PENDING_SPAN_REAL_PARENT_KEY, ATTRIBUTES_SAMPLE_RATE_KEY as ATTRIBUTES_SAMPLE_RATE_KEY, ATTRIBUTES_SCRUBBED_KEY as ATTRIBUTES_SCRUBBED_KEY, ATTRIBUTES_SPAN_TYPE_KEY as ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY as ATTRIBUTES_TAGS_KEY, RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS as RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS
3+
from .constants import ATTRIBUTES_JSON_SCHEMA_KEY as ATTRIBUTES_JSON_SCHEMA_KEY, ATTRIBUTES_LOGGING_NAME as ATTRIBUTES_LOGGING_NAME, ATTRIBUTES_LOG_LEVEL_NAME_KEY as ATTRIBUTES_LOG_LEVEL_NAME_KEY, ATTRIBUTES_LOG_LEVEL_NUM_KEY as ATTRIBUTES_LOG_LEVEL_NUM_KEY, ATTRIBUTES_MESSAGE_KEY as ATTRIBUTES_MESSAGE_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY as ATTRIBUTES_MESSAGE_TEMPLATE_KEY, ATTRIBUTES_PENDING_SPAN_REAL_PARENT_KEY as ATTRIBUTES_PENDING_SPAN_REAL_PARENT_KEY, ATTRIBUTES_SAMPLE_RATE_KEY as ATTRIBUTES_SAMPLE_RATE_KEY, ATTRIBUTES_SCRUBBED_KEY as ATTRIBUTES_SCRUBBED_KEY, ATTRIBUTES_SPAN_TYPE_KEY as ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY as ATTRIBUTES_TAGS_KEY, MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT as MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT, RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS as RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS
44
from .stack_info import STACK_INFO_KEYS as STACK_INFO_KEYS
5-
from .utils import ReadableSpanDict as ReadableSpanDict
5+
from .utils import ReadableSpanDict as ReadableSpanDict, truncate_string as truncate_string
66
from _typeshed import Incomplete
77
from abc import ABC, abstractmethod
88
from collections.abc import Sequence
@@ -73,3 +73,27 @@ class SpanScrubber:
7373
`path` is a list of keys and indices leading to `value` in the span.
7474
Similar to the truncation code, it should use the field names in the frontend, e.g. `otel_events`.
7575
"""
76+
77+
class MessageValueCleaner:
78+
"""Scrubs and truncates formatted field values to be included in the message attribute.
79+
80+
Use to construct the message for a single span, e.g:
81+
82+
cleaner = MessageValueCleaner(scrubber, check_keys=...)
83+
message_parts = [cleaner.clean_value(field_name, str(value)) for field_name, value in fields]
84+
message = <construct from message parts>
85+
attributes = {**other_attributes, **cleaner.extra_attrs(), ATTRIBUTES_MESSAGE_KEY: message}
86+
87+
check_keys determines whether the key should be accounted for in scrubbing.
88+
Set to False if the user explicitly provided the key, e.g. `logfire.info(f'... {password} ...')`
89+
means that the password is clearly expected to be logged.
90+
The password will therefore not be scrubbed here and will appear in the message.
91+
However it may still be scrubbed out of the attributes, just because that process is independent.
92+
"""
93+
scrubber: Incomplete
94+
scrubbed: list[ScrubbedNote]
95+
check_keys: Incomplete
96+
def __init__(self, scrubber: BaseScrubber, *, check_keys: bool) -> None: ...
97+
def clean_value(self, field_name: str, value: str) -> str: ...
98+
def truncate(self, value: str) -> str: ...
99+
def extra_attrs(self) -> dict[str, Any]: ...

logfire-api/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "logfire-api"
7-
version = "4.3.6"
7+
version = "4.4.0"
88
description = "Shim for the Logfire SDK which does nothing unless Logfire is installed"
99
authors = [
1010
{ name = "Pydantic Team", email = "[email protected]" },

0 commit comments

Comments
 (0)