diff --git a/CHANGELOG.md b/CHANGELOG.md index 50f61c2e..c9912754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Release Notes +## [v2.11.0] (2024-12-23) + +* Add `capture_request_text_body` param to `instrument_httpx` by @alexmojaki in [#722](https://github.com/pydantic/logfire/pull/722) +* Support for `AnthropicBedrock` client by @stephenhibbert in [#701](https://github.com/pydantic/logfire/pull/701) + ## [v2.10.0] (2024-12-23) * Add `capture_request_form_data` param to `instrument_httpx` by @alexmojaki in [#711](https://github.com/pydantic/logfire/pull/711) @@ -493,3 +498,4 @@ First release from new repo! [v2.8.0]: https://github.com/pydantic/logfire/compare/v2.7.1...v2.8.0 [v2.9.0]: https://github.com/pydantic/logfire/compare/v2.8.0...v2.9.0 [v2.10.0]: https://github.com/pydantic/logfire/compare/v2.9.0...v2.10.0 +[v2.11.0]: https://github.com/pydantic/logfire/compare/v2.10.0...v2.11.0 diff --git a/logfire-api/logfire_api/_internal/integrations/httpx.pyi b/logfire-api/logfire_api/_internal/integrations/httpx.pyi index 23bd1078..56cab73c 100644 --- a/logfire-api/logfire_api/_internal/integrations/httpx.pyi +++ b/logfire-api/logfire_api/_internal/integrations/httpx.pyi @@ -8,7 +8,7 @@ from logfire._internal.utils import handle_internal_errors as handle_internal_er from logfire.integrations.httpx import AsyncRequestHook as AsyncRequestHook, AsyncResponseHook as AsyncResponseHook, RequestHook as RequestHook, RequestInfo as RequestInfo, ResponseHook as ResponseHook, ResponseInfo as ResponseInfo from logfire.propagate import attach_context as attach_context, get_context as get_context from opentelemetry.trace import Span -from typing import Any, Callable, Literal, ParamSpec, TypeVar, TypedDict +from typing import Any, Callable, Literal, Mapping, ParamSpec, TypeVar, TypedDict class AsyncClientKwargs(TypedDict, total=False): request_hook: RequestHook | AsyncRequestHook @@ -32,28 +32,55 @@ Hook = TypeVar('Hook', RequestHook, ResponseHook) AsyncHook = TypeVar('AsyncHook', AsyncRequestHook, AsyncResponseHook) P = ParamSpec('P') -def instrument_httpx(logfire_instance: Logfire, client: httpx.Client | httpx.AsyncClient | None, capture_headers: bool, capture_request_json_body: bool, capture_response_json_body: bool, capture_request_form_data: bool, **kwargs: Any) -> None: +def instrument_httpx(logfire_instance: Logfire, client: httpx.Client | httpx.AsyncClient | None, capture_headers: bool, capture_request_json_body: bool, capture_request_text_body: bool, capture_response_json_body: bool, capture_request_form_data: bool, **kwargs: Any) -> None: """Instrument the `httpx` module so that spans are automatically created for each request. See the `Logfire.instrument_httpx` method for details. """ -def make_request_hook(hook: RequestHook | None, should_capture_headers: bool, should_capture_json: bool, should_capture_form_data: bool) -> RequestHook | None: ... -def make_async_request_hook(hook: AsyncRequestHook | RequestHook | None, should_capture_headers: bool, should_capture_json: bool, should_capture_form_data: bool) -> AsyncRequestHook | None: ... -def capture_request(request: RequestInfo, span: Span, should_capture_headers: bool, should_capture_json: bool, should_capture_form_data: bool) -> None: ... + +class LogfireHttpxRequestInfo(RequestInfo): + span: Span + def capture_headers(self) -> None: ... + def capture_body_if_json(self, attr_name: str = 'http.request.body.json'): ... + def capture_body_if_text(self, attr_name: str = 'http.request.body.text'): ... + def capture_body_if_form(self, attr_name: str = 'http.request.body.form'): ... + def capture_text_as_json(self, attr_name: str = 'http.request.body.json', text: str | None = None): ... + @property + def body_is_streaming(self): ... + @property + def content_type_header_object(self) -> ContentTypeHeader: ... + @property + def content_type_header_string(self) -> str: ... + @property + def content_type_is_json(self): ... + @property + def content_type_is_text(self): ... + @property + def content_type_is_form(self): ... + @property + def content_type_charset(self): ... + @property + def content(self) -> bytes: ... + @property + def text(self) -> str: ... + @property + def form_data(self) -> Mapping[str, Any] | None: ... + def set_complex_span_attributes(self, attributes: dict[str, Any]): ... + +def make_request_hook(hook: RequestHook | None, should_capture_headers: bool, should_capture_json: bool, should_capture_text: bool, should_capture_form_data: bool) -> RequestHook | None: ... +def make_async_request_hook(hook: AsyncRequestHook | RequestHook | None, should_capture_headers: bool, should_capture_json: bool, should_capture_text: bool, should_capture_form_data: bool) -> AsyncRequestHook | None: ... +def capture_request(request: LogfireHttpxRequestInfo, should_capture_headers: bool, should_capture_json: bool, should_capture_text: bool, should_capture_form_data: bool) -> None: ... def make_response_hook(hook: ResponseHook | None, should_capture_headers: bool, should_capture_json: bool, logfire_instance: Logfire) -> ResponseHook | None: ... def make_async_response_hook(hook: ResponseHook | AsyncResponseHook | None, should_capture_headers: bool, should_capture_json: bool, logfire_instance: Logfire) -> AsyncResponseHook | None: ... def capture_response_json(logfire_instance: Logfire, response_info: ResponseInfo, is_async: bool) -> None: ... async def run_async_hook(hook: Callable[P, Any] | None, *args: P.args, **kwargs: P.kwargs) -> None: ... def run_hook(hook: Callable[P, Any] | None, *args: P.args, **kwargs: P.kwargs) -> None: ... def capture_response_headers(span: Span, response: ResponseInfo) -> None: ... -def capture_request_headers(span: Span, request: RequestInfo) -> None: ... def capture_headers(span: Span, headers: httpx.Headers, request_or_response: Literal['request', 'response']) -> None: ... def decode_body(body: bytes, charset: str): ... -def capture_request_body(span: Span, request: RequestInfo) -> None: ... CODES_FOR_METHODS_WITH_DATA_PARAM: Incomplete -def capture_request_form_data(span: Span, request: RequestInfo) -> None: ... def content_type_header_from_string(content_type: str) -> ContentTypeHeader: ... def content_type_subtypes(subtype: str) -> set[str]: ... def is_json_type(content_type: str) -> bool: ... diff --git a/logfire-api/logfire_api/_internal/main.pyi b/logfire-api/logfire_api/_internal/main.pyi index f56a6f15..f8e8af29 100644 --- a/logfire-api/logfire_api/_internal/main.pyi +++ b/logfire-api/logfire_api/_internal/main.pyi @@ -501,10 +501,10 @@ class Logfire: A context manager that will revert the instrumentation when exited. Use of this context manager is optional. """ - def instrument_anthropic(self, anthropic_client: anthropic.Anthropic | anthropic.AsyncAnthropic | type[anthropic.Anthropic] | type[anthropic.AsyncAnthropic] | None = None, *, suppress_other_instrumentation: bool = True) -> ContextManager[None]: + def instrument_anthropic(self, anthropic_client: anthropic.Anthropic | anthropic.AsyncAnthropic | anthropic.AnthropicBedrock | anthropic.AsyncAnthropicBedrock | type[anthropic.Anthropic] | type[anthropic.AsyncAnthropic] | type[anthropic.AnthropicBedrock] | type[anthropic.AsyncAnthropicBedrock] | None = None, *, suppress_other_instrumentation: bool = True) -> ContextManager[None]: """Instrument an Anthropic client so that spans are automatically created for each request. - The following methods are instrumented for both the sync and the async clients: + The following methods are instrumented for both the sync and async clients: - [`client.messages.create`](https://docs.anthropic.com/en/api/messages) - [`client.messages.stream`](https://docs.anthropic.com/en/api/messages-streaming) @@ -519,6 +519,7 @@ class Logfire: import anthropic client = anthropic.Anthropic() + logfire.configure() logfire.instrument_anthropic(client) @@ -534,13 +535,10 @@ class Logfire: Args: anthropic_client: The Anthropic client or class to instrument: - - - `None` (the default) to instrument both the - `anthropic.Anthropic` and `anthropic.AsyncAnthropic` classes. - - The `anthropic.Anthropic` class or a subclass - - The `anthropic.AsyncAnthropic` class or a subclass - - An instance of `anthropic.Anthropic` - - An instance of `anthropic.AsyncAnthropic` + - `None` (the default) to instrument all Anthropic client types + - The `anthropic.Anthropic` or `anthropic.AnthropicBedrock` class or subclass + - The `anthropic.AsyncAnthropic` or `anthropic.AsyncAnthropicBedrock` class or subclass + - An instance of any of the above classes suppress_other_instrumentation: If True, suppress any other OTEL instrumentation that may be otherwise enabled. In reality, this means the HTTPX instrumentation, which could otherwise be called since @@ -553,11 +551,11 @@ class Logfire: def instrument_asyncpg(self, **kwargs: Unpack[AsyncPGInstrumentKwargs]) -> None: """Instrument the `asyncpg` module so that spans are automatically created for each query.""" @overload - def instrument_httpx(self, client: httpx.Client, *, capture_headers: bool = False, capture_request_json_body: bool = False, capture_response_json_body: bool = False, capture_request_form_data: bool = False, **kwargs: Unpack[ClientKwargs]) -> None: ... + def instrument_httpx(self, client: httpx.Client, *, capture_headers: bool = False, capture_request_text_body: bool = False, capture_request_json_body: bool = False, capture_response_json_body: bool = False, capture_request_form_data: bool = False, **kwargs: Unpack[ClientKwargs]) -> None: ... @overload - def instrument_httpx(self, client: httpx.AsyncClient, *, capture_headers: bool = False, capture_request_json_body: bool = False, capture_response_json_body: bool = False, capture_request_form_data: bool = False, **kwargs: Unpack[AsyncClientKwargs]) -> None: ... + def instrument_httpx(self, client: httpx.AsyncClient, *, capture_headers: bool = False, capture_request_json_body: bool = False, capture_request_text_body: bool = False, capture_response_json_body: bool = False, capture_request_form_data: bool = False, **kwargs: Unpack[AsyncClientKwargs]) -> None: ... @overload - def instrument_httpx(self, client: None = None, *, capture_headers: bool = False, capture_request_json_body: bool = False, capture_response_json_body: bool = False, capture_request_form_data: bool = False, **kwargs: Unpack[HTTPXInstrumentKwargs]) -> None: ... + def instrument_httpx(self, client: None = None, *, capture_headers: bool = False, capture_request_json_body: bool = False, capture_request_text_body: bool = False, capture_response_json_body: bool = False, capture_request_form_data: bool = False, **kwargs: Unpack[HTTPXInstrumentKwargs]) -> None: ... def instrument_celery(self, **kwargs: Any) -> None: """Instrument `celery` so that spans are automatically created for each task. diff --git a/logfire-api/pyproject.toml b/logfire-api/pyproject.toml index e16223ef..9eeb86f5 100644 --- a/logfire-api/pyproject.toml +++ b/logfire-api/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire-api" -version = "2.10.0" +version = "2.11.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 085f1b86..836623ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire" -version = "2.10.0" +version = "2.11.0" description = "The best Python observability tool! 🪵🔥" requires-python = ">=3.8" authors = [ diff --git a/uv.lock b/uv.lock index af502d41..1f5be2fa 100644 --- a/uv.lock +++ b/uv.lock @@ -1435,7 +1435,7 @@ wheels = [ [[package]] name = "logfire" -version = "2.10.0" +version = "2.11.0" source = { editable = "." } dependencies = [ { name = "executing" }, @@ -1716,7 +1716,7 @@ docs = [ [[package]] name = "logfire-api" -version = "2.10.0" +version = "2.11.0" source = { editable = "logfire-api" } [package.metadata]