diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e7316f8a..d86a0b8ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Release Notes +## [v2.8.0] (2024-12-18) + +* Add `capture_(request|response)_headers` ([#671](https://github.com/pydantic/logfire/pull/671)) and `capture_request_json_body` ([#682](https://github.com/pydantic/logfire/pull/682)) to `instrument_httpx` by @Kludex +* Fix patching of ProcessPoolExecutor by @alexmojaki in [#690](https://github.com/pydantic/logfire/pull/690) +* Rearrange span processors to avoid repeating scrubbing and other tweaking by @alexmojaki in [#658](https://github.com/pydantic/logfire/pull/658) +* Remove end-on-exit stuff by @dmontagu in [#676](https://github.com/pydantic/logfire/pull/676) + ## [v2.7.1] (2024-12-13) * Fix erroneous `` when object is repeated in list by @alexmojaki in [#664](https://github.com/pydantic/logfire/pull/664) @@ -472,3 +479,4 @@ First release from new repo! [v2.6.2]: https://github.com/pydantic/logfire/compare/v2.6.1...v2.6.2 [v2.7.0]: https://github.com/pydantic/logfire/compare/v2.6.2...v2.7.0 [v2.7.1]: https://github.com/pydantic/logfire/compare/v2.7.0...v2.7.1 +[v2.8.0]: https://github.com/pydantic/logfire/compare/v2.7.1...v2.8.0 diff --git a/logfire-api/logfire_api/_internal/config.pyi b/logfire-api/logfire_api/_internal/config.pyi index b7b194f22..26c03b6a0 100644 --- a/logfire-api/logfire_api/_internal/config.pyi +++ b/logfire-api/logfire_api/_internal/config.pyi @@ -7,7 +7,7 @@ from .exporters.console import ConsoleColorsValues as ConsoleColorsValues, Inden from .exporters.fallback import FallbackSpanExporter as FallbackSpanExporter from .exporters.file import FileSpanExporter as FileSpanExporter from .exporters.otlp import OTLPExporterHttpSession as OTLPExporterHttpSession, RetryFewerSpansSpanExporter as RetryFewerSpansSpanExporter -from .exporters.processor_wrapper import MainSpanProcessorWrapper as MainSpanProcessorWrapper +from .exporters.processor_wrapper import CheckSuppressInstrumentationProcessorWrapper as CheckSuppressInstrumentationProcessorWrapper, MainSpanProcessorWrapper as MainSpanProcessorWrapper from .exporters.quiet_metrics import QuietMetricExporter as QuietMetricExporter from .exporters.remove_pending import RemovePendingSpansExporter as RemovePendingSpansExporter from .exporters.test import TestExporter as TestExporter diff --git a/logfire-api/logfire_api/_internal/exporters/processor_wrapper.pyi b/logfire-api/logfire_api/_internal/exporters/processor_wrapper.pyi index 2145f063a..4b755f3c7 100644 --- a/logfire-api/logfire_api/_internal/exporters/processor_wrapper.pyi +++ b/logfire-api/logfire_api/_internal/exporters/processor_wrapper.pyi @@ -3,17 +3,25 @@ from ..db_statement_summary import message_from_db_statement as message_from_db_ from ..scrubbing import BaseScrubber as BaseScrubber from ..utils import ReadableSpanDict as ReadableSpanDict, is_asgi_send_receive_span_name as is_asgi_send_receive_span_name, is_instrumentation_suppressed as is_instrumentation_suppressed, span_to_dict as span_to_dict, truncate_string as truncate_string from .wrapper import WrapperSpanProcessor as WrapperSpanProcessor -from _typeshed import Incomplete +from dataclasses import dataclass from opentelemetry import context -from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor +from opentelemetry.sdk.trace import ReadableSpan, Span +class CheckSuppressInstrumentationProcessorWrapper(WrapperSpanProcessor): + """Checks if instrumentation is suppressed, then suppresses instrumentation itself. + + Placed at the root of the tree of processors. + """ + def on_start(self, span: Span, parent_context: context.Context | None = None) -> None: ... + def on_end(self, span: ReadableSpan) -> None: ... + +@dataclass class MainSpanProcessorWrapper(WrapperSpanProcessor): """Wrapper around other processors to intercept starting and ending spans with our own global logic. Suppresses starting/ending if the current context has a `suppress_instrumentation` value. Tweaks the send/receive span names generated by the ASGI middleware. """ - scrubber: Incomplete - def __init__(self, processor: SpanProcessor, scrubber: BaseScrubber) -> None: ... + scrubber: BaseScrubber def on_start(self, span: Span, parent_context: context.Context | None = None) -> None: ... def on_end(self, span: ReadableSpan) -> None: ... diff --git a/logfire-api/logfire_api/_internal/exporters/wrapper.pyi b/logfire-api/logfire_api/_internal/exporters/wrapper.pyi index c329d4a07..877deaf3d 100644 --- a/logfire-api/logfire_api/_internal/exporters/wrapper.pyi +++ b/logfire-api/logfire_api/_internal/exporters/wrapper.pyi @@ -1,4 +1,5 @@ from _typeshed import Incomplete +from dataclasses import dataclass from opentelemetry import context from opentelemetry.sdk.metrics.export import AggregationTemporality as AggregationTemporality, MetricExportResult, MetricExporter, MetricsData from opentelemetry.sdk.metrics.view import Aggregation as Aggregation @@ -22,10 +23,10 @@ class WrapperMetricExporter(MetricExporter): def force_flush(self, timeout_millis: float = 10000) -> bool: ... def shutdown(self, timeout_millis: float = 30000, **kwargs: Any) -> None: ... +@dataclass class WrapperSpanProcessor(SpanProcessor): """A base class for SpanProcessors that wrap another processor.""" - processor: Incomplete - def __init__(self, processor: SpanProcessor) -> None: ... + processor: SpanProcessor def on_start(self, span: Span, parent_context: context.Context | None = None) -> None: ... def on_end(self, span: ReadableSpan) -> None: ... def shutdown(self) -> None: ... diff --git a/logfire-api/logfire_api/_internal/integrations/httpx.pyi b/logfire-api/logfire_api/_internal/integrations/httpx.pyi index b829da3be..22f5f2951 100644 --- a/logfire-api/logfire_api/_internal/integrations/httpx.pyi +++ b/logfire-api/logfire_api/_internal/integrations/httpx.pyi @@ -1,5 +1,7 @@ import httpx from logfire import Logfire as Logfire +from logfire._internal.main import set_user_attributes_on_raw_span as set_user_attributes_on_raw_span +from logfire._internal.utils import handle_internal_errors as handle_internal_errors from opentelemetry.instrumentation.httpx import AsyncRequestHook, AsyncResponseHook, RequestHook, RequestInfo, ResponseHook, ResponseInfo from opentelemetry.trace import Span from typing import Any, Callable, Literal, ParamSpec, TypeVar, TypedDict, Unpack, overload @@ -27,17 +29,20 @@ AsyncHook = TypeVar('AsyncHook', AsyncRequestHook, AsyncResponseHook) P = ParamSpec('P') @overload -def instrument_httpx(logfire_instance: Logfire, client: httpx.Client, capture_request_headers: bool, capture_response_headers: bool, **kwargs: Unpack[ClientKwargs]) -> None: ... +def instrument_httpx(logfire_instance: Logfire, client: httpx.Client, capture_request_headers: bool, capture_response_headers: bool, capture_request_json_body: bool, **kwargs: Unpack[ClientKwargs]) -> None: ... @overload -def instrument_httpx(logfire_instance: Logfire, client: httpx.AsyncClient, capture_request_headers: bool, capture_response_headers: bool, **kwargs: Unpack[AsyncClientKwargs]) -> None: ... +def instrument_httpx(logfire_instance: Logfire, client: httpx.AsyncClient, capture_request_headers: bool, capture_response_headers: bool, capture_request_json_body: bool, **kwargs: Unpack[AsyncClientKwargs]) -> None: ... @overload -def instrument_httpx(logfire_instance: Logfire, client: None, capture_request_headers: bool, capture_response_headers: bool, **kwargs: Unpack[HTTPXInstrumentKwargs]) -> None: ... -def make_capture_response_headers_hook(hook: ResponseHook | None) -> ResponseHook: ... -def make_capture_async_response_headers_hook(hook: AsyncResponseHook | None) -> AsyncResponseHook: ... -def make_capture_request_headers_hook(hook: RequestHook | None) -> RequestHook: ... -def make_capture_async_request_headers_hook(hook: AsyncRequestHook | None) -> AsyncRequestHook: ... +def instrument_httpx(logfire_instance: Logfire, client: None, capture_request_headers: bool, capture_response_headers: bool, capture_request_json_body: bool, **kwargs: Unpack[HTTPXInstrumentKwargs]) -> None: ... +def make_request_hook(hook: RequestHook | None, should_capture_headers: bool, should_capture_json: bool) -> RequestHook | None: ... +def make_async_request_hook(hook: AsyncRequestHook | RequestHook | None, should_capture_headers: bool, should_capture_json: bool) -> AsyncRequestHook | None: ... +def make_response_hook(hook: ResponseHook | None, should_capture_headers: bool) -> ResponseHook | None: ... +def make_async_response_hook(hook: ResponseHook | AsyncResponseHook | None, should_capture_headers: bool) -> AsyncResponseHook | 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, request: RequestInfo, response: ResponseInfo) -> 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 get_charset(content_type: str) -> str: ... +def decode_body(body: bytes, content_type: str): ... +def capture_request_body(span: Span, request: RequestInfo) -> None: ... diff --git a/logfire-api/logfire_api/_internal/main.pyi b/logfire-api/logfire_api/_internal/main.pyi index ab3414953..208cc278b 100644 --- a/logfire-api/logfire_api/_internal/main.pyi +++ b/logfire-api/logfire_api/_internal/main.pyi @@ -551,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_request_headers: bool = False, capture_response_headers: bool = False, **kwargs: Unpack[ClientKwargs]) -> None: ... + def instrument_httpx(self, client: httpx.Client, capture_request_headers: bool = False, capture_response_headers: bool = False, capture_request_json_body: bool = False, **kwargs: Unpack[ClientKwargs]) -> None: ... @overload - def instrument_httpx(self, client: httpx.AsyncClient, capture_request_headers: bool = False, capture_response_headers: bool = False, **kwargs: Unpack[AsyncClientKwargs]) -> None: ... + def instrument_httpx(self, client: httpx.AsyncClient, capture_request_headers: bool = False, capture_response_headers: bool = False, capture_request_json_body: bool = False, **kwargs: Unpack[AsyncClientKwargs]) -> None: ... @overload - def instrument_httpx(self, client: None = None, capture_request_headers: bool = False, capture_response_headers: bool = False, **kwargs: Unpack[HTTPXInstrumentKwargs]) -> None: ... + def instrument_httpx(self, client: None = None, capture_request_headers: bool = False, capture_response_headers: bool = False, capture_request_json_body: 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/logfire_api/_internal/tracer.pyi b/logfire-api/logfire_api/_internal/tracer.pyi index f4a719bb5..4dedab6e3 100644 --- a/logfire-api/logfire_api/_internal/tracer.pyi +++ b/logfire-api/logfire_api/_internal/tracer.pyi @@ -70,10 +70,13 @@ class SuppressedTracer(Tracer): class PendingSpanProcessor(SpanProcessor): """Span processor that emits an extra pending span for each span as it starts. - The pending span is emitted by calling `on_end` on all other processors. + The pending span is emitted by calling `on_end` on the inner `processor`. + This is intentionally not a `WrapperSpanProcessor` to avoid the default implementations of `on_end` + and `shutdown`. This processor is expected to contain processors which are already included + elsewhere in the pipeline where `on_end` and `shutdown` are called normally. """ id_generator: IdGenerator - other_processors: tuple[SpanProcessor, ...] + processor: SpanProcessor def on_start(self, span: Span, parent_context: context_api.Context | None = None) -> None: ... def should_sample(span_context: SpanContext, attributes: Mapping[str, otel_types.AttributeValue]) -> bool: diff --git a/logfire-api/pyproject.toml b/logfire-api/pyproject.toml index 52a8a8993..f4c82dc4c 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.7.1" +version = "2.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 d5a2d5879..aefc328e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire" -version = "2.7.1" +version = "2.8.0" description = "The best Python observability tool! 🪵🔥" requires-python = ">=3.8" authors = [ diff --git a/uv.lock b/uv.lock index 0e5471954..bff874855 100644 --- a/uv.lock +++ b/uv.lock @@ -1386,7 +1386,7 @@ wheels = [ [[package]] name = "logfire" -version = "2.7.1" +version = "2.8.0" source = { editable = "." } dependencies = [ { name = "executing" }, @@ -1661,7 +1661,7 @@ docs = [ [[package]] name = "logfire-api" -version = "2.7.1" +version = "2.8.0" source = { editable = "logfire-api" } [package.metadata]