Pydantic Logfire is an observability platform built on OpenTelemetry. This repository contains the Python SDK for Logfire and documentation. The server application for recording and displaying data is closed source.
Key aspects:
- Opinionated wrapper around OpenTelemetry (traces, metrics, logs)
- Extensive integrations with popular Python packages
- SQL-based querying of telemetry data
Pre-commit automatically runs ruff and pyright, but you can also run make format/lint/typecheck to run them explicitly, particularly to check files that haven't been changed.
Docs are rendered and deployed through the pydantic/unified-docs pipeline. Do not use MkDocs checks in this repository.
When linking between pages in this repository, use source-relative .md links. Published routes can
differ from source paths because docs/navigation.yml defines the published
sidebar and route map. When adding, moving, or removing a public docs page, update that manifest in
the same PR. Its public schema provides editor validation; contributors do not need a unified-docs
checkout. Verify the page and any anchor in a rendered preview, and never include the
deployment-specific /docs prefix in source links.
The TypeScript subtree under Instrument is imported from
pydantic/logfire-js/docs/navigation.yml. Change its page order, labels, slugs,
and aliases in logfire-js; this manifest owns only the subtree's placement.
The full documentation style guide is dev-docs/documentation-style-guide.md — page templates, the terminology glossary, the pre-publish checklist, the anti-pattern catalog, and the rules for AI-assisted authoring. Read it before writing or substantially editing a docs page.
Every page in the public docs (docs/) is held to one standard:
If an expert in some other field who has just started building with AI tools wouldn't know what it means, we spell out the acronym, we explain the term in place, or we rewrite the sentence to be human friendly.
This is about introducing terms, not avoiding them — give the real word plus a plain-language hand-hold at first use. Docstrings and other in-code text follow normal API-reference conventions and are out of scope.
logfire-sdk/
└── logfire/
├── __init__.py # Public API via DEFAULT_LOGFIRE_INSTANCE
├── _internal/ # Internal implementation
│ ├── main.py # Logfire and LogfireSpan classes
│ ├── config.py # LogfireConfig, configuration setup
│ ├── config_params.py # Environment variable and config file handling
│ ├── tracer.py # ProxyTracerProvider, tracer wrapping
│ ├── metrics.py # ProxyMeterProvider, metrics handling
│ ├── exporters/ # OTLP, console, test exporters and processors
│ ├── integrations/ # Framework-specific instrumentation
│ ├── auto_trace/ # AST rewriting for auto-instrumentation
│ └── ...
├── integrations/ # Public integration APIs
└── experimental/ # Experimental features
logfire/ # Compatibility package that installs the SDK and CLI
logfire-api/ # No-op shim package for libraries
tests/ # Test suite
docs/ # Documentation source for unified docs
Tests that create spans should follow this pattern:
from inline_snapshot import snapshot
from logfire.testing import TestExporter
import logfire
def test_my_thing(exporter: TestExporter):
# create spans, e.g:
with logfire.span("a span"):
...
assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot()Then run uv run pytest -k test_my_thing --inline-snapshot=fix to automatically fill in snapshot() with a list of dicts and check that the results are sane.
If the output changes, running again will automatically update the snapshot in the code.
TestExporter normalizes common things. If some remaining fields are non-deterministic (e.g., IDs, timestamps), use dirty_equals matchers, e.g:
from dirty_equals import IsStr
from inline_snapshot import snapshot
assert ... == snapshot({
'name': 'foo',
'random_id': IsStr(),
})Use @pytest.mark.anyio for async tests.
Emitting a warning is safe even though the test suite converts warnings to errors. When a warning is intentional, update affected tests to expect it or narrow their warning filters instead of suppressing the warning in production code. Suppress a warning at a call site only when it would be a known duplicate or is intentionally irrelevant there.
Some tests are decorated with @pytest.mark.vcr() and use pytest-recording to record HTTP interactions. Existing VCR cassette files should suffice. When creating a new test like this, run uv run pytest -k test_my_thing --inline-snapshot=fix --record-mode=rewrite.
Tests should use user-facing APIs as much as possible. Minimize mocking and reaching into internals.
Avoid constructing LogfireConfig or Logfire instances unless absolutely necessary. Use logfire.configure() instead, typically with the config_kwargs fixture, and even then only if the default configuration done for each test doesn't already suffice.
There's no need to write lf = logfire.configure(...); lf.foo(). Write logfire.configure(...); logfire.foo(). There's also generally no need to explicitly call logfire.shutdown().
The logfire-api package is a no-op shim that libraries can depend on to avoid hard dependencies on Logfire itself. It provides minimal 'implementations' in logfire-api/logfire_api/__init__.py, which needs to be kept up to date with the public API of the logfire module, especially if test_logfire_api.py starts failing. The rest is just .pyi stubs which should be ignored and are autogenerated when needed during release.
CI is required to pass on main, so pre-existing CI failures are unlikely. If the same test fails across multiple Python version jobs, it's almost certainly caused by your changes — investigate rather than assuming it's a flaky pre-existing issue.
Coverage must be 100%. The bar is the uv run coverage report --fail-under 100 step of the coverage job in .github/workflows/main.yml, not pyproject.toml — [tool.coverage.report] sets no fail_under, so reading pyproject.toml suggests there is no bar and a plain coverage report exits 0 on a regression.
Let CI measure it rather than running the suite under coverage locally: CI combines coverage across the whole test matrix, so a line reached only by another matrix job looks missed on one machine, and a local number is both slow to get and misleading. When the coverage job fails it names the file and the missing lines and branches; cover those and push again. Reach for a local run only to check a specific file you are iterating on, with uv run coverage run -m pytest <the relevant tests> followed by uv run coverage report --include='*/<file>.py'.
The check job runs no checks of its own: it uses the alls-green action to combine the other jobs' results into a single pass or fail. Its log therefore only names which job failed, so open that job and read its log to find the cause.
PR CI only tests pydantic latest plus one extra job at pydantic 2.4. The full set of supported minor versions (2.4, 2.5, 2.6, ... up through main) is exercised by the twice-monthly job in .github/workflows/deps_test.yml, scheduled for the 1st and 15th. This workflow is the contract: every minor version listed there is meant to keep working.
When something fails on pydantic 2.4, do not assume it is a 2.4-only quirk. The same problem is likely to affect some of the other listed versions as well. Investigate which versions are actually affected (e.g. read the upstream changelog, install one of the in-between versions locally and reproduce) and fix or work around for the whole affected range. A green PR CI is not enough — if you only verify against 2.4 and latest, the twice-monthly job will fail later even though the PR merged cleanly.
A pull request is ready when CI is green and no review thread is unresolved. Run this loop until both hold.
- Watch the checks until they settle, then fix every failure.
- Read each new review comment. Several AI reviewers comment on pull requests here; verify each finding against this repository's code, configuration and history before acting on it. Do not assume a reviewer is right, and do not assume it is wrong.
- Fix what the valid findings call for. Reply to every thread with what you changed, or with the evidence that the finding does not apply here.
- Resolve each thread once you have replied. Leave a thread unresolved only to hold an open question that needs a maintainer's decision — one the repository's own docs, configuration and history cannot settle.
- Return to step 1 after every push, because reviewers comment again on the new commit.
A reviewer can be right about Python in general and wrong about this repository. For example, a reviewer may report a Ruff S106 violation (hardcoded password passed as an argument). [tool.ruff.lint] in pyproject.toml selects E4, E7, E9 and F, plus an extend-select list that does not contain S, and uv run ruff check --select S106 reports many pre-existing hits. The rule is not enabled, so there is nothing to fix. Check the configuration that governs a finding before you accept it.
Use git push origin HEAD to push, not just git push, so that it pushes to the current branch without needing to set upstream explicitly.