Skip to content
Open
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
4 changes: 1 addition & 3 deletions src/microsoft/opentelemetry/_genai/_langchain/_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,7 @@ def _start_trace(self, run: Run) -> None:

if is_nested_agent:
ancestor_run = self.run_map.get(str(ancestor_id))
ancestor_name = (
self._resolve_agent_name(ancestor_run, use_config=False) if ancestor_run else None
)
ancestor_name = self._resolve_agent_name(ancestor_run, use_config=False) if ancestor_run else None
this_name = self._resolve_agent_name(run, use_config=False)
if this_name and ancestor_name and this_name.lower() == ancestor_name.lower():
return
Expand Down
1 change: 0 additions & 1 deletion src/microsoft/opentelemetry/_otlp/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ def create_otlp_components(
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor


# from opentelemetry.sdk.trace.export import SpanExporter
# from opentelemetry.sdk.metrics.export import MetricExporter
# from opentelemetry.sdk._logs.export import LogRecordExporter
Expand Down
1 change: 0 additions & 1 deletion src/microsoft/opentelemetry/_sdkstats/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
# --------------------------------------------------------------------------



from azure.monitor.opentelemetry.exporter._constants import ( # type: ignore[import-not-found]
_REQ_DURATION_NAME,
_REQ_EXCEPTION_NAME,
Expand Down
6 changes: 1 addition & 5 deletions src/microsoft/opentelemetry/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,7 @@ def _append_azure_monitor_components(

def _disable_openai_v2_instrumentation(otel_kwargs: Dict[str, Any]) -> None:
options = otel_kwargs.get(INSTRUMENTATION_OPTIONS_ARG)
if (
isinstance(options, dict)
and isinstance(options.get("openai"), dict)
and "enabled" in options["openai"]
):
if isinstance(options, dict) and isinstance(options.get("openai"), dict) and "enabled" in options["openai"]:
return # User has explicitly set openai instrumentation options; do not override

overlapping_present = any(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ def _extract_token_identity(headers: dict[str, str | bytes]) -> dict[str, str]:
auth = auth.decode("utf-8", errors="replace")
if not auth.startswith("Bearer "):
return {}
parts = auth[len("Bearer "):].split(".")
parts = auth[len("Bearer ") :].split(".")
if len(parts) != 3:
return {}
payload_b64 = parts[1] + "=" * (4 - len(parts[1]) % 4)
Expand Down
4 changes: 1 addition & 3 deletions src/microsoft/opentelemetry/a365/core/message_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ def serialize_messages(
message parts contain non-JSON-serializable values.
"""
try:
serialized_list = [
asdict(msg, dict_factory=_message_dict_factory) for msg in wrapper.messages
]
serialized_list = [asdict(msg, dict_factory=_message_dict_factory) for msg in wrapper.messages]
return json.dumps(
serialized_list,
default=str,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ def add_numbers(a: float, b: float) -> float:
class TestAgentFrameworkTraceProcessorIntegration:
"""Integration tests for AgentFramework trace processor with real Azure OpenAI."""

def test_agentframework_trace_processor_integration(
self, distro_exporter, azure_openai_config, agent365_config
):
def test_agentframework_trace_processor_integration(self, distro_exporter, azure_openai_config, agent365_config):
"""Test AgentFramework trace processor with real Azure OpenAI call."""

# Create Azure OpenAI ChatClient
Expand Down Expand Up @@ -149,10 +147,7 @@ def _validate_span_attributes(self, distro_exporter, agent365_config):
assert attributes[TENANT_ID_KEY] == agent365_config["tenant_id"]

# Check for LLM spans (generation spans)
if (
GEN_AI_PROVIDER_NAME_KEY in attributes
and attributes[GEN_AI_PROVIDER_NAME_KEY] == "openai"
):
if GEN_AI_PROVIDER_NAME_KEY in attributes and attributes[GEN_AI_PROVIDER_NAME_KEY] == "openai":
if GEN_AI_REQUEST_MODEL_KEY in attributes:
llm_spans_found += 1
# Validate LLM span attributes
Expand Down
22 changes: 5 additions & 17 deletions tests/a365/integration/agentframework/test_message_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,10 @@ def _find_chat_spans(self, distro_exporter) -> list[ReadableSpan]:
"""
get_tracer_provider().force_flush()
time.sleep(0.5)
return [
s
for s in distro_exporter.spans
if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes
]
return [s for s in distro_exporter.spans if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes]

@pytest.mark.asyncio
async def test_simple_chat_message_mapping(
self, distro_exporter, chat_client: OpenAIChatClient
) -> None:
async def test_simple_chat_message_mapping(self, distro_exporter, chat_client: OpenAIChatClient) -> None:
"""Simple chat: verify exported spans contain structured A365 messages
after enrichment (no manual mapper call)."""
agent = RawAgent(
Expand All @@ -92,9 +86,7 @@ async def test_simple_chat_message_mapping(
assert len(result.text) > 0

chat_spans = self._find_chat_spans(distro_exporter)
assert len(chat_spans) > 0, (
f"No chat spans found. All spans: {[s.name for s in distro_exporter.spans]}"
)
assert len(chat_spans) > 0, f"No chat spans found. All spans: {[s.name for s in distro_exporter.spans]}"

attrs = dict(chat_spans[-1].attributes or {})

Expand Down Expand Up @@ -132,9 +124,7 @@ async def test_simple_chat_message_mapping(
print(f"\n=== Enriched output ===\n{json.dumps(output_data, indent=2)}")

@pytest.mark.asyncio
async def test_tool_call_message_mapping(
self, distro_exporter, chat_client: OpenAIChatClient
) -> None:
async def test_tool_call_message_mapping(self, distro_exporter, chat_client: OpenAIChatClient) -> None:
"""Tool-calling chat: verify tool_call and tool_call_response parts
survive enrichment in exported spans."""
agent = RawAgent(
Expand Down Expand Up @@ -170,7 +160,5 @@ async def test_tool_call_message_mapping(
part_types.add(part.get("type", ""))

assert "tool_call" in part_types, f"Expected tool_call in exported parts: {part_types}"
assert "tool_call_response" in part_types, (
f"Expected tool_call_response in exported parts: {part_types}"
)
assert "tool_call_response" in part_types, f"Expected tool_call_response in exported parts: {part_types}"
print(f"\n Exported part types: {part_types}")
Original file line number Diff line number Diff line change
Expand Up @@ -170,22 +170,19 @@ async def test_pipeline_invoke_agent_with_tool_call( # pylint: disable=too-many

# --- 1. All spans share the same trace_id ---
invoke_spans = _find_spans_by_name_prefix(spans, "invoke_agent")
assert len(invoke_spans) >= 1, (
f"Expected at least 1 invoke_agent span, got: {[s.name for s in spans]}"
)
assert len(invoke_spans) >= 1, f"Expected at least 1 invoke_agent span, got: {[s.name for s in spans]}"
invoke_span = invoke_spans[0]
trace_id = invoke_span.context.trace_id

for s in spans:
assert s.context.trace_id == trace_id, (
f"Span '{s.name}' has different trace_id: "
f"{s.context.trace_id:032x} vs {trace_id:032x}"
f"Span '{s.name}' has different trace_id: " f"{s.context.trace_id:032x} vs {trace_id:032x}"
)

# --- 2. invoke_agent span is the root (no parent) ---
assert invoke_span.parent is None, (
f"invoke_agent span should be root but has parent: {invoke_span.parent.span_id:016x}"
)
assert (
invoke_span.parent is None
), f"invoke_agent span should be root but has parent: {invoke_span.parent.span_id:016x}"

# --- 3. invoke_agent has correct operation name ---
assert _get_span_attr(invoke_span, GEN_AI_OPERATION_NAME_KEY) == INVOKE_AGENT_OPERATION_NAME
Expand All @@ -200,15 +197,11 @@ async def test_pipeline_invoke_agent_with_tool_call( # pylint: disable=too-many
if _get_span_attr(s, GEN_AI_OPERATION_NAME_KEY) == "chat"
or (s.name.startswith("chat") and _get_span_attr(s, GEN_AI_REQUEST_MODEL_KEY))
]
assert len(chat_spans) >= 1, (
f"Expected at least 1 chat span, got: {[s.name for s in spans]}"
)
assert len(chat_spans) >= 1, f"Expected at least 1 chat span, got: {[s.name for s in spans]}"

invoke_span_id = invoke_span.context.span_id
for chat_span in chat_spans:
assert chat_span.parent is not None, (
f"Chat span '{chat_span.name}' should have a parent"
)
assert chat_span.parent is not None, f"Chat span '{chat_span.name}' should have a parent"
# Chat span should be a child of invoke_agent (directly or transitively)
self._assert_ancestor(
chat_span,
Expand All @@ -223,13 +216,9 @@ async def test_pipeline_invoke_agent_with_tool_call( # pylint: disable=too-many
# Also check by operation name
tool_spans = _find_spans_by_operation(spans, EXECUTE_TOOL_OPERATION_NAME)

assert len(tool_spans) >= 1, (
f"Expected at least 1 execute_tool span. All spans: {[s.name for s in spans]}"
)
assert len(tool_spans) >= 1, f"Expected at least 1 execute_tool span. All spans: {[s.name for s in spans]}"
for tool_span in tool_spans:
assert tool_span.parent is not None, (
f"Tool span '{tool_span.name}' should have a parent"
)
assert tool_span.parent is not None, f"Tool span '{tool_span.name}' should have a parent"
self._assert_ancestor(
tool_span,
invoke_span_id,
Expand Down Expand Up @@ -259,9 +248,9 @@ async def test_pipeline_invoke_agent_with_tool_call( # pylint: disable=too-many
attrs = dict(tool_span.attributes or {})
op = str(attrs.get(GEN_AI_OPERATION_NAME_KEY, ""))
if op == EXECUTE_TOOL_OPERATION_NAME or tool_span.name.startswith("execute_tool"):
assert GEN_AI_TOOL_NAME_KEY in attrs or "add_numbers" in tool_span.name, (
f"Tool span missing tool name attribute: {list(attrs.keys())}"
)
assert (
GEN_AI_TOOL_NAME_KEY in attrs or "add_numbers" in tool_span.name
), f"Tool span missing tool name attribute: {list(attrs.keys())}"

print("\n✓ All pipeline assertions passed")

Expand Down
14 changes: 3 additions & 11 deletions tests/a365/integration/langchain/test_message_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,7 @@ def _find_chat_spans(distro_exporter: SpanCapturingExporter) -> list[ReadableSpa
"""Find exported spans that have gen_ai.input.messages."""
get_tracer_provider().force_flush()
time.sleep(0.5)
return [
s
for s in distro_exporter.spans
if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes
]
return [s for s in distro_exporter.spans if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes]

@pytest.mark.asyncio
async def test_simple_chat_message_mapping(
Expand All @@ -82,9 +78,7 @@ async def test_simple_chat_message_mapping(
assert len(result.content) > 0

chat_spans = self._find_chat_spans(distro_exporter)
assert len(chat_spans) > 0, (
f"No chat spans found. All spans: {[s.name for s in distro_exporter.spans]}"
)
assert len(chat_spans) > 0, f"No chat spans found. All spans: {[s.name for s in distro_exporter.spans]}"

print(f"\n=== All exported spans ({len(distro_exporter.spans)}) ===")
for s in distro_exporter.spans:
Expand Down Expand Up @@ -123,9 +117,7 @@ async def test_simple_chat_message_mapping(
for part in item.get("parts", []):
if isinstance(part, dict) and "content" in part:
flat_text += part["content"].lower()
assert "capital" in flat_text, (
f"Expected 'capital' in input messages content, got: {input_data}"
)
assert "capital" in flat_text, f"Expected 'capital' in input messages content, got: {input_data}"
print("\n → List format (pre-mapper)")

# --- Output messages ---
Expand Down
21 changes: 7 additions & 14 deletions tests/a365/integration/langchain/test_observability_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,23 +197,20 @@ async def test_pipeline_invoke_agent_with_tool_call(

# --- 1. Find invoke_agent span ---
invoke_spans = _find_spans_by_name_prefix(spans, "invoke_agent")
assert len(invoke_spans) >= 1, (
f"Expected at least 1 invoke_agent span, got: {[s.name for s in spans]}"
)
assert len(invoke_spans) >= 1, f"Expected at least 1 invoke_agent span, got: {[s.name for s in spans]}"
invoke_span = invoke_spans[0]
trace_id = invoke_span.context.trace_id

# --- 2. All spans share the same trace_id ---
for s in spans:
assert s.context.trace_id == trace_id, (
f"Span '{s.name}' has different trace_id: "
f"{s.context.trace_id:032x} vs {trace_id:032x}"
f"Span '{s.name}' has different trace_id: " f"{s.context.trace_id:032x} vs {trace_id:032x}"
)

# --- 3. invoke_agent span is the root ---
assert invoke_span.parent is None, (
f"invoke_agent should be root but has parent: {invoke_span.parent.span_id:016x}"
)
assert (
invoke_span.parent is None
), f"invoke_agent should be root but has parent: {invoke_span.parent.span_id:016x}"

# --- 4. invoke_agent has correct operation name ---
assert _get_span_attr(invoke_span, GEN_AI_OPERATION_NAME_KEY) == INVOKE_AGENT_OPERATION_NAME
Expand All @@ -233,9 +230,7 @@ async def test_pipeline_invoke_agent_with_tool_call(
)
and not s.name.startswith("execute_tool")
]
assert len(inference_spans) >= 1, (
f"Expected at least 1 inference span, got: {[s.name for s in spans]}"
)
assert len(inference_spans) >= 1, f"Expected at least 1 inference span, got: {[s.name for s in spans]}"

invoke_span_id = invoke_span.context.span_id
for inf_span in inference_spans:
Expand Down Expand Up @@ -334,9 +329,7 @@ async def test_pipeline_invoke_agent_simple_inference(

# Inference spans are descendants
inference_spans = [
s
for s in spans
if s != invoke_span and _get_span_attr(s, GEN_AI_INPUT_MESSAGES_KEY) is not None
s for s in spans if s != invoke_span and _get_span_attr(s, GEN_AI_INPUT_MESSAGES_KEY) is not None
]
assert len(inference_spans) >= 1

Expand Down
28 changes: 13 additions & 15 deletions tests/a365/integration/openai/test_message_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,20 @@ def _span_to_json(span: ReadableSpan) -> dict[str, object]:

events_list: list[dict[str, object]] = []
for e in getattr(span, "events", None) or []:
events_list.append({
"name": e.name,
"attributes": dict(e.attributes) if e.attributes else {},
})
events_list.append(
{
"name": e.name,
"attributes": dict(e.attributes) if e.attributes else {},
}
)

links_list: list[dict[str, object]] = []
for lnk in getattr(span, "links", None) or []:
links_list.append({
"attributes": dict(lnk.attributes) if lnk.attributes else {},
})
links_list.append(
{
"attributes": dict(lnk.attributes) if lnk.attributes else {},
}
)

result: dict[str, object] = {
"name": span.name,
Expand Down Expand Up @@ -116,11 +120,7 @@ def _find_message_spans(self, distro_exporter) -> list[ReadableSpan]:
"""Find exported spans that have gen_ai.input.messages."""
get_tracer_provider().force_flush()
time.sleep(0.5)
return [
s
for s in distro_exporter.spans
if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes
]
return [s for s in distro_exporter.spans if s.attributes and GEN_AI_INPUT_MESSAGES_KEY in s.attributes]

@pytest.mark.asyncio
async def test_simple_chat_message_mapping(
Expand Down Expand Up @@ -153,9 +153,7 @@ async def test_simple_chat_message_mapping(
print(json.dumps(span_json, indent=2, default=str))

message_spans = self._find_message_spans(distro_exporter)
assert len(message_spans) > 0, (
f"No message spans found. All spans: {[s.name for s in distro_exporter.spans]}"
)
assert len(message_spans) > 0, f"No message spans found. All spans: {[s.name for s in distro_exporter.spans]}"

# Verify at least one span has structured A365 array format
found_structured = False
Expand Down
Loading
Loading