Skip to content

Commit 18f3605

Browse files
Refactor telemetry integration to use Azure Monitor, removing Application Insights dependency and enhancing event tracking
1 parent 858077e commit 18f3605

8 files changed

Lines changed: 180 additions & 445 deletions

File tree

src/backend/api/event_utils.py

Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,69 +3,18 @@
33
import os
44

55
# Third-party
6-
from applicationinsights import TelemetryClient
7-
from applicationinsights.channel import SynchronousQueue, SynchronousSender, TelemetryChannel
6+
from azure.monitor.events.extension import track_event
87

98
from dotenv import load_dotenv
109

1110
load_dotenv()
1211

13-
# Global telemetry client (initialized once)
14-
_telemetry_client = None
15-
16-
17-
def _get_telemetry_client():
18-
"""Get or create the Application Insights telemetry client."""
19-
global _telemetry_client
20-
21-
if _telemetry_client is None:
22-
connection_string = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
23-
if connection_string:
24-
try:
25-
# Extract instrumentation key from connection string
26-
# Format: InstrumentationKey=xxx;IngestionEndpoint=https://...
27-
parts = dict(part.split('=', 1) for part in connection_string.split(';') if '=' in part)
28-
instrumentation_key = parts.get('InstrumentationKey')
29-
30-
if instrumentation_key:
31-
# Create a synchronous channel for immediate sending
32-
sender = SynchronousSender()
33-
queue = SynchronousQueue(sender)
34-
channel = TelemetryChannel(None, queue)
35-
36-
_telemetry_client = TelemetryClient(instrumentation_key, channel)
37-
logging.info("Application Insights TelemetryClient initialized successfully")
38-
else:
39-
logging.error("Could not extract InstrumentationKey from connection string")
40-
except Exception as e:
41-
logging.error(f"Failed to initialize TelemetryClient: {e}")
42-
43-
return _telemetry_client
44-
4512

