Coverage for src / mysingle / core / metrics / middleware.py: 0%
71 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
1"""Enhanced metrics middleware with performance optimizations."""
3import time
4from typing import Any
6from fastapi import Request, Response
7from starlette.middleware.base import BaseHTTPMiddleware
9from ..logging import get_structured_logger
10from .collector import MetricsCollector, MetricsConfig
12logger = get_structured_logger(__name__)
14# Global metrics collector
15_metrics_collector: MetricsCollector | None = None
18def get_metrics_collector() -> MetricsCollector:
19 """Get the global metrics collector."""
20 global _metrics_collector
21 if _metrics_collector is None:
22 raise RuntimeError("Metrics collector not initialized.")
23 return _metrics_collector
26class MetricsMiddleware(BaseHTTPMiddleware):
27 """Enhanced middleware to collect HTTP request metrics with performance optimizations."""
29 def __init__(
30 self,
31 app: Any,
32 collector: MetricsCollector,
33 exclude_paths: set[str] | None = None,
34 include_response_headers: bool = True,
35 track_user_agents: bool = False,
36 ) -> None:
37 super().__init__(app)
38 self.collector = collector
39 # 성능을 위해 메트릭에서 제외할 경로들 (health check 등)
40 self.exclude_paths = exclude_paths or {
41 "/health",
42 "/metrics",
43 "/docs",
44 "/redoc",
45 "/openapi.json",
46 }
47 self.include_response_headers = include_response_headers
48 self.track_user_agents = track_user_agents
50 def _should_track_request(self, request: Request) -> bool:
51 """Determine if request should be tracked."""
52 path = request.url.path
54 # 제외 경로 확인
55 if path in self.exclude_paths:
56 return False
58 # 정적 파일 제외 (성능 최적화)
59 return not path.startswith(("/static/", "/assets/", "/favicon"))
61 def _extract_route_pattern(self, request: Request) -> str:
62 """Extract normalized route pattern from request."""
63 try:
64 # FastAPI route pattern 추출
65 if hasattr(request, "scope") and "route" in request.scope:
66 route = request.scope["route"]
67 if hasattr(route, "path"):
68 return str(route.path)
70 # 경로에서 ID 패턴 정규화 (성능 최적화)
71 path = request.url.path
73 # UUID 패턴 정규화
74 import re
76 uuid_pattern = (
77 r"/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
78 )
79 path = re.sub(uuid_pattern, "/{uuid}", path, flags=re.IGNORECASE)
81 # 숫자 ID 패턴 정규화
82 numeric_pattern = r"/\d+"
83 path = re.sub(numeric_pattern, "/{id}", path)
85 return path
87 except Exception as e:
88 logger.debug(f"Error extracting route pattern: {e}")
89 return request.url.path
91 async def dispatch(self, request: Request, call_next: Any) -> Response:
92 """Process request and collect metrics with enhanced performance."""
93 # 성능 최적화: 추적하지 않을 요청은 빠르게 처리
94 if not self._should_track_request(request):
95 early_response: Response = await call_next(request)
96 return early_response
98 start_time = time.time()
100 # 경로 패턴 추출
101 route_path = self._extract_route_pattern(request)
103 try:
104 # 요청 처리
105 response: Response = await call_next(request)
106 except Exception as e:
107 # 예외 발생 시에도 메트릭 기록
108 duration = time.time() - start_time
109 self.collector.record_request_sync(
110 method=request.method,
111 path=route_path,
112 status_code=500,
113 duration=duration,
114 )
115 logger.error(f"Error processing request {request.method} {route_path}: {e}")
116 raise
118 # 지속 시간 계산
119 duration = time.time() - start_time
121 # 메트릭 기록 (비동기로 처리하여 응답 지연 최소화)
122 try:
123 self.collector.record_request_sync(
124 method=request.method,
125 path=route_path,
126 status_code=response.status_code,
127 duration=duration,
128 )
129 except Exception as e:
130 logger.warning(f"Error recording metrics: {e}")
132 # 응답 헤더 추가 (선택적)
133 if self.include_response_headers:
134 response.headers["X-Response-Time"] = f"{duration:.4f}s"
135 response.headers["X-Service-Name"] = self.collector.service_name
137 return response
140def create_metrics_middleware(
141 service_name: str,
142 config: MetricsConfig | None = None,
143 exclude_paths: set[str] | None = None,
144) -> None:
145 """Create and configure metrics middleware for the given service.
147 Args:
148 service_name: Name of the service
149 config: Metrics configuration
150 exclude_paths: Paths to exclude from metrics collection
152 Returns:
153 None (sets up global collector)
154 """
155 global _metrics_collector
157 try:
158 # 설정 기본값
159 metrics_config = config or MetricsConfig()
161 # 메트릭 컬렉터 초기화
162 _metrics_collector = MetricsCollector(service_name, metrics_config)
164 logger.info(f"✅ Metrics collector initialized for {service_name}")
165 logger.debug(f"Metrics config: {metrics_config}")
167 except Exception as e:
168 logger.error(f"❌ Failed to create metrics middleware for {service_name}: {e}")
169 raise