Files
StarPunk/starpunk/monitoring/http.py
Phil Skentelbery 1e2135a49a fix: Resolve v1.1.2-rc.1 production issues - Static files and metrics
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>
2025-11-28 09:46:31 -07:00

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
)