Skip to content

Commit d5da333

Browse files
authored
Merge pull request #31 from tanzilahmed0/task-b23
Task B23: Implemetning middleware
2 parents d104753 + f43c147 commit d5da333

8 files changed

Lines changed: 370 additions & 8 deletions

File tree

backend/api/health.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from fastapi import APIRouter
66

7+
from middleware.monitoring import query_performance_tracker
78
from services.database_service import get_db_service
89
from services.redis_service import redis_service
910
from services.storage_service import storage_service
@@ -77,3 +78,73 @@ async def health_check() -> Dict[str, Any]:
7778
},
7879
},
7980
}
81+
82+
83+
@router.get("/metrics")
84+
async def get_performance_metrics() -> Dict[str, Any]:
85+
"""Get performance metrics for monitoring and bottleneck identification"""
86+
87+
try:
88+
# Get operation performance statistics
89+
operations_summary = query_performance_tracker.get_all_operations_summary()
90+
91+
# Calculate overall statistics
92+
total_operations = sum(
93+
stats["call_count"] for stats in operations_summary.values()
94+
)
95+
total_time = sum(stats["total_time"] for stats in operations_summary.values())
96+
avg_time_overall = total_time / total_operations if total_operations > 0 else 0
97+
98+
# Identify slowest operations
99+
slowest_operations = sorted(
100+
[
101+
{
102+
"operation": operation,
103+
"avg_time": stats["avg_time"],
104+
"call_count": stats["call_count"],
105+
"total_time": stats["total_time"],
106+
}
107+
for operation, stats in operations_summary.items()
108+
],
109+
key=lambda x: x["avg_time"],
110+
reverse=True,
111+
)[
112+
:5
113+
] # Top 5 slowest
114+
115+
# Identify bottlenecks (operations taking > 2 seconds on average)
116+
bottlenecks = [op for op in slowest_operations if op["avg_time"] > 2.0]
117+
118+
return {
119+
"success": True,
120+
"data": {
121+
"timestamp": datetime.utcnow().isoformat() + "Z",
122+
"summary": {
123+
"total_operations": total_operations,
124+
"total_time": round(total_time, 3),
125+
"average_time": round(avg_time_overall, 3),
126+
"unique_operations": len(operations_summary),
127+
},
128+
"operations": operations_summary,
129+
"slowest_operations": slowest_operations,
130+
"bottlenecks": bottlenecks,
131+
"performance_alerts": [
132+
f"Operation '{op['operation']}' averages {op['avg_time']:.3f}s per call"
133+
for op in bottlenecks
134+
],
135+
},
136+
}
137+
138+
except Exception as e:
139+
return {
140+
"success": False,
141+
"error": f"Failed to retrieve performance metrics: {str(e)}",
142+
"data": {
143+
"timestamp": datetime.utcnow().isoformat() + "Z",
144+
"summary": {},
145+
"operations": {},
146+
"slowest_operations": [],
147+
"bottlenecks": [],
148+
"performance_alerts": [],
149+
},
150+
}

backend/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from api.health import router as health_router
1313
from api.middleware.cors import setup_cors
1414
from api.projects import router as projects_router
15+
from middleware.monitoring import PerformanceMonitoringMiddleware
1516

