""" 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 )