diff --git a/CHANGELOG.md b/CHANGELOG.md index b6014621..0c1a770a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Release Notes +## [v3.1.0] (2025-01-09) + +* Add `capture_all` to `instrument_httpx` by @Kludex in [#780](https://github.com/pydantic/logfire/pull/780) +* Ensure cleanup when forked process ends by @alexmojaki in [#785](https://github.com/pydantic/logfire/pull/785) +* Generate trace IDs as ULIDs by default by @adriangb in [#783](https://github.com/pydantic/logfire/pull/783) + ## [v3.0.0] (2025-01-07) * **BREAKING CHANGE**: Removed `capture_request_json_body`, `capture_request_text_body`, `capture_request_form_data`, and `capture_response_json_body` parameters from `logfire.instrument_httpx()`, replaced with `capture_request_body` `capture_response_body` by @Kludex in [#769](https://github.com/pydantic/logfire/pull/769) @@ -517,3 +523,4 @@ First release from new repo! [v2.11.0]: https://github.com/pydantic/logfire/compare/v2.10.0...v2.11.0 [v2.11.1]: https://github.com/pydantic/logfire/compare/v2.11.0...v2.11.1 [v3.0.0]: https://github.com/pydantic/logfire/compare/v2.11.1...v3.0.0 +[v3.1.0]: https://github.com/pydantic/logfire/compare/v3.0.0...v3.1.0 diff --git a/logfire-api/logfire_api/_internal/integrations/httpx.pyi b/logfire-api/logfire_api/_internal/integrations/httpx.pyi index be098f22..d0a94ad8 100644 --- a/logfire-api/logfire_api/_internal/integrations/httpx.pyi +++ b/logfire-api/logfire_api/_internal/integrations/httpx.pyi @@ -13,7 +13,7 @@ from typing import Any, Awaitable, Callable, Literal, Mapping, ParamSpec P = ParamSpec('P') -def instrument_httpx(logfire_instance: Logfire, client: httpx.Client | httpx.AsyncClient | None, capture_headers: bool, capture_request_body: bool, capture_response_body: bool, request_hook: RequestHook | AsyncRequestHook | None, response_hook: ResponseHook | AsyncResponseHook | None, async_request_hook: AsyncRequestHook | None, async_response_hook: AsyncResponseHook | None, **kwargs: Any) -> None: +def instrument_httpx(logfire_instance: Logfire, client: httpx.Client | httpx.AsyncClient | None, capture_all: bool, capture_headers: bool, capture_request_body: bool, capture_response_body: bool, request_hook: RequestHook | AsyncRequestHook | None, response_hook: ResponseHook | AsyncResponseHook | None, async_request_hook: AsyncRequestHook | None, async_response_hook: AsyncResponseHook | None, **kwargs: Any) -> None: """Instrument the `httpx` module so that spans are automatically created for each request. See the `Logfire.instrument_httpx` method for details. diff --git a/logfire-api/logfire_api/_internal/main.pyi b/logfire-api/logfire_api/_internal/main.pyi index 956da860..199b5031 100644 --- a/logfire-api/logfire_api/_internal/main.pyi +++ b/logfire-api/logfire_api/_internal/main.pyi @@ -553,11 +553,11 @@ class Logfire: def instrument_asyncpg(self, **kwargs: Any) -> 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_body: bool = False, capture_response_body: bool = False, request_hook: HttpxRequestHook | None = None, response_hook: HttpxResponseHook | None = None, **kwargs: Any) -> None: ... + def instrument_httpx(self, client: httpx.Client, *, capture_all: bool = False, capture_headers: bool = False, capture_request_body: bool = False, capture_response_body: bool = False, request_hook: HttpxRequestHook | None = None, response_hook: HttpxResponseHook | None = None, **kwargs: Any) -> None: ... @overload - def instrument_httpx(self, client: httpx.AsyncClient, *, capture_headers: bool = False, capture_request_body: bool = False, capture_response_body: bool = False, request_hook: HttpxRequestHook | HttpxAsyncRequestHook | None = None, response_hook: HttpxResponseHook | HttpxAsyncResponseHook | None = None, **kwargs: Any) -> None: ... + def instrument_httpx(self, client: httpx.AsyncClient, *, capture_all: bool = False, capture_headers: bool = False, capture_request_body: bool = False, capture_response_body: bool = False, request_hook: HttpxRequestHook | HttpxAsyncRequestHook | None = None, response_hook: HttpxResponseHook | HttpxAsyncResponseHook | None = None, **kwargs: Any) -> None: ... @overload - def instrument_httpx(self, client: None = None, *, capture_headers: bool = False, capture_request_body: bool = False, capture_response_body: bool = False, request_hook: HttpxRequestHook | None = None, response_hook: HttpxResponseHook | None = None, async_request_hook: HttpxAsyncRequestHook | None = None, async_response_hook: HttpxAsyncResponseHook | None = None, **kwargs: Any) -> None: ... + def instrument_httpx(self, client: None = None, *, capture_all: bool = False, capture_headers: bool = False, capture_request_body: bool = False, capture_response_body: bool = False, request_hook: HttpxRequestHook | None = None, response_hook: HttpxResponseHook | None = None, async_request_hook: HttpxAsyncRequestHook | None = None, async_response_hook: HttpxAsyncResponseHook | None = None, **kwargs: Any) -> 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/ulid.pyi b/logfire-api/logfire_api/_internal/ulid.pyi new file mode 100644 index 00000000..6a2724c5 --- /dev/null +++ b/logfire-api/logfire_api/_internal/ulid.pyi @@ -0,0 +1,29 @@ +from random import Random +from typing import Callable + +def ulid(random: Random, ms_timestamp_generator: Callable[[], int]) -> int: + """Generate an integer ULID compatible with UUID v4. + + ULIDs as defined by the [spec](https://github.com/ulid/spec) look like this: + + 01AN4Z07BY 79KA1307SR9X4MV3 + |----------| |----------------| + Timestamp Randomness + 48bits 80bits + + In the future it would be nice to make this compatible with a UUID, + e.g. v4 UUIDs by setting the version and variant bits correctly. + We can't currently do this because setting these bits would leave us with only 7 bytes of randomness, + which isn't enough for the Python SDK's sampler that currently expects 8 bytes of randomness. + In the future OTEL will probably adopt https://www.w3.org/TR/trace-context-2/#random-trace-id-flag + which relies only on the lower 7 bytes of the trace ID, then all SDKs and tooling should be updated + and leaving only 7 bytes of randomness should be fine. + + Right now we only care about: + - Our SDK / Python SDK's in general. + - The OTEL collector. + + And both behave properly with 8 bytes of randomness because trace IDs were originally 64 bits + so to be compatible with old trace IDs nothing in OTEL can assume >8 bytes of randomness in trace IDs + unless they generated the trace ID themselves (e.g. the Go SDK _does_ expect >8 bytes of randomness internally). + """ diff --git a/logfire-api/logfire_api/_internal/utils.pyi b/logfire-api/logfire_api/_internal/utils.pyi index d99fb228..c1c11086 100644 --- a/logfire-api/logfire_api/_internal/utils.pyi +++ b/logfire-api/logfire_api/_internal/utils.pyi @@ -2,6 +2,7 @@ from _typeshed import Incomplete from collections.abc import Generator from dataclasses import dataclass from logfire._internal.stack_info import is_user_code as is_user_code +from logfire._internal.ulid import ulid as ulid from opentelemetry import trace as trace_api from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import Event as Event, ReadableSpan @@ -91,7 +92,7 @@ def handle_internal_errors() -> Generator[None]: ... def maybe_capture_server_headers(capture: bool): ... def is_asgi_send_receive_span_name(name: str) -> bool: ... -@dataclass(repr=True) +@dataclass(repr=True, eq=True) class SeededRandomIdGenerator(IdGenerator): """Generate random span/trace IDs from a seed for deterministic tests. diff --git a/logfire-api/pyproject.toml b/logfire-api/pyproject.toml index b5cdad4f..6d87e9a8 100644 --- a/logfire-api/pyproject.toml +++ b/logfire-api/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire-api" -version = "3.0.0" +version = "3.1.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 cc682032..ff865807 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire" -version = "3.0.0" +version = "3.1.0" description = "The best Python observability tool! 🪵🔥" requires-python = ">=3.8" authors = [ diff --git a/uv.lock b/uv.lock index b1a9e9a8..6ce28a37 100644 --- a/uv.lock +++ b/uv.lock @@ -903,7 +903,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -1911,7 +1911,7 @@ wheels = [ [[package]] name = "logfire" -version = "3.0.0" +version = "3.1.0" source = { editable = "." } dependencies = [ { name = "executing" }, @@ -2213,7 +2213,7 @@ docs = [ [[package]] name = "logfire-api" -version = "3.0.0" +version = "3.1.0" source = { editable = "logfire-api" } [package.metadata] @@ -2437,7 +2437,7 @@ version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, { name = "ghp-import" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, @@ -5068,7 +5068,7 @@ name = "tqdm" version = "4.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } wheels = [