Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion A365_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,11 @@ ObservabilityHostingManager.configure(

## Baggage

Baggage sets per-request context (tenant, agent, user) that flows to all spans. **Without `tenant_id` and `agent_id`, the exporter silently drops spans.**
Baggage sets per-request context (tenant, agent, user) that flows to recognized GenAI spans only. **Without `tenant_id` and `agent_id`, the exporter silently drops spans.**

A span is recognized as GenAI at span start by evaluating these signals in order: a supported `gen_ai.operation.name` attribute; if that attribute is present but unrecognized (`chain`, `embeddings`, `text_completion`, `generate_content`, `create_agent`, ...) it is authoritative, so baggage and span-name inference are skipped and only the instrumentation scope can still classify the span; otherwise a span name matching a supported operation (`invoke_agent ...`, `chat ...`, ...) or a known pre-rename name (`chat.completions ...`), then a recognized `gen_ai.operation.name` baggage entry, then a supported GenAI instrumentation scope (`Agent365Sdk`, `semantic_kernel.*`, `agent_framework`, `microsoft.opentelemetry._genai.*`, `opentelemetry.instrumentation.openai_v2`, `opentelemetry.instrumentation.openai_agents`). The scope signal matters because LangChain, Semantic Kernel, Agent Framework, and OpenAI Agents all set `gen_ai.operation.name` *after* the span starts — a LangChain chat span begins life named `ChatOpenAI`, and a Semantic Kernel one as `chat.completions gpt-4o`.

Spans classified only by instrumentation scope are GenAI with an unknown operation: they receive the common baggage attributes, but never the `invoke_agent`-only ones (caller agent details, `server.address`, `server.port`).

### BaggageBuilder

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
- Remove optional dependency for langchain-core and document guidance for installation
([#269](https://github.com/microsoft/opentelemetry-distro-python/pull/269))

### Bugs Fixed
- Restrict A365 identity and baggage enrichment to recognized GenAI spans while preserving supported span-start signals. ([#265](https://github.com/microsoft/opentelemetry-distro-python/pull/265))

# 1.3.9 (2026-09-09)
### Features Added
- Update OpenTelemetry dependencies to latest versions, bump `langchain-core` minimum version to address S360, and support the new `httpx2` entry point exposed by `opentelemetry-instrumentation-httpx`.
Expand Down
2 changes: 1 addition & 1 deletion src/microsoft/opentelemetry/a365/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Span export pipeline — processors and exporters for Agent365 and Spectra backe
| `agent365_exporter_options.py` | `Agent365ExporterOptions` — configuration for the Agent365 exporter (cluster category, token resolver, endpoint flags, batch settings). |
| `enriched_span.py` | `EnrichedReadableSpan` — wrapper allowing extra attributes on immutable `ReadableSpan` objects. |
| `enriching_span_processor.py` | Span enrichment support with registration for platform instrumentors (LangChain, Semantic Kernel, OpenAI Agents). `_EnrichingBatchSpanProcessor` applies enrichers before batching. |
| `span_processor.py` | `A365SpanProcessor` — propagates documented OpenTelemetry baggage entries onto spans, keeps invoke_agent-specific handling, and copies only opted-in custom baggage keys onto recognized GenAI spans. |
| `span_processor.py` | `A365SpanProcessor` — propagates documented and opted-in custom baggage entries onto recognized GenAI spans only (operation attribute, span name, operation baggage, or supported GenAI instrumentation scope), with special handling for invoke_agent spans. |
| `spectra_exporter_options.py` | `SpectraExporterOptions` — configuration for OTLP export to a Spectra Collector sidecar (gRPC or HTTP, tuned for Kubernetes). |
| `utils.py` | Exporter utilities: hex encoding for trace/span IDs, span size truncation, span partitioning, environment variable handling, payload building helpers. |

Expand Down
67 changes: 19 additions & 48 deletions src/microsoft/opentelemetry/a365/core/exporters/span_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,9 @@
# license information.
# --------------------------------------------------------------------------

"""Span processor for propagating OpenTelemetry baggage entries onto spans.

For every new span:
* Retrieve the current (or parent) context
* Obtain all baggage entries
* For each documented key with a truthy value not already present as a span
attribute, add it via span.set_attribute
* Never overwrites existing attributes

Custom baggage is propagated only to recognized GenAI spans. A span is
recognized as GenAI by evaluating these signals in order at ``on_start``:

1. An explicit ``gen_ai.operation.name`` attribute holding a recognized
operation: GenAI with a known operation.
2. An explicit but *unrecognized* ``gen_ai.operation.name`` attribute: the
attribute is authoritative, so the baggage and span-name inference of
signals 3 and 4 is skipped. The span is still GenAI when a supported
instrumentation emitted it (signal 5), with an unknown operation.
3. A recognized ``gen_ai.operation.name`` baggage entry.
4. A span name that is (or starts with) a recognized operation name, or a
name a supported instrumentation is known to use before it renames the
span (Semantic Kernel ``chat.completions <model>``).
5. The instrumentation scope (source) name of a supported GenAI
instrumentation: GenAI with an unknown operation.

Signals 4 and 5 exist because most GenAI instrumentations apply
``gen_ai.operation.name`` *after* the span starts: LangChain chat spans start
as ``ChatOpenAI`` and the OpenAI Agents processor starts workflow spans as
``Agent workflow``. Signal 5 also keeps spans whose operation this processor
does not model (``chain``, ``embeddings``, ``text_completion``,
``generate_content``, ``create_agent``) from being dropped. Only signals 1, 3
and 4 identify *which* operation a span represents, which is what gates the
invoke_agent-only attributes.
"""Propagate A365 identity and baggage to recognized GenAI spans.

Existing span attributes are never overwritten.
"""

from __future__ import annotations
Expand Down Expand Up @@ -84,7 +54,8 @@

# mypy: disable-error-code="no-untyped-def"

# Generic / common tracing attributes propagated from baggage to all spans

# Baggage attributes for all recognized GenAI spans.
COMMON_ATTRIBUTES = [
TENANT_ID_KEY,
CUSTOM_PARENT_SPAN_ID_KEY,
Expand All @@ -111,7 +82,7 @@
SERVICE_NAME_KEY,
]

# Invoke Agent-specific attributes (only propagated to invoke_agent spans)
# Additional baggage attributes for invoke_agent spans.
INVOKE_AGENT_ATTRIBUTES = [
GEN_AI_CALLER_AGENT_ID_KEY,
GEN_AI_CALLER_AGENT_NAME_KEY,
Expand Down Expand Up @@ -145,7 +116,7 @@ def _custom_baggage_keys(baggage_map) -> list[str]:
class A365SpanProcessor(BaseSpanProcessor):
"""Span processor that stamps agent identity and propagates baggage to span attributes.

Static identity (tenant_id, agent_id) is set from configuration on every span.
Static identity (tenant_id, agent_id) is set from configuration on qualifying GenAI spans.
Additional baggage entries are propagated selectively for documented keys.
Never overwrites existing attributes.
"""
Expand All @@ -162,12 +133,23 @@ def __init__(
def on_start(self, span, parent_context=None): # type: ignore[override]
ctx = parent_context or context.get_current()

# Stamp static identity from configuration (never overwrite existing)
try:
existing = getattr(span, "attributes", {}) or {}
except Exception:
existing = {}

if ctx is None:
baggage_map = {}
else:
try:
baggage_map = baggage.get_all(ctx) or {}
except Exception:
baggage_map = {}

classification = _classify_gen_ai_span(span, existing, baggage_map)
if not classification.is_gen_ai_span:
return super().on_start(span, parent_context)

if self._tenant_id and TENANT_ID_KEY not in existing:
try:
span.set_attribute(TENANT_ID_KEY, self._tenant_id)
Expand All @@ -179,22 +161,11 @@ def on_start(self, span, parent_context=None): # type: ignore[override]
except Exception:
pass

# Refresh existing after stamping identity
try:
existing = getattr(span, "attributes", {}) or {}
except Exception:
existing = {}

if ctx is None:
return super().on_start(span, parent_context)

try:
baggage_map = baggage.get_all(ctx) or {}
except Exception:
baggage_map = {}

classification = _classify_gen_ai_span(span, existing, baggage_map)

target_keys = list(COMMON_ATTRIBUTES)
if classification.operation_name == INVOKE_AGENT_OPERATION_NAME:
for k in INVOKE_AGENT_ATTRIBUTES:
Expand Down
4 changes: 1 addition & 3 deletions src/microsoft/opentelemetry/a365/core/exporters/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@
# Maximum allowed span size in bytes (250KB)
MAX_SPAN_SIZE_BYTES = 250 * 1024

# Operation names that identify a span as eligible for export to the Agent 365
# observability ingest service. Only spans whose gen_ai.operation.name matches
# one of these values are included; all other spans are filtered out.
# Export eligibility is intentionally narrower than GenAI span recognition.
GEN_AI_OPERATION_NAMES: frozenset[str] = frozenset(
{
INVOKE_AGENT_OPERATION_NAME,
Expand Down
Loading
Loading