Skip to content

Commit 58b32f7

Browse files
fix: Enhance logging and error tracking across API routes and services
1 parent 93bc48f commit 58b32f7

File tree

5 files changed

+148
-44
lines changed

5 files changed

+148
-44
lines changed

src/api/api/api_routes.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,13 @@
1212
from services.chart_service import ChartService
1313
from common.logging.event_utils import track_event_if_configured
1414
from helpers.azure_credential_utils import get_azure_credential
15-
from azure.monitor.opentelemetry import configure_azure_monitor
1615
from opentelemetry import trace
1716
from opentelemetry.trace import Status, StatusCode
1817

1918
router = APIRouter()
2019

2120
logger = logging.getLogger(__name__)
2221

23-
# Check if the Application Insights Instrumentation Key is set in the environment variables
24-
instrumentation_key = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
25-
if instrumentation_key:
26-
# Configure Application Insights if the Instrumentation Key is found
27-
configure_azure_monitor(connection_string=instrumentation_key)
28-
logging.info("Application Insights configured with the provided Instrumentation Key")
29-
else:
30-
# Log a warning if the Instrumentation Key is not found
31-
logging.warning("No Application Insights Instrumentation Key found. Skipping configuration")
32-
3322

3423
@router.get("/fetchChartData")
3524
async def fetch_chart_data():
@@ -43,6 +32,10 @@ async def fetch_chart_data():
4332
return JSONResponse(content=response)
4433
except Exception as e:
4534
logger.exception("Error in fetch_chart_data: %s", str(e))
35+
track_event_if_configured("FetchChartDataError", {
36+
"error": str(e),
37+
"error_type": type(e).__name__
38+
})
4639
span = trace.get_current_span()
4740
if span is not None:
4841
span.record_exception(e)
@@ -53,7 +46,7 @@ async def fetch_chart_data():
5346
@router.post("/fetchChartDataWithFilters")
5447
async def fetch_chart_data_with_filters(chart_filters: ChartFilters):
5548
try:
56-
logger.info(f"Received filters: {chart_filters}")
49+
logger.info("Received filters: %s", chart_filters)
5750
chart_service = ChartService()
5851
response = await chart_service.fetch_chart_data_with_filters(chart_filters)
5952
track_event_if_configured(
@@ -69,6 +62,10 @@ async def fetch_chart_data_with_filters(chart_filters: ChartFilters):
6962
return JSONResponse(content=response)
7063
except Exception as e:
7164
logger.exception("Error in fetch_chart_data_with_filters: %s", str(e))
65+
track_event_if_configured("FetchChartDataWithFiltersError", {
66+
"error": str(e),
67+
"error_type": type(e).__name__
68+
})
7269
span = trace.get_current_span()
7370
if span is not None:
7471
span.record_exception(e)
@@ -88,6 +85,10 @@ async def fetch_filter_data():
8885
return JSONResponse(content=response)
8986
except Exception as e:
9087
logger.exception("Error in fetch_filter_data: %s", str(e))
88+
track_event_if_configured("FetchFilterDataError", {
89+
"error": str(e),
90+
"error_type": type(e).__name__
91+
})
9192
span = trace.get_current_span()
9293
if span is not None:
9394
span.record_exception(e)
@@ -102,6 +103,17 @@ async def conversation(request: Request):
102103
request_json = await request.json()
103104
conversation_id = request_json.get("conversation_id")
104105
query = request_json.get("query")
106+
107+
# Track chat request initiation
108+
track_event_if_configured("ChatRequestReceived", {
109+
"conversation_id": conversation_id
110+
})
111+
112+
# Attach conversation_id to current span for Application Insights correlation
113+
span = trace.get_current_span()
114+
if span and conversation_id:
115+
span.set_attribute("conversation_id", conversation_id)
116+
105117
chat_service = ChatService()
106118
result = await chat_service.stream_chat_request(conversation_id, query)
107119
track_event_if_configured(
@@ -112,6 +124,14 @@ async def conversation(request: Request):
112124

113125
except Exception as ex:
114126
logger.exception("Error in conversation endpoint: %s", str(ex))
127+
128+
# Track specific error type
129+
track_event_if_configured("ChatRequestError", {
130+
"conversation_id": request_json.get("conversation_id") if 'request_json' in locals() else "",
131+
"error": str(ex),
132+
"error_type": type(ex).__name__
133+
})
134+
115135
span = trace.get_current_span()
116136
if span is not None:
117137
span.record_exception(ex)
@@ -129,6 +149,10 @@ async def get_layout_config():
129149
return JSONResponse(content=layout_config_json) # Return the parsed JSON
130150
except json.JSONDecodeError as e:
131151
logger.exception("Failed to parse layout config JSON: %s", str(e))
152+
track_event_if_configured("LayoutConfigParseError", {
153+
"error": str(e),
154+
"error_type": "JSONDecodeError"
155+
})
132156
span = trace.get_current_span()
133157
if span is not None:
134158
span.record_exception(e)

src/api/api/history_routes.py

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,13 @@
55
from auth.auth_utils import get_authenticated_user_details
66
from services.history_service import HistoryService
77
from common.logging.event_utils import track_event_if_configured
8-
from azure.monitor.opentelemetry import configure_azure_monitor
98
from opentelemetry import trace
109
from opentelemetry.trace import Status, StatusCode
1110

1211
router = APIRouter()
1312

1413
logger = logging.getLogger(__name__)
1514

16-
# Check if the Application Insights Instrumentation Key is set in the environment variables
17-
instrumentation_key = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
18-
if instrumentation_key:
19-
# Configure Application Insights if the Instrumentation Key is found
20-
configure_azure_monitor(connection_string=instrumentation_key)
21-
logging.info("Application Insights configured with the provided Instrumentation Key")
22-
else:
23-
# Log a warning if the Instrumentation Key is not found
24-
logging.warning("No Application Insights Instrumentation Key found. Skipping configuration")
25-
2615
# Single instance of HistoryService (if applicable)
2716
history_service = HistoryService()
2817

@@ -41,6 +30,11 @@ async def update_conversation(request: Request):
4130
if not conversation_id:
4231
raise HTTPException(status_code=400, detail="No conversation_id found")
4332

33+
# Attach conversation_id to current span for Application Insights correlation
34+
span = trace.get_current_span()
35+
if span and conversation_id:
36+
span.set_attribute("conversation_id", conversation_id)
37+
4438
# Call HistoryService to update conversation
4539
update_response = await history_service.update_conversation(user_id, request_json)
4640

@@ -65,6 +59,10 @@ async def update_conversation(request: Request):
6559
)
6660
except Exception as e:
6761
logger.exception("Exception in /history/update: %s", str(e))
62+
track_event_if_configured("ConversationUpdateError", {
63+
"error": str(e),
64+
"error_type": type(e).__name__
65+
})
6866
span = trace.get_current_span()
6967
if span is not None:
7068
span.record_exception(e)
@@ -126,6 +124,10 @@ async def update_message_feedback(request: Request):
126124

127125
except Exception as e:
128126
logger.exception("Exception in /history/message_feedback: %s", str(e))
127+
track_event_if_configured("MessageFeedbackError", {
128+
"error": str(e),
129+
"error_type": type(e).__name__
130+
})
129131
span = trace.get_current_span()
130132
if span is not None:
131133
span.record_exception(e)
@@ -150,6 +152,11 @@ async def delete_conversation(request: Request):
150152
})
151153
raise HTTPException(status_code=400, detail="conversation_id is required")
152154

