feat(util-genai): add per-signal context-scoped attributes - #337
Conversation
4384636 to
c9c0bad
Compare
Pull request dashboard statusClosed · refreshed 2026-08-04 15:04 UTC Status above doesn't look right?
|
9e1f153 to
fe62d76
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new opentelemetry-util-genai utility for attaching per-signal attribute bags (span vs log/event) to an OpenTelemetry Context, and wires the util’s span/event emission points to merge those context-scoped attributes at invocation start/finish.
Changes:
- Introduce
set_context_scoped_attributes(...)to build a derivedContextcarrying GenAI-only, non-propagating attribute bags for spans and inference events. - Apply context-scoped span attributes at invocation start (so samplers can see them) and context-scoped log attributes when emitting the inference details event.
- Add unit tests, README documentation, and a towncrier changelog fragment for the new API.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| util/opentelemetry-util-genai/src/opentelemetry/util/genai/context_attributes.py | New context helper storing per-signal attribute bags under a private context key. |
| util/opentelemetry-util-genai/src/opentelemetry/util/genai/_invocation.py | Reads context-scoped attributes once at invocation start and merges span-targeted attributes into start_span(...). |
| util/opentelemetry-util-genai/src/opentelemetry/util/genai/_inference_invocation.py | Merges log-targeted context attributes into the inference event attribute set. |
| util/opentelemetry-util-genai/tests/test_context_attributes.py | New tests validating per-signal targeting, precedence, nesting merge behavior, sampler visibility, and scoping. |
| util/opentelemetry-util-genai/README.rst | Documents the new context-scoped attributes feature and its intended semantics/limitations. |
| util/opentelemetry-util-genai/.changelog/337.added | Changelog fragment announcing the new API. |
Suppressed comments (2)
util/opentelemetry-util-genai/tests/test_context_attributes.py:163
- Use
GenAI.GEN_AI_AGENT_NAMEinstead of hardcoding"gen_ai.agent.name"so the test stays aligned with the semconv constants used elsewhere in this repo’s tests.
with self._inference(
set_context_scoped_attributes(
span_attributes={"gen_ai.agent.name": "trip-planner"}
)
):
pass
(seen,) = self.sampler.seen
self.assertEqual(seen["gen_ai.agent.name"], "trip-planner")
util/opentelemetry-util-genai/tests/test_context_attributes.py:177
- Use
GenAI.GEN_AI_AGENT_NAMEinstead of hardcoding"gen_ai.agent.name"for the semconv attribute key, consistent with other tests in this package.
with self._inference(
set_context_scoped_attributes(
span_attributes={"gen_ai.agent.name": "trip-planner"}
)
):
pass
self.span_exporter.clear()
with self.handler.inference("test-provider"):
pass
self.assertNotIn("gen_ai.agent.name", self.span_attributes)
Adds `set_context_scoped_attributes` so a caller can attach attributes to an OTel context and have GenAI telemetry emitted within it carry them, with each attribute declaring whether it applies to spans or to events. This bridges the layering of the GenAI ecosystem: an agentic framework knows which agent, workflow, or conversation is running, while the model-client instrumentation that emits the inference telemetry sits a layer below it and has no way to learn any of it. Span-targeted attributes are applied when the span starts, so they are visible to samplers. Metrics are deliberately not supported. Assisted-by: Claude Opus 5
fe62d76 to
f519f58
Compare
lmolkova
left a comment
There was a problem hiding this comment.
This should come from otel-python following https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/4931-context-scoped-attributes.md and the spec changes (not spec-ed out yet).
We should not polyfill missing SDK features here
Are you proposing then making our own OTEP to extend https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/4931-context-scoped-attributes.md? What's currently missing for our use-case is:
In any case, I'd love to hear your thoughts on the general direction we should take |
|
@RKest I'm saying context-scoped attributes should be implemented in https://github.com/open-telemetry/opentelemetry-python, not in this repo, but before that the API and implementation details should be covered in the specification. For now, user apps can implement context-based processors |
SG, thank you. I'll get the ball rolling on the OTEP proposal, and in the meantime I'll try to have an interim quick fix in ADK to get around the current limitations. |
Description
Adds
set_context_scoped_attributestoopentelemetry-util-genai: a callerattaches attributes to an OpenTelemetry context, and GenAI telemetry emitted
within that context carries them. Each attribute declares which signal it
applies to.
Motivation
GenAI instrumentation is layered across packages that share no call path. An
agentic framework knows which agent, workflow, or conversation is running. The
model-client instrumentation that emits the inference telemetry
(
opentelemetry-instrumentation-genai-openaiand friends) sits a layer belowit and has no way to learn any of it:
OTel spans are write-only — an inference instrumentation cannot read
gen_ai.agent.nameoff its parent span even when one exists.through
openai.chat.completions.create().per-invocation.
The context is the only in-process channel between a component that knows the
fact and a component that emits the telemetry needing it. Concretely,
gen_ai.agent.name,gen_ai.agent.id, andgen_ai.conversation.idaresemconv-defined attributes that plainly belong on inference telemetry and that
no code path can currently populate — a spec-defined population gap rather than
a vendor extension.
Per-signal targeting is the second half of the requirement: content that is
unsafe on a sampled, widely-read span (user identifiers, tenant data) can still
be recorded on the event.
Semantics
instrumentation, and telemetry the application emits directly, are unaffected.
under a private key, not in Baggage or any wire format.
span_attributesare merged into thestart_spancall, so they are visibleto samplers.
log_attributesapply togen_ai.client.inference.operation.details, the onlyevent this package emits — so they reach inference invocations only.
Why metrics are excluded
Spans and logs absorb an extra attribute harmlessly. Metrics do not: every
distinct value forks a new time series, so one context-scoped
user.idorgen_ai.conversation.idon the inference histograms is an unbounded cardinalityexplosion — expensive, and often noticed only once a backend starts dropping
series. The failure is easy to cause, hard to detect, and hard to undo.
Rather than ship a foot-gun with a warning, there is no metrics target member at
all. If a pressing need appears, the safe form is an explicit allow-list of
attributes permitted on metrics, not a free-form bag. Adding that later is
straightforward; taking a free-form bag away after users depend on it is not.
Alternatives considered
The requirement — attributes on the inference event but not the span, set by a
layer above the instrumentation — was first attacked outside the
instrumentation, via the log pipeline. None of those hold up:
LogRecordProcessorwrapping the exporting processor(
add_log_record_processor(BatchLogRecordProcessor(StampingProcessor(exporter)))).Requires knowing and wrapping every processor the application installs, and
re-wrapping whenever that set changes. Fragile by construction.
Processor order is not a stability guarantee to build on.
LoggerProvideroverridingemit. The provider is global and setby the application; a library cannot force its own implementation on users.
then differ by where the agent runs, and deployment-specific code leaks into
framework core.
A
SpanProcessorcounterpart to any of these is possible for the span half,with the caveat the OTEP itself notes:
OnStartruns after the sampler, sothose attributes would not reach sampling decisions.
OTEP requires the feature be off unless the user opts in per-signal on the
provider, which is exactly the provider control a library does not have. It
also cannot express per-attribute signal targeting (below).
finish — after sampling — and merges outermost-wins. Both are wrong for this
use case; this module is the generalisation it should rebase onto.
Relation to OTEP 4931
This borrows the vocabulary of
OTEP 4931
but is not an implementation of it, and does not claim conformance.
opentelemetry-util-genaiThe OTEP assigns stamping exclusively to the SDK, and only when the user has
opted in per-signal; it never contemplates an instrumentation doing the
stamping, so its SDK requirements do not bind here. What this package does is
read a context variable to decide which attributes to put on its own telemetry —
ordinary instrumentation behaviour, and what
GENERATE_CONTENT_EXTRA_ATTRIBUTES_CONTEXT_KEYinopentelemetry-instrumentation-google-genaialready does today in a hardcoded,single-package form.
The OTEP does consider a non-SDK-core stamper, in the rejected "built-in
Processor" alternative, and rejects it for two mechanical reasons: a processor's
OnStartruns after the sampler, and it would need the SDK's internal contextkey. Neither applies here — attributes are merged into the
start_spancall, sosamplers see them, and this module owns its own key.
Two deliberate consequences worth flagging:
become it. 4931 cannot express "on the event, not on the span":
AddContextScopedAttributes(Context, Attributes)has no signal parameter, andits gating is on the provider, so disabling traces would drop every
context-scoped attribute from spans rather than the one that is sensitive.
explicitly calls
set_context_scoped_attributes, so the feature costs nothingwhen unused and needs no provider-level configuration — which is what makes it
usable by libraries that do not control SDK setup.
Scope
Deliberately limited to the public API plus the two stamping sites. Follow-ups:
GENERATE_CONTENT_EXTRA_ATTRIBUTES_CONTEXT_KEYin favour of this.gen_ai.conversation_root) onto this module.Open question
Should there be an env kill switch (
OTEL_INSTRUMENTATION_GENAI_CONTEXT_SCOPED_ATTRIBUTES=false)so an application owner can disable stamping when a dependency writes attributes
they do not want? Not included; happy to add if maintainers prefer it. This is
adjacent to the OTEP's own open question about whether instrumentation relying
on this feature must always expose it as opt-in.
How has this been tested?
New
util/opentelemetry-util-genai/tests/test_context_attributes.py(5 tests)covering: per-signal targeting, invocation attributes winning over
context-scoped ones, nested inner-wins merge, visibility to a recording sampler,
and no leakage outside the attached context.
Also verified end-to-end against a live provider:
opentelemetry-instrumentation-google-genaicalling Gemini on Vertex AI, with attributes set as in the example above and
both signals exported over OTLP to a real backend (Google Cloud). Reading the
telemetry back from the backend:
gen_ai.agent.name=trip-plannerand nouser.id;gen_ai.client.inference.operation.detailslog record carriesuser.id=<generated>and nogen_ai.agent.name;So the per-signal split survives serialisation, export, and ingestion, not just
the in-process exporter assertions.
uv run tox -e typecheckpasses.uv run tox -e precommitpasses except theuv-lockhook, which rewrites indexes in my environment for unrelated reasons.Checklist