diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c8407e7..c9a67fa75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release Notes +## [v4.8.0] (2025-09-18) + +* Allow capturing headers and response body with `logfire.instrument_aiohttp_client()` by @adtyavrdhn in [#1405](https://github.com/pydantic/logfire/pull/1405) and [#1409](https://github.com/pydantic/logfire/pull/1409) + ## [v4.7.0] (2025-09-12) * Update to OpenTelemetry SDK 1.37.0, drop support for <1.35.0 by @alexmojaki in [#1398](https://github.com/pydantic/logfire/pull/1398) @@ -901,3 +905,4 @@ First release from new repo! [v4.5.0]: https://github.com/pydantic/logfire/compare/v4.4.0...v4.5.0 [v4.6.0]: https://github.com/pydantic/logfire/compare/v4.5.0...v4.6.0 [v4.7.0]: https://github.com/pydantic/logfire/compare/v4.6.0...v4.7.0 +[v4.8.0]: https://github.com/pydantic/logfire/compare/v4.7.0...v4.8.0 diff --git a/logfire-api/logfire_api/_internal/integrations/aiohttp_client.pyi b/logfire-api/logfire_api/_internal/integrations/aiohttp_client.pyi index a109a1b5d..953bd98c9 100644 --- a/logfire-api/logfire_api/_internal/integrations/aiohttp_client.pyi +++ b/logfire-api/logfire_api/_internal/integrations/aiohttp_client.pyi @@ -1,8 +1,45 @@ -from logfire import Logfire as Logfire -from typing import Any +from aiohttp.client_reqrep import ClientResponse +from aiohttp.tracing import TraceRequestEndParams, TraceRequestExceptionParams, TraceRequestStartParams +from logfire import Logfire as Logfire, LogfireSpan as LogfireSpan +from logfire._internal.utils import handle_internal_errors as handle_internal_errors +from logfire.integrations.aiohttp_client import AioHttpRequestHeaders as AioHttpRequestHeaders, AioHttpResponseHeaders as AioHttpResponseHeaders, RequestHook as RequestHook, ResponseHook as ResponseHook +from opentelemetry.trace import Span +from typing import Any, Callable, Literal, ParamSpec +from yarl import URL -def instrument_aiohttp_client(logfire_instance: Logfire, **kwargs: Any): +P = ParamSpec('P') + +def instrument_aiohttp_client(logfire_instance: Logfire, capture_response_body: bool, capture_headers: bool, request_hook: RequestHook | None, response_hook: ResponseHook | None, **kwargs: Any) -> None: """Instrument the `aiohttp` module so that spans are automatically created for each client request. See the `Logfire.instrument_aiohttp_client` method for details. """ + +class LogfireClientInfoMixin: + headers: AioHttpRequestHeaders + +class LogfireAioHttpRequestInfo(TraceRequestStartParams, LogfireClientInfoMixin): + span: Span + def capture_headers(self) -> None: ... + +class LogfireAioHttpResponseInfo(LogfireClientInfoMixin): + span: Span + method: str + url: URL + headers: AioHttpRequestHeaders + response: ClientResponse | None + exception: BaseException | None + logfire_instance: Logfire + body_captured: bool + def capture_headers(self) -> None: ... + def capture_body_if_text(self, attr_name: str = 'http.response.body.text') -> None: ... + def capture_text_as_json(self, span: LogfireSpan, *, text: str, attr_name: str) -> None: ... + @classmethod + def create_from_trace_params(cls, span: Span, params: TraceRequestEndParams | TraceRequestExceptionParams, logfire_instance: Logfire) -> LogfireAioHttpResponseInfo: ... + +def make_request_hook(hook: RequestHook | None, capture_headers: bool) -> RequestHook | None: ... +def make_response_hook(hook: ResponseHook | None, logfire_instance: Logfire, capture_headers: bool, capture_response_body: bool) -> ResponseHook | None: ... +def capture_request(span: Span, request: TraceRequestStartParams, capture_headers: bool) -> LogfireAioHttpRequestInfo: ... +def capture_response(span: Span, response: TraceRequestEndParams | TraceRequestExceptionParams, logfire_instance: Logfire, capture_headers: bool, capture_response_body: bool) -> LogfireAioHttpResponseInfo: ... +def run_hook(hook: Callable[P, Any] | None, *args: P.args, **kwargs: P.kwargs) -> None: ... +def capture_request_or_response_headers(span: Span, headers: AioHttpRequestHeaders | AioHttpResponseHeaders, request_or_response: Literal['request', 'response']) -> None: ... diff --git a/logfire-api/logfire_api/_internal/integrations/google_genai.pyi b/logfire-api/logfire_api/_internal/integrations/google_genai.pyi index 389a671b0..3888a8b11 100644 --- a/logfire-api/logfire_api/_internal/integrations/google_genai.pyi +++ b/logfire-api/logfire_api/_internal/integrations/google_genai.pyi @@ -22,4 +22,4 @@ def transform_part(part: Part) -> Part: ... class SpanEventLoggerProvider(EventLoggerProvider): def get_event_logger(self, *args: Any, **kwargs: Any) -> SpanEventLogger: ... -def instrument_google_genai(logfire_instance: logfire.Logfire): ... +def instrument_google_genai(logfire_instance: logfire.Logfire, **kwargs: Any): ... diff --git a/logfire-api/logfire_api/_internal/main.pyi b/logfire-api/logfire_api/_internal/main.pyi index e86a6e06c..5cbe73a12 100644 --- a/logfire-api/logfire_api/_internal/main.pyi +++ b/logfire-api/logfire_api/_internal/main.pyi @@ -6,6 +6,7 @@ import pydantic_ai import pydantic_ai.models import requests from . import async_ as async_ +from ..integrations.aiohttp_client import RequestHook as AiohttpClientRequestHook, ResponseHook as AiohttpClientResponseHook from ..integrations.flask import CommenterOptions as FlaskCommenterOptions, RequestHook as FlaskRequestHook, ResponseHook as FlaskResponseHook from ..integrations.httpx import AsyncRequestHook as HttpxAsyncRequestHook, AsyncResponseHook as HttpxAsyncResponseHook, RequestHook as HttpxRequestHook, ResponseHook as HttpxResponseHook from ..integrations.psycopg import CommenterOptions as PsycopgCommenterOptions @@ -401,10 +402,14 @@ class Logfire: Set to `'warn'` to issue a warning instead, or `'ignore'` to skip the check. """ def instrument_mcp(self, *, propagate_otel_context: bool = True) -> None: - """Instrument [MCP](https://modelcontextprotocol.io/) requests such as tool calls. + """Instrument the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk). + + Instruments both the client and server side. If possible, calling this in both the client and server + processes is recommended for nice distributed traces. Args: - propagate_otel_context: Whether to enable propagation of the OpenTelemetry context. + propagate_otel_context: Whether to enable propagation of the OpenTelemetry context + for distributed tracing. Set to False to prevent setting extra fields like `traceparent` on the metadata of requests. """ def instrument_pydantic(self, record: PydanticPluginRecordValues = 'all', include: Iterable[str] = (), exclude: Iterable[str] = ()) -> None: @@ -577,7 +582,17 @@ class Logfire: A context manager that will revert the instrumentation when exited. Use of this context manager is optional. """ - def instrument_google_genai(self) -> None: ... + def instrument_google_genai(self, **kwargs: Any): + """Instrument the [Google Gen AI SDK (`google-genai`)](https://googleapis.github.io/python-genai/). + + !!! note + To capture message contents (i.e. prompts and completions), set the environment variable + `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` to `true`. + + Uses the `GoogleGenAiSdkInstrumentor().instrument()` method of the + [`opentelemetry-instrumentation-google-genai`](https://pypi.org/project/opentelemetry-instrumentation-google-genai/) + package, to which it passes `**kwargs`. + """ def instrument_litellm(self, **kwargs: Any): ... def instrument_print(self) -> AbstractContextManager[None]: """Instrument the built-in `print` function so that calls to it are logged. @@ -738,7 +753,7 @@ class Logfire: Returns: The instrumented WSGI application. """ - def instrument_aiohttp_client(self, **kwargs: Any) -> None: + def instrument_aiohttp_client(self, *, capture_headers: bool = False, capture_response_body: bool = False, request_hook: AiohttpClientRequestHook | None = None, response_hook: AiohttpClientResponseHook | None = None, **kwargs: Any) -> None: """Instrument the `aiohttp` module so that spans are automatically created for each client request. Uses the diff --git a/logfire-api/logfire_api/integrations/aiohttp_client.pyi b/logfire-api/logfire_api/integrations/aiohttp_client.pyi new file mode 100644 index 000000000..7ae8bbcb2 --- /dev/null +++ b/logfire-api/logfire_api/integrations/aiohttp_client.pyi @@ -0,0 +1,9 @@ +from aiohttp.tracing import TraceRequestEndParams, TraceRequestExceptionParams, TraceRequestStartParams +from collections.abc import Callable +from multidict import CIMultiDict, CIMultiDictProxy +from opentelemetry.trace import Span + +AioHttpRequestHeaders = CIMultiDict[str] +AioHttpResponseHeaders = CIMultiDictProxy[str] +RequestHook = Callable[[Span, TraceRequestStartParams], None] +ResponseHook = Callable[[Span, TraceRequestEndParams | TraceRequestExceptionParams], None] diff --git a/logfire-api/pyproject.toml b/logfire-api/pyproject.toml index 9f16c70af..6a52bd8bf 100644 --- a/logfire-api/pyproject.toml +++ b/logfire-api/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire-api" -version = "4.7.0" +version = "4.8.0" description = "Shim for the Logfire SDK which does nothing unless Logfire is installed" authors = [ { name = "Pydantic Team", email = "engineering@pydantic.dev" }, diff --git a/pyproject.toml b/pyproject.toml index 93981c96a..0a5d590a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire" -version = "4.7.0" +version = "4.8.0" description = "The best Python observability tool! 🪵🔥" requires-python = ">=3.9" authors = [ diff --git a/uv.lock b/uv.lock index 6aa8bb00f..7871d7460 100644 --- a/uv.lock +++ b/uv.lock @@ -1857,7 +1857,7 @@ wheels = [ [[package]] name = "logfire" -version = "4.7.0" +version = "4.8.0" source = { editable = "." } dependencies = [ { name = "executing" }, @@ -1908,6 +1908,9 @@ google-genai = [ httpx = [ { name = "opentelemetry-instrumentation-httpx" }, ] +litellm = [ + { name = "openinference-instrumentation-litellm" }, +] mysql = [ { name = "opentelemetry-instrumentation-mysql" }, ] @@ -2047,6 +2050,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "executing", specifier = ">=2.0.1" }, + { name = "openinference-instrumentation-litellm", marker = "extra == 'litellm'", specifier = ">=0" }, { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.35.0,<1.38.0" }, { name = "opentelemetry-instrumentation", specifier = ">=0.41b0" }, { name = "opentelemetry-instrumentation-aiohttp-client", marker = "extra == 'aiohttp'", specifier = ">=0.42b0" }, @@ -2081,7 +2085,7 @@ requires-dist = [ { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.1" }, { name = "typing-extensions", specifier = ">=4.1.0" }, ] -provides-extras = ["system-metrics", "asgi", "wsgi", "aiohttp", "aiohttp-client", "aiohttp-server", "celery", "django", "fastapi", "flask", "httpx", "starlette", "sqlalchemy", "asyncpg", "psycopg", "psycopg2", "pymongo", "redis", "requests", "mysql", "sqlite3", "aws-lambda", "google-genai"] +provides-extras = ["system-metrics", "asgi", "wsgi", "aiohttp", "aiohttp-client", "aiohttp-server", "celery", "django", "fastapi", "flask", "httpx", "starlette", "sqlalchemy", "asyncpg", "psycopg", "psycopg2", "pymongo", "redis", "requests", "mysql", "sqlite3", "aws-lambda", "google-genai", "litellm"] [package.metadata.requires-dev] dev = [ @@ -2182,7 +2186,7 @@ docs = [ [[package]] name = "logfire-api" -version = "4.7.0" +version = "4.8.0" source = { editable = "logfire-api" } [package.metadata]