This release candidate fixes two critical production issues discovered in v1.1.2-rc.1:
1. CRITICAL: Static files returning 500 errors
- HTTP monitoring middleware was accessing response.data on streaming responses
- Fixed by checking direct_passthrough flag before accessing response data
- Static files (CSS, JS, images) now load correctly
- File: starpunk/monitoring/http.py
2. HIGH: Database metrics showing zero
- Configuration key mismatch: config set METRICS_SAMPLING_RATE (singular),
buffer read METRICS_SAMPLING_RATES (plural)
- Fixed by standardizing on singular key name
- Modified MetricsBuffer to accept both float and dict for flexibility
- Changed default sampling from 10% to 100% for better visibility
- Files: starpunk/monitoring/metrics.py, starpunk/config.py
Version: 1.1.2-rc.2
Documentation:
- Investigation report: docs/reports/2025-11-28-v1.1.2-rc.1-production-issues.md
- Architect review: docs/reviews/2025-11-28-v1.1.2-rc.1-architect-review.md
- Implementation report: docs/reports/2025-11-28-v1.1.2-rc.2-fixes.md
Testing: All monitoring tests pass (28/28)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
134 lines
4.0 KiB
Python
134 lines
4.0 KiB
Python
"""
|
|
HTTP request/response metrics middleware
|
|
|
|
Per v1.1.2 Phase 1 and developer Q&A IQ2:
|
|
- Times all HTTP requests
|
|
- Generates request IDs for tracking (IQ2)
|
|
- Records status codes, methods, routes
|
|
- Tracks request and response sizes
|
|
- Adds X-Request-ID header to all responses (not just debug mode)
|
|
|
|
Example usage:
|
|
>>> from starpunk.monitoring.http import setup_http_metrics
|
|
>>> app = Flask(__name__)
|
|
>>> setup_http_metrics(app)
|
|
"""
|
|
|
|
import time
|
|
import uuid
|
|
from flask import g, request, Flask
|
|
from typing import Any
|
|
|
|
from starpunk.monitoring.metrics import record_metric
|
|
|
|
|
|
def setup_http_metrics(app: Flask) -> None:
|
|
"""
|
|
Setup HTTP metrics collection for Flask app
|
|
|
|
Per IQ2: Generates request IDs and adds X-Request-ID header in all modes
|
|
|
|
Args:
|
|
app: Flask application instance
|
|
"""
|
|
|
|
@app.before_request
|
|
def start_request_metrics():
|
|
"""
|
|
Initialize request metrics tracking
|
|
|
|
Per IQ2: Generate UUID request ID and store in g
|
|
"""
|
|
# Generate request ID (IQ2: in all modes, not just debug)
|
|
g.request_id = str(uuid.uuid4())
|
|
|
|
# Store request start time and metadata
|
|
g.request_start_time = time.perf_counter()
|
|
g.request_metadata = {
|
|
'method': request.method,
|
|
'endpoint': request.endpoint or 'unknown',
|
|
'path': request.path,
|
|
'content_length': request.content_length or 0,
|
|
}
|
|
|
|
@app.after_request
|
|
def record_response_metrics(response):
|
|
"""
|
|
Record HTTP response metrics
|
|
|
|
Args:
|
|
response: Flask response object
|
|
|
|
Returns:
|
|
Modified response with X-Request-ID header
|
|
"""
|
|
# Skip if metrics not initialized (shouldn't happen in normal flow)
|
|
if not hasattr(g, 'request_start_time'):
|
|
return response
|
|
|
|
# Calculate request duration
|
|
duration_sec = time.perf_counter() - g.request_start_time
|
|
duration_ms = duration_sec * 1000
|
|
|
|
# Get response size
|
|
response_size = 0
|
|
|
|
# Check if response is in direct passthrough mode (streaming)
|
|
if hasattr(response, 'direct_passthrough') and response.direct_passthrough:
|
|
# For streaming responses, use content_length if available
|
|
if hasattr(response, 'content_length') and response.content_length:
|
|
response_size = response.content_length
|
|
# Otherwise leave as 0 (unknown size for streaming)
|
|
elif response.data:
|
|
# For buffered responses, we can safely get the data
|
|
response_size = len(response.data)
|
|
elif hasattr(response, 'content_length') and response.content_length:
|
|
response_size = response.content_length
|
|
|
|
# Build metadata
|
|
metadata = {
|
|
**g.request_metadata,
|
|
'status_code': response.status_code,
|
|
'response_size': response_size,
|
|
}
|
|
|
|
# Record metric
|
|
operation_name = f"{g.request_metadata['method']} {g.request_metadata['endpoint']}"
|
|
record_metric(
|
|
'http',
|
|
operation_name,
|
|
duration_ms,
|
|
metadata
|
|
)
|
|
|
|
# Add request ID header (IQ2: in all modes)
|
|
response.headers['X-Request-ID'] = g.request_id
|
|
|
|
return response
|
|
|
|
@app.teardown_request
|
|
def record_error_metrics(error=None):
|
|
"""
|
|
Record metrics for requests that result in errors
|
|
|
|
Args:
|
|
error: Exception if request failed
|
|
"""
|
|
if error and hasattr(g, 'request_start_time'):
|
|
duration_ms = (time.perf_counter() - g.request_start_time) * 1000
|
|
|
|
metadata = {
|
|
**g.request_metadata,
|
|
'error': str(error),
|
|
'error_type': type(error).__name__,
|
|
}
|
|
|
|
operation_name = f"{g.request_metadata['method']} {g.request_metadata['endpoint']} ERROR"
|
|
record_metric(
|
|
'http',
|
|
operation_name,
|
|
duration_ms,
|
|
metadata,
|
|
force=True # Always record errors
|
|
)
|