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
15 changes: 14 additions & 1 deletion A365_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,23 @@ Supported instrumentors:
| Library | Instrumentor | Package |
|---|---|---|
| Semantic Kernel | `SemanticKernelInstrumentor` | Bundled in distro |
| OpenAI Agents SDK | Via `opentelemetry-instrumentation-openai-agents-v2` | Dependency of distro |
| OpenAI Agents SDK | `A365OpenAIAgentsInstrumentor` (A365) / `opentelemetry-instrumentation-openai-agents-v2` (non-A365) | Bundled / Dependency |
| Agent Framework | `AgentFrameworkInstrumentor` | Bundled in distro |
| LangChain | `LangChainInstrumentor` | Bundled in distro |

### OpenAI Agents SDK — Dual Instrumentation

The distro ships **two** instrumentors for the OpenAI Agents SDK, selected automatically based on whether A365 is enabled:

| Mode | Instrumentor | Span format |
|---|---|---|
| `enable_a365=True` | `A365OpenAIAgentsInstrumentor` (bundled) | A365 versioned envelope with `custom.parent.span.id`, per-message indexed attributes, detailed token counts, `graph_node_parent_id` for handoffs |
| `enable_a365=False` | `opentelemetry-instrumentation-openai-agents-v2` (upstream) | Standard OpenTelemetry GenAI semantic conventions |

The selection is automatic — no manual `instrument()` calls are needed. When `enable_a365=True`, the upstream instrumentor for `openai_agents` is **skipped** and the A365 instrumentor is used instead. All other instrumentors (LangChain, Semantic Kernel, etc.) are unaffected.

> **Important:** When both `enable_a365=True` and `enable_azure_monitor=True` are set, Azure Monitor and OTLP exporters will see spans in the A365 format (versioned envelope) rather than upstream OTel format. This is expected — the A365 instrumentor writes to the global `TracerProvider`, so all attached exporters receive the same spans.

### Noisy Spans — A365-Only Mode

When `enable_a365=True` (and `enable_azure_monitor` is **not** set), the distro **disables web-framework and HTTP-client instrumentations by default** so only GenAI-related spans appear:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Release History

## Unreleased

### Features Added
- Add A365-specific OpenAI Agents SDK instrumentor (`A365OpenAIAgentsInstrumentor`). When `enable_a365=True`, the distro uses this bundled instrumentor instead of the upstream `opentelemetry-instrumentation-openai-agents-v2`, producing spans with the A365 versioned envelope format, `custom.parent.span.id`, per-message indexed attributes, detailed token counts, and `graph_node_parent_id` for handoffs.

## 1.1.0 (2026-05-11)