1617
# Create FastAPI application
1718
app = FastAPI(
@@ -25,6 +26,9 @@
2526
# Setup CORS middleware
2627
setup_cors(app)
2728

29+
# Add performance monitoring middleware
30+
app.add_middleware(PerformanceMonitoringMiddleware)
31+
2832
# Include routers
2933
app.include_router(health_router)
3034
app.include_router(auth_router)

backend/middleware/monitoring.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import logging
2+
import time
3+
from typing import Dict, List, Optional
4+
5+
try:
6+
from fastapi import Request, Response
7+
from fastapi.middleware.base import BaseHTTPMiddleware
8+
9+
FASTAPI_AVAILABLE = True
10+
except ImportError:
11+
# FastAPI not available in test environment
12+
FASTAPI_AVAILABLE = False
13+
Request = None
14+
Response = None
15+
BaseHTTPMiddleware = None
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
if FASTAPI_AVAILABLE:
21+
22+
class PerformanceMonitoringMiddleware(BaseHTTPMiddleware):
23+
"""Middleware to monitor API performance and response times"""
24+
25+
def __init__(self, app, enable_detailed_logging: bool = True):
26+
super().__init__(app)
27+
self.enable_detailed_logging = enable_detailed_logging
28+
self.metrics: Dict[str, List[float]] = {}
29+
30+
async def dispatch(self, request: Request, call_next):
31+
"""Monitor request processing time"""
32+
start_time = time.time()
33+
34+
# Process the request
35+
response = await call_next(request)
36+
37+
# Calculate processing time
38+
process_time = time.time() - start_time
39+
40+
# Track metrics
41+
endpoint = f"{request.method} {request.url.path}"
42+
self._record_metric(endpoint, process_time)
43+
44+
# Add performance header
45+
response.headers["X-Process-Time"] = str(process_time)
46+
47+
# Log performance if enabled
48+
if self.enable_detailed_logging:
49+
self._log_performance(request, response, process_time)
50+
51+
return response
52+
53+
def _record_metric(self, endpoint: str, process_time: float):
54+
"""Record performance metric for endpoint"""
55+
if endpoint not in self.metrics:
56+
self.metrics[endpoint] = []
57+
58+
# Keep only last 100 measurements to prevent memory bloat
59+
if len(self.metrics[endpoint]) >= 100:
60+
self.metrics[endpoint].pop(0)
61+
62+
self.metrics[endpoint].append(process_time)
63+
64+
def _log_performance(
65+
self, request: Request, response: Response, process_time: float
66+
):
67+
"""Log performance information"""
68+
endpoint = f"{request.method} {request.url.path}"
69+
status_code = response.status_code
70+
71+
# Determine log level based on performance and status
72+
if process_time > 5.0: # Very slow requests
73+
log_level = logging.WARNING
74+
performance_indicator = "SLOW"
75+
elif process_time > 2.0: # Moderately slow requests
76+
log_level = logging.INFO
77+
performance_indicator = "MODERATE"
78+
else:
79+
log_level = logging.DEBUG
80+
performance_indicator = "FAST"
81+
82+
# Log with appropriate level
83+
logger.log(
84+
log_level,
85+
f"[{performance_indicator}] {endpoint} - {status_code} - {process_time:.3f}s",
86+
)
87+
88+
# Log additional warning for very slow requests
89+
if process_time > 5.0:
90+
avg_time = self.get_average_response_time(endpoint)
91+
logger.warning(
92+
f"Performance bottleneck detected: {endpoint} took {process_time:.3f}s "
93+
f"(avg: {avg_time:.3f}s)"
94+
)
95+
96+
def get_metrics_summary(self) -> Dict[str, Dict[str, float]]:
97+
"""Get performance metrics summary for all endpoints"""
98+
summary = {}
99+
100+
for endpoint, times in self.metrics.items():
101+
if times:
102+
summary[endpoint] = {
103+
"avg_time": sum(times) / len(times),
104+
"min_time": min(times),
105+
"max_time": max(times),
106+
"request_count": len(times),
107+
"total_time": sum(times),
108+
}
109+
110+
return summary
111+
112+
def get_average_response_time(self, endpoint: str) -> float:
113+
"""Get average response time for specific endpoint"""
114+
if endpoint in self.metrics and self.metrics[endpoint]:
115+
return sum(self.metrics[endpoint]) / len(self.metrics[endpoint])
116+
return 0.0
117+
118+
def get_slowest_endpoints(self, limit: int = 5) -> List[Dict[str, float]]:
119+
"""Get the slowest endpoints by average response time"""
120+
summary = self.get_metrics_summary()
121+
122+
sorted_endpoints = sorted(
123+
summary.items(), key=lambda x: x[1]["avg_time"], reverse=True
124+
)
125+
126+
return [
127+
{
128+
"endpoint": endpoint,
129+
"avg_time": metrics["avg_time"],
130+
"request_count": metrics["request_count"],
131+
}
132+
for endpoint, metrics in sorted_endpoints[:limit]
133+
]
134+
135+
def clear_metrics(self):
136+
"""Clear all collected metrics"""
137+
self.metrics.clear()
138+
logger.info("Performance metrics cleared")
139+
140+
else:
141+
# Stub class when FastAPI is not available
142+
class PerformanceMonitoringMiddleware:
143+
def __init__(self, app, enable_detailed_logging: bool = True):
144+
self.app = app
145+
146+
async def __call__(self, scope, receive, send):
147+
# Pass through to the app without monitoring in test mode
148+
await self.app(scope, receive, send)
149+
150+
151+
class QueryPerformanceTracker:
152+
"""Track performance of specific operations like database queries and AI calls"""
153+
154+
def __init__(self):
155+
self.operation_metrics: Dict[str, List[float]] = {}
156+
157+
def track_operation(self, operation_name: str, duration: float):
158+
"""Track duration of a specific operation"""
159+
if operation_name not in self.operation_metrics:
160+
self.operation_metrics[operation_name] = []
161+
162+
# Keep only last 50 measurements per operation
163+
if len(self.operation_metrics[operation_name]) >= 50:
164+
self.operation_metrics[operation_name].pop(0)
165+
166+
self.operation_metrics[operation_name].append(duration)
167+
168+
# Log slow operations
169+
if duration > 3.0:
170+
avg_duration = sum(self.operation_metrics[operation_name]) / len(
171+
self.operation_metrics[operation_name]
172+
)
173+
logger.warning(
174+
f"Slow operation detected: {operation_name} took {duration:.3f}s "
175+
f"(avg: {avg_duration:.3f}s)"
176+
)
177+
178+
def get_operation_stats(self, operation_name: str) -> Optional[Dict[str, float]]:
179+
"""Get statistics for a specific operation"""
180+
if (
181+
operation_name not in self.operation_metrics
182+
or not self.operation_metrics[operation_name]
183+
):
184+
return None
185+
186+
times = self.operation_metrics[operation_name]
187+
return {
188+
"avg_time": sum(times) / len(times),
189+
"min_time": min(times),
190+
"max_time": max(times),
191+
"call_count": len(times),
192+
"total_time": sum(times),
193+
}
194+
195+
def get_all_operations_summary(self) -> Dict[str, Dict[str, float]]:
196+
"""Get summary of all tracked operations"""
197+
summary = {}
198+
for operation_name in self.operation_metrics:
199+
stats = self.get_operation_stats(operation_name)
200+
if stats:
201+
summary[operation_name] = stats
202+
return summary
203+
204+
205+
# Global tracker instance for operation monitoring
206+
query_performance_tracker = QueryPerformanceTracker()
207+
208+
209+
def track_performance(operation_name: str):
210+
"""Decorator to track performance of functions"""
211+
212+
def decorator(func):
213+
import asyncio
214+
import inspect
215+
216+
if inspect.iscoroutinefunction(func):
217+
# Async function wrapper
218+
async def async_wrapper(*args, **kwargs):
219+
start_time = time.time()
220+
try:
221+
result = await func(*args, **kwargs)
222+
return result
223+
finally:
224+
duration = time.time() - start_time
225+
query_performance_tracker.track_operation(operation_name, duration)
226+
227+
return async_wrapper
228+
else:
229+
# Sync function wrapper
230+
def sync_wrapper(*args, **kwargs):
231+
start_time = time.time()
232+
try:
233+
result = func(*args, **kwargs)
234+
return result
235+
finally:
236+
duration = time.time() - start_time
237+
query_performance_tracker.track_operation(operation_name, duration)
238+
239+
return sync_wrapper
240+
241+
return decorator

backend/services/database_service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from sqlalchemy.orm import sessionmaker
77

88
from models.base import Base
9+
from middleware.monitoring import track_performance
910

1011
logger = logging.getLogger(__name__)
1112

@@ -44,6 +45,7 @@ def reconnect(self):
4445
"""Force a reconnection to the database."""
4546
self.connect()
4647

48+
@track_performance("database_health_check")
4749
def health_check(self) -> Dict[str, Any]:
4850
"""Check database health"""
4951
try:

0 commit comments

Comments
 (0)