Skip to content

Add span origin provenance#570

Merged
Abhijeet Prasad (AbhiPrasad) merged 5 commits into
mainfrom
agent/span-origin-provenance
Jul 15, 2026
Merged

Add span origin provenance#570
Abhijeet Prasad (AbhiPrasad) merged 5 commits into
mainfrom
agent/span-origin-provenance

Conversation

@Qard

Copy link
Copy Markdown
Contributor

Summary

Adds span origin provenance to the Python SDK direct logger and OpenTelemetry processor, including environment detection support.

Validation

  • python3 -m py_compile py/src/braintrust/env.py py/src/braintrust/logger.py py/src/braintrust/otel/init.py py/src/braintrust/span_origin.py
  • aggregate git diff --check

@Qard
Stephen Belanger (Qard) force-pushed the agent/span-origin-provenance branch from a5900fc to bb908fa Compare July 15, 2026 06:45

@lforst Luca Forstner (lforst) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small test would probably be good!

Comment thread py/src/braintrust/span_origin.py
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) merged commit 4e3911a into main Jul 15, 2026
82 checks passed
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) deleted the agent/span-origin-provenance branch July 15, 2026 15:55
Abhijeet Prasad (AbhiPrasad) pushed a commit that referenced this pull request Jul 16, 2026
## Summary

Every span an integration creates now carries a stable identifier in
`context.span_origin.instrumentation.name` — `openai-auto`,
`anthropic-auto`, `temporal-auto`, and so on — matching the naming
convention in the Braintrust instrumentation spec's `span_origin`
example. Previously all integration spans were tagged with the shared
`braintrust-python-logger` default, which made per-integration filtering
on `span_origin` impossible.

Builds on #570 (which added the `span_origin` scaffolding).

## API surface

`start_span` (and every provider-level `start_span` method) gains an
`internal: dict | None = None` kwarg reserved for Braintrust SDK
internals. Integrations pass `internal={"instrumentation":
"<provider>-auto"}` to stamp the span origin; the `internal` name
signals to external callers that they shouldn't touch it and its keys
can change without notice.

```python
from braintrust import start_span

with start_span(name="my-op", internal={"instrumentation": "my-integration-auto"}) as span:
    ...
```

- Threaded through every span-creation surface: top-level `start_span`,
`Span.start_span`, `SpanImpl.start_span`, `Experiment.start_span`,
`Dataset.start_span`, `Logger.start_span`, and `_NoopSpan.start_span`.
- When set, `SpanImpl` reads `internal["instrumentation"]` and stamps it
into `context.span_origin.instrumentation.name` via
`merge_span_origin_context`.
- When unset, spans fall back to the channel default —
`braintrust-python-logger` for direct logging, `braintrust-python-otel`
for the OTel processor.
- **Does NOT propagate through the parent/child edge.** Each
`start_span` call independently decides its own value, so a user's
`@traced` scorer nested inside a wrapped provider call keeps the default
identifier — the integration's name only lands on spans the integration
itself opens. This matches the spec's "the module that _directly
created_ the span" definition and mirrors how Sentry's `origin` and
OTel's `InstrumentationScope` behave.

Suggested for external Braintrust integrations authored outside this
repo:

```python
from braintrust.logger import start_span as _bt_start_span

_INSTRUMENTATION = "my-provider-auto"

def start_span(*args, **kwargs):
    internal = dict(kwargs.get("internal") or {})
    internal.setdefault("instrumentation", _INSTRUMENTATION)
    kwargs["internal"] = internal
    return _bt_start_span(*args, **kwargs)
```

Every module-scope `start_span(...)` call then flows through the shadow
with zero further edits.

## Integration migration (23 integrations)

**21 shadow-based** (`adk`, `agentscope`, `agno`, `anthropic`,
`autogen`, `bedrock_runtime`, `claude_agent_sdk`, `cohere`, `crewai`,
`dspy`, `google_genai`, `huggingface_hub`, `instructor`, `langchain`,
`litellm`, `livekit_agents`, `llamaindex`, `mistral`, `openai`,
`openai_agents`, `openrouter`, `pydantic_ai`) — the shadow above at the
top of each `tracing.py`/`callbacks.py`.

**Explicit at call sites** (2 integrations):
- `temporal/plugin.py`: `internal={"instrumentation": "temporal-auto"}`
at each `logger.start_span(...)` site — Logger method calls don't go
through the module-level shadow.
- `openai_agents/tracing.py`: `internal={"instrumentation":
_INSTRUMENTATION}` on the three `current_context.start_span(...)` /
`self._logger.start_span(...)` / `parent.start_span(...)` sites.

## Tests

- `py/src/braintrust/test_otel.py` — new tests covering
`merge_span_origin_context` resolution and an end-to-end `SpanImpl`
assertion that child spans do NOT inherit the parent's instrumentation
name (child falls back to `braintrust-python-logger`).
- Per-integration inline assertion: each of the 23 integrations'
existing VCR tests now asserts
`span["context"]["span_origin"]["instrumentation"]["name"] ==
"<provider>-auto"` in place, next to the real provider-facing coverage.
If a shadow ever regresses, that specific integration's test suite fails
with a clear signal.

## Docs

- `.agents/skills/sdk-integrations/SKILL.md` gains a **Span origin**
section documenting the shadow pattern, the no-inheritance rule, and the
`<provider>-auto` naming convention.
- Same file relaxes the earlier strict "metadata MUST NOT capture
anything outside the spec" guidance to an allowlist-per-provider model —
spec-defined keys must be captured, provider-specific detail fields can
be included deliberately, no dumping raw request/response objects.
- Spec reference now links to the actual URL
(https://github.com/braintrustdata/braintrust-spec/blob/main/docs/instrumentation-guide.md).

## Validation

- `nox -s test_core`: **647 passed, 66 skipped, 12 xfailed**
- `nox -s test_types`: **16 passed** (pyright + mypy clean)
- `nox -s pylint`: successful (also ran ruff format + ruff check --fix
as part of pre-commit)
- **30 provider `(latest)` sessions** — every one whose Python 3.14
wheels are available. Zero failures. `test_livekit_agents` and
`test_crewai` skipped due to pre-existing Python 3.14 wheel gaps in
`onnxruntime` / `pydantic-core`, unrelated to this change.

## Follow-ups

- JS SDK's mirror PR (braintrustdata/braintrust-sdk-javascript#2226) is
still open with only `braintrust-js-logger` / `braintrust-otel-js`
hardcoded and no per-integration wiring. Aligning JS on the same
`<provider>-auto` names is worth doing before this ships so cross-SDK
dashboards work — the spec only gives `openai-auto` as an illustrative
example, so a coordinated commit-to-a-convention is needed on both
sides.
- Naming convention isn't currently prescribed by the spec
(`docs/instrumentation-guide.md` just shows `openai-auto` in a JSON
blob). Worth a spec PR to make it normative once JS and Python agree.

<!-- sfk:created-approved-by -->
Created by abhijeet

<!-- sfk:slack-thread -->
[Slack
thread](https://starfolkai.slack.com/archives/C0AQDETAVT3/p1784153314321549?thread_ts=1784153314.321549&cid=C0AQDETAVT3)

---------

Co-authored-by: Starfolk <noreply@starfolk.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants