Support public ingestion keys in Python logging - #845
Closed
Luca Forstner (lforst) wants to merge 5 commits into
Closed
Luca Forstner (lforst) wants to merge 5 commits into
Luca Forstner (lforst) wants to merge 5 commits into
Conversation
`init_logger(ingestion_key=...)` and `BRAINTRUST_INGESTION_KEY` create a logger that writes traces to a single project with an ingestion key URL. The logger has its own queue and transport per endpoint and key, sends the key as a bearer token, and never logs in, discovers the project, or falls back to private credentials. An explicit `api_key` takes precedence over the env var, and passing both options is an error. Attachments and oversized batches go through the data plane's chunked `/v1/uploads` API before the rows that reference them are published. Rows only carry trace fields, and feedback is limited to scores, expected values, and tags, since comments and audit fields are writable provenance. Slug and W3C parents, plus global `update_span`, route through the current ingestion key logger so they don't trigger a private login.
The data plane rejects `external_attachment` references from ingestion keys, since they can point at arbitrary object store content. Rows containing an `ExternalAttachment` are now dropped with a clear error instead of being published. Batches above 512 KiB now overflow through an upload, since Lambda base64-encodes binary bodies and ingress accepts 1 MiB by default. An explicit `ingestion_key=""` or malformed URL now raises instead of falling back to `BRAINTRUST_INGESTION_KEY` or the private login. Upload grants with malformed lifetimes, chunk counts, or ids are rejected, a 410 on a chunk or commit starts a new upload, and request timeouts and retry waits never outlive the grant. Requests don't follow redirects, so the key can't be forwarded elsewhere, and transport errors are redacted. The README now says that anyone holding a key can update any project row whose ID they know.
Empty `&` segments are now skipped instead of rejecting the URL, which matches how the other SDKs and the data plane read the query.
Windows has a coarser monotonic clock and words refused connections differently, so expire the grant with a delayed response and match the urllib3 error text instead.
The closed-port check now relies on the SDK's retry message instead of urllib3 wording, and the grant expiry test pins the exact request sequence so nothing can be sent against the expired grant.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
init_logger(ingestion_key=...)andBRAINTRUST_INGESTION_KEY, so code running on machines we don't control (desktop apps, CLIs shipped to users) can send traces to one project without an API key.This is part of the public trace ingestion keys work and depends on a backend that serves the ingestion key routes. Don't release it until a compatible data plane and key issuance are deployed.
The wire contract lives in braintrustdata/braintrust#21591. I checked this PR against its shared fixture (
typespecs/src/public-ingestion.fixture.jsonat74ce022): all valid and invalid URLs, the derived endpoints, the chunk plans, and the upload request, grant, chunk, complete, and overflow shapes match. The fixture isn't vendored here; the tests use an inline fake data plane that follows the same contract. End-to-end coverage against the real server is planned separately.What changes
The ingestion key is a URL like
https://dp.example/base/ingest?ingestKey=bt-ik-.... The SDK parses it locally, strips the query from every request, and sends the key asAuthorization: Bearer .... The base path is preserved, so rows go to{root}/v1/logs.An ingestion key logger gets its own background logger per endpoint and key, isolated from the global private one. It never logs in, never registers or looks up the project, and never falls back to
BRAINTRUST_API_KEY, an earlierbraintrust.login(), or the private URL, including when a write is rejected. Precedence:api_keyandingestion_keytogether raiseapi_keyuses the private path, even ifBRAINTRUST_INGESTION_KEYis setingestion_key(including an empty or malformed one, which raises) or the env var selects the public pathRows only carry trace fields (
id, span ids,created,project_id/log_id,input/output/…, merge fields). If the logger has noproject_id, rows only carrylog_id: "g"and the data plane resolves the project from the key. An explicitproject_idis sent as-is, so the server rejects mismatches instead of the SDK hiding them. Feedback can only include scores, expected, and tags, since comments and audit fields are writable provenance.Attachments upload through the chunked
/v1/uploadsAPI before the rows that reference them, using the chunk size the server grants. The returned server reference replaces the attachment in the row.ExternalAttachmentis rejected, since its URL can point at any object store content. Batches above 512 KiB overflow through the same API as alogs3_overflowupload, and there's no/versionlookup. Upload grants are validated, timeouts and retry waits never outlive the grant, and an expired or410grant starts a new upload a bounded number of times.Requests don't follow redirects, retries only cover 408/429/5xx and network errors (honoring
Retry-Afterup to 60s), and the key is redacted from errors and never part of a URL orrepr.Slug and W3C parents,
@traced, and globalupdate_span()route through the current ingestion key logger, so they don't trigger a private login. Public and private spans can still share a trace in both directions.The README documents that anyone holding the key can update any project row whose ID they know, including rows written with an API key or another ingestion key, and how to point a standard OTLP exporter at
{root}/otel/v1/traces.Verification
pytest src/braintrust/test_ingestion_key.py: 69 passed. These run the real logger, batcher, and upload flow against a fake data plane over HTTP.nox -s test_core: 1013 passed, 4 failed. The same 4 fail onmainin the sandbox I ran this in, because itsHTTP_PROXYturns the connection-close scenarios into 502s (api/test_transport.py×2,test_git_metadata_vcr.py,test_http.py).pre-commit(ruff format, ruff check, codespell) passes on the changed files, andpylint --errors-onlyon the changed files passes. The fullnox -s pylintsession didn't run because the tool request was rejected.27662d0: all 82 checks pass across Linux and Windows (1 skipped).