88import logging
99from typing import Callable
1010
11+ # Prometheus metrics imports
12+ from prometheus_client import Counter , Histogram , Gauge , generate_latest , CONTENT_TYPE_LATEST
13+
1114from request_models import ChatRequest , RecipeIndexRequest , RecipeDeleteRequest , RecipeSuggestionRequest
1215from response_models import ChatResponse , RecipeIndexResponse , RecipeDeleteResponse , RecipeSuggestionResponse , HealthResponse
1316from llm import RecipeLLM
1417
18+ # Prometheus metrics
19+ REQUEST_COUNT = Counter ('genai_requests_total' , 'Total number of requests' , ['method' , 'endpoint' , 'status' ])
20+ REQUEST_DURATION = Histogram ('genai_request_duration_seconds' , 'Request duration in seconds' , ['method' , 'endpoint' ])
21+ ACTIVE_REQUESTS = Gauge ('genai_active_requests' , 'Number of active requests' )
22+ LLM_OPERATIONS = Counter ('genai_llm_operations_total' , 'Total number of LLM operations' , ['operation_type' , 'status' ])
23+ VECTOR_OPERATIONS = Counter ('genai_vector_operations_total' , 'Total number of vector operations' , ['operation_type' , 'status' ])
24+ SERVICE_HEALTH = Gauge ('genai_service_health' , 'Service health status (1=healthy, 0=unhealthy)' )
25+
1526# Enhanced logging configuration
1627class StructuredFormatter (logging .Formatter ):
1728 """Custom formatter for structured logging"""
@@ -60,12 +71,15 @@ def format(self, record):
6071
6172logger = logging .getLogger (__name__ )
6273
63- # Add request ID tracking middleware
74+ # Add request ID tracking and Prometheus metrics middleware
6475async def add_request_id_middleware (request : Request , call_next : Callable ) -> Response :
65- """Add request ID to all requests for tracing"""
76+ """Add request ID to all requests for tracing and collect Prometheus metrics """
6677 request_id = str (uuid .uuid4 ())
6778 request .state .request_id = request_id
6879
80+ # Track active requests
81+ ACTIVE_REQUESTS .inc ()
82+
6983 # Log incoming request
7084 start_time = time .time ()
7185 structured_logger .info (
@@ -87,6 +101,18 @@ async def add_request_id_middleware(request: Request, call_next: Callable) -> Re
87101
88102 # Calculate request duration
89103 duration_ms = round ((time .time () - start_time ) * 1000 , 2 )
104+ duration_seconds = (time .time () - start_time )
105+
106+ # Update Prometheus metrics
107+ REQUEST_COUNT .labels (
108+ method = request .method ,
109+ endpoint = request .url .path ,
110+ status = str (response .status_code )
111+ ).inc ()
112+ REQUEST_DURATION .labels (
113+ method = request .method ,
114+ endpoint = request .url .path
115+ ).observe (duration_seconds )
90116
91117 # Log response
92118 structured_logger .info (
@@ -108,6 +134,19 @@ async def add_request_id_middleware(request: Request, call_next: Callable) -> Re
108134
109135 except Exception as e :
110136 duration_ms = round ((time .time () - start_time ) * 1000 , 2 )
137+ duration_seconds = (time .time () - start_time )
138+
139+ # Update Prometheus metrics for failed requests
140+ REQUEST_COUNT .labels (
141+ method = request .method ,
142+ endpoint = request .url .path ,
143+ status = "500"
144+ ).inc ()
145+ REQUEST_DURATION .labels (
146+ method = request .method ,
147+ endpoint = request .url .path
148+ ).observe (duration_seconds )
149+
111150 structured_logger .error (
112151 f"Request failed: { request .method } { request .url .path } - { str (e )} " ,
113152 extra = {
@@ -120,6 +159,9 @@ async def add_request_id_middleware(request: Request, call_next: Callable) -> Re
120159 }
121160 )
122161 raise
162+ finally :
163+ # Decrement active requests
164+ ACTIVE_REQUESTS .dec ()
123165
124166# Global LLM instance
125167llm_instance : RecipeLLM = None
@@ -264,6 +306,22 @@ async def health_check(request: Request):
264306 services = {"error" : str (e )}
265307 )
266308
309+ @app .get ("/metrics" )
310+ async def metrics ():
311+ """Prometheus metrics endpoint"""
312+ # Update service health metric based on current status
313+ try :
314+ if llm_instance :
315+ services_status = llm_instance .get_health_status ()
316+ overall_health = 1 if all ("healthy" in status for status in services_status .values ()) else 0
317+ else :
318+ overall_health = 0
319+ SERVICE_HEALTH .set (overall_health )
320+ except Exception :
321+ SERVICE_HEALTH .set (0 )
322+
323+ return Response (content = generate_latest (), media_type = CONTENT_TYPE_LATEST )
324+
267325@app .post ("/genai/chat" , response_model = ChatResponse )
268326async def chat (request : ChatRequest , http_request : Request ):
269327 """Chat with the AI assistant for recipe search and creation"""
@@ -296,6 +354,9 @@ async def chat(request: ChatRequest, http_request: Request):
296354 response = llm_instance .chat (request .message )
297355 duration_ms = round ((time .time () - start_time ) * 1000 , 2 )
298356
357+ # Update LLM operation metrics
358+ LLM_OPERATIONS .labels (operation_type = 'chat' , status = 'success' ).inc ()
359+
299360 structured_logger .info (
300361 "Chat request completed successfully" ,
301362 extra = {
@@ -315,6 +376,9 @@ async def chat(request: ChatRequest, http_request: Request):
315376 except Exception as e :
316377 duration_ms = round ((time .time () - start_time ) * 1000 , 2 )
317378
379+ # Update LLM operation metrics for failures
380+ LLM_OPERATIONS .labels (operation_type = 'chat' , status = 'error' ).inc ()
381+
318382 logger .error (f"Error in chat: { e } " , exc_info = True )
319383 structured_logger .error (
320384 f"Chat request failed: { str (e )} " ,
@@ -366,6 +430,9 @@ async def index_recipe(request: RecipeIndexRequest, http_request: Request):
366430 duration_ms = round ((time .time () - start_time ) * 1000 , 2 )
367431
368432 if success :
433+ # Update vector operation metrics for success
434+ VECTOR_OPERATIONS .labels (operation_type = 'index' , status = 'success' ).inc ()
435+
369436 structured_logger .info (
370437 f"Recipe indexed successfully: { request .recipe .metadata .title } " ,
371438 extra = {
@@ -384,6 +451,9 @@ async def index_recipe(request: RecipeIndexRequest, http_request: Request):
384451 recipe_id = request .recipe .metadata .id
385452 )
386453 else :
454+ # Update vector operation metrics for failure
455+ VECTOR_OPERATIONS .labels (operation_type = 'index' , status = 'error' ).inc ()
456+
387457 structured_logger .error (
388458 f"Recipe indexing failed: { request .recipe .metadata .title } " ,
389459 extra = {
@@ -402,6 +472,9 @@ async def index_recipe(request: RecipeIndexRequest, http_request: Request):
402472 except Exception as e :
403473 duration_ms = round ((time .time () - start_time ) * 1000 , 2 )
404474
475+ # Update vector operation metrics for exceptions
476+ VECTOR_OPERATIONS .labels (operation_type = 'index' , status = 'error' ).inc ()
477+
405478 logger .error (f"Error indexing recipe: { e } " , exc_info = True )
406479 structured_logger .error (
407480 f"Recipe indexing failed: { str (e )} " ,
@@ -451,6 +524,9 @@ async def delete_recipe(recipe_id: str, http_request: Request):
451524 duration_ms = round ((time .time () - start_time ) * 1000 , 2 )
452525
453526 if success :
527+ # Update vector operation metrics for success
528+ VECTOR_OPERATIONS .labels (operation_type = 'delete' , status = 'success' ).inc ()
529+
454530 structured_logger .info (
455531 f"Recipe deleted successfully: { recipe_id } " ,
456532 extra = {
@@ -468,6 +544,9 @@ async def delete_recipe(recipe_id: str, http_request: Request):
468544 recipe_id = recipe_id
469545 )
470546 else :
547+ # Update vector operation metrics for failure
548+ VECTOR_OPERATIONS .labels (operation_type = 'delete' , status = 'error' ).inc ()
549+
471550 structured_logger .error (
472551 f"Recipe deletion failed: { recipe_id } " ,
473552 extra = {
@@ -485,6 +564,9 @@ async def delete_recipe(recipe_id: str, http_request: Request):
485564 except Exception as e :
486565 duration_ms = round ((time .time () - start_time ) * 1000 , 2 )
487566
567+ # Update vector operation metrics for exceptions
568+ VECTOR_OPERATIONS .labels (operation_type = 'delete' , status = 'error' ).inc ()
569+
488570 logger .error (f"Error deleting recipe: { e } " , exc_info = True )
489571 structured_logger .error (
490572 f"Recipe deletion failed: { str (e )} " ,
@@ -533,6 +615,9 @@ async def suggest_recipe(request: RecipeSuggestionRequest, http_request: Request
533615 response = llm_instance .suggest_recipe (request .query )
534616 duration_ms = round ((time .time () - start_time ) * 1000 , 2 )
535617
618+ # Update LLM operation metrics for success
619+ LLM_OPERATIONS .labels (operation_type = 'suggest' , status = 'success' ).inc ()
620+
536621 structured_logger .info (
537622 "Recipe suggestion completed successfully" ,
538623 extra = {
@@ -551,6 +636,9 @@ async def suggest_recipe(request: RecipeSuggestionRequest, http_request: Request
551636 except Exception as e :
552637 duration_ms = round ((time .time () - start_time ) * 1000 , 2 )
553638
639+ # Update LLM operation metrics for failures
640+ LLM_OPERATIONS .labels (operation_type = 'suggest' , status = 'error' ).inc ()
641+
554642 logger .error (f"Error in recipe suggestion: { e } " , exc_info = True )
555643 structured_logger .error (
556644 f"Recipe suggestion failed: { str (e )} " ,
0 commit comments