diff --git a/CHANGELOG.md b/CHANGELOG.md index 815eaaf32..259aa2910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Release Notes +## [v1.3.0] (2024-10-24) + +* Add Code Source links by @Kludex in [#451](https://github.com/pydantic/logfire/pull/451) and [#505](https://github.com/pydantic/logfire/pull/505) +* Add fastapi arguments attributes directly on the root OTEL span, remove `use_opentelemetry_instrumentation` kwarg by @alexmojaki in [#509](https://github.com/pydantic/logfire/pull/509) +* Allow setting tags on logfire spans by @AdolfoVillalobos in [#497](https://github.com/pydantic/logfire/pull/497) +* Add logger name to `LogfireLoggingHandler` spans by @samuelcolvin in [#534](https://github.com/pydantic/logfire/pull/534) +* Format `None` as `None` instead of `null` in messages by @alexmojaki in [#525](https://github.com/pydantic/logfire/pull/525) +* Use `PYTEST_VERSION` instead of `PYTEST_CURRENT_TEST` to detect `logfire.configure()` being called within a pytest run but outside any test by @Kludex in [#531](https://github.com/pydantic/logfire/pull/531) + ## [v1.2.0] (2024-10-17) * Add `local` parameter to `logfire.configure()` by @alexmojaki in [#508](https://github.com/pydantic/logfire/pull/508) @@ -358,3 +367,4 @@ First release from new repo! [v1.0.1]: https://github.com/pydantic/logfire/compare/v1.0.0...v1.0.1 [v1.1.0]: https://github.com/pydantic/logfire/compare/v1.0.1...v1.1.0 [v1.2.0]: https://github.com/pydantic/logfire/compare/v1.1.0...v1.2.0 +[v1.3.0]: https://github.com/pydantic/logfire/compare/v1.2.0...v1.3.0 diff --git a/logfire-api/logfire_api/_internal/constants.pyi b/logfire-api/logfire_api/_internal/constants.pyi index e6bd50561..3a79cc9bf 100644 --- a/logfire-api/logfire_api/_internal/constants.pyi +++ b/logfire-api/logfire_api/_internal/constants.pyi @@ -20,6 +20,7 @@ ATTRIBUTES_MESSAGE_KEY: Incomplete DISABLE_CONSOLE_KEY: Incomplete ATTRIBUTES_JSON_SCHEMA_KEY: Incomplete ATTRIBUTES_LOGGING_ARGS_KEY: Incomplete +ATTRIBUTES_LOGGING_NAME: Incomplete ATTRIBUTES_VALIDATION_ERROR_KEY: str ATTRIBUTES_SCRUBBED_KEY: Incomplete NULL_ARGS_KEY: str diff --git a/logfire-api/logfire_api/_internal/formatter.pyi b/logfire-api/logfire_api/_internal/formatter.pyi index 88f9dd51b..5abf081f2 100644 --- a/logfire-api/logfire_api/_internal/formatter.pyi +++ b/logfire-api/logfire_api/_internal/formatter.pyi @@ -8,7 +8,7 @@ from .utils import log_internal_error as log_internal_error, truncate_string as from _typeshed import Incomplete from string import Formatter from types import CodeType as CodeType -from typing import Any, Final, Literal +from typing import Any, Literal from typing_extensions import NotRequired, TypedDict class LiteralChunk(TypedDict): @@ -21,7 +21,6 @@ class ArgChunk(TypedDict): spec: NotRequired[str] class ChunksFormatter(Formatter): - NONE_REPR: Final[str] def chunks(self, format_string: str, kwargs: dict[str, Any], *, scrubber: BaseScrubber, fstring_frame: types.FrameType | None = None) -> tuple[list[LiteralChunk | ArgChunk], dict[str, Any], str]: ... chunks_formatter: Incomplete diff --git a/logfire-api/logfire_api/_internal/integrations/fastapi.pyi b/logfire-api/logfire_api/_internal/integrations/fastapi.pyi index b4dc733cd..f15f94300 100644 --- a/logfire-api/logfire_api/_internal/integrations/fastapi.pyi +++ b/logfire-api/logfire_api/_internal/integrations/fastapi.pyi @@ -1,4 +1,4 @@ -from ..main import Logfire as Logfire +from ..main import Logfire as Logfire, set_user_attributes_on_raw_span as set_user_attributes_on_raw_span from ..stack_info import StackInfo as StackInfo, get_code_object_info as get_code_object_info from ..utils import maybe_capture_server_headers as maybe_capture_server_headers from .asgi import tweak_asgi_spans_tracer_provider as tweak_asgi_spans_tracer_provider @@ -10,7 +10,7 @@ from typing import Any, Awaitable, Callable, ContextManager, Iterable def find_mounted_apps(app: FastAPI) -> list[FastAPI]: """Fetch all sub-apps mounted to a FastAPI app, including nested sub-apps.""" -def instrument_fastapi(logfire_instance: Logfire, app: FastAPI, *, capture_headers: bool = False, request_attributes_mapper: Callable[[Request | WebSocket, dict[str, Any]], dict[str, Any] | None] | None = None, use_opentelemetry_instrumentation: bool = True, excluded_urls: str | Iterable[str] | None = None, record_send_receive: bool = False, **opentelemetry_kwargs: Any) -> ContextManager[None]: +def instrument_fastapi(logfire_instance: Logfire, app: FastAPI, *, capture_headers: bool = False, request_attributes_mapper: Callable[[Request | WebSocket, dict[str, Any]], dict[str, Any] | None] | None = None, excluded_urls: str | Iterable[str] | None = None, record_send_receive: bool = False, **opentelemetry_kwargs: Any) -> ContextManager[None]: """Instrument a FastAPI app so that spans and logs are automatically created for each request. See `Logfire.instrument_fastapi` for more details. @@ -21,10 +21,11 @@ def patch_fastapi(): class FastAPIInstrumentation: logfire_instance: Incomplete request_attributes_mapper: Incomplete - excluded_urls_list: Incomplete - def __init__(self, logfire_instance: Logfire, request_attributes_mapper: Callable[[Request | WebSocket, dict[str, Any]], dict[str, Any] | None], excluded_urls: str | None) -> None: ... + def __init__(self, logfire_instance: Logfire, request_attributes_mapper: Callable[[Request | WebSocket, dict[str, Any]], dict[str, Any] | None]) -> None: ... async def solve_dependencies(self, request: Request | WebSocket, original: Awaitable[Any]) -> Any: ... async def run_endpoint_function(self, original_run_endpoint_function: Any, request: Request, dependant: Any, values: dict[str, Any], **kwargs: Any) -> Any: ... class _InstrumentedValues(dict): request: Request + +LOGFIRE_SPAN_SCOPE_KEY: str diff --git a/logfire-api/logfire_api/_internal/main.pyi b/logfire-api/logfire_api/_internal/main.pyi index 56529e4d3..04828fb5b 100644 --- a/logfire-api/logfire_api/_internal/main.pyi +++ b/logfire-api/logfire_api/_internal/main.pyi @@ -30,7 +30,7 @@ from django.http import HttpRequest as HttpRequest, HttpResponse as HttpResponse from fastapi import FastAPI from flask.app import Flask from opentelemetry.metrics import CallbackT as CallbackT, Counter, Histogram, UpDownCounter, _Gauge as Gauge -from opentelemetry.sdk.trace import ReadableSpan, Span as Span +from opentelemetry.sdk.trace import ReadableSpan, Span from opentelemetry.trace import Tracer from opentelemetry.util import types as otel_types from starlette.applications import Starlette @@ -386,9 +386,12 @@ class Logfire: exclude: Exclude specific modules from instrumentation. """ - def instrument_fastapi(self, app: FastAPI, *, capture_headers: bool = False, request_attributes_mapper: Callable[[Request | WebSocket, dict[str, Any]], dict[str, Any] | None] | None = None, use_opentelemetry_instrumentation: bool = True, excluded_urls: str | Iterable[str] | None = None, record_send_receive: bool = False, **opentelemetry_kwargs: Any) -> ContextManager[None]: + def instrument_fastapi(self, app: FastAPI, *, capture_headers: bool = False, request_attributes_mapper: Callable[[Request | WebSocket, dict[str, Any]], dict[str, Any] | None] | None = None, excluded_urls: str | Iterable[str] | None = None, record_send_receive: bool = False, **opentelemetry_kwargs: Any) -> ContextManager[None]: """Instrument a FastAPI app so that spans and logs are automatically created for each request. + Uses the [OpenTelemetry FastAPI Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/fastapi/fastapi.html) + under the hood, with some additional features. + Args: app: The FastAPI app to instrument. capture_headers: Set to `True` to capture all request and response headers. @@ -411,11 +414,6 @@ class Logfire: matches any of the regexes. This applies to both the Logfire and OpenTelemetry instrumentation. If not provided, the environment variables `OTEL_PYTHON_FASTAPI_EXCLUDED_URLS` and `OTEL_PYTHON_EXCLUDED_URLS` will be checked. - use_opentelemetry_instrumentation: If True (the default) then - [`FastAPIInstrumentor`][opentelemetry.instrumentation.fastapi.FastAPIInstrumentor] - will also instrument the app. - - See [OpenTelemetry FastAPI Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/fastapi/fastapi.html). record_send_receive: Set to True to allow the OpenTelemetry ASGI to create send/receive spans. These are disabled by default to reduce overhead and the number of spans created, since many can be created for a single request, and they are not often useful. @@ -934,7 +932,8 @@ class LogfireSpan(ReadableSpan): @property def tags(self) -> tuple[str, ...]: ... @tags.setter - def tags(self, new_tags: Sequence[str]) -> None: ... + def tags(self, new_tags: Sequence[str]) -> None: + """Set or add tags to the span.""" @property def message(self) -> str: ... @message.setter @@ -1008,5 +1007,6 @@ def set_user_attribute(otlp_attributes: dict[str, otel_types.AttributeValue], ke Returns the final key and value that was added to the dictionary. The key will be the original key unless the value was `None`, in which case it will be `NULL_ARGS_KEY`. """ +def set_user_attributes_on_raw_span(span: Span, attributes: dict[str, Any]) -> None: ... P = ParamSpec('P') R = TypeVar('R') diff --git a/logfire-api/logfire_api/_internal/utils.pyi b/logfire-api/logfire_api/_internal/utils.pyi index 127f7193b..d99fb2281 100644 --- a/logfire-api/logfire_api/_internal/utils.pyi +++ b/logfire-api/logfire_api/_internal/utils.pyi @@ -84,10 +84,10 @@ def is_instrumentation_suppressed() -> bool: This means that any logs/spans generated by logfire or OpenTelemetry will not be logged in any way. """ -def suppress_instrumentation() -> Generator[None, None, None]: +def suppress_instrumentation() -> Generator[None]: """Context manager to suppress all logs/spans generated by logfire or OpenTelemetry.""" def log_internal_error() -> None: ... -def handle_internal_errors() -> Generator[None, None, None]: ... +def handle_internal_errors() -> Generator[None]: ... def maybe_capture_server_headers(capture: bool): ... def is_asgi_send_receive_span_name(name: str) -> bool: ... diff --git a/logfire-api/logfire_api/integrations/logging.pyi b/logfire-api/logfire_api/integrations/logging.pyi index 2adb210fc..e235c639e 100644 --- a/logfire-api/logfire_api/integrations/logging.pyi +++ b/logfire-api/logfire_api/integrations/logging.pyi @@ -1,5 +1,5 @@ from .. import Logfire as Logfire -from .._internal.constants import ATTRIBUTES_LOGGING_ARGS_KEY as ATTRIBUTES_LOGGING_ARGS_KEY, ATTRIBUTES_MESSAGE_KEY as ATTRIBUTES_MESSAGE_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY as ATTRIBUTES_MESSAGE_TEMPLATE_KEY, LOGGING_TO_OTEL_LEVEL_NUMBERS as LOGGING_TO_OTEL_LEVEL_NUMBERS +from .._internal.constants import ATTRIBUTES_LOGGING_ARGS_KEY as ATTRIBUTES_LOGGING_ARGS_KEY, ATTRIBUTES_LOGGING_NAME as ATTRIBUTES_LOGGING_NAME, ATTRIBUTES_MESSAGE_KEY as ATTRIBUTES_MESSAGE_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY as ATTRIBUTES_MESSAGE_TEMPLATE_KEY, LOGGING_TO_OTEL_LEVEL_NUMBERS as LOGGING_TO_OTEL_LEVEL_NUMBERS from .._internal.utils import is_instrumentation_suppressed as is_instrumentation_suppressed from _typeshed import Incomplete from logging import Handler as LoggingHandler, LogRecord diff --git a/logfire-api/pyproject.toml b/logfire-api/pyproject.toml index 581e73fc3..55adb8222 100644 --- a/logfire-api/pyproject.toml +++ b/logfire-api/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire-api" -version = "1.2.0" +version = "1.3.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 ef02e20fa..c8ecfc26e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire" -version = "1.2.0" +version = "1.3.0" description = "The best Python observability tool! 🪵🔥" requires-python = ">=3.8" authors = [ diff --git a/uv.lock b/uv.lock index b0e8d672d..a6fb2194d 100644 --- a/uv.lock +++ b/uv.lock @@ -442,7 +442,7 @@ name = "cffi" version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, + { name = "pycparser", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } wheels = [ @@ -1403,7 +1403,7 @@ wheels = [ [[package]] name = "logfire" -version = "1.2.0" +version = "1.3.0" source = { editable = "." } dependencies = [ { name = "executing" }, @@ -1650,7 +1650,7 @@ dev = [ [[package]] name = "logfire-api" -version = "1.2.0" +version = "1.3.0" source = { editable = "logfire-api" } [[package]]