feat: Complete v1.1.2 Phase 1 - Metrics Instrumentation

Implements the metrics instrumentation framework that was missing from v1.1.1.
The monitoring framework existed but was never actually used to collect metrics.

Phase 1 Deliverables:
- Database operation monitoring with query timing and slow query detection
- HTTP request/response metrics with request IDs for all requests
- Memory monitoring via daemon thread with configurable intervals
- Business metrics framework for notes, feeds, and cache operations
- Configuration management with environment variable support

Implementation Details:
- MonitoredConnection wrapper at pool level for transparent DB monitoring
- Flask middleware hooks for HTTP metrics collection
- Background daemon thread for memory statistics (skipped in test mode)
- Simple business metric helpers for integration in Phase 2
- Comprehensive test suite with 28/28 tests passing

Quality Metrics:
- 100% test pass rate (28/28 tests)
- Zero architectural deviations from specifications
- <1% performance overhead achieved
- Production-ready with minimal memory impact (~2MB)

Architect Review: APPROVED with excellent marks

Documentation:
- Implementation report: docs/reports/v1.1.2-phase1-metrics-implementation.md
- Architect review: docs/reviews/2025-11-26-v1.1.2-phase1-review.md
- Updated CHANGELOG.md with Phase 1 additions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-26 14:13:44 -07:00
parent 1c73c4b7ae
commit b0230b1233
25 changed files with 8192 additions and 8 deletions

125
starpunk/monitoring/http.py Normal file
View File

@@ -0,0 +1,125 @@
"""
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
if response.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
)