diff --git a/docs/reference/advanced/artifacts.md b/docs/reference/advanced/artifacts.md new file mode 100644 index 000000000..9537b538c --- /dev/null +++ b/docs/reference/advanced/artifacts.md @@ -0,0 +1,88 @@ +# Artifacts + +Artifacts let you attach a **binary blob** — an image, audio clip, PDF, or a large JSON +payload — to a span. The blob is stored separately from your telemetry, so it is not +subject to span attribute size limits and does not bloat your traces. The span itself +carries only a small reference; Logfire uploads the blob out of band. + +## Logging an artifact + +Wrap your data in `logfire.Artifact` and pass it as a span or log attribute: + +```python skip="true" +import logfire + +logfire.configure() + +with open('chart.png', 'rb') as f: + image_bytes = f.read() + +logfire.info('chart generated', chart=logfire.Artifact(image_bytes, content_type='image/png')) +``` + +The `chart` argument renders as an image preview on the trace in the Logfire UI, with a +download link — not as a wall of base64. + +### From a file or a file handle + +`Artifact.from_file` reads a path lazily (no need to load the bytes yourself), and +`Artifact.from_file_handle` accepts any open binary handle, including temporary files: + +```python skip="true" +import logfire + +logfire.configure() + +# From a path — the content type is guessed from the extension. +logfire.info('report ready', report=logfire.Artifact.from_file('report.pdf')) + +# From an open binary handle. +with open('clip.mp3', 'rb') as handle: + logfire.info('audio processed', clip=logfire.Artifact.from_file_handle(handle)) +``` + +## When the upload happens + +Each artifact chooses when its bytes are uploaded, via the `upload` argument: + +- **`background`** (the default) — the upload is handed to a background thread and the + logging call never blocks. If uploads cannot keep up, queued artifacts are dropped + with a warning rather than stalling your program. +- **`sync`** — the upload runs inline; the logging call returns only once the blob is + stored. Use this when you need delivery guaranteed, or when you want to free the + source bytes/file immediately afterwards. + +```python skip="true" +import logfire + +logfire.configure() + +# Block until this artifact is uploaded. +logfire.info('critical input', data=logfire.Artifact(payload, upload='sync')) +``` + +## How it works + +Artifacts are **content-addressed**: an artifact's identity is the sha256 of its bytes. +The same blob logged repeatedly — within a project — is uploaded and stored only once. +The reference embedded in the span looks like: + +```json +{ + "type": "logfire.artifact", + "sha256": "9f86d0818...", + "filename": "chart.png", + "content_type": "image/png", + "size_bytes": 28421 +} +``` + +The blob never travels through the telemetry pipeline. On SaaS the SDK uploads it +directly to object storage via a signed URL; self-hosted deployments route it through +the Logfire backend. + +## Viewing artifacts + +Open a span in the Logfire UI: any artifact-valued argument renders inline — images, +audio, video, and PDFs as previews, everything else as a download link — alongside its +filename, content type, and size. diff --git a/logfire-api/logfire_api/__init__.py b/logfire-api/logfire_api/__init__.py index 7931b4374..b581d4aee 100644 --- a/logfire-api/logfire_api/__init__.py +++ b/logfire-api/logfire_api/__init__.py @@ -2,11 +2,11 @@ import importlib import sys +from collections.abc import Sequence from contextlib import contextmanager, nullcontext -from typing import Any, ContextManager, Literal, TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING, Any, ContextManager, Literal from unittest.mock import MagicMock - try: logfire_module = importlib.import_module('logfire') sys.modules[__name__] = logfire_module @@ -85,7 +85,7 @@ def end_time(self): def parent(self): return None - def set_attribute(self, key: str, value: Any) -> None: ... # pragma: no cover + def set_attribute(self, key: str, value: Any) -> None: ... # pragma: no cover class Logfire: def __getattr__(self, attr): @@ -208,7 +208,6 @@ def url_from_eval(self, *args, **kwargs) -> str | None: ... def shutdown(self, *args, **kwargs) -> None: ... - DEFAULT_LOGFIRE_INSTANCE = Logfire() span = DEFAULT_LOGFIRE_INSTANCE.span log = DEFAULT_LOGFIRE_INSTANCE.log @@ -293,7 +292,6 @@ def __init__(self, *args, **kwargs) -> None: ... class MetricsOptions: def __init__(self, *args, **kwargs) -> None: ... - class PydanticPlugin: def __init__(self, *args, **kwargs) -> None: ... @@ -309,6 +307,19 @@ def __init__(self, *args, **kwargs) -> None: ... class LogfireLoggingHandler: def __init__(self, *args, **kwargs) -> None: ... + UploadMode = Literal['sync', 'background'] + + class Artifact: + def __init__(self, *args, **kwargs) -> None: ... + + @classmethod + def from_file(cls, *args, **kwargs) -> Artifact: + return cls() + + @classmethod + def from_file_handle(cls, *args, **kwargs) -> Artifact: + return cls() + def logfire_info() -> str: """Show versions of logfire, OS and related packages.""" return 'logfire_info() is not implement by logfire-api' diff --git a/logfire-api/logfire_api/__init__.pyi b/logfire-api/logfire_api/__init__.pyi index e73789536..ce561a653 100644 --- a/logfire-api/logfire_api/__init__.pyi +++ b/logfire-api/logfire_api/__init__.pyi @@ -1,22 +1,34 @@ +from typing import Any + +from logfire.propagate import attach_context as attach_context, get_context as get_context +from logfire.sampling import SamplingOptions as SamplingOptions + from . import variables as variables +from ._internal.artifacts import Artifact as Artifact, UploadMode as UploadMode from ._internal.auto_trace import AutoTraceModule as AutoTraceModule from ._internal.auto_trace.rewrite_ast import no_auto_trace as no_auto_trace from ._internal.baggage import get_baggage as get_baggage, set_baggage as set_baggage from ._internal.cli import logfire_info as logfire_info -from ._internal.config import AdvancedOptions as AdvancedOptions, CodeSource as CodeSource, ConsoleOptions as ConsoleOptions, LocalVariablesOptions as LocalVariablesOptions, MetricsOptions as MetricsOptions, PydanticPlugin as PydanticPlugin, VariablesOptions as VariablesOptions, configure as configure +from ._internal.config import ( + AdvancedOptions as AdvancedOptions, + CodeSource as CodeSource, + ConsoleOptions as ConsoleOptions, + LocalVariablesOptions as LocalVariablesOptions, + MetricsOptions as MetricsOptions, + PydanticPlugin as PydanticPlugin, + VariablesOptions as VariablesOptions, + configure as configure, +) from ._internal.constants import LevelName as LevelName from ._internal.main import Logfire as Logfire, LogfireSpan as LogfireSpan -from ._internal.scrubbing import ScrubMatch as ScrubMatch, ScrubbingOptions as ScrubbingOptions +from ._internal.scrubbing import ScrubbingOptions as ScrubbingOptions, ScrubMatch as ScrubMatch from ._internal.stack_info import add_non_user_code_prefix as add_non_user_code_prefix from ._internal.utils import suppress_instrumentation as suppress_instrumentation from .integrations.logging import LogfireLoggingHandler as LogfireLoggingHandler from .integrations.structlog import LogfireProcessor as StructlogProcessor from .version import VERSION as VERSION -from logfire.propagate import attach_context as attach_context, get_context as get_context -from logfire.sampling import SamplingOptions as SamplingOptions -from typing import Any -__all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'AdvancedOptions', 'ConsoleOptions', 'CodeSource', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'warning', 'error', 'exception', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_asgi', 'instrument_wsgi', 'instrument_pydantic', 'instrument_pydantic_ai', 'instrument_fastapi', 'instrument_openai', 'instrument_openai_agents', 'instrument_anthropic', 'instrument_google_genai', 'instrument_litellm', 'instrument_dspy', 'instrument_print', 'instrument_asyncpg', 'instrument_httpx', 'instrument_celery', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_aiohttp_server', 'instrument_sqlalchemy', 'instrument_sqlite3', 'instrument_aws_lambda', 'instrument_redis', 'instrument_pymongo', 'instrument_mysql', 'instrument_surrealdb', 'instrument_system_metrics', 'instrument_mcp', 'instrument_claude_agent_sdk', 'AutoTraceModule', 'with_tags', 'with_settings', 'suppress_scopes', 'shutdown', 'no_auto_trace', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'add_non_user_code_prefix', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'loguru_handler', 'SamplingOptions', 'MetricsOptions', 'VariablesOptions', 'LocalVariablesOptions', 'variables', 'var', 'variables_clear', 'variables_get', 'variables_push', 'variables_push_types', 'variables_validate', 'variables_push_config', 'variables_pull_config', 'variables_build_config', 'logfire_info', 'get_baggage', 'set_baggage', 'get_context', 'attach_context', 'url_from_eval'] +__all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'AdvancedOptions', 'ConsoleOptions', 'CodeSource', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'warning', 'error', 'exception', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_asgi', 'instrument_wsgi', 'instrument_pydantic', 'instrument_pydantic_ai', 'instrument_fastapi', 'instrument_openai', 'instrument_openai_agents', 'instrument_anthropic', 'instrument_google_genai', 'instrument_litellm', 'instrument_dspy', 'instrument_print', 'instrument_asyncpg', 'instrument_httpx', 'instrument_celery', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_aiohttp_server', 'instrument_sqlalchemy', 'instrument_sqlite3', 'instrument_aws_lambda', 'instrument_redis', 'instrument_pymongo', 'instrument_mysql', 'instrument_surrealdb', 'instrument_system_metrics', 'instrument_mcp', 'instrument_claude_agent_sdk', 'AutoTraceModule', 'with_tags', 'with_settings', 'suppress_scopes', 'shutdown', 'no_auto_trace', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'add_non_user_code_prefix', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'loguru_handler', 'SamplingOptions', 'MetricsOptions', 'VariablesOptions', 'LocalVariablesOptions', 'variables', 'var', 'variables_clear', 'variables_get', 'variables_push', 'variables_push_types', 'variables_validate', 'variables_push_config', 'variables_pull_config', 'variables_build_config', 'logfire_info', 'get_baggage', 'set_baggage', 'get_context', 'attach_context', 'url_from_eval', 'Artifact', 'UploadMode'] DEFAULT_LOGFIRE_INSTANCE = Logfire() span = DEFAULT_LOGFIRE_INSTANCE.span diff --git a/logfire-api/logfire_api/_internal/artifacts.pyi b/logfire-api/logfire_api/_internal/artifacts.pyi new file mode 100644 index 000000000..409e083b7 --- /dev/null +++ b/logfire-api/logfire_api/_internal/artifacts.pyi @@ -0,0 +1,111 @@ +import os +from abc import ABC, abstractmethod +from typing import IO, Any + +from _typeshed import Incomplete + +__all__ = ['Artifact', 'UploadMode'] + +UploadMode: Incomplete + +class ArtifactSource(ABC): + """A source of artifact bytes — normalises bytes, files, and handles to one surface. + + Deliberately small so that streaming sources can be added later as a new subclass + without touching `Artifact`, the upload handshake, or the backend. + """ + @abstractmethod + def digest(self) -> tuple[str, int]: + """Return `(sha256_hex, size_bytes)` for the content.""" + @abstractmethod + def read(self) -> bytes: + """Return the full content as bytes, for upload.""" + +class _BytesSource(ArtifactSource): + """An in-memory blob.""" + def __init__(self, data: bytes) -> None: ... + def digest(self) -> tuple[str, int]: ... + def read(self) -> bytes: ... + +class _PathSource(ArtifactSource): + """A file on disk. + + Hashed and uploaded by reading the path, so a `background` upload of a file path + holds no bytes in memory. + """ + def __init__(self, path: str | os.PathLike[str]) -> None: ... + def digest(self) -> tuple[str, int]: ... + def read(self) -> bytes: ... + +class Artifact: + """A binary blob to attach to a span — an image, audio clip, PDF, large JSON, etc. + + Pass an `Artifact` as a span or log attribute value. Logfire uploads the blob to + object storage out of band and embeds a small reference in the span. + + Examples: + ```python + import logfire + + logfire.configure() + + # From a file path. + logfire.info('chart generated', chart=logfire.Artifact.from_file('chart.png')) + + # From in-memory bytes. + logfire.info('thumbnail', image=logfire.Artifact(png_bytes, content_type='image/png')) + + # From an open binary handle (including temporary / spooled files). + with open('report.pdf', 'rb') as handle: + logfire.info('report', report=logfire.Artifact.from_file_handle(handle)) + ``` + """ + def __init__(self, data: bytes, *, filename: str | None = None, content_type: str | None = None, upload: UploadMode = 'background') -> None: + """Create an artifact from in-memory bytes. + + Args: + data: The blob bytes. + filename: Optional original filename, shown in the UI and used to guess the + content type. + content_type: MIME type of the blob. Guessed from `filename` when omitted, + falling back to `application/octet-stream`. + upload: When to upload the blob — see [`UploadMode`][logfire.UploadMode]. + Defaults to `background`. + """ + @classmethod + def from_file(cls, path: str | os.PathLike[str], *, filename: str | None = None, content_type: str | None = None, upload: UploadMode = 'background') -> Artifact: + """Create an artifact from a file path. + + The file is read once to hash it and again to upload it, so a `background` + upload of a file path holds no bytes in memory. + + Args: + path: Path to the file. + filename: Original filename to record. Defaults to the basename of `path`. + content_type: MIME type. Guessed from the path/filename when omitted. + upload: When to upload the blob — see [`UploadMode`][logfire.UploadMode]. + """ + @classmethod + def from_file_handle(cls, handle: IO[bytes], *, filename: str | None = None, content_type: str | None = None, upload: UploadMode = 'background') -> Artifact: + """Create an artifact from an open binary file handle. + + Works with any binary handle, including `tempfile.SpooledTemporaryFile` and + `tempfile.NamedTemporaryFile`. The handle is read in full immediately, so the + caller may close it as soon as this returns. + + Args: + handle: An open binary (`'rb'`) file-like object. + filename: Original filename to record. Defaults to the handle's `name`. + content_type: MIME type. Guessed from the filename when omitted. + upload: When to upload the blob — see [`UploadMode`][logfire.UploadMode]. + """ + @property + def sha256(self) -> str: + """The hex sha256 of the blob — its content-addressed identity.""" + @property + def size_bytes(self) -> int: + """The size of the blob in bytes.""" + def read(self) -> bytes: + """Read the full blob into memory (used by the uploader).""" + def reference(self) -> dict[str, Any]: + """The reference object embedded into the span attribute in place of the blob.""" diff --git a/logfire-api/logfire_api/_internal/config.pyi b/logfire-api/logfire_api/_internal/config.pyi index 7cfc1f567..c7218a0ed 100644 --- a/logfire-api/logfire_api/_internal/config.pyi +++ b/logfire-api/logfire_api/_internal/config.pyi @@ -1,31 +1,19 @@ import dataclasses -import requests -from ..propagate import NoExtractTraceContextPropagator as NoExtractTraceContextPropagator, WarnOnExtractTraceContextPropagator as WarnOnExtractTraceContextPropagator -from ..types import ExceptionCallback as ExceptionCallback -from .client import InvalidProjectName as InvalidProjectName, LogfireClient as LogfireClient, ProjectAlreadyExists as ProjectAlreadyExists -from .config_params import ParamManager as ParamManager, PydanticPluginRecordValues as PydanticPluginRecordValues, normalize_token as normalize_token -from .constants import ATTRIBUTES_CONFIG as ATTRIBUTES_CONFIG, ATTRIBUTES_PACKAGE_VERSIONS as ATTRIBUTES_PACKAGE_VERSIONS, LEVEL_NUMBERS as LEVEL_NUMBERS, LevelName as LevelName, RESOURCE_ATTRIBUTES_CODE_ROOT_PATH as RESOURCE_ATTRIBUTES_CODE_ROOT_PATH, RESOURCE_ATTRIBUTES_CODE_WORK_DIR as RESOURCE_ATTRIBUTES_CODE_WORK_DIR, RESOURCE_ATTRIBUTES_DEPLOYMENT_ENVIRONMENT_NAME as RESOURCE_ATTRIBUTES_DEPLOYMENT_ENVIRONMENT_NAME, RESOURCE_ATTRIBUTES_VCS_REPOSITORY_REF_REVISION as RESOURCE_ATTRIBUTES_VCS_REPOSITORY_REF_REVISION, RESOURCE_ATTRIBUTES_VCS_REPOSITORY_URL as RESOURCE_ATTRIBUTES_VCS_REPOSITORY_URL, RESOURCE_ATTRIBUTES_VERSION as RESOURCE_ATTRIBUTES_VERSION -from .exporters.console import ConsoleColorsValues as ConsoleColorsValues, ConsoleLogExporter as ConsoleLogExporter, IndentedConsoleSpanExporter as IndentedConsoleSpanExporter, ShowParentsConsoleSpanExporter as ShowParentsConsoleSpanExporter, SimpleConsoleSpanExporter as SimpleConsoleSpanExporter -from .exporters.dynamic_batch import DynamicBatchSpanProcessor as DynamicBatchSpanProcessor -from .exporters.logs import CheckSuppressInstrumentationLogProcessorWrapper as CheckSuppressInstrumentationLogProcessorWrapper, MainLogProcessorWrapper as MainLogProcessorWrapper -from .exporters.otlp import BodySizeCheckingOTLPSpanExporter as BodySizeCheckingOTLPSpanExporter, OTLPExporterHttpSession as OTLPExporterHttpSession, QuietLogExporter as QuietLogExporter, QuietSpanExporter as QuietSpanExporter, RetryFewerSpansSpanExporter as RetryFewerSpansSpanExporter, cleanup_disk_retryers as cleanup_disk_retryers -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 -from .integrations.executors import instrument_executors as instrument_executors -from .logs import ProxyLoggerProvider as ProxyLoggerProvider -from .main import Logfire as Logfire -from .metrics import ProxyMeterProvider as ProxyMeterProvider -from .scrubbing import BaseScrubber as BaseScrubber, NOOP_SCRUBBER as NOOP_SCRUBBER, Scrubber as Scrubber, ScrubbingOptions as ScrubbingOptions -from .server_response import ServerResponseCallback as ServerResponseCallback, install_logfire_response_hook as install_logfire_response_hook -from .stack_info import warn_at_user_stacklevel as warn_at_user_stacklevel -from .tracer import OPEN_SPANS as OPEN_SPANS, PendingSpanProcessor as PendingSpanProcessor, ProxyTracerProvider as ProxyTracerProvider -from .utils import SeededRandomIdGenerator as SeededRandomIdGenerator, ensure_data_dir_exists as ensure_data_dir_exists, handle_internal_errors as handle_internal_errors, platform_is_emscripten as platform_is_emscripten, suppress_instrumentation as suppress_instrumentation -from _typeshed import Incomplete from collections.abc import Callable, Sequence from dataclasses import dataclass, field from datetime import timedelta +from pathlib import Path +from typing import Any, ClassVar, Literal, TextIO, TypedDict + +import requests +from _typeshed import Incomplete +from opentelemetry.sdk._logs import LogRecordProcessor as LogRecordProcessor +from opentelemetry.sdk.metrics.export import MetricReader as MetricReader +from opentelemetry.sdk.metrics.view import View +from opentelemetry.sdk.trace import SpanProcessor +from opentelemetry.sdk.trace.id_generator import IdGenerator +from typing_extensions import Self, Unpack + from logfire._internal.auth import PYDANTIC_LOGFIRE_TOKEN_PATTERN as PYDANTIC_LOGFIRE_TOKEN_PATTERN, REGIONS as REGIONS from logfire._internal.baggage import DirectBaggageAttributesSpanProcessor as DirectBaggageAttributesSpanProcessor from logfire._internal.collect_system_info import collect_package_info as collect_package_info @@ -33,16 +21,95 @@ from logfire.exceptions import LogfireConfigError as LogfireConfigError from logfire.sampling import SamplingOptions as SamplingOptions from logfire.sampling._tail_sampling import TailSamplingProcessor as TailSamplingProcessor from logfire.variables import VariablesConfig as VariablesConfig -from logfire.variables.abstract import NoOpVariableProvider as NoOpVariableProvider, VariableProvider as VariableProvider +from logfire.variables.abstract import ( + NoOpVariableProvider as NoOpVariableProvider, + VariableProvider as VariableProvider, +) from logfire.version import VERSION as VERSION -from opentelemetry.sdk._logs import LogRecordProcessor as LogRecordProcessor -from opentelemetry.sdk.metrics.export import MetricReader as MetricReader -from opentelemetry.sdk.metrics.view import View -from opentelemetry.sdk.trace import SpanProcessor -from opentelemetry.sdk.trace.id_generator import IdGenerator -from pathlib import Path -from typing import Any, ClassVar, Literal, TextIO, TypedDict -from typing_extensions import Self, Unpack + +from ..propagate import ( + NoExtractTraceContextPropagator as NoExtractTraceContextPropagator, + WarnOnExtractTraceContextPropagator as WarnOnExtractTraceContextPropagator, +) +from ..types import ExceptionCallback as ExceptionCallback +from .artifacts import Artifact as Artifact +from .client import ( + InvalidProjectName as InvalidProjectName, + LogfireClient as LogfireClient, + ProjectAlreadyExists as ProjectAlreadyExists, +) +from .config_params import ( + ParamManager as ParamManager, + PydanticPluginRecordValues as PydanticPluginRecordValues, + normalize_token as normalize_token, +) +from .constants import ( + ATTRIBUTES_CONFIG as ATTRIBUTES_CONFIG, + ATTRIBUTES_PACKAGE_VERSIONS as ATTRIBUTES_PACKAGE_VERSIONS, + LEVEL_NUMBERS as LEVEL_NUMBERS, + RESOURCE_ATTRIBUTES_CODE_ROOT_PATH as RESOURCE_ATTRIBUTES_CODE_ROOT_PATH, + RESOURCE_ATTRIBUTES_CODE_WORK_DIR as RESOURCE_ATTRIBUTES_CODE_WORK_DIR, + RESOURCE_ATTRIBUTES_DEPLOYMENT_ENVIRONMENT_NAME as RESOURCE_ATTRIBUTES_DEPLOYMENT_ENVIRONMENT_NAME, + RESOURCE_ATTRIBUTES_VCS_REPOSITORY_REF_REVISION as RESOURCE_ATTRIBUTES_VCS_REPOSITORY_REF_REVISION, + RESOURCE_ATTRIBUTES_VCS_REPOSITORY_URL as RESOURCE_ATTRIBUTES_VCS_REPOSITORY_URL, + RESOURCE_ATTRIBUTES_VERSION as RESOURCE_ATTRIBUTES_VERSION, + LevelName as LevelName, +) +from .exporters.artifact_uploader import ArtifactUploader as ArtifactUploader +from .exporters.console import ( + ConsoleColorsValues as ConsoleColorsValues, + ConsoleLogExporter as ConsoleLogExporter, + IndentedConsoleSpanExporter as IndentedConsoleSpanExporter, + ShowParentsConsoleSpanExporter as ShowParentsConsoleSpanExporter, + SimpleConsoleSpanExporter as SimpleConsoleSpanExporter, +) +from .exporters.dynamic_batch import DynamicBatchSpanProcessor as DynamicBatchSpanProcessor +from .exporters.logs import ( + CheckSuppressInstrumentationLogProcessorWrapper as CheckSuppressInstrumentationLogProcessorWrapper, + MainLogProcessorWrapper as MainLogProcessorWrapper, +) +from .exporters.otlp import ( + BodySizeCheckingOTLPSpanExporter as BodySizeCheckingOTLPSpanExporter, + OTLPExporterHttpSession as OTLPExporterHttpSession, + QuietLogExporter as QuietLogExporter, + QuietSpanExporter as QuietSpanExporter, + RetryFewerSpansSpanExporter as RetryFewerSpansSpanExporter, + cleanup_disk_retryers as cleanup_disk_retryers, +) +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 +from .integrations.executors import instrument_executors as instrument_executors +from .logs import ProxyLoggerProvider as ProxyLoggerProvider +from .main import Logfire as Logfire +from .metrics import ProxyMeterProvider as ProxyMeterProvider +from .scrubbing import ( + NOOP_SCRUBBER as NOOP_SCRUBBER, + BaseScrubber as BaseScrubber, + Scrubber as Scrubber, + ScrubbingOptions as ScrubbingOptions, +) +from .server_response import ( + ServerResponseCallback as ServerResponseCallback, + install_logfire_response_hook as install_logfire_response_hook, +) +from .stack_info import warn_at_user_stacklevel as warn_at_user_stacklevel +from .tracer import ( + OPEN_SPANS as OPEN_SPANS, + PendingSpanProcessor as PendingSpanProcessor, + ProxyTracerProvider as ProxyTracerProvider, +) +from .utils import ( + SeededRandomIdGenerator as SeededRandomIdGenerator, + ensure_data_dir_exists as ensure_data_dir_exists, + handle_internal_errors as handle_internal_errors, + platform_is_emscripten as platform_is_emscripten, + suppress_instrumentation as suppress_instrumentation, +) CREDENTIALS_FILENAME: str COMMON_REQUEST_HEADERS: Incomplete @@ -253,6 +320,12 @@ class LogfireConfig(_LogfireConfigData): Returns: Whether the flush of spans was successful. """ + def submit_artifact(self, artifact: Artifact) -> None: + """Upload an artifact's blob out of band — inline (`sync`) or queued (`background`). + + No-op when there is no configured token (and hence no uploader): the artifact + reference is still recorded on the span, but the blob cannot be uploaded. + """ def get_tracer_provider(self) -> ProxyTracerProvider: """Get a tracer provider from this `LogfireConfig`. diff --git a/logfire-api/logfire_api/_internal/exporters/artifact_uploader.pyi b/logfire-api/logfire_api/_internal/exporters/artifact_uploader.pyi new file mode 100644 index 000000000..a85290d8b --- /dev/null +++ b/logfire-api/logfire_api/_internal/exporters/artifact_uploader.pyi @@ -0,0 +1,26 @@ +from _typeshed import Incomplete + +from ..artifacts import Artifact as Artifact +from ..utils import log_internal_error as log_internal_error + +DEFAULT_MAX_QUEUE_BYTES: Incomplete + +class ArtifactUploader: + """Uploads artifact blobs to the Logfire backend. + + Owns a daemon worker thread for `background` artifacts; `sync` artifacts are uploaded + on the calling thread. + """ + def __init__(self, *, base_url: str, token: str, max_queue_bytes: int = ...) -> None: ... + def submit(self, artifact: Artifact) -> None: + """Upload an artifact's blob — inline for `sync`, queued for `background`. + + A `background` submit **never blocks the caller**: if the queue is already over + its byte budget, the artifact is dropped with a warning rather than applying + backpressure. A `sync` submit uploads inline (and so blocks) by the caller's + explicit choice. Never raises — upload failures are handled internally. + """ + def flush(self, timeout: float | None = None) -> bool: + """Block until queued background uploads have drained. Returns whether they did.""" + def shutdown(self, timeout: float = 5.0) -> None: + """Drain in-flight uploads and stop the worker thread.""" diff --git a/logfire-api/logfire_api/_internal/json_encoder.pyi b/logfire-api/logfire_api/_internal/json_encoder.pyi index 1496d336d..20b7649ca 100644 --- a/logfire-api/logfire_api/_internal/json_encoder.pyi +++ b/logfire-api/logfire_api/_internal/json_encoder.pyi @@ -1,8 +1,11 @@ -from .utils import JsonValue as JsonValue, safe_repr as safe_repr -from _typeshed import Incomplete from functools import cache, lru_cache from typing import Any +from _typeshed import Incomplete + +from .artifacts import Artifact as Artifact +from .utils import JsonValue as JsonValue, safe_repr as safe_repr + NUMPY_DIMENSION_MAX_SIZE: int EncoderFunction: Incomplete diff --git a/logfire-api/logfire_api/_internal/main.pyi b/logfire-api/logfire_api/_internal/main.pyi index 4bb0d8b67..ec4580a55 100644 --- a/logfire-api/logfire_api/_internal/main.pyi +++ b/logfire-api/logfire_api/_internal/main.pyi @@ -1,3 +1,9 @@ +from collections.abc import Iterable, Mapping, Sequence +from contextlib import AbstractContextManager +from types import ModuleType +from typing import Any, Callable, Literal, TypeVar, overload +from wsgiref.types import WSGIApplication + import anthropic import httpx import openai @@ -5,20 +11,76 @@ import opentelemetry.trace as trace_api import pydantic_ai import pydantic_ai.models import requests -from . import async_ as async_ -from ..integrations.aiohttp_client import RequestHook as AiohttpClientRequestHook, ResponseHook as AiohttpClientResponseHook -from ..integrations.flask import CommenterOptions as FlaskCommenterOptions, RequestHook as FlaskRequestHook, ResponseHook as FlaskResponseHook -from ..integrations.httpx import AsyncRequestHook as HttpxAsyncRequestHook, AsyncResponseHook as HttpxAsyncResponseHook, RequestHook as HttpxRequestHook, ResponseHook as HttpxResponseHook +from anthropic.lib.bedrock import AnthropicBedrock as _AnthropicBedrock, AsyncAnthropicBedrock as _AsyncAnthropicBedrock +from django.http import HttpRequest as HttpRequest, HttpResponse as HttpResponse +from fastapi import FastAPI +from flask.app import Flask +from opentelemetry.context import Context as Context +from opentelemetry.instrumentation.asgi.types import ClientRequestHook, ClientResponseHook, ServerRequestHook +from opentelemetry.metrics import CallbackT as CallbackT, Counter, Histogram, UpDownCounter, _Gauge as Gauge +from opentelemetry.sdk.trace import ReadableSpan, Span +from opentelemetry.trace import SpanContext, SpanKind +from opentelemetry.util import types as otel_types +from pydantic_evals.reporting import EvaluationReport +from pymongo.monitoring import ( + CommandFailedEvent as CommandFailedEvent, + CommandStartedEvent as CommandStartedEvent, + CommandSucceededEvent as CommandSucceededEvent, +) +from sqlalchemy import Engine +from sqlalchemy.ext.asyncio import AsyncEngine +from starlette.applications import Starlette +from starlette.requests import Request as Request +from starlette.websockets import WebSocket as WebSocket +from surrealdb.connections.async_template import AsyncTemplate +from surrealdb.connections.sync_template import SyncTemplate +from typing_extensions import LiteralString, ParamSpec, Unpack + +from ..integrations.aiohttp_client import ( + RequestHook as AiohttpClientRequestHook, + ResponseHook as AiohttpClientResponseHook, +) +from ..integrations.flask import ( + CommenterOptions as FlaskCommenterOptions, + RequestHook as FlaskRequestHook, + ResponseHook as FlaskResponseHook, +) +from ..integrations.httpx import ( + AsyncRequestHook as HttpxAsyncRequestHook, + AsyncResponseHook as HttpxAsyncResponseHook, + RequestHook as HttpxRequestHook, + ResponseHook as HttpxResponseHook, +) from ..integrations.psycopg import CommenterOptions as PsycopgCommenterOptions from ..integrations.redis import RequestHook as RedisRequestHook, ResponseHook as RedisResponseHook from ..integrations.sqlalchemy import CommenterOptions as SQLAlchemyCommenterOptions from ..integrations.wsgi import RequestHook as WSGIRequestHook, ResponseHook as WSGIResponseHook -from ..variables import ResolveFunction as ResolveFunction, ValidationReport as ValidationReport, Variable as Variable, VariablesConfig as VariablesConfig +from ..variables import ( + ResolveFunction as ResolveFunction, + ValidationReport as ValidationReport, + Variable as Variable, + VariablesConfig as VariablesConfig, +) from ..version import VERSION as VERSION +from . import async_ as async_ +from .artifacts import Artifact as Artifact from .auto_trace import AutoTraceModule as AutoTraceModule, install_auto_tracing as install_auto_tracing from .config import GLOBAL_CONFIG as GLOBAL_CONFIG, LogfireConfig as LogfireConfig from .config_params import PydanticPluginRecordValues as PydanticPluginRecordValues -from .constants import ATTRIBUTES_JSON_SCHEMA_KEY as ATTRIBUTES_JSON_SCHEMA_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_SAMPLE_RATE_KEY as ATTRIBUTES_SAMPLE_RATE_KEY, ATTRIBUTES_SPAN_TYPE_KEY as ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY as ATTRIBUTES_TAGS_KEY, DISABLE_CONSOLE_KEY as DISABLE_CONSOLE_KEY, LEVEL_NUMBERS as LEVEL_NUMBERS, LevelName as LevelName, OTLP_MAX_INT_SIZE as OTLP_MAX_INT_SIZE, log_level_attributes as log_level_attributes +from .constants import ( + ATTRIBUTES_JSON_SCHEMA_KEY as ATTRIBUTES_JSON_SCHEMA_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_SAMPLE_RATE_KEY as ATTRIBUTES_SAMPLE_RATE_KEY, + ATTRIBUTES_SPAN_TYPE_KEY as ATTRIBUTES_SPAN_TYPE_KEY, + ATTRIBUTES_TAGS_KEY as ATTRIBUTES_TAGS_KEY, + DISABLE_CONSOLE_KEY as DISABLE_CONSOLE_KEY, + LEVEL_NUMBERS as LEVEL_NUMBERS, + OTLP_MAX_INT_SIZE as OTLP_MAX_INT_SIZE, + LevelName as LevelName, + 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 instrument as instrument from .integrations.asgi import ASGIApp as ASGIApp, ASGIInstrumentKwargs as ASGIInstrumentKwargs @@ -29,36 +91,26 @@ from .integrations.psycopg import Psycopg2Connection as Psycopg2Connection, Psyc from .integrations.sqlite3 import SQLite3Connection as SQLite3Connection from .integrations.system_metrics import Base as SystemMetricsBase, Config as SystemMetricsConfig from .json_encoder import logfire_json_dumps as logfire_json_dumps -from .json_schema import JsonSchemaProperties as JsonSchemaProperties, attributes_json_schema as attributes_json_schema, attributes_json_schema_properties as attributes_json_schema_properties, create_json_schema as create_json_schema +from .json_schema import ( + JsonSchemaProperties as JsonSchemaProperties, + attributes_json_schema as attributes_json_schema, + attributes_json_schema_properties as attributes_json_schema_properties, + create_json_schema as create_json_schema, +) from .metrics import ProxyMeterProvider as ProxyMeterProvider from .stack_info import get_user_stack_info as get_user_stack_info -from .tracer import ProxyTracerProvider as ProxyTracerProvider, _ProxyTracer, set_exception_status as set_exception_status -from .utils import SysExcInfo as SysExcInfo, get_version as get_version, handle_internal_errors as handle_internal_errors, log_internal_error as log_internal_error, uniquify_sequence as uniquify_sequence -from anthropic.lib.bedrock import AnthropicBedrock as _AnthropicBedrock, AsyncAnthropicBedrock as _AsyncAnthropicBedrock -from collections.abc import Iterable, Mapping, Sequence -from contextlib import AbstractContextManager -from django.http import HttpRequest as HttpRequest, HttpResponse as HttpResponse -from fastapi import FastAPI -from flask.app import Flask -from opentelemetry.context import Context as Context -from opentelemetry.instrumentation.asgi.types import ClientRequestHook, ClientResponseHook, ServerRequestHook -from opentelemetry.metrics import CallbackT as CallbackT, Counter, Histogram, UpDownCounter, _Gauge as Gauge -from opentelemetry.sdk.trace import ReadableSpan, Span -from opentelemetry.trace import SpanContext, SpanKind -from opentelemetry.util import types as otel_types -from pydantic_evals.reporting import EvaluationReport -from pymongo.monitoring import CommandFailedEvent as CommandFailedEvent, CommandStartedEvent as CommandStartedEvent, CommandSucceededEvent as CommandSucceededEvent -from sqlalchemy import Engine -from sqlalchemy.ext.asyncio import AsyncEngine -from starlette.applications import Starlette -from starlette.requests import Request as Request -from starlette.websockets import WebSocket as WebSocket -from surrealdb.connections.async_template import AsyncTemplate -from surrealdb.connections.sync_template import SyncTemplate -from types import ModuleType -from typing import Any, Callable, Literal, TypeVar, overload -from typing_extensions import LiteralString, ParamSpec, Unpack -from wsgiref.types import WSGIApplication +from .tracer import ( + ProxyTracerProvider as ProxyTracerProvider, + _ProxyTracer, + set_exception_status as set_exception_status, +) +from .utils import ( + SysExcInfo as SysExcInfo, + get_version as get_version, + handle_internal_errors as handle_internal_errors, + log_internal_error as log_internal_error, + uniquify_sequence as uniquify_sequence, +) ExcInfo = SysExcInfo | BaseException | bool | None T = TypeVar('T') @@ -525,7 +577,7 @@ class Logfire: i.e. it's not necessary to use this as a context manager. """ def instrument_openai(self, openai_client: openai.OpenAI | openai.AsyncOpenAI | type[openai.OpenAI] | type[openai.AsyncOpenAI] | None = None, *, suppress_other_instrumentation: bool = True, version: SemconvVersion | Sequence[SemconvVersion] = 1) -> AbstractContextManager[None]: - '''Instrument an OpenAI client so that spans are automatically created for each request. + """Instrument an OpenAI client so that spans are automatically created for each request. This instruments the [standard OpenAI SDK](https://pypi.org/project/openai/) package, for instrumentation of the OpenAI "agents" framework, see [`instrument_openai_agents()`][logfire.Logfire.instrument_openai_agents]. @@ -587,7 +639,7 @@ class Logfire: Returns: A context manager that will revert the instrumentation when exited. Use of this context manager is optional. - ''' + """ def instrument_openai_agents(self) -> None: """Instrument the [`agents`](https://github.com/openai/openai-agents-python) framework from OpenAI. @@ -595,7 +647,7 @@ class Logfire: see [`instrument_openai()`][logfire.Logfire.instrument_openai]. """ def instrument_anthropic(self, anthropic_client: anthropic.Anthropic | anthropic.AsyncAnthropic | _AnthropicBedrock | _AsyncAnthropicBedrock | type[anthropic.Anthropic] | type[anthropic.AsyncAnthropic] | type[_AnthropicBedrock] | type[_AsyncAnthropicBedrock] | None = None, *, suppress_other_instrumentation: bool = True, version: SemconvVersion | Sequence[SemconvVersion] = 1) -> AbstractContextManager[None]: - '''Instrument an Anthropic client so that spans are automatically created for each request. + """Instrument an Anthropic client so that spans are automatically created for each request. The following methods are instrumented for both the sync and async clients: @@ -652,7 +704,7 @@ class Logfire: Returns: A context manager that will revert the instrumentation when exited. Use of this context manager is optional. - ''' + """ def instrument_google_genai(self, **kwargs: Any): """Instrument the [Google Gen AI SDK (`google-genai`)](https://googleapis.github.io/python-genai/). @@ -1346,7 +1398,7 @@ class Logfire: ``` """ def variables_push_config(self, config: VariablesConfig, *, mode: Literal['merge', 'replace'] = 'merge', dry_run: bool = False, yes: bool = False) -> bool: - '''Push a VariablesConfig to the configured provider. + """Push a VariablesConfig to the configured provider. This method pushes a complete VariablesConfig (including labels and rollouts) to the provider. It\'s useful for: @@ -1375,9 +1427,9 @@ class Logfire: # Or merge just a subset of variables logfire.variables_push_config(config, mode=\'merge\') ``` - ''' + """ def variables_pull_config(self) -> VariablesConfig: - '''Pull the current variable configuration from the provider. + """Pull the current variable configuration from the provider. This method fetches the complete configuration from the provider, useful for generating local copies of the config that can be modified. @@ -1393,9 +1445,9 @@ class Logfire: config = logfire.variables_pull_config() print(config.model_dump_json(indent=2)) ``` - ''' + """ def variables_build_config(self, variables: list[Variable[Any]] | None = None) -> VariablesConfig: - '''Build a VariablesConfig from registered Variable instances. + """Build a VariablesConfig from registered Variable instances. This creates a minimal config with just the name, schema, and example for each variable. No labels or versions are created - use this to build a template config that can be edited. @@ -1417,7 +1469,7 @@ class Logfire: config = logfire.variables_build_config() print(config.model_dump_json(indent=2)) ``` - ''' + """ class FastLogfireSpan: """A simple version of `LogfireSpan` optimized for auto-tracing.""" diff --git a/logfire-api/logfire_api/variables/abstract.pyi b/logfire-api/logfire_api/variables/abstract.pyi index 08212c9a3..311d60cda 100644 --- a/logfire-api/logfire_api/variables/abstract.pyi +++ b/logfire-api/logfire_api/variables/abstract.pyi @@ -1,12 +1,14 @@ -import logfire -from _typeshed import Incomplete from abc import ABC, abstractmethod from collections.abc import Mapping, Sequence from dataclasses import dataclass -from logfire.variables.config import VariableConfig, VariableTypeConfig, VariablesConfig -from logfire.variables.variable import Variable from typing import Any, Generic, TypeVar +from _typeshed import Incomplete + +import logfire +from logfire.variables.config import VariableConfig, VariablesConfig, VariableTypeConfig +from logfire.variables.variable import Variable + __all__ = ['ResolvedVariable', 'SyncMode', 'ValidationReport', 'VariableProvider', 'NoOpVariableProvider', 'VariableWriteError', 'VariableNotFoundError', 'VariableAlreadyExistsError'] SyncMode: Incomplete @@ -22,22 +24,24 @@ class VariableAlreadyExistsError(VariableWriteError): @dataclass(kw_only=True) class ResolvedVariable(Generic[T_co]): - '''Details about a variable resolution including value, label, version, and any errors. + """Details about a variable resolution including value, label, version, and any errors. This class can be used as a context manager. When used as a context manager, it - automatically sets baggage with the variable name and label, enabling downstream - spans and logs to be associated with the variable resolution that was active at the time. + automatically sets baggage with the variable name, label, and (when applicable) + version, enabling downstream spans and logs to be associated with the variable + resolution that was active at the time. Example: ```python skip="true" my_var = logfire.var(name=\'my_var\', type=str, default=\'default\') with my_var.get() as details: # Inside this context, baggage is set with: - # logfire.variables.my_var =