155+
# Attach conversation_id to current span for Application Insights correlation
156+
span = trace.get_current_span()
157+
if span and conversation_id:
158+
span.set_attribute("conversation_id", conversation_id)
159+
153160
# Delete conversation using HistoryService
154161
deleted = await history_service.delete_conversation(user_id, conversation_id)
155162
if deleted:
@@ -173,6 +180,10 @@ async def delete_conversation(request: Request):
173180
detail=f"Conversation {conversation_id} not found or user does not have permission.")
174181
except Exception as e:
175182
logger.exception("Exception in /history/delete: %s", str(e))
183+
track_event_if_configured("ConversationDeleteError", {
184+
"error": str(e),
185+
"error_type": type(e).__name__
186+
})
176187
span = trace.get_current_span()
177188
if span is not None:
178189
span.record_exception(e)
@@ -191,7 +202,7 @@ async def list_conversations(
191202
request_headers=request.headers)
192203
user_id = authenticated_user["user_principal_id"]
193204

194-
logger.info(f"user_id: {user_id}, offset: {offset}, limit: {limit}")
205+
logger.info("Fetching conversations - user_id: %s, offset: %s, limit: %s", user_id, offset, limit)
195206

196207
# Get conversations
197208
conversations = await history_service.get_conversations(user_id, offset=offset, limit=limit)
@@ -216,6 +227,10 @@ async def list_conversations(
216227

217228
except Exception as e:
218229
logger.exception("Exception in /history/list: %s", str(e))
230+
track_event_if_configured("ConversationsListError", {
231+
"error": str(e),
232+
"error_type": type(e).__name__
233+
})
219234
span = trace.get_current_span()
220235
if span is not None:
221236
span.record_exception(e)
@@ -241,6 +256,11 @@ async def get_conversation_messages(request: Request):
241256
})
242257
raise HTTPException(status_code=400, detail="conversation_id is required")
243258

