diff --git a/CHANGELOG.md b/CHANGELOG.md index f89582502..762f46b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Release Notes +## [v0.48.0] (2024-07-24) + +* Add `instrument_celery` method by @Kludex in https://github.com/pydantic/logfire/pull/322 +* `capture_headers` by @alexmojaki in https://github.com/pydantic/logfire/pull/318 +* Handle message formatting errors by @alexmojaki in https://github.com/pydantic/logfire/pull/329 +* Handle logging `None` with `loguru` by @alexmojaki in https://github.com/pydantic/logfire/pull/331 + ## [v0.47.0] (2024-07-20) * Fix recursive logging from OTEL's `BatchSpanProcessor` by @alexmojaki in https://github.com/pydantic/logfire/pull/306 @@ -214,6 +221,7 @@ First release from new repo! * Ensure `logfire.testing` doesn't depend on pydantic and eval_type_backport by @alexmojaki in https://github.com/pydantic/logfire/pull/40 * Allow using pydantic plugin with models defined before calling logfire.configure by @alexmojaki in https://github.com/pydantic/logfire/pull/36 +[v0.48.0]: https://github.com/pydantic/logfire/compare/v0.47.0...v0.48.0 [v0.47.0]: https://github.com/pydantic/logfire/compare/v0.46.1...v0.47.0 [v0.46.1]: https://github.com/pydantic/logfire/compare/v0.46.0...v0.46.1 [v0.46.0]: https://github.com/pydantic/logfire/compare/v0.45.1...v0.46.0 diff --git a/logfire-api/logfire_api/__init__.py b/logfire-api/logfire_api/__init__.py index 54b84fc6a..463595b36 100644 --- a/logfire-api/logfire_api/__init__.py +++ b/logfire-api/logfire_api/__init__.py @@ -79,7 +79,7 @@ def with_settings(self, *args, **kwargs) -> Logfire: def force_flush(self, *args, **kwargs) -> None: ... - def log_slow_async_callbacks(self, *args, **kwargs) -> None: # pragma: no branch + def log_slow_async_callbacks(self, *args, **kwargs) -> None: # pragma: no branch return nullcontext() def install_auto_tracing(self, *args, **kwargs) -> None: ... @@ -143,6 +143,7 @@ def shutdown(self, *args, **kwargs) -> None: ... instrument_openai = DEFAULT_LOGFIRE_INSTANCE.instrument_openai instrument_anthropic = DEFAULT_LOGFIRE_INSTANCE.instrument_anthropic instrument_asyncpg = DEFAULT_LOGFIRE_INSTANCE.instrument_asyncpg + instrument_celery = DEFAULT_LOGFIRE_INSTANCE.instrument_celery instrument_httpx = DEFAULT_LOGFIRE_INSTANCE.instrument_httpx instrument_requests = DEFAULT_LOGFIRE_INSTANCE.instrument_requests instrument_psycopg = DEFAULT_LOGFIRE_INSTANCE.instrument_psycopg diff --git a/logfire-api/logfire_api/__init__.pyi b/logfire-api/logfire_api/__init__.pyi index 4b4074551..d3fa6c56b 100644 --- a/logfire-api/logfire_api/__init__.pyi +++ b/logfire-api/logfire_api/__init__.pyi @@ -1,6 +1,11 @@ from ._internal.auto_trace import AutoTraceModule as AutoTraceModule from ._internal.auto_trace.rewrite_ast import no_auto_trace as no_auto_trace -from ._internal.config import ConsoleOptions as ConsoleOptions, METRICS_PREFERRED_TEMPORALITY as METRICS_PREFERRED_TEMPORALITY, PydanticPlugin as PydanticPlugin, configure as configure +from ._internal.config import ( + ConsoleOptions as ConsoleOptions, + METRICS_PREFERRED_TEMPORALITY as METRICS_PREFERRED_TEMPORALITY, + PydanticPlugin as PydanticPlugin, + configure as configure, +) from ._internal.constants import LevelName as LevelName from ._internal.exporters.file import load_file as load_spans_from_file from ._internal.exporters.tail_sampling import TailSamplingOptions as TailSamplingOptions @@ -11,7 +16,56 @@ from .integrations.logging import LogfireLoggingHandler as LogfireLoggingHandler from .integrations.structlog import LogfireProcessor as StructlogProcessor from .version import VERSION as VERSION -__all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'ConsoleOptions', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'error', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_fastapi', 'instrument_openai', 'instrument_anthropic', 'instrument_asyncpg', 'instrument_httpx', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_sqlalchemy', 'instrument_redis', 'instrument_pymongo', 'AutoTraceModule', 'with_tags', 'with_settings', 'shutdown', 'load_spans_from_file', 'no_auto_trace', 'METRICS_PREFERRED_TEMPORALITY', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'TailSamplingOptions'] +__all__ = [ + 'Logfire', + 'LogfireSpan', + 'LevelName', + 'ConsoleOptions', + 'PydanticPlugin', + 'configure', + 'span', + 'instrument', + 'log', + 'trace', + 'debug', + 'notice', + 'info', + 'warn', + 'error', + 'fatal', + 'force_flush', + 'log_slow_async_callbacks', + 'install_auto_tracing', + 'instrument_fastapi', + 'instrument_openai', + 'instrument_anthropic', + 'instrument_asyncpg', + 'instrument_httpx', + 'instrument_celery', + 'instrument_requests', + 'instrument_psycopg', + 'instrument_django', + 'instrument_flask', + 'instrument_starlette', + 'instrument_aiohttp_client', + 'instrument_sqlalchemy', + 'instrument_redis', + 'instrument_pymongo', + 'AutoTraceModule', + 'with_tags', + 'with_settings', + 'shutdown', + 'load_spans_from_file', + 'no_auto_trace', + 'METRICS_PREFERRED_TEMPORALITY', + 'ScrubMatch', + 'ScrubbingOptions', + 'VERSION', + 'suppress_instrumentation', + 'StructlogProcessor', + 'LogfireLoggingHandler', + 'TailSamplingOptions', +] DEFAULT_LOGFIRE_INSTANCE = Logfire() span = DEFAULT_LOGFIRE_INSTANCE.span @@ -24,6 +78,7 @@ instrument_openai = DEFAULT_LOGFIRE_INSTANCE.instrument_openai instrument_anthropic = DEFAULT_LOGFIRE_INSTANCE.instrument_anthropic instrument_asyncpg = DEFAULT_LOGFIRE_INSTANCE.instrument_asyncpg instrument_httpx = DEFAULT_LOGFIRE_INSTANCE.instrument_httpx +instrument_celery = DEFAULT_LOGFIRE_INSTANCE.instrument_celery instrument_requests = DEFAULT_LOGFIRE_INSTANCE.instrument_requests instrument_psycopg = DEFAULT_LOGFIRE_INSTANCE.instrument_psycopg instrument_django = DEFAULT_LOGFIRE_INSTANCE.instrument_django diff --git a/logfire-api/logfire_api/_internal/config.pyi b/logfire-api/logfire_api/_internal/config.pyi index 8a7c8033d..70bd3c8bc 100644 --- a/logfire-api/logfire_api/_internal/config.pyi +++ b/logfire-api/logfire_api/_internal/config.pyi @@ -14,7 +14,7 @@ from .exporters.remove_pending import RemovePendingSpansExporter as RemovePendin from .exporters.tail_sampling import TailSamplingOptions as TailSamplingOptions, TailSamplingProcessor as TailSamplingProcessor from .integrations.executors import instrument_executors as instrument_executors from .metrics import ProxyMeterProvider as ProxyMeterProvider, configure_metrics as configure_metrics -from .scrubbing import BaseScrubber as BaseScrubber, NoopScrubber as NoopScrubber, ScrubCallback as ScrubCallback, Scrubber as Scrubber, ScrubbingOptions as ScrubbingOptions +from .scrubbing import BaseScrubber as BaseScrubber, NOOP_SCRUBBER as NOOP_SCRUBBER, ScrubCallback as ScrubCallback, Scrubber as Scrubber, ScrubbingOptions as ScrubbingOptions from .stack_info import get_user_frame_and_stacklevel as get_user_frame_and_stacklevel from .tracer import PendingSpanProcessor as PendingSpanProcessor, ProxyTracerProvider as ProxyTracerProvider from .utils import UnexpectedResponse as UnexpectedResponse, ensure_data_dir_exists as ensure_data_dir_exists, get_version as get_version, read_toml_file as read_toml_file, suppress_instrumentation as suppress_instrumentation diff --git a/logfire-api/logfire_api/_internal/formatter.pyi b/logfire-api/logfire_api/_internal/formatter.pyi index 40f8afc99..b960c8188 100644 --- a/logfire-api/logfire_api/_internal/formatter.pyi +++ b/logfire-api/logfire_api/_internal/formatter.pyi @@ -2,13 +2,13 @@ import ast import executing import types from .constants import ATTRIBUTES_SCRUBBED_KEY as ATTRIBUTES_SCRUBBED_KEY, MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT as MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT -from .scrubbing import BaseScrubber as BaseScrubber, ScrubbedNote as ScrubbedNote -from .utils import truncate_string as truncate_string +from .scrubbing import BaseScrubber as BaseScrubber, NOOP_SCRUBBER as NOOP_SCRUBBER, ScrubbedNote as ScrubbedNote +from .utils import log_internal_error as log_internal_error, truncate_string as truncate_string from _typeshed import Incomplete from logfire._internal.stack_info import get_user_frame_and_stacklevel as get_user_frame_and_stacklevel from string import Formatter from types import CodeType as CodeType -from typing import Any, Final, Literal, Mapping +from typing import Any, Final, Literal from typing_extensions import NotRequired, TypedDict class LiteralChunk(TypedDict): @@ -22,7 +22,7 @@ class ArgChunk(TypedDict): class ChunksFormatter(Formatter): NONE_REPR: Final[str] - def chunks(self, format_string: str, kwargs: Mapping[str, Any], *, scrubber: BaseScrubber, fstring_frame: types.FrameType | None = None) -> tuple[list[LiteralChunk | ArgChunk], dict[str, Any], 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 @@ -48,3 +48,13 @@ def get_stacklevel(frame: types.FrameType): ... class InspectArgumentsFailedWarning(Warning): ... def warn_inspect_arguments(msg: str, stacklevel: int): ... + +class KnownFormattingError(Exception): + """An error raised when there's something wrong with a format string or the field values. + + In other words this should correspond to errors that would be raised when using `str.format`, + and generally indicate a user error, most likely that they weren't trying to pass a template string at all. + """ +class FormattingFailedWarning(UserWarning): ... + +def warn_formatting(msg: str): ... diff --git a/logfire-api/logfire_api/_internal/integrations/celery.pyi b/logfire-api/logfire_api/_internal/integrations/celery.pyi new file mode 100644 index 000000000..f7ef6bbcc --- /dev/null +++ b/logfire-api/logfire_api/_internal/integrations/celery.pyi @@ -0,0 +1,10 @@ +from typing_extensions import TypedDict, Unpack + +class CeleryInstrumentKwargs(TypedDict, total=False): + skip_dep_check: bool + +def instrument_celery(**kwargs: Unpack[CeleryInstrumentKwargs]) -> None: + """Instrument the `celery` module so that spans are automatically created for each task. + + See the `Logfire.instrument_celery` method for details. + """ diff --git a/logfire-api/logfire_api/_internal/integrations/django.pyi b/logfire-api/logfire_api/_internal/integrations/django.pyi index 348875163..830066c91 100644 --- a/logfire-api/logfire_api/_internal/integrations/django.pyi +++ b/logfire-api/logfire_api/_internal/integrations/django.pyi @@ -1,6 +1,7 @@ +from logfire._internal.utils import maybe_capture_server_headers as maybe_capture_server_headers from typing import Any -def instrument_django(**kwargs: Any): +def instrument_django(*, capture_headers: bool = False, **kwargs: Any): """Instrument the `django` module so that spans are automatically created for each web request. See the `Logfire.instrument_django` method for details. diff --git a/logfire-api/logfire_api/_internal/integrations/fastapi.pyi b/logfire-api/logfire_api/_internal/integrations/fastapi.pyi index 7055463b7..9161c0556 100644 --- a/logfire-api/logfire_api/_internal/integrations/fastapi.pyi +++ b/logfire-api/logfire_api/_internal/integrations/fastapi.pyi @@ -1,5 +1,6 @@ from ..main import Logfire as Logfire 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 _typeshed import Incomplete from fastapi import FastAPI from starlette.requests import Request @@ -8,7 +9,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, *, 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, **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, use_opentelemetry_instrumentation: bool = True, excluded_urls: str | Iterable[str] | None = None, **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. diff --git a/logfire-api/logfire_api/_internal/integrations/flask.pyi b/logfire-api/logfire_api/_internal/integrations/flask.pyi index d8d4531b0..c441bb23b 100644 --- a/logfire-api/logfire_api/_internal/integrations/flask.pyi +++ b/logfire-api/logfire_api/_internal/integrations/flask.pyi @@ -1,4 +1,5 @@ from flask.app import Flask +from logfire._internal.utils import maybe_capture_server_headers as maybe_capture_server_headers from opentelemetry.trace import Span from typing_extensions import Protocol, TypedDict, Unpack from wsgiref.types import WSGIEnvironment @@ -16,7 +17,7 @@ class FlaskInstrumentKwargs(TypedDict, total=False): enable_commenter: bool | None commenter_options: dict[str, str] | None -def instrument_flask(app: Flask, **kwargs: Unpack[FlaskInstrumentKwargs]): +def instrument_flask(app: Flask, capture_headers: bool = False, **kwargs: Unpack[FlaskInstrumentKwargs]): """Instrument `app` so that spans are automatically created for each request. See the `Logfire.instrument_flask` method for details. diff --git a/logfire-api/logfire_api/_internal/integrations/starlette.pyi b/logfire-api/logfire_api/_internal/integrations/starlette.pyi index 46fafd138..3a3ed28aa 100644 --- a/logfire-api/logfire_api/_internal/integrations/starlette.pyi +++ b/logfire-api/logfire_api/_internal/integrations/starlette.pyi @@ -1,3 +1,4 @@ +from logfire._internal.utils import maybe_capture_server_headers as maybe_capture_server_headers from opentelemetry.trace import Span from starlette.applications import Starlette from typing import Any @@ -17,7 +18,7 @@ class StarletteInstrumentKwargs(TypedDict, total=False): client_request_hook: ClientRequestHook | None client_response_hook: ClientResponseHook | None -def instrument_starlette(app: Starlette, **kwargs: Unpack[StarletteInstrumentKwargs]): +def instrument_starlette(app: Starlette, *, capture_headers: bool = False, **kwargs: Unpack[StarletteInstrumentKwargs]): """Instrument `app` so that spans are automatically created for each request. See the `Logfire.instrument_starlette` method for details. diff --git a/logfire-api/logfire_api/_internal/main.pyi b/logfire-api/logfire_api/_internal/main.pyi index 2e4edf046..f86c66273 100644 --- a/logfire-api/logfire_api/_internal/main.pyi +++ b/logfire-api/logfire_api/_internal/main.pyi @@ -9,6 +9,7 @@ from .config import GLOBAL_CONFIG as GLOBAL_CONFIG, LogfireConfig as LogfireConf from .constants import ATTRIBUTES_JSON_SCHEMA_KEY as ATTRIBUTES_JSON_SCHEMA_KEY, ATTRIBUTES_MESSAGE_KEY as ATTRIBUTES_MESSAGE_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY as ATTRIBUTES_MESSAGE_TEMPLATE_KEY, ATTRIBUTES_SAMPLE_RATE_KEY as ATTRIBUTES_SAMPLE_RATE_KEY, ATTRIBUTES_SPAN_TYPE_KEY as ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY as ATTRIBUTES_TAGS_KEY, ATTRIBUTES_VALIDATION_ERROR_KEY as ATTRIBUTES_VALIDATION_ERROR_KEY, DISABLE_CONSOLE_KEY as DISABLE_CONSOLE_KEY, LevelName as LevelName, NULL_ARGS_KEY as NULL_ARGS_KEY, OTLP_MAX_INT_SIZE as OTLP_MAX_INT_SIZE, log_level_attributes as log_level_attributes from .formatter import logfire_format as logfire_format, logfire_format_with_magic as logfire_format_with_magic from .instrument import LogfireArgs as LogfireArgs, instrument as instrument +from .integrations.celery import CeleryInstrumentKwargs as CeleryInstrumentKwargs from .integrations.flask import FlaskInstrumentKwargs as FlaskInstrumentKwargs from .integrations.psycopg import PsycopgInstrumentKwargs as PsycopgInstrumentKwargs from .integrations.pymongo import PymongoInstrumentKwargs as PymongoInstrumentKwargs @@ -365,11 +366,12 @@ class Logfire: Otherwise, the first time(s) each function is called, it will be timed but not traced. Only after the function has run for at least `min_duration` will it be traced in subsequent calls. """ - def instrument_fastapi(self, app: FastAPI, *, 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, **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, use_opentelemetry_instrumentation: bool = True, excluded_urls: str | Iterable[str] | None = None, **opentelemetry_kwargs: Any) -> ContextManager[None]: """Instrument a FastAPI app so that spans and logs are automatically created for each request. Args: app: The FastAPI app to instrument. + capture_headers: Set to `True` to capture all request and response headers. request_attributes_mapper: A function that takes a [`Request`][fastapi.Request] or [`WebSocket`][fastapi.WebSocket] and a dictionary of attributes and returns a new dictionary of attributes. The input dictionary will contain: @@ -510,7 +512,14 @@ class Logfire: [OpenTelemetry HTTPX Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/httpx/httpx.html) library, specifically `HTTPXClientInstrumentor().instrument()`, to which it passes `**kwargs`. """ - def instrument_django(self, is_sql_commentor_enabled: bool | None = None, request_hook: Callable[[Span, HttpRequest], None] | None = None, response_hook: Callable[[Span, HttpRequest, HttpResponse], None] | None = None, excluded_urls: str | None = None, **kwargs: Any) -> None: + def instrument_celery(self, **kwargs: Unpack[CeleryInstrumentKwargs]) -> None: + """Instrument `celery` so that spans are automatically created for each task. + + Uses the + [OpenTelemetry Celery Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/celery/celery.html) + library. + """ + def instrument_django(self, capture_headers: bool = False, is_sql_commentor_enabled: bool | None = None, request_hook: Callable[[Span, HttpRequest], None] | None = None, response_hook: Callable[[Span, HttpRequest, HttpResponse], None] | None = None, excluded_urls: str | None = None, **kwargs: Any) -> None: """Instrument `django` so that spans are automatically created for each web request. Uses the @@ -518,6 +527,7 @@ class Logfire: library. Args: + capture_headers: Set to `True` to capture all request and response headers. is_sql_commentor_enabled: Adds comments to SQL queries performed by Django, so that database logs have additional context. @@ -567,16 +577,20 @@ class Logfire: **kwargs: Additional keyword arguments to pass to the OpenTelemetry `instrument` methods, particularly `enable_commenter` and `commenter_options`. """ - def instrument_flask(self, app: Flask, **kwargs: Unpack[FlaskInstrumentKwargs]) -> None: + def instrument_flask(self, app: Flask, *, capture_headers: bool = False, **kwargs: Unpack[FlaskInstrumentKwargs]) -> None: """Instrument `app` so that spans are automatically created for each request. + Set `capture_headers` to `True` to capture all request and response headers. + Uses the [OpenTelemetry Flask Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/flask/flask.html) library, specifically `FlaskInstrumentor().instrument_app()`, to which it passes `**kwargs`. """ - def instrument_starlette(self, app: Starlette, **kwargs: Unpack[StarletteInstrumentKwargs]) -> None: + def instrument_starlette(self, app: Starlette, *, capture_headers: bool = False, **kwargs: Unpack[StarletteInstrumentKwargs]) -> None: """Instrument `app` so that spans are automatically created for each request. + Set `capture_headers` to `True` to capture all request and response headers. + Uses the [OpenTelemetry Starlette Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/starlette/starlette.html) library, specifically `StarletteInstrumentor.instrument_app()`, to which it passes `**kwargs`. diff --git a/logfire-api/logfire_api/_internal/scrubbing.pyi b/logfire-api/logfire_api/_internal/scrubbing.pyi index f5dc41ea6..0002a1271 100644 --- a/logfire-api/logfire_api/_internal/scrubbing.pyi +++ b/logfire-api/logfire_api/_internal/scrubbing.pyi @@ -1,19 +1,6 @@ import re import typing_extensions -from .constants import ( - ATTRIBUTES_JSON_SCHEMA_KEY as ATTRIBUTES_JSON_SCHEMA_KEY, - ATTRIBUTES_LOG_LEVEL_NAME_KEY as ATTRIBUTES_LOG_LEVEL_NAME_KEY, - ATTRIBUTES_LOG_LEVEL_NUM_KEY as ATTRIBUTES_LOG_LEVEL_NUM_KEY, - ATTRIBUTES_MESSAGE_KEY as ATTRIBUTES_MESSAGE_KEY, - ATTRIBUTES_MESSAGE_TEMPLATE_KEY as ATTRIBUTES_MESSAGE_TEMPLATE_KEY, - ATTRIBUTES_PENDING_SPAN_REAL_PARENT_KEY as ATTRIBUTES_PENDING_SPAN_REAL_PARENT_KEY, - ATTRIBUTES_SAMPLE_RATE_KEY as ATTRIBUTES_SAMPLE_RATE_KEY, - ATTRIBUTES_SCRUBBED_KEY as ATTRIBUTES_SCRUBBED_KEY, - ATTRIBUTES_SPAN_TYPE_KEY as ATTRIBUTES_SPAN_TYPE_KEY, - ATTRIBUTES_TAGS_KEY as ATTRIBUTES_TAGS_KEY, - NULL_ARGS_KEY as NULL_ARGS_KEY, - RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS as RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS, -) +from .constants import ATTRIBUTES_JSON_SCHEMA_KEY as ATTRIBUTES_JSON_SCHEMA_KEY, ATTRIBUTES_LOG_LEVEL_NAME_KEY as ATTRIBUTES_LOG_LEVEL_NAME_KEY, ATTRIBUTES_LOG_LEVEL_NUM_KEY as ATTRIBUTES_LOG_LEVEL_NUM_KEY, ATTRIBUTES_MESSAGE_KEY as ATTRIBUTES_MESSAGE_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY as ATTRIBUTES_MESSAGE_TEMPLATE_KEY, ATTRIBUTES_PENDING_SPAN_REAL_PARENT_KEY as ATTRIBUTES_PENDING_SPAN_REAL_PARENT_KEY, ATTRIBUTES_SAMPLE_RATE_KEY as ATTRIBUTES_SAMPLE_RATE_KEY, ATTRIBUTES_SCRUBBED_KEY as ATTRIBUTES_SCRUBBED_KEY, ATTRIBUTES_SPAN_TYPE_KEY as ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY as ATTRIBUTES_TAGS_KEY, NULL_ARGS_KEY as NULL_ARGS_KEY, RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS as RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS from .stack_info import STACK_INFO_KEYS as STACK_INFO_KEYS from .utils import ReadableSpanDict as ReadableSpanDict from _typeshed import Incomplete @@ -23,65 +10,52 @@ from opentelemetry.sdk.trace import Event from typing import Any, Callable, Sequence, TypedDict DEFAULT_PATTERNS: Incomplete -JsonPath: typing_extensions.TypeAlias = 'tuple[str | int, ...]' - +JsonPath: typing_extensions.TypeAlias @dataclass class ScrubMatch: """An object passed to the [`scrubbing_callback`][logfire.configure(scrubbing_callback)] function.""" - path: JsonPath value: Any pattern_match: re.Match[str] - - ScrubCallback = Callable[[ScrubMatch], Any] - class ScrubbedNote(TypedDict): path: JsonPath matched_substring: str - @dataclass class ScrubbingOptions: """Options for redacting sensitive data.""" - callback: ScrubCallback | None = ... extra_patterns: Sequence[str] | None = ... - class BaseScrubber(ABC): SAFE_KEYS: Incomplete - @abstractmethod def scrub_span(self, span: ReadableSpanDict): ... @abstractmethod def scrub_value(self, path: JsonPath, value: Any) -> tuple[Any, list[ScrubbedNote]]: ... - class NoopScrubber(BaseScrubber): def scrub_span(self, span: ReadableSpanDict): ... def scrub_value(self, path: JsonPath, value: Any) -> tuple[Any, list[ScrubbedNote]]: ... +NOOP_SCRUBBER: Incomplete class Scrubber(BaseScrubber): """Redacts potentially sensitive data.""" - def __init__(self, patterns: Sequence[str] | None, callback: ScrubCallback | None = None) -> None: ... def scrub_span(self, span: ReadableSpanDict): ... def scrub_value(self, path: JsonPath, value: Any) -> tuple[Any, list[ScrubbedNote]]: ... - class SpanScrubber: """Does the actual scrubbing work. This class is separate from Scrubber so that it can be instantiated more regularly and hold and mutate state about the span being scrubbed, specifically the scrubbed notes. """ - - scrubbed: list[ScrubbedNote] - + scrubbed: Incomplete def __init__(self, parent: Scrubber) -> None: ... def scrub_span(self, span: ReadableSpanDict): ... def scrub_event_attributes(self, event: Event, index: int): ... diff --git a/logfire-api/logfire_api/_internal/utils.pyi b/logfire-api/logfire_api/_internal/utils.pyi index 348281a3e..6ad139178 100644 --- a/logfire-api/logfire_api/_internal/utils.pyi +++ b/logfire-api/logfire_api/_internal/utils.pyi @@ -83,3 +83,4 @@ def suppress_instrumentation() -> Generator[None, None, 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 maybe_capture_server_headers(capture: bool): ... diff --git a/logfire-api/pyproject.toml b/logfire-api/pyproject.toml index 140d2aaba..33fc33ace 100644 --- a/logfire-api/pyproject.toml +++ b/logfire-api/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire-api" -version = "0.47.0" +version = "0.48.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/logfire/__init__.py b/logfire/__init__.py index 218baa600..3bd887963 100644 --- a/logfire/__init__.py +++ b/logfire/__init__.py @@ -102,6 +102,7 @@ def loguru_handler() -> dict[str, Any]: 'instrument_anthropic', 'instrument_asyncpg', 'instrument_httpx', + 'instrument_celery', 'instrument_requests', 'instrument_psycopg', 'instrument_django', diff --git a/pyproject.toml b/pyproject.toml index 0d6dd8af9..1899cb53a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "logfire" -version = "0.47.0" +version = "0.48.0" description = "The best Python observability tool! 🪵🔥" authors = [ { name = "Pydantic Team", email = "engineering@pydantic.dev" }, diff --git a/tests/test_logfire_api.py b/tests/test_logfire_api.py index 3ed932fdb..da0cfd9c3 100644 --- a/tests/test_logfire_api.py +++ b/tests/test_logfire_api.py @@ -42,6 +42,10 @@ def test_runtime(logfire_api_factory: Callable[[], ModuleType], module_name: str logfire_api = logfire_api_factory() assert logfire_api is not None + for member in dir(logfire_api): + if member.startswith('instrument_'): + assert member in logfire__all__, member + assert hasattr(logfire_api, 'Logfire') assert module_name in str(logfire_api.Logfire()) logfire__all__.remove('Logfire')