mcp-code-indexer 4.0.1__py3-none-any.whl → 4.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_code_indexer/__init__.py +7 -5
- mcp_code_indexer/ask_handler.py +2 -2
- mcp_code_indexer/claude_api_handler.py +10 -5
- mcp_code_indexer/cleanup_manager.py +20 -12
- mcp_code_indexer/commands/makelocal.py +85 -63
- mcp_code_indexer/data/stop_words_english.txt +1 -1
- mcp_code_indexer/database/connection_health.py +29 -20
- mcp_code_indexer/database/database.py +44 -31
- mcp_code_indexer/database/database_factory.py +19 -20
- mcp_code_indexer/database/exceptions.py +10 -10
- mcp_code_indexer/database/models.py +126 -1
- mcp_code_indexer/database/path_resolver.py +22 -21
- mcp_code_indexer/database/retry_executor.py +37 -19
- mcp_code_indexer/deepask_handler.py +3 -3
- mcp_code_indexer/error_handler.py +46 -20
- mcp_code_indexer/file_scanner.py +15 -12
- mcp_code_indexer/git_hook_handler.py +71 -76
- mcp_code_indexer/logging_config.py +13 -5
- mcp_code_indexer/main.py +85 -22
- mcp_code_indexer/middleware/__init__.py +1 -1
- mcp_code_indexer/middleware/auth.py +47 -43
- mcp_code_indexer/middleware/error_middleware.py +15 -15
- mcp_code_indexer/middleware/logging.py +44 -42
- mcp_code_indexer/middleware/security.py +84 -76
- mcp_code_indexer/migrations/002_performance_indexes.sql +1 -1
- mcp_code_indexer/migrations/004_remove_branch_dependency.sql +14 -14
- mcp_code_indexer/migrations/006_vector_mode.sql +189 -0
- mcp_code_indexer/query_preprocessor.py +2 -2
- mcp_code_indexer/server/mcp_server.py +158 -94
- mcp_code_indexer/transport/__init__.py +1 -1
- mcp_code_indexer/transport/base.py +19 -17
- mcp_code_indexer/transport/http_transport.py +89 -76
- mcp_code_indexer/transport/stdio_transport.py +12 -8
- mcp_code_indexer/vector_mode/__init__.py +36 -0
- mcp_code_indexer/vector_mode/chunking/__init__.py +19 -0
- mcp_code_indexer/vector_mode/chunking/ast_chunker.py +403 -0
- mcp_code_indexer/vector_mode/chunking/chunk_optimizer.py +500 -0
- mcp_code_indexer/vector_mode/chunking/language_handlers.py +478 -0
- mcp_code_indexer/vector_mode/config.py +155 -0
- mcp_code_indexer/vector_mode/daemon.py +335 -0
- mcp_code_indexer/vector_mode/monitoring/__init__.py +19 -0
- mcp_code_indexer/vector_mode/monitoring/change_detector.py +312 -0
- mcp_code_indexer/vector_mode/monitoring/file_watcher.py +445 -0
- mcp_code_indexer/vector_mode/monitoring/merkle_tree.py +418 -0
- mcp_code_indexer/vector_mode/providers/__init__.py +72 -0
- mcp_code_indexer/vector_mode/providers/base_provider.py +230 -0
- mcp_code_indexer/vector_mode/providers/turbopuffer_client.py +338 -0
- mcp_code_indexer/vector_mode/providers/voyage_client.py +212 -0
- mcp_code_indexer/vector_mode/security/__init__.py +11 -0
- mcp_code_indexer/vector_mode/security/patterns.py +297 -0
- mcp_code_indexer/vector_mode/security/redactor.py +368 -0
- {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/METADATA +82 -24
- mcp_code_indexer-4.1.0.dist-info/RECORD +66 -0
- mcp_code_indexer-4.0.1.dist-info/RECORD +0 -47
- {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/LICENSE +0 -0
- {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/WHEEL +0 -0
- {mcp_code_indexer-4.0.1.dist-info → mcp_code_indexer-4.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -5,12 +5,12 @@ Provides Bearer token authentication for HTTP transport.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import Optional
|
|
8
|
+
from typing import Any, Awaitable, Callable, List, Optional
|
|
9
9
|
|
|
10
10
|
try:
|
|
11
11
|
from fastapi import HTTPException, Request
|
|
12
|
-
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
13
12
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
13
|
+
from starlette.responses import Response
|
|
14
14
|
except ImportError as e:
|
|
15
15
|
raise ImportError(
|
|
16
16
|
"HTTP middleware dependencies not installed. "
|
|
@@ -24,20 +24,20 @@ logger = logging.getLogger(__name__)
|
|
|
24
24
|
class HTTPAuthMiddleware(BaseHTTPMiddleware):
|
|
25
25
|
"""
|
|
26
26
|
HTTP authentication middleware using Bearer tokens.
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
Validates Bearer tokens for protected endpoints while allowing
|
|
29
29
|
public endpoints to pass through.
|
|
30
30
|
"""
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
def __init__(
|
|
33
33
|
self,
|
|
34
|
-
app,
|
|
34
|
+
app: Any,
|
|
35
35
|
auth_token: Optional[str] = None,
|
|
36
|
-
public_paths: Optional[
|
|
37
|
-
):
|
|
36
|
+
public_paths: Optional[List[str]] = None,
|
|
37
|
+
) -> None:
|
|
38
38
|
"""
|
|
39
39
|
Initialize HTTP authentication middleware.
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
Args:
|
|
42
42
|
app: FastAPI application instance
|
|
43
43
|
auth_token: Expected Bearer token for authentication
|
|
@@ -47,36 +47,38 @@ class HTTPAuthMiddleware(BaseHTTPMiddleware):
|
|
|
47
47
|
self.auth_token = auth_token
|
|
48
48
|
self.public_paths = public_paths or ["/health", "/docs", "/openapi.json"]
|
|
49
49
|
self.logger = logger.getChild("http_auth")
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
# Only enable auth if token is provided
|
|
52
52
|
self.auth_enabled = auth_token is not None
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
if self.auth_enabled:
|
|
55
55
|
self.logger.info("HTTP authentication enabled")
|
|
56
56
|
else:
|
|
57
57
|
self.logger.info("HTTP authentication disabled")
|
|
58
|
-
|
|
59
|
-
async def dispatch(
|
|
58
|
+
|
|
59
|
+
async def dispatch(
|
|
60
|
+
self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
|
61
|
+
) -> Response:
|
|
60
62
|
"""
|
|
61
63
|
Process HTTP request and validate authentication.
|
|
62
|
-
|
|
64
|
+
|
|
63
65
|
Args:
|
|
64
66
|
request: FastAPI request object
|
|
65
67
|
call_next: Next middleware in chain
|
|
66
|
-
|
|
68
|
+
|
|
67
69
|
Returns:
|
|
68
70
|
HTTP response
|
|
69
|
-
|
|
71
|
+
|
|
70
72
|
Raises:
|
|
71
73
|
HTTPException: If authentication fails
|
|
72
74
|
"""
|
|
73
75
|
# Skip authentication for public paths
|
|
74
76
|
if not self.auth_enabled or request.url.path in self.public_paths:
|
|
75
77
|
return await call_next(request)
|
|
76
|
-
|
|
78
|
+
|
|
77
79
|
# Extract Authorization header
|
|
78
80
|
auth_header = request.headers.get("authorization")
|
|
79
|
-
|
|
81
|
+
|
|
80
82
|
if not auth_header:
|
|
81
83
|
self.logger.warning(
|
|
82
84
|
f"Missing authorization header for {request.url.path}",
|
|
@@ -93,7 +95,7 @@ class HTTPAuthMiddleware(BaseHTTPMiddleware):
|
|
|
93
95
|
detail="Authorization header required",
|
|
94
96
|
headers={"WWW-Authenticate": "Bearer"},
|
|
95
97
|
)
|
|
96
|
-
|
|
98
|
+
|
|
97
99
|
# Validate Bearer token format
|
|
98
100
|
if not auth_header.startswith("Bearer "):
|
|
99
101
|
self.logger.warning(
|
|
@@ -101,7 +103,9 @@ class HTTPAuthMiddleware(BaseHTTPMiddleware):
|
|
|
101
103
|
extra={
|
|
102
104
|
"structured_data": {
|
|
103
105
|
"path": request.url.path,
|
|
104
|
-
"auth_format": auth_header.split(" ")[0]
|
|
106
|
+
"auth_format": auth_header.split(" ")[0]
|
|
107
|
+
if " " in auth_header
|
|
108
|
+
else auth_header,
|
|
105
109
|
"client_ip": self._get_client_ip(request),
|
|
106
110
|
}
|
|
107
111
|
},
|
|
@@ -111,10 +115,10 @@ class HTTPAuthMiddleware(BaseHTTPMiddleware):
|
|
|
111
115
|
detail="Invalid authorization format. Use: Bearer <token>",
|
|
112
116
|
headers={"WWW-Authenticate": "Bearer"},
|
|
113
117
|
)
|
|
114
|
-
|
|
118
|
+
|
|
115
119
|
# Extract and validate token
|
|
116
120
|
token = auth_header[7:] # Remove "Bearer " prefix
|
|
117
|
-
|
|
121
|
+
|
|
118
122
|
if token != self.auth_token:
|
|
119
123
|
self.logger.warning(
|
|
120
124
|
f"Invalid token for {request.url.path}",
|
|
@@ -131,7 +135,7 @@ class HTTPAuthMiddleware(BaseHTTPMiddleware):
|
|
|
131
135
|
detail="Invalid authentication token",
|
|
132
136
|
headers={"WWW-Authenticate": "Bearer"},
|
|
133
137
|
)
|
|
134
|
-
|
|
138
|
+
|
|
135
139
|
# Token is valid, proceed with request
|
|
136
140
|
self.logger.debug(
|
|
137
141
|
f"Authentication successful for {request.url.path}",
|
|
@@ -142,87 +146,87 @@ class HTTPAuthMiddleware(BaseHTTPMiddleware):
|
|
|
142
146
|
}
|
|
143
147
|
},
|
|
144
148
|
)
|
|
145
|
-
|
|
149
|
+
|
|
146
150
|
return await call_next(request)
|
|
147
|
-
|
|
151
|
+
|
|
148
152
|
def _get_client_ip(self, request: Request) -> str:
|
|
149
153
|
"""Extract client IP address from request."""
|
|
150
154
|
# Check for forwarded headers
|
|
151
155
|
forwarded_for = request.headers.get("x-forwarded-for")
|
|
152
156
|
if forwarded_for:
|
|
153
157
|
return forwarded_for.split(",")[0].strip()
|
|
154
|
-
|
|
158
|
+
|
|
155
159
|
real_ip = request.headers.get("x-real-ip")
|
|
156
160
|
if real_ip:
|
|
157
161
|
return real_ip
|
|
158
|
-
|
|
162
|
+
|
|
159
163
|
# Fall back to direct client IP
|
|
160
|
-
if hasattr(request.client, "host"):
|
|
164
|
+
if request.client and hasattr(request.client, "host"):
|
|
161
165
|
return request.client.host
|
|
162
|
-
|
|
166
|
+
|
|
163
167
|
return "unknown"
|
|
164
168
|
|
|
165
169
|
|
|
166
170
|
class TokenValidator:
|
|
167
171
|
"""
|
|
168
172
|
Utility class for token validation logic.
|
|
169
|
-
|
|
173
|
+
|
|
170
174
|
Provides methods for validating different token formats
|
|
171
175
|
and managing token-based authentication.
|
|
172
176
|
"""
|
|
173
|
-
|
|
177
|
+
|
|
174
178
|
@staticmethod
|
|
175
179
|
def validate_bearer_token(token: str, expected_token: str) -> bool:
|
|
176
180
|
"""
|
|
177
181
|
Validate Bearer token against expected value.
|
|
178
|
-
|
|
182
|
+
|
|
179
183
|
Args:
|
|
180
184
|
token: Token to validate
|
|
181
185
|
expected_token: Expected token value
|
|
182
|
-
|
|
186
|
+
|
|
183
187
|
Returns:
|
|
184
188
|
True if token is valid, False otherwise
|
|
185
189
|
"""
|
|
186
190
|
if not token or not expected_token:
|
|
187
191
|
return False
|
|
188
|
-
|
|
192
|
+
|
|
189
193
|
# Simple string comparison for now
|
|
190
194
|
# In production, consider using constant-time comparison
|
|
191
195
|
return token == expected_token
|
|
192
|
-
|
|
196
|
+
|
|
193
197
|
@staticmethod
|
|
194
198
|
def generate_token(length: int = 32) -> str:
|
|
195
199
|
"""
|
|
196
200
|
Generate a random token for authentication.
|
|
197
|
-
|
|
201
|
+
|
|
198
202
|
Args:
|
|
199
203
|
length: Length of token to generate
|
|
200
|
-
|
|
204
|
+
|
|
201
205
|
Returns:
|
|
202
206
|
Random token string
|
|
203
207
|
"""
|
|
204
208
|
import secrets
|
|
205
209
|
import string
|
|
206
|
-
|
|
210
|
+
|
|
207
211
|
alphabet = string.ascii_letters + string.digits
|
|
208
|
-
return
|
|
209
|
-
|
|
212
|
+
return "".join(secrets.choice(alphabet) for _ in range(length))
|
|
213
|
+
|
|
210
214
|
@staticmethod
|
|
211
215
|
def mask_token(token: str, visible_chars: int = 8) -> str:
|
|
212
216
|
"""
|
|
213
217
|
Mask token for logging purposes.
|
|
214
|
-
|
|
218
|
+
|
|
215
219
|
Args:
|
|
216
220
|
token: Token to mask
|
|
217
221
|
visible_chars: Number of characters to show
|
|
218
|
-
|
|
222
|
+
|
|
219
223
|
Returns:
|
|
220
224
|
Masked token string
|
|
221
225
|
"""
|
|
222
226
|
if not token:
|
|
223
227
|
return ""
|
|
224
|
-
|
|
228
|
+
|
|
225
229
|
if len(token) <= visible_chars:
|
|
226
230
|
return "*" * len(token)
|
|
227
|
-
|
|
231
|
+
|
|
228
232
|
return token[:visible_chars] + "..." + "*" * (len(token) - visible_chars)
|
|
@@ -8,7 +8,7 @@ error handling across all MCP tool implementations.
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import functools
|
|
10
10
|
import time
|
|
11
|
-
from typing import Any, Callable, Dict, List
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
12
12
|
|
|
13
13
|
import aiosqlite
|
|
14
14
|
from mcp import types
|
|
@@ -16,8 +16,8 @@ from mcp import types
|
|
|
16
16
|
from mcp_code_indexer.error_handler import ErrorHandler
|
|
17
17
|
from mcp_code_indexer.logging_config import (
|
|
18
18
|
get_logger,
|
|
19
|
-
log_tool_usage,
|
|
20
19
|
log_performance_metrics,
|
|
20
|
+
log_tool_usage,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
logger = get_logger(__name__)
|
|
@@ -30,7 +30,7 @@ class ToolMiddleware:
|
|
|
30
30
|
"""Initialize middleware with error handler."""
|
|
31
31
|
self.error_handler = error_handler
|
|
32
32
|
|
|
33
|
-
def wrap_tool_handler(self, tool_name: str):
|
|
33
|
+
def wrap_tool_handler(self, tool_name: str) -> Callable[[Callable], Callable]:
|
|
34
34
|
"""
|
|
35
35
|
Decorator to wrap tool handlers with error handling and logging.
|
|
36
36
|
|
|
@@ -84,7 +84,7 @@ class ToolMiddleware:
|
|
|
84
84
|
arguments_count=len(arguments),
|
|
85
85
|
)
|
|
86
86
|
|
|
87
|
-
return result
|
|
87
|
+
return result # type: ignore
|
|
88
88
|
|
|
89
89
|
except Exception as e:
|
|
90
90
|
duration = time.time() - start_time
|
|
@@ -135,8 +135,8 @@ class ToolMiddleware:
|
|
|
135
135
|
return decorator
|
|
136
136
|
|
|
137
137
|
def validate_tool_arguments(
|
|
138
|
-
self, required_fields: List[str], optional_fields: List[str] = None
|
|
139
|
-
):
|
|
138
|
+
self, required_fields: List[str], optional_fields: Optional[List[str]] = None
|
|
139
|
+
) -> Callable[[Callable], Callable]:
|
|
140
140
|
"""
|
|
141
141
|
Decorator to validate tool arguments.
|
|
142
142
|
|
|
@@ -220,7 +220,7 @@ class AsyncTaskManager:
|
|
|
220
220
|
self.error_handler = error_handler
|
|
221
221
|
self._tasks: List[asyncio.Task] = []
|
|
222
222
|
|
|
223
|
-
def create_task(self, coro, name: str = None) -> asyncio.Task:
|
|
223
|
+
def create_task(self, coro: Any, name: Optional[str] = None) -> asyncio.Task:
|
|
224
224
|
"""
|
|
225
225
|
Create a managed async task.
|
|
226
226
|
|
|
@@ -257,7 +257,7 @@ class AsyncTaskManager:
|
|
|
257
257
|
if task in self._tasks:
|
|
258
258
|
self._tasks.remove(task)
|
|
259
259
|
|
|
260
|
-
async def wait_for_all(self, timeout: float = None) -> None:
|
|
260
|
+
async def wait_for_all(self, timeout: Optional[float] = None) -> None:
|
|
261
261
|
"""
|
|
262
262
|
Wait for all managed tasks to complete.
|
|
263
263
|
|
|
@@ -309,12 +309,12 @@ def create_tool_middleware(error_handler: ErrorHandler) -> ToolMiddleware:
|
|
|
309
309
|
# Convenience decorators for common patterns
|
|
310
310
|
|
|
311
311
|
|
|
312
|
-
def require_fields(*required_fields):
|
|
312
|
+
def require_fields(*required_fields: str) -> Callable[[Callable], Callable]:
|
|
313
313
|
"""Decorator that requires specific fields in arguments."""
|
|
314
314
|
|
|
315
|
-
def decorator(func):
|
|
315
|
+
def decorator(func: Callable) -> Callable:
|
|
316
316
|
@functools.wraps(func)
|
|
317
|
-
async def wrapper(self, arguments: Dict[str, Any]):
|
|
317
|
+
async def wrapper(self: Any, arguments: Dict[str, Any]) -> Any:
|
|
318
318
|
from ..error_handler import ValidationError
|
|
319
319
|
|
|
320
320
|
missing = [field for field in required_fields if field not in arguments]
|
|
@@ -328,11 +328,11 @@ def require_fields(*required_fields):
|
|
|
328
328
|
return decorator
|
|
329
329
|
|
|
330
330
|
|
|
331
|
-
def handle_file_operations(func):
|
|
331
|
+
def handle_file_operations(func: Callable) -> Callable:
|
|
332
332
|
"""Decorator for file operation error handling."""
|
|
333
333
|
|
|
334
334
|
@functools.wraps(func)
|
|
335
|
-
async def wrapper(*args, **kwargs):
|
|
335
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
336
336
|
try:
|
|
337
337
|
return await func(*args, **kwargs)
|
|
338
338
|
except (FileNotFoundError, PermissionError, OSError) as e:
|
|
@@ -343,11 +343,11 @@ def handle_file_operations(func):
|
|
|
343
343
|
return wrapper
|
|
344
344
|
|
|
345
345
|
|
|
346
|
-
def handle_database_operations(func):
|
|
346
|
+
def handle_database_operations(func: Callable) -> Callable:
|
|
347
347
|
"""Decorator for database operation error handling."""
|
|
348
348
|
|
|
349
349
|
@functools.wraps(func)
|
|
350
|
-
async def wrapper(*args, **kwargs):
|
|
350
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
351
351
|
try:
|
|
352
352
|
return await func(*args, **kwargs)
|
|
353
353
|
except Exception as e:
|
|
@@ -4,10 +4,9 @@ HTTP logging middleware for MCP Code Indexer.
|
|
|
4
4
|
Provides request/response logging and monitoring for HTTP transport.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import json
|
|
8
7
|
import logging
|
|
9
8
|
import time
|
|
10
|
-
from typing import Any, Dict,
|
|
9
|
+
from typing import Any, Awaitable, Callable, Dict, List
|
|
11
10
|
|
|
12
11
|
try:
|
|
13
12
|
from fastapi import Request, Response
|
|
@@ -26,15 +25,15 @@ logger = logging.getLogger(__name__)
|
|
|
26
25
|
class HTTPLoggingMiddleware(BaseHTTPMiddleware):
|
|
27
26
|
"""
|
|
28
27
|
HTTP request/response logging middleware.
|
|
29
|
-
|
|
28
|
+
|
|
30
29
|
Logs HTTP requests and responses with performance metrics
|
|
31
30
|
and structured data for monitoring.
|
|
32
31
|
"""
|
|
33
|
-
|
|
32
|
+
|
|
34
33
|
def __init__(self, app: Any, log_level: str = "INFO"):
|
|
35
34
|
"""
|
|
36
35
|
Initialize HTTP logging middleware.
|
|
37
|
-
|
|
36
|
+
|
|
38
37
|
Args:
|
|
39
38
|
app: FastAPI application instance
|
|
40
39
|
log_level: Logging level for HTTP requests
|
|
@@ -43,25 +42,27 @@ class HTTPLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
43
42
|
self.log_level = getattr(logging, log_level.upper())
|
|
44
43
|
self.logger = logger.getChild("http_access")
|
|
45
44
|
self.logger.setLevel(self.log_level)
|
|
46
|
-
|
|
47
|
-
async def dispatch(
|
|
45
|
+
|
|
46
|
+
async def dispatch(
|
|
47
|
+
self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
|
48
|
+
) -> Response:
|
|
48
49
|
"""
|
|
49
50
|
Process HTTP request and log access information.
|
|
50
|
-
|
|
51
|
+
|
|
51
52
|
Args:
|
|
52
53
|
request: FastAPI request object
|
|
53
54
|
call_next: Next middleware in chain
|
|
54
|
-
|
|
55
|
+
|
|
55
56
|
Returns:
|
|
56
57
|
HTTP response
|
|
57
58
|
"""
|
|
58
59
|
start_time = time.time()
|
|
59
60
|
request_id = self._generate_request_id(request)
|
|
60
|
-
|
|
61
|
+
|
|
61
62
|
# Extract client information
|
|
62
63
|
client_ip = self._get_client_ip(request)
|
|
63
64
|
user_agent = request.headers.get("user-agent", "")
|
|
64
|
-
|
|
65
|
+
|
|
65
66
|
# Log incoming request
|
|
66
67
|
self.logger.info(
|
|
67
68
|
f"HTTP {request.method} {request.url.path}",
|
|
@@ -79,17 +80,17 @@ class HTTPLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
79
80
|
}
|
|
80
81
|
},
|
|
81
82
|
)
|
|
82
|
-
|
|
83
|
+
|
|
83
84
|
# Add request ID to request state for use in handlers
|
|
84
85
|
request.state.request_id = request_id
|
|
85
|
-
|
|
86
|
+
|
|
86
87
|
try:
|
|
87
88
|
# Process request
|
|
88
89
|
response = await call_next(request)
|
|
89
|
-
|
|
90
|
+
|
|
90
91
|
# Calculate processing time
|
|
91
92
|
process_time = time.time() - start_time
|
|
92
|
-
|
|
93
|
+
|
|
93
94
|
# Log response
|
|
94
95
|
self._log_response(
|
|
95
96
|
request_id=request_id,
|
|
@@ -99,17 +100,17 @@ class HTTPLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
99
100
|
process_time=process_time,
|
|
100
101
|
response=response,
|
|
101
102
|
)
|
|
102
|
-
|
|
103
|
+
|
|
103
104
|
# Add performance headers
|
|
104
105
|
response.headers["X-Process-Time"] = str(process_time)
|
|
105
106
|
response.headers["X-Request-ID"] = request_id
|
|
106
|
-
|
|
107
|
+
|
|
107
108
|
return response
|
|
108
|
-
|
|
109
|
+
|
|
109
110
|
except Exception as e:
|
|
110
111
|
# Log error
|
|
111
112
|
process_time = time.time() - start_time
|
|
112
|
-
|
|
113
|
+
|
|
113
114
|
self.logger.error(
|
|
114
115
|
f"HTTP {request.method} {request.url.path} - ERROR",
|
|
115
116
|
extra={
|
|
@@ -124,31 +125,32 @@ class HTTPLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
124
125
|
}
|
|
125
126
|
},
|
|
126
127
|
)
|
|
127
|
-
|
|
128
|
+
|
|
128
129
|
raise
|
|
129
|
-
|
|
130
|
+
|
|
130
131
|
def _generate_request_id(self, request: Request) -> str:
|
|
131
132
|
"""Generate unique request ID."""
|
|
132
133
|
import uuid
|
|
134
|
+
|
|
133
135
|
return str(uuid.uuid4())[:8]
|
|
134
|
-
|
|
136
|
+
|
|
135
137
|
def _get_client_ip(self, request: Request) -> str:
|
|
136
138
|
"""Extract client IP address from request."""
|
|
137
139
|
# Check for forwarded headers
|
|
138
140
|
forwarded_for = request.headers.get("x-forwarded-for")
|
|
139
141
|
if forwarded_for:
|
|
140
142
|
return forwarded_for.split(",")[0].strip()
|
|
141
|
-
|
|
143
|
+
|
|
142
144
|
real_ip = request.headers.get("x-real-ip")
|
|
143
145
|
if real_ip:
|
|
144
146
|
return real_ip
|
|
145
|
-
|
|
147
|
+
|
|
146
148
|
# Fall back to direct client IP
|
|
147
|
-
if hasattr(request.client, "host"):
|
|
149
|
+
if request.client and hasattr(request.client, "host"):
|
|
148
150
|
return request.client.host
|
|
149
|
-
|
|
151
|
+
|
|
150
152
|
return "unknown"
|
|
151
|
-
|
|
153
|
+
|
|
152
154
|
def _log_response(
|
|
153
155
|
self,
|
|
154
156
|
request_id: str,
|
|
@@ -166,14 +168,14 @@ class HTTPLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
166
168
|
log_level = logging.WARNING
|
|
167
169
|
else:
|
|
168
170
|
log_level = self.log_level
|
|
169
|
-
|
|
171
|
+
|
|
170
172
|
# Extract response information
|
|
171
173
|
content_length = response.headers.get("content-length")
|
|
172
174
|
content_type = response.headers.get("content-type")
|
|
173
|
-
|
|
175
|
+
|
|
174
176
|
# Check if this is a streaming response
|
|
175
177
|
is_streaming = isinstance(response, StreamingResponse)
|
|
176
|
-
|
|
178
|
+
|
|
177
179
|
self.logger.log(
|
|
178
180
|
log_level,
|
|
179
181
|
f"HTTP {method} {path} - {status_code}",
|
|
@@ -192,7 +194,7 @@ class HTTPLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
192
194
|
}
|
|
193
195
|
},
|
|
194
196
|
)
|
|
195
|
-
|
|
197
|
+
|
|
196
198
|
def _categorize_performance(self, process_time: float) -> str:
|
|
197
199
|
"""Categorize request performance."""
|
|
198
200
|
if process_time < 0.1:
|
|
@@ -208,18 +210,18 @@ class HTTPLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
208
210
|
class HTTPMetricsCollector:
|
|
209
211
|
"""
|
|
210
212
|
Collect HTTP metrics for monitoring.
|
|
211
|
-
|
|
213
|
+
|
|
212
214
|
Tracks request counts, response times, and error rates.
|
|
213
215
|
"""
|
|
214
|
-
|
|
215
|
-
def __init__(self):
|
|
216
|
+
|
|
217
|
+
def __init__(self) -> None:
|
|
216
218
|
"""Initialize metrics collector."""
|
|
217
219
|
self.request_count = 0
|
|
218
220
|
self.error_count = 0
|
|
219
221
|
self.total_response_time = 0.0
|
|
220
|
-
self.response_times = []
|
|
222
|
+
self.response_times: List[float] = []
|
|
221
223
|
self.max_response_times = 1000 # Keep last 1000 response times
|
|
222
|
-
|
|
224
|
+
|
|
223
225
|
def record_request(
|
|
224
226
|
self,
|
|
225
227
|
method: str,
|
|
@@ -230,16 +232,16 @@ class HTTPMetricsCollector:
|
|
|
230
232
|
"""Record HTTP request metrics."""
|
|
231
233
|
self.request_count += 1
|
|
232
234
|
self.total_response_time += response_time
|
|
233
|
-
|
|
235
|
+
|
|
234
236
|
# Track error rates
|
|
235
237
|
if status_code >= 400:
|
|
236
238
|
self.error_count += 1
|
|
237
|
-
|
|
239
|
+
|
|
238
240
|
# Track response times
|
|
239
241
|
self.response_times.append(response_time)
|
|
240
242
|
if len(self.response_times) > self.max_response_times:
|
|
241
243
|
self.response_times.pop(0)
|
|
242
|
-
|
|
244
|
+
|
|
243
245
|
def get_metrics(self) -> Dict[str, Any]:
|
|
244
246
|
"""Get current metrics summary."""
|
|
245
247
|
if not self.request_count:
|
|
@@ -251,12 +253,12 @@ class HTTPMetricsCollector:
|
|
|
251
253
|
"p95_response_time": 0.0,
|
|
252
254
|
"p99_response_time": 0.0,
|
|
253
255
|
}
|
|
254
|
-
|
|
256
|
+
|
|
255
257
|
# Calculate percentiles
|
|
256
258
|
sorted_times = sorted(self.response_times)
|
|
257
259
|
p95_index = int(0.95 * len(sorted_times))
|
|
258
260
|
p99_index = int(0.99 * len(sorted_times))
|
|
259
|
-
|
|
261
|
+
|
|
260
262
|
return {
|
|
261
263
|
"request_count": self.request_count,
|
|
262
264
|
"error_count": self.error_count,
|
|
@@ -265,7 +267,7 @@ class HTTPMetricsCollector:
|
|
|
265
267
|
"p95_response_time": sorted_times[p95_index] if sorted_times else 0.0,
|
|
266
268
|
"p99_response_time": sorted_times[p99_index] if sorted_times else 0.0,
|
|
267
269
|
}
|
|
268
|
-
|
|
270
|
+
|
|
269
271
|
def reset_metrics(self) -> None:
|
|
270
272
|
"""Reset all metrics."""
|
|
271
273
|
self.request_count = 0
|