259+
# Attach conversation_id to current span for Application Insights correlation
260+
span = trace.get_current_span()
261+
if span and conversation_id:
262+
span.set_attribute("conversation_id", conversation_id)
263+
244264
# Get conversation details
245265
conversationMessages = await history_service.get_conversation_messages(user_id, conversation_id)
246266
if not conversationMessages:
@@ -266,6 +286,10 @@ async def get_conversation_messages(request: Request):
266286

267287
except Exception as e:
268288
logger.exception("Exception in /history/read: %s", str(e))
289+
track_event_if_configured("ConversationReadError", {
290+
"error": str(e),
291+
"error_type": type(e).__name__
292+
})
269293
span = trace.get_current_span()
270294
if span is not None:
271295
span.record_exception(e)
@@ -291,6 +315,12 @@ async def rename_conversation(request: Request):
291315
"user_id": user_id
292316
})
293317
raise HTTPException(status_code=400, detail="conversation_id is required")
318+
319+
# Attach conversation_id to current span for Application Insights correlation
320+
span = trace.get_current_span()
321+
if span and conversation_id:
322+
span.set_attribute("conversation_id", conversation_id)
323+
294324
if not title:
295325
track_event_if_configured("RenameConversationValidationError", {
296326
"error": "title is required",
@@ -310,6 +340,10 @@ async def rename_conversation(request: Request):
310340

311341
except Exception as e:
312342
logger.exception("Exception in /history/rename: %s", str(e))
343+
track_event_if_configured("ConversationRenameError", {
344+
"error": str(e),
345+
"error_type": type(e).__name__
346+
})
313347
span = trace.get_current_span()
314348
if span is not None:
315349
span.record_exception(e)
@@ -351,6 +385,10 @@ async def delete_all_conversations(request: Request):
351385

352386
except Exception as e:
353387
logging.exception("Exception in /history/delete_all: %s", str(e))
388+
track_event_if_configured("AllConversationsDeleteError", {
389+
"error": str(e),
390+
"error_type": type(e).__name__
391+
})
354392
span = trace.get_current_span()
355393
if span is not None:
356394
span.record_exception(e)
@@ -377,6 +415,11 @@ async def clear_messages(request: Request):
377415
})
378416
raise HTTPException(status_code=400, detail="conversation_id is required")
379417

418+
# Attach conversation_id to current span for Application Insights correlation
419+
span = trace.get_current_span()
420+
if span and conversation_id:
421+
span.set_attribute("conversation_id", conversation_id)
422+
380423
# Delete conversation messages from CosmosDB
381424
success = await history_service.clear_messages(user_id, conversation_id)
382425