4613
def track_event_if_configured(event_name: str, event_data: dict):
47-
"""Track a custom event to Application Insights customEvents table.
48-
49-
This uses the Application Insights SDK TelemetryClient which properly
50-
sends custom events to the customEvents table in Application Insights.
51-
"""
14+
"""Track a custom event to Application Insights customEvents table."""
5215
instrumentation_key = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
5316
if instrumentation_key:
54-
try:
55-
client = _get_telemetry_client()
56-
if client:
57-
# Convert all values to strings to ensure compatibility
58-
properties = {k: str(v) for k, v in event_data.items()}
59-
60-
# Track the custom event
61-
client.track_event(event_name, properties=properties)
62-
client.flush() # Ensure immediate sending
63-
64-
logging.debug(f"Tracked custom event: {event_name} with data: {event_data}")
65-
else:
66-
logging.warning("TelemetryClient not available, custom event not tracked")
67-
except Exception as e:
68-
logging.error(f"Failed to track event {event_name}: {e}")
17+
track_event(event_name, event_data)
6918
else:
7019
logging.warning(
7120
f"Skipping track_event for {event_name} as Application Insights is not configured"

src/backend/app.py

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55

66
from api.api_routes import router as backend_router
77

8-
from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter, AzureMonitorTraceExporter
8+
from azure.monitor.opentelemetry import configure_azure_monitor
99

1010
from common.config.config import app_config
1111
from common.logger.app_logger import AppLogger
12+
from common.telemetry import patch_instrumentors
1213

1314
from dotenv import load_dotenv
1415

@@ -17,13 +18,7 @@
1718

1819
from helper.azure_credential_utils import get_azure_credential
1920

20-
from opentelemetry import trace
21-
from opentelemetry._logs import set_logger_provider
2221
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
23-
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
24-
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
25-
from opentelemetry.sdk.trace import TracerProvider
26-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
2722

2823
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent # pylint: disable=E0611
2924

@@ -138,52 +133,23 @@ def create_app() -> FastAPI:
138133
# This must happen AFTER app creation but BEFORE route registration
139134
instrumentation_key = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
140135
if instrumentation_key:
141-
# SOLUTION: Use manual telemetry setup instead of configure_azure_monitor
142-
# This gives us precise control over what gets instrumented, avoiding interference
143-
# with Semantic Kernel's async generators while still tracking Azure SDK calls
144-
145-
# Set up Azure Monitor exporter for traces
146-
azure_trace_exporter = AzureMonitorTraceExporter(connection_string=instrumentation_key)
147-
148-
# Create a tracer provider and add the Azure Monitor exporter
149-
tracer_provider = TracerProvider()
150-
tracer_provider.add_span_processor(BatchSpanProcessor(azure_trace_exporter))
151-
152-
# Set the global tracer provider
153-
trace.set_tracer_provider(tracer_provider)
154-
155-
# Set up Azure Monitor exporter for logs (appears in traces table)
156-
azure_log_exporter = AzureMonitorLogExporter(connection_string=instrumentation_key)
157-
158-
# Create a logger provider and add the Azure Monitor exporter
159-
logger_provider = LoggerProvider()
160-
logger_provider.add_log_record_processor(BatchLogRecordProcessor(azure_log_exporter))
161-
set_logger_provider(logger_provider)
162-
163-
# Attach OpenTelemetry handler to Python's root logger
164-
handler = LoggingHandler(logger_provider=logger_provider)
165-
logging.getLogger().addHandler(handler)
136+
# Fix: Patch buggy instrumentors before Azure Monitor loads them
137+
# See: https://github.com/microsoft/semantic-kernel/issues/13715
138+
patch_instrumentors()
139+
140+
# Configure Azure Monitor with FULL auto-instrumentation
141+
configure_azure_monitor(
142+
connection_string=instrumentation_key,
143+
enable_live_metrics=True
144+
)
166145

167-
# Instrument ONLY FastAPI for HTTP request/response tracing
168-
# This is safe because it only wraps HTTP handlers, not internal async operations
146+
# Instrument FastAPI for HTTP request/response tracing
169147
FastAPIInstrumentor.instrument_app(
170148
app,
171-
excluded_urls="socket,ws", # Exclude WebSocket URLs to reduce noise
172-
tracer_provider=tracer_provider
149+
excluded_urls="health,socket,ws",
173150
)
174151

175-
# Optional: Add manual spans in your code for Azure SDK operations using:
176-
# from opentelemetry import trace
177-
# tracer = trace.get_tracer(__name__)
178-
# with tracer.start_as_current_span("operation_name"):
179-
# # your Azure SDK call here
180-
181-
logger.logger.info("Application Insights configured with selective instrumentation")
182-
logger.logger.info("✓ FastAPI HTTP tracing enabled")
183-
logger.logger.info("✓ Python logging export to Application Insights enabled")
184-
logger.logger.info("✓ Manual span support enabled for Azure SDK operations")
185-
logger.logger.info("✓ Custom events via OpenTelemetry enabled")
186-
logger.logger.info("✓ Semantic Kernel async generators unaffected")
152+
logger.logger.info("Application Insights configured with full auto-instrumentation")
187153
else:
188154
logger.logger.warning("No Application Insights connection string found. Telemetry disabled.")
189155

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
"""Telemetry utilities for Application Insights integration."""
22

3-
from common.telemetry.telemetry_helper import (
4-
add_span_attributes,
5-
get_tracer,
6-
trace_context,
7-
trace_operation,
8-
trace_sync_context,
9-
)
3+
from common.telemetry.patch_instrumentor import patch_instrumentors
104

115
__all__ = [
12-
"trace_operation",
13-
"trace_context",
14-
"trace_sync_context",
15-
"get_tracer",
16-
"add_span_attributes",
6+
"patch_instrumentors",
177
]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""\nPatch for Azure AI telemetry instrumentors.\n\nFixes GitHub issue: https://github.com/microsoft/semantic-kernel/issues/13715\n\nThe bug: agent_api_response_to_str() in both azure.ai.agents and azure.ai.projects\nraises ValueError when response_format is a dict (e.g. from Semantic Kernel's AzureAIAgent).\nIt only handles str and None types.\n\nThe fix: Monkey-patch that method to convert dict/other types to JSON string instead of raising.\nMust be called BEFORE configure_azure_monitor() triggers any instrumentation.\n"""
2+
3+
import json
4+
import logging
5+
from typing import Any, Optional
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def _fixed_response_to_str(response_format: Any) -> Optional[str]:
11+
"""Convert response_format to string, handling dict types."""
12+
if response_format is None:
13+
return None
14+
if isinstance(response_format, str):
15+
return response_format
16+
try:
17+
return json.dumps(response_format, default=str)
18+
except (TypeError, ValueError):
19+
return str(response_format)
20+
21+
22+
def patch_instrumentors():
23+
"""
24+
Patch Azure AI telemetry instrumentors to handle dict response_format.
25+
26+
This fixes the ValueError: \"Unknown response format <class 'dict'>\" error\n that occurs when Semantic Kernel's AzureAIAgent passes a dict as response_format\n and Azure Monitor telemetry instrumentor tries to serialize it.\n\n Patches both azure.ai.agents and azure.ai.projects packages.\n Must be called BEFORE configure_azure_monitor().\n """
27+
# Patch azure.ai.agents (primary package with the bug)
28+
try:
29+
from azure.ai.agents.telemetry._ai_agents_instrumentor import (
30+
_AIAgentsInstrumentorPreview as _AgentsPreview,
31+
)
32+
_AgentsPreview.agent_api_response_to_str = staticmethod(_fixed_response_to_str)
33+
logger.info("Patched azure.ai.agents instrumentor")
34+
except ImportError:
35+
logger.debug("azure.ai.agents telemetry not installed, skipping patch")
36+
37+
# Patch azure.ai.projects (in case it exists in the environment)
38+
try:
39+
from azure.ai.projects.telemetry._ai_project_instrumentor import (
40+
_AIAgentsInstrumentorPreview as _ProjectsPreview,
41+
)
42+
_ProjectsPreview.agent_api_response_to_str = staticmethod(_fixed_response_to_str)
43+
logger.info("Patched azure.ai.projects instrumentor")
44+
except ImportError:
45+
logger.debug("azure.ai.projects telemetry not installed, skipping patch")

src/backend/common/telemetry/telemetry_helper.py

Lines changed: 0 additions & 160 deletions
This file was deleted.

0 commit comments

Comments
 (0)