|
| 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 |
0 commit comments