@@ -400,6 +443,10 @@ async def clear_messages(request: Request):
400443

401444
except Exception as e:
402445
logger.exception("Exception in /history/clear: %s", str(e))
446+
track_event_if_configured("MessagesClearError", {
447+
"error": str(e),
448+
"error_type": type(e).__name__
449+
})
403450
span = trace.get_current_span()
404451
if span is not None:
405452
span.record_exception(e)
@@ -429,6 +476,10 @@ async def ensure_cosmos():
429476
status_code=200)
430477
except Exception as e:
431478
logger.exception("Exception in /history/ensure: %s", str(e))
479+
track_event_if_configured("CosmosDBEnsureError", {
480+
"error": str(e),
481+
"error_type": type(e).__name__
482+
})
432483
span = trace.get_current_span()
433484
if span is not None:
434485
span.record_exception(e)

src/api/app.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@
4040
for logger_name in AZURE_LOGGING_PACKAGES:
4141
logging.getLogger(logger_name).setLevel(getattr(logging, AZURE_PACKAGE_LOGGING_LEVEL, logging.WARNING))
4242

43+
# Suppress noisy OpenTelemetry and Azure Monitor logs
44+
logging.getLogger("opentelemetry.sdk").setLevel(logging.ERROR)
45+
logging.getLogger("opentelemetry.instrumentation.httpx").setLevel(logging.WARNING)
46+
logging.getLogger("opentelemetry.instrumentation.aiohttp-client").setLevel(logging.WARNING)
47+
logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(logging.WARNING)
48+
logging.getLogger("azure.monitor.opentelemetry.exporter.export._base").setLevel(logging.WARNING)
49+
50+
# Configure Azure Monitor and OpenTelemetry before importing routes
51+
from azure.monitor.opentelemetry import configure_azure_monitor
52+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
53+
4354

4455
def build_app() -> FastAPI:
4556
"""
@@ -67,6 +78,24 @@ async def health_check():
6778
"""Health check endpoint"""
6879
return {"status": "healthy"}
6980

81+
# Configure Azure Monitor and instrument FastAPI for OpenTelemetry
82+
# This enables automatic request tracing, dependency tracking, and proper operation_id
83+
instrumentation_key = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
84+
if instrumentation_key:
85+
# Configure Application Insights telemetry with live metrics
86+
# Set disable_offline_storage=True to reduce logs about offline storage
87+
configure_azure_monitor(
88+
connection_string=instrumentation_key,
89+
enable_live_metrics=True,
90+
disable_offline_storage=True # Reduces "Storing events" logs
91+
)
92+
93+
# Instrument FastAPI app to automatically trace all requests
94+
FastAPIInstrumentor.instrument_app(fastapi_app)
95+
logging.info("Application Insights configured with live metrics and FastAPI instrumentation enabled")
96+
else:
97+
logging.warning("No Application Insights connection string found. Telemetry disabled.")
98+
7099
return fastapi_app
71100

72101

src/api/helpers/chat_helper.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def process_rag_response(rag_response, query):
3535
{query}
3636
{rag_response}
3737
"""
38-
logger.info(f">>> Processing chart data for response: {rag_response}")
38+
logger.info("Processing chart data for response: %s", rag_response)
3939
completion = client.chat.completions.create(
4040
model=config.azure_openai_deployment_model,
4141
messages=[
@@ -45,10 +45,10 @@ def process_rag_response(rag_response, query):
4545
temperature=0,
4646
)
4747
chart_data = completion.choices[0].message.content.strip().replace("```json", "").replace("```", "")
48-
logger.info(f">>> Generated chart data: {chart_data}")
48+
logger.info("Generated chart data: %s", chart_data)
4949
return json.loads(chart_data)
5050
except Exception as e:
51-
logger.error(f"Error processing RAG response: {e}")
51+
logger.error("Error processing RAG response: %s", str(e))
5252
return {"error": "Chart could not be generated from this data. Please ask a different question."}
5353

5454

0 commit comments

Comments
 (0)