logfire-api
logfire-api is an optional-integration shim for Python library authors. Code imports logfire_api and writes against a clone of Pydantic Logfire's public Python API. If the real logfire package is installed, the shim replaces its own module entry with logfire and calls become real telemetry. If logfire is absent, logging, spans, decorators, integrations, baggage, and configuration quietly become no-ops. It is not an observability backend, exporter, or lightweight edition of the Logfire SDK.
A clever dependency-boundary tool for library authors, and the wrong package for most applications. If you expect to see telemetry, install and configure logfire rather than trusting a shim whose core promise is silence.
Use it if
- You maintain a reusable package that should emit Logfire telemetry only when its application opts in
- You want a zero-runtime-dependency import path with bundled type stubs matching the current Logfire API
- Your library must keep working when Logfire is not installed, without scattered try and except ImportError blocks
- You accept silent no-op behavior as the explicit contract for the optional integration
- You are instrumenting an application you control: install logfire itself, because this shim silently records nothing when the SDK is missing
- You need a vendor-neutral instrumentation boundary: the stubs clone Logfire's large API, so your library surface and examples still become coupled to one SDK's names and behavior
- You need confirmation that telemetry was exported: without logfire, configure returns nothing, spans report is_recording false, force_flush does nothing, and calls intentionally do not warn
- You cannot keep versions aligned: the shim mirrors Logfire's evolving public API and includes a very broad generated stub tree, so an old shim can lag features expected by a newer integration
- You only need structured application logging: structlog or the standard logging module has a clearer failure model than an observability API designed to disappear
Setup reality
For a reusable library, add logfire-api as a normal dependency and import it as logfire_api. Do not depend on logfire unless telemetry is mandatory; the whole point is that the host application chooses whether to install the full SDK. Version 4.40.0 requires Python 3.10 or newer and declares no runtime dependencies. At import time it tries importlib.import_module('logfire'). When successful, it assigns that real module into sys.modules under the shim name, so subsequent logfire_api calls are actual Logfire calls. When the import fails, the local implementation supplies no-op functions, context managers, decorators, integration methods, baggage and context helpers, and placeholder option classes. That means development can look perfectly healthy while production emits nothing, or the reverse, unless you test both dependency states. The fallback is intentionally forgiving: unknown instance attributes return MagicMock, configure accepts arbitrary arguments, instrument returns the original function, ASGI and WSGI helpers return the original app, span returns a context manager whose is_recording() is false, and logfire_info reports that it is not implemented. This is convenient for compatibility but poor at catching misspellings dynamically; depend on the shipped py.typed stubs and run a type checker. Avoid using runtime-specific return values from optional calls, because fallback methods often return None, empty dictionaries, null context managers, or mocks rather than meaningful SDK objects. Import through logfire_api consistently inside the library. Importing logfire directly defeats optional installation, while mixing both names makes tests dependent on import order and the sys.modules substitution. Document an extra such as your-package[logfire] that installs a compatible logfire version for application owners. In CI, run one environment with only logfire-api and another with the full SDK. The no-SDK test should prove functionality is unchanged; the SDK test should assert emitted spans or use Logfire's test facilities. This package does not configure credentials, an exporter, a project, scrubbing, or sampling for users. Those are application-level Logfire responsibilities.
Patterns
Emit a structured event from a libraryemit-optional-log
import logfire_api
def parse_document(document_id: str) -> None:
logfire_api.info('Parsing document {document_id}', document_id=document_id)
# library work continues whether Logfire is installed or notWith only logfire-api installed, this call returns silently and records no event.
Wrap work in an optional spantrace-optional-span
import logfire_api
def rebuild_index(index_name: str) -> None:
with logfire_api.span('Rebuild index {index_name}', index_name=index_name):
run_rebuild(index_name)The fallback span is a valid context manager, so library control flow stays the same without the SDK.
Decorate a function without requiring Logfireinstrument-function
import logfire_api
@logfire_api.instrument('Resolve customer')
def resolve_customer(customer_id: str):
return repository.get(customer_id)When Logfire is absent, the fallback decorator returns the original function unchanged.
Create a tagged optional loggerattach-tags
import logfire_api
telemetry = logfire_api.with_tags('payments', 'library')
telemetry.info('Payment adapter selected {adapter}', adapter='example')The fallback returns the same no-op Logfire instance; do not use tags as application state.
Record and re-raise an exceptionrecord-exception
import logfire_api
def load_record(record_id: str):
try:
return backend.load(record_id)
except Exception:
logfire_api.exception('Failed to load {record_id}', record_id=record_id)
raiseThe exception still propagates without Logfire because the explicit raise, not the telemetry call, preserves behavior.
Pass optional trace context across a boundarypropagate-context
import logfire_api
def outgoing_headers() -> dict[str, str]:
context = logfire_api.get_context()
return {f'x-trace-{key}': str(value) for key, value in context.items()}
def consume(headers: dict[str, str]):
context = {key[8:]: value for key, value in headers.items() if key.startswith('x-trace-')}
with logfire_api.attach_context(context):
process_message()Without the real SDK, get_context returns an empty dictionary and attach_context is a no-op context manager.
Suppress telemetry around internal transportsuppress-instrumentation
import logfire_api
def export_internal_batch(batch):
with logfire_api.suppress_instrumentation():
transport.send(batch)The fallback is a null context manager; this pattern matters only when the full SDK is present.
Detect whether the real SDK is installeddetect-real-sdk
from importlib.util import find_spec
LOGFIRE_AVAILABLE = find_spec('logfire') is not NoneCheck the real package name before importing the shim; after import, logfire_api may be an alias to the logfire module.
Keep library behavior independent of telemetrytest-both-modes
def test_processing_result_is_not_telemetry_dependent(monkeypatch):
import logfire_api
monkeypatch.setattr(logfire_api, 'info', lambda *args, **kwargs: None)
assert process('input') == 'expected'Run a separate integration environment with logfire installed to test real event emission; this only proves the library tolerates no-op telemetry.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| logfire | PyPI | You own the application and actually want the Logfire SDK, exporters, integrations, and configuration |
| opentelemetry-api | PyPI | You want a vendor-neutral instrumentation API whose provider can be installed separately |
| structlog | PyPI | Your requirement is structured application logging rather than optional traces, metrics, and Logfire integrations |