mcp-code-indexer 3.5.6__py3-none-any.whl → 4.0.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/main.py +66 -1
- mcp_code_indexer/middleware/__init__.py +8 -3
- mcp_code_indexer/middleware/auth.py +228 -0
- mcp_code_indexer/middleware/logging.py +274 -0
- mcp_code_indexer/middleware/security.py +368 -0
- mcp_code_indexer/server/mcp_server.py +33 -82
- mcp_code_indexer/transport/__init__.py +12 -0
- mcp_code_indexer/transport/base.py +108 -0
- mcp_code_indexer/transport/http_transport.py +365 -0
- mcp_code_indexer/transport/stdio_transport.py +129 -0
- {mcp_code_indexer-3.5.6.dist-info → mcp_code_indexer-4.0.0.dist-info}/METADATA +102 -49
- {mcp_code_indexer-3.5.6.dist-info → mcp_code_indexer-4.0.0.dist-info}/RECORD +15 -8
- {mcp_code_indexer-3.5.6.dist-info → mcp_code_indexer-4.0.0.dist-info}/LICENSE +0 -0
- {mcp_code_indexer-3.5.6.dist-info → mcp_code_indexer-4.0.0.dist-info}/WHEEL +0 -0
- {mcp_code_indexer-3.5.6.dist-info → mcp_code_indexer-4.0.0.dist-info}/entry_points.txt +0 -0
mcp_code_indexer/main.py
CHANGED
|
@@ -116,6 +116,41 @@ def parse_arguments() -> argparse.Namespace:
|
|
|
116
116
|
help="Create local database in specified folder and migrate project data from global DB",
|
|
117
117
|
)
|
|
118
118
|
|
|
119
|
+
# HTTP transport options
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"--http",
|
|
122
|
+
action="store_true",
|
|
123
|
+
help="Enable HTTP transport instead of stdio (requires 'http' extras)",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--host",
|
|
128
|
+
type=str,
|
|
129
|
+
default="127.0.0.1",
|
|
130
|
+
help="Host to bind HTTP server to (default: 127.0.0.1)",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--port",
|
|
135
|
+
type=int,
|
|
136
|
+
default=7557,
|
|
137
|
+
help="Port to bind HTTP server to (default: 7557)",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
parser.add_argument(
|
|
141
|
+
"--auth-token",
|
|
142
|
+
type=str,
|
|
143
|
+
help="Bearer token for HTTP authentication (optional)",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--cors-origins",
|
|
148
|
+
type=str,
|
|
149
|
+
nargs="*",
|
|
150
|
+
default=["*"],
|
|
151
|
+
help="Allowed CORS origins for HTTP transport (default: allow all)",
|
|
152
|
+
)
|
|
153
|
+
|
|
119
154
|
return parser.parse_args()
|
|
120
155
|
|
|
121
156
|
|
|
@@ -961,10 +996,40 @@ async def main() -> None:
|
|
|
961
996
|
try:
|
|
962
997
|
# Import and run the MCP server
|
|
963
998
|
from .server.mcp_server import MCPCodeIndexServer
|
|
999
|
+
|
|
1000
|
+
# Create transport based on arguments
|
|
1001
|
+
transport = None
|
|
1002
|
+
if args.http:
|
|
1003
|
+
from .transport.http_transport import HTTPTransport
|
|
1004
|
+
transport = HTTPTransport(
|
|
1005
|
+
server_instance=None, # Will be set after server creation
|
|
1006
|
+
host=args.host,
|
|
1007
|
+
port=args.port,
|
|
1008
|
+
auth_token=args.auth_token,
|
|
1009
|
+
cors_origins=args.cors_origins,
|
|
1010
|
+
)
|
|
1011
|
+
logger.info(
|
|
1012
|
+
"HTTP transport configured",
|
|
1013
|
+
extra={
|
|
1014
|
+
"structured_data": {
|
|
1015
|
+
"host": args.host,
|
|
1016
|
+
"port": args.port,
|
|
1017
|
+
"auth_enabled": transport.auth_token is not None,
|
|
1018
|
+
"cors_origins": args.cors_origins,
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
)
|
|
964
1022
|
|
|
965
1023
|
server = MCPCodeIndexServer(
|
|
966
|
-
token_limit=args.token_limit,
|
|
1024
|
+
token_limit=args.token_limit,
|
|
1025
|
+
db_path=db_path,
|
|
1026
|
+
cache_dir=cache_dir,
|
|
1027
|
+
transport=transport,
|
|
967
1028
|
)
|
|
1029
|
+
|
|
1030
|
+
# Set server instance in transport after server creation
|
|
1031
|
+
if transport:
|
|
1032
|
+
transport.server = server
|
|
968
1033
|
|
|
969
1034
|
await server.run()
|
|
970
1035
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Middleware components for
|
|
2
|
+
Middleware components for MCP Code Indexer.
|
|
3
|
+
|
|
4
|
+
This module provides middleware for HTTP transport features like
|
|
5
|
+
logging, authentication, and security.
|
|
3
6
|
"""
|
|
4
7
|
|
|
5
|
-
from .
|
|
8
|
+
from .logging import HTTPLoggingMiddleware
|
|
9
|
+
from .auth import HTTPAuthMiddleware
|
|
10
|
+
from .security import HTTPSecurityMiddleware
|
|
6
11
|
|
|
7
|
-
__all__ = ["
|
|
12
|
+
__all__ = ["HTTPLoggingMiddleware", "HTTPAuthMiddleware", "HTTPSecurityMiddleware"]
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP authentication middleware for MCP Code Indexer.
|
|
3
|
+
|
|
4
|
+
Provides Bearer token authentication for HTTP transport.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from fastapi import HTTPException, Request
|
|
12
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
13
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
14
|
+
except ImportError as e:
|
|
15
|
+
raise ImportError(
|
|
16
|
+
"HTTP middleware dependencies not installed. "
|
|
17
|
+
"This should not happen as they are now required dependencies. "
|
|
18
|
+
"Please reinstall mcp-code-indexer."
|
|
19
|
+
) from e
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HTTPAuthMiddleware(BaseHTTPMiddleware):
|
|
25
|
+
"""
|
|
26
|
+
HTTP authentication middleware using Bearer tokens.
|
|
27
|
+
|
|
28
|
+
Validates Bearer tokens for protected endpoints while allowing
|
|
29
|
+
public endpoints to pass through.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
app,
|
|
35
|
+
auth_token: Optional[str] = None,
|
|
36
|
+
public_paths: Optional[list] = None,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Initialize HTTP authentication middleware.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
app: FastAPI application instance
|
|
43
|
+
auth_token: Expected Bearer token for authentication
|
|
44
|
+
public_paths: List of paths that don't require authentication
|
|
45
|
+
"""
|
|
46
|
+
super().__init__(app)
|
|
47
|
+
self.auth_token = auth_token
|
|
48
|
+
self.public_paths = public_paths or ["/health", "/docs", "/openapi.json"]
|
|
49
|
+
self.logger = logger.getChild("http_auth")
|
|
50
|
+
|
|
51
|
+
# Only enable auth if token is provided
|
|
52
|
+
self.auth_enabled = auth_token is not None
|
|
53
|
+
|
|
54
|
+
if self.auth_enabled:
|
|
55
|
+
self.logger.info("HTTP authentication enabled")
|
|
56
|
+
else:
|
|
57
|
+
self.logger.info("HTTP authentication disabled")
|
|
58
|
+
|
|
59
|
+
async def dispatch(self, request: Request, call_next):
|
|
60
|
+
"""
|
|
61
|
+
Process HTTP request and validate authentication.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
request: FastAPI request object
|
|
65
|
+
call_next: Next middleware in chain
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
HTTP response
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
HTTPException: If authentication fails
|
|
72
|
+
"""
|
|
73
|
+
# Skip authentication for public paths
|
|
74
|
+
if not self.auth_enabled or request.url.path in self.public_paths:
|
|
75
|
+
return await call_next(request)
|
|
76
|
+
|
|
77
|
+
# Extract Authorization header
|
|
78
|
+
auth_header = request.headers.get("authorization")
|
|
79
|
+
|
|
80
|
+
if not auth_header:
|
|
81
|
+
self.logger.warning(
|
|
82
|
+
f"Missing authorization header for {request.url.path}",
|
|
83
|
+
extra={
|
|
84
|
+
"structured_data": {
|
|
85
|
+
"path": request.url.path,
|
|
86
|
+
"client_ip": self._get_client_ip(request),
|
|
87
|
+
"user_agent": request.headers.get("user-agent", ""),
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
raise HTTPException(
|
|
92
|
+
status_code=401,
|
|
93
|
+
detail="Authorization header required",
|
|
94
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Validate Bearer token format
|
|
98
|
+
if not auth_header.startswith("Bearer "):
|
|
99
|
+
self.logger.warning(
|
|
100
|
+
f"Invalid authorization format for {request.url.path}",
|
|
101
|
+
extra={
|
|
102
|
+
"structured_data": {
|
|
103
|
+
"path": request.url.path,
|
|
104
|
+
"auth_format": auth_header.split(" ")[0] if " " in auth_header else auth_header,
|
|
105
|
+
"client_ip": self._get_client_ip(request),
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
raise HTTPException(
|
|
110
|
+
status_code=401,
|
|
111
|
+
detail="Invalid authorization format. Use: Bearer <token>",
|
|
112
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Extract and validate token
|
|
116
|
+
token = auth_header[7:] # Remove "Bearer " prefix
|
|
117
|
+
|
|
118
|
+
if token != self.auth_token:
|
|
119
|
+
self.logger.warning(
|
|
120
|
+
f"Invalid token for {request.url.path}",
|
|
121
|
+
extra={
|
|
122
|
+
"structured_data": {
|
|
123
|
+
"path": request.url.path,
|
|
124
|
+
"token_prefix": token[:8] + "..." if len(token) > 8 else token,
|
|
125
|
+
"client_ip": self._get_client_ip(request),
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
raise HTTPException(
|
|
130
|
+
status_code=401,
|
|
131
|
+
detail="Invalid authentication token",
|
|
132
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Token is valid, proceed with request
|
|
136
|
+
self.logger.debug(
|
|
137
|
+
f"Authentication successful for {request.url.path}",
|
|
138
|
+
extra={
|
|
139
|
+
"structured_data": {
|
|
140
|
+
"path": request.url.path,
|
|
141
|
+
"client_ip": self._get_client_ip(request),
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return await call_next(request)
|
|
147
|
+
|
|
148
|
+
def _get_client_ip(self, request: Request) -> str:
|
|
149
|
+
"""Extract client IP address from request."""
|
|
150
|
+
# Check for forwarded headers
|
|
151
|
+
forwarded_for = request.headers.get("x-forwarded-for")
|
|
152
|
+
if forwarded_for:
|
|
153
|
+
return forwarded_for.split(",")[0].strip()
|
|
154
|
+
|
|
155
|
+
real_ip = request.headers.get("x-real-ip")
|
|
156
|
+
if real_ip:
|
|
157
|
+
return real_ip
|
|
158
|
+
|
|
159
|
+
# Fall back to direct client IP
|
|
160
|
+
if hasattr(request.client, "host"):
|
|
161
|
+
return request.client.host
|
|
162
|
+
|
|
163
|
+
return "unknown"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TokenValidator:
|
|
167
|
+
"""
|
|
168
|
+
Utility class for token validation logic.
|
|
169
|
+
|
|
170
|
+
Provides methods for validating different token formats
|
|
171
|
+
and managing token-based authentication.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def validate_bearer_token(token: str, expected_token: str) -> bool:
|
|
176
|
+
"""
|
|
177
|
+
Validate Bearer token against expected value.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
token: Token to validate
|
|
181
|
+
expected_token: Expected token value
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if token is valid, False otherwise
|
|
185
|
+
"""
|
|
186
|
+
if not token or not expected_token:
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
# Simple string comparison for now
|
|
190
|
+
# In production, consider using constant-time comparison
|
|
191
|
+
return token == expected_token
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def generate_token(length: int = 32) -> str:
|
|
195
|
+
"""
|
|
196
|
+
Generate a random token for authentication.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
length: Length of token to generate
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Random token string
|
|
203
|
+
"""
|
|
204
|
+
import secrets
|
|
205
|
+
import string
|
|
206
|
+
|
|
207
|
+
alphabet = string.ascii_letters + string.digits
|
|
208
|
+
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def mask_token(token: str, visible_chars: int = 8) -> str:
|
|
212
|
+
"""
|
|
213
|
+
Mask token for logging purposes.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
token: Token to mask
|
|
217
|
+
visible_chars: Number of characters to show
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Masked token string
|
|
221
|
+
"""
|
|
222
|
+
if not token:
|
|
223
|
+
return ""
|
|
224
|
+
|
|
225
|
+
if len(token) <= visible_chars:
|
|
226
|
+
return "*" * len(token)
|
|
227
|
+
|
|
228
|
+
return token[:visible_chars] + "..." + "*" * (len(token) - visible_chars)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP logging middleware for MCP Code Indexer.
|
|
3
|
+
|
|
4
|
+
Provides request/response logging and monitoring for HTTP transport.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from fastapi import Request, Response
|
|
14
|
+
from fastapi.responses import StreamingResponse
|
|
15
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
16
|
+
except ImportError as e:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"HTTP middleware dependencies not installed. "
|
|
19
|
+
"This should not happen as they are now required dependencies. "
|
|
20
|
+
"Please reinstall mcp-code-indexer."
|
|
21
|
+
) from e
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HTTPLoggingMiddleware(BaseHTTPMiddleware):
|
|
27
|
+
"""
|
|
28
|
+
HTTP request/response logging middleware.
|
|
29
|
+
|
|
30
|
+
Logs HTTP requests and responses with performance metrics
|
|
31
|
+
and structured data for monitoring.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, app: Any, log_level: str = "INFO"):
|
|
35
|
+
"""
|
|
36
|
+
Initialize HTTP logging middleware.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
app: FastAPI application instance
|
|
40
|
+
log_level: Logging level for HTTP requests
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(app)
|
|
43
|
+
self.log_level = getattr(logging, log_level.upper())
|
|
44
|
+
self.logger = logger.getChild("http_access")
|
|
45
|
+
self.logger.setLevel(self.log_level)
|
|
46
|
+
|
|
47
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
48
|
+
"""
|
|
49
|
+
Process HTTP request and log access information.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
request: FastAPI request object
|
|
53
|
+
call_next: Next middleware in chain
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
HTTP response
|
|
57
|
+
"""
|
|
58
|
+
start_time = time.time()
|
|
59
|
+
request_id = self._generate_request_id(request)
|
|
60
|
+
|
|
61
|
+
# Extract client information
|
|
62
|
+
client_ip = self._get_client_ip(request)
|
|
63
|
+
user_agent = request.headers.get("user-agent", "")
|
|
64
|
+
|
|
65
|
+
# Log incoming request
|
|
66
|
+
self.logger.info(
|
|
67
|
+
f"HTTP {request.method} {request.url.path}",
|
|
68
|
+
extra={
|
|
69
|
+
"structured_data": {
|
|
70
|
+
"event_type": "http_request",
|
|
71
|
+
"request_id": request_id,
|
|
72
|
+
"method": request.method,
|
|
73
|
+
"path": request.url.path,
|
|
74
|
+
"query_params": dict(request.query_params),
|
|
75
|
+
"client_ip": client_ip,
|
|
76
|
+
"user_agent": user_agent,
|
|
77
|
+
"content_length": request.headers.get("content-length"),
|
|
78
|
+
"content_type": request.headers.get("content-type"),
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Add request ID to request state for use in handlers
|
|
84
|
+
request.state.request_id = request_id
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Process request
|
|
88
|
+
response = await call_next(request)
|
|
89
|
+
|
|
90
|
+
# Calculate processing time
|
|
91
|
+
process_time = time.time() - start_time
|
|
92
|
+
|
|
93
|
+
# Log response
|
|
94
|
+
self._log_response(
|
|
95
|
+
request_id=request_id,
|
|
96
|
+
method=request.method,
|
|
97
|
+
path=request.url.path,
|
|
98
|
+
status_code=response.status_code,
|
|
99
|
+
process_time=process_time,
|
|
100
|
+
response=response,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Add performance headers
|
|
104
|
+
response.headers["X-Process-Time"] = str(process_time)
|
|
105
|
+
response.headers["X-Request-ID"] = request_id
|
|
106
|
+
|
|
107
|
+
return response
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
# Log error
|
|
111
|
+
process_time = time.time() - start_time
|
|
112
|
+
|
|
113
|
+
self.logger.error(
|
|
114
|
+
f"HTTP {request.method} {request.url.path} - ERROR",
|
|
115
|
+
extra={
|
|
116
|
+
"structured_data": {
|
|
117
|
+
"event_type": "http_error",
|
|
118
|
+
"request_id": request_id,
|
|
119
|
+
"method": request.method,
|
|
120
|
+
"path": request.url.path,
|
|
121
|
+
"error_type": type(e).__name__,
|
|
122
|
+
"error_message": str(e),
|
|
123
|
+
"process_time": process_time,
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
def _generate_request_id(self, request: Request) -> str:
|
|
131
|
+
"""Generate unique request ID."""
|
|
132
|
+
import uuid
|
|
133
|
+
return str(uuid.uuid4())[:8]
|
|
134
|
+
|
|
135
|
+
def _get_client_ip(self, request: Request) -> str:
|
|
136
|
+
"""Extract client IP address from request."""
|
|
137
|
+
# Check for forwarded headers
|
|
138
|
+
forwarded_for = request.headers.get("x-forwarded-for")
|
|
139
|
+
if forwarded_for:
|
|
140
|
+
return forwarded_for.split(",")[0].strip()
|
|
141
|
+
|
|
142
|
+
real_ip = request.headers.get("x-real-ip")
|
|
143
|
+
if real_ip:
|
|
144
|
+
return real_ip
|
|
145
|
+
|
|
146
|
+
# Fall back to direct client IP
|
|
147
|
+
if hasattr(request.client, "host"):
|
|
148
|
+
return request.client.host
|
|
149
|
+
|
|
150
|
+
return "unknown"
|
|
151
|
+
|
|
152
|
+
def _log_response(
|
|
153
|
+
self,
|
|
154
|
+
request_id: str,
|
|
155
|
+
method: str,
|
|
156
|
+
path: str,
|
|
157
|
+
status_code: int,
|
|
158
|
+
process_time: float,
|
|
159
|
+
response: Response,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Log HTTP response information."""
|
|
162
|
+
# Determine log level based on status code
|
|
163
|
+
if status_code >= 500:
|
|
164
|
+
log_level = logging.ERROR
|
|
165
|
+
elif status_code >= 400:
|
|
166
|
+
log_level = logging.WARNING
|
|
167
|
+
else:
|
|
168
|
+
log_level = self.log_level
|
|
169
|
+
|
|
170
|
+
# Extract response information
|
|
171
|
+
content_length = response.headers.get("content-length")
|
|
172
|
+
content_type = response.headers.get("content-type")
|
|
173
|
+
|
|
174
|
+
# Check if this is a streaming response
|
|
175
|
+
is_streaming = isinstance(response, StreamingResponse)
|
|
176
|
+
|
|
177
|
+
self.logger.log(
|
|
178
|
+
log_level,
|
|
179
|
+
f"HTTP {method} {path} - {status_code}",
|
|
180
|
+
extra={
|
|
181
|
+
"structured_data": {
|
|
182
|
+
"event_type": "http_response",
|
|
183
|
+
"request_id": request_id,
|
|
184
|
+
"method": method,
|
|
185
|
+
"path": path,
|
|
186
|
+
"status_code": status_code,
|
|
187
|
+
"process_time": process_time,
|
|
188
|
+
"content_length": content_length,
|
|
189
|
+
"content_type": content_type,
|
|
190
|
+
"is_streaming": is_streaming,
|
|
191
|
+
"performance_category": self._categorize_performance(process_time),
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _categorize_performance(self, process_time: float) -> str:
|
|
197
|
+
"""Categorize request performance."""
|
|
198
|
+
if process_time < 0.1:
|
|
199
|
+
return "fast"
|
|
200
|
+
elif process_time < 1.0:
|
|
201
|
+
return "normal"
|
|
202
|
+
elif process_time < 5.0:
|
|
203
|
+
return "slow"
|
|
204
|
+
else:
|
|
205
|
+
return "very_slow"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class HTTPMetricsCollector:
|
|
209
|
+
"""
|
|
210
|
+
Collect HTTP metrics for monitoring.
|
|
211
|
+
|
|
212
|
+
Tracks request counts, response times, and error rates.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def __init__(self):
|
|
216
|
+
"""Initialize metrics collector."""
|
|
217
|
+
self.request_count = 0
|
|
218
|
+
self.error_count = 0
|
|
219
|
+
self.total_response_time = 0.0
|
|
220
|
+
self.response_times = []
|
|
221
|
+
self.max_response_times = 1000 # Keep last 1000 response times
|
|
222
|
+
|
|
223
|
+
def record_request(
|
|
224
|
+
self,
|
|
225
|
+
method: str,
|
|
226
|
+
path: str,
|
|
227
|
+
status_code: int,
|
|
228
|
+
response_time: float,
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Record HTTP request metrics."""
|
|
231
|
+
self.request_count += 1
|
|
232
|
+
self.total_response_time += response_time
|
|
233
|
+
|
|
234
|
+
# Track error rates
|
|
235
|
+
if status_code >= 400:
|
|
236
|
+
self.error_count += 1
|
|
237
|
+
|
|
238
|
+
# Track response times
|
|
239
|
+
self.response_times.append(response_time)
|
|
240
|
+
if len(self.response_times) > self.max_response_times:
|
|
241
|
+
self.response_times.pop(0)
|
|
242
|
+
|
|
243
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
244
|
+
"""Get current metrics summary."""
|
|
245
|
+
if not self.request_count:
|
|
246
|
+
return {
|
|
247
|
+
"request_count": 0,
|
|
248
|
+
"error_count": 0,
|
|
249
|
+
"error_rate": 0.0,
|
|
250
|
+
"avg_response_time": 0.0,
|
|
251
|
+
"p95_response_time": 0.0,
|
|
252
|
+
"p99_response_time": 0.0,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Calculate percentiles
|
|
256
|
+
sorted_times = sorted(self.response_times)
|
|
257
|
+
p95_index = int(0.95 * len(sorted_times))
|
|
258
|
+
p99_index = int(0.99 * len(sorted_times))
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
"request_count": self.request_count,
|
|
262
|
+
"error_count": self.error_count,
|
|
263
|
+
"error_rate": self.error_count / self.request_count,
|
|
264
|
+
"avg_response_time": self.total_response_time / self.request_count,
|
|
265
|
+
"p95_response_time": sorted_times[p95_index] if sorted_times else 0.0,
|
|
266
|
+
"p99_response_time": sorted_times[p99_index] if sorted_times else 0.0,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
def reset_metrics(self) -> None:
|
|
270
|
+
"""Reset all metrics."""
|
|
271
|
+
self.request_count = 0
|
|
272
|
+
self.error_count = 0
|
|
273
|
+
self.total_response_time = 0.0
|
|
274
|
+
self.response_times.clear()
|