### Features Added
Expand Down
6 changes: 5 additions & 1 deletion MIGRATION_A365.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,11 @@ OpenAIAgentsTraceInstrumentor().instrument()
# ✅ NEW — auto-instrumented by distro, no manual setup needed
# Set ENABLE_A365_OBSERVABILITY_EXPORTER=true in env
use_microsoft_opentelemetry(enable_a365=True)
# OpenAI instrumentation is handled automatically
# When enable_a365=True, the distro uses a bundled A365-specific instrumentor
# (A365OpenAIAgentsInstrumentor) that produces the same A365 versioned envelope
# format as the old OpenAIAgentsTraceInstrumentor. The upstream OTel instrumentor
# (opentelemetry-instrumentation-openai-agents-v2) is skipped automatically.
# When enable_a365=False, the upstream instrumentor is used instead.
```

### Extensions — Semantic Kernel (observability-extensions-semantic-kernel)
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,14 @@ Microsoft OpenTelemetry automatically instruments the following libraries when i
| `urllib` | HTTP client |
| `urllib3` | HTTP client |
| `openai` | GenAI |
| `openai_agents` | GenAI |
| `openai_agents` | GenAI (see note below) |
| `langchain` | GenAI |
| `semantic_kernel` | GenAI |
| `agent_framework` | GenAI |
| `azure_sdk` | Azure (enabled when Azure Monitor is active) |

> **OpenAI Agents SDK:** The distro includes two instrumentors for `openai_agents`. When `enable_a365=True`, a bundled A365-specific instrumentor (`A365OpenAIAgentsInstrumentor`) is used, producing spans with the A365 versioned envelope format. When A365 is not enabled, the upstream `opentelemetry-instrumentation-openai-agents-v2` instrumentor is used with standard OTel semantic conventions. See [A365_DOCUMENTATION.md](A365_DOCUMENTATION.md) for details.

Toggle individual instrumentations:

```python
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ langchain = [
agent-framework = [
"agent-framework>=1.0.0",
]
openai-agents = [
"openai-agents>=0.0.7",
]
test = [
"pytest>=8.0",
"pytest-cov>=5.0",
Expand Down
28 changes: 26 additions & 2 deletions src/microsoft/opentelemetry/_distro.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def use_microsoft_opentelemetry(**kwargs: object) -> None: # pylint: disable=to
# ---- GenAI main-agent attribute propagation ----
# Prepended to the processor lists so on_start/on_emit run BEFORE any
# Batch* export processor appended below; this enriches once per
# span/log and is then visible to the Azure Monitor exporter.
# span/log and is then visible to the Azure Monitor exporter.
if enable_azure_monitor:
if not otel_kwargs.get(DISABLE_TRACING_ARG, False):
otel_kwargs[SPAN_PROCESSORS_ARG] = [
Expand Down Expand Up @@ -337,7 +337,7 @@ def use_microsoft_opentelemetry(**kwargs: object) -> None: # pylint: disable=to
set_logger_provider(logger_provider)

# ---- Instrumentations (always, after providers are set) ----
_setup_instrumentations(otel_kwargs, **{ENABLE_SENSITIVE_DATA_ARG: enable_sensitive_data})
_setup_instrumentations(otel_kwargs, enable_a365=enable_a365, **{ENABLE_SENSITIVE_DATA_ARG: enable_sensitive_data})

# ---- SDKStats manager (after providers, before returning) ----
_initialize_sdkstats(enable_azure_monitor)
Expand Down Expand Up @@ -698,6 +698,7 @@ def _is_instrumentation_enabled(otel_kwargs: Dict[str, Any], lib_name: str) -> b

def _setup_instrumentations(otel_kwargs: Dict[str, Any], **kwargs: Any) -> None:
"""Discover and activate OTel instrumentations for supported libraries."""
enable_a365: bool = kwargs.pop("enable_a365", False)
entry_point_finder = _EntryPointDistFinder()
for entry_point in entry_points(group="opentelemetry_instrumentor"):
lib_name = entry_point.name
Expand All @@ -706,6 +707,12 @@ def _setup_instrumentations(otel_kwargs: Dict[str, Any], **kwargs: Any) -> None:
if not _is_instrumentation_enabled(otel_kwargs, lib_name):
_logger.debug("Instrumentation skipped for library %s", lib_name)
continue
# When A365 is enabled, use the A365-specific OpenAI Agents
# instrumentation instead of the upstream entry point so that
# spans carry the versioned message format A365 consumers expect.
if lib_name == "openai_agents" and enable_a365:
_setup_a365_openai_agents_instrumentation()
continue
try:
entry_point_dist = entry_point_finder.dist_for(entry_point) # type: ignore
conflict = get_dist_dependency_conflicts(entry_point_dist) # type: ignore
Expand All @@ -725,3 +732,20 @@ def _setup_instrumentations(otel_kwargs: Dict[str, Any], **kwargs: Any) -> None:
lib_name,
exc_info=ex,
)


def _setup_a365_openai_agents_instrumentation() -> None:
"""Register the A365 OpenAI Agents trace processor."""
try:
from microsoft.opentelemetry._genai._openai_agents._trace_instrumentor import (
A365OpenAIAgentsInstrumentor,
)

A365OpenAIAgentsInstrumentor().instrument()
set_sdkstats_instrumentation_by_name("openai_agents")
_logger.debug("A365 OpenAI Agents instrumentation enabled.")
except Exception as ex: # pylint: disable=broad-except
_logger.warning(
"Failed to set up A365 OpenAI Agents instrumentation.",
exc_info=ex,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
35 changes: 35 additions & 0 deletions src/microsoft/opentelemetry/_genai/_openai_agents/_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

# Span Attribute Types
from microsoft.opentelemetry.a365.core.constants import (
EXECUTE_TOOL_OPERATION_NAME,
INVOKE_AGENT_OPERATION_NAME,
)
from microsoft.opentelemetry.a365.core.inference_operation_type import InferenceOperationType

GEN_AI_SPAN_KIND_AGENT_KEY = INVOKE_AGENT_OPERATION_NAME
GEN_AI_SPAN_KIND_TOOL_KEY = EXECUTE_TOOL_OPERATION_NAME
GEN_AI_SPAN_KIND_CHAIN_KEY = "chain"
GEN_AI_SPAN_KIND_LLM_KEY = InferenceOperationType.CHAT.value.lower()
GEN_AI_SPAN_KIND_RETRIEVER_KEY = "retriever"
GEN_AI_SPAN_KIND_EMBEDDING_KEY = "embedding"
GEN_AI_SPAN_KIND_RERANKER_KEY = "reranker"
GEN_AI_SPAN_KIND_GUARDRAIL_KEY = "guardrail"
GEN_AI_SPAN_KIND_EVALUATOR_KEY = "evaluator"
GEN_AI_SPAN_KIND_UNKNOWN_KEY = "unknown"

# PREFIXES
GEN_AI_MESSAGE_ROLE = "message_role"
GEN_AI_MESSAGE_CONTENT = "message_content"
GEN_AI_MESSAGE_CONTENTS = "message_contents"
GEN_AI_MESSAGE_CONTENT_TYPE = "content_type"
GEN_AI_MESSAGE_TOOL_CALLS = "message_tool_calls"
GEN_AI_MESSAGE_TOOL_CALL_ID = "message_tool_id"
GEN_AI_MESSAGE_TOOL_CALL_NAME = "message_tool_name"
GEN_AI_TOOL_JSON_SCHEMA = "tool_json_schema"
GEN_AI_LLM_TOKEN_COUNT_TOTAL = "llm_token_count_total"
GEN_AI_LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHED_READ = "llm_token_count_prompt_details_cached_read"
GEN_AI_LLM_TOKEN_COUNT_COMPLETION_DETAILS_REASONING = "llm_token_count_completion_details_reasoning"
GEN_AI_GRAPH_NODE_ID = "graph_node_id"
GEN_AI_GRAPH_NODE_PARENT_ID = "graph_node_parent_id"
Loading
Loading