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>
This commit is contained in:
2025-11-28 09:46:31 -07:00
parent 34b576ff79
commit 1e2135a49a
7 changed files with 875 additions and 16 deletions

View File

@@ -72,7 +72,15 @@ def setup_http_metrics(app: Flask) -> None:
# Get response size
response_size = 0
if response.data:
# 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

View File

@@ -26,7 +26,7 @@ from collections import deque
from dataclasses import dataclass, field, asdict
from datetime import datetime
from threading import Lock
from typing import Any, Deque, Dict, List, Literal, Optional
from typing import Any, Deque, Dict, List, Literal, Optional, Union
# Operation types for categorizing metrics
OperationType = Literal["database", "http", "render"]
@@ -75,7 +75,7 @@ class MetricsBuffer:
Per developer Q&A Q12:
- Configurable sampling rates per operation type
- Default 10% sampling
- Default 100% sampling (suitable for low-traffic sites)
- Slow queries always logged regardless of sampling
Example:
@@ -87,27 +87,42 @@ class MetricsBuffer:
def __init__(
self,
max_size: int = 1000,
sampling_rates: Optional[Dict[OperationType, float]] = None
sampling_rates: Optional[Union[Dict[OperationType, float], float]] = None
):
"""
Initialize metrics buffer
Args:
max_size: Maximum number of metrics to store
sampling_rates: Dict mapping operation type to sampling rate (0.0-1.0)
Default: {'database': 0.1, 'http': 0.1, 'render': 0.1}
sampling_rates: Either:
- float: Global sampling rate for all operation types (0.0-1.0)
- dict: Mapping operation type to sampling rate
Default: 1.0 (100% sampling)
"""
self.max_size = max_size
self._buffer: Deque[Metric] = deque(maxlen=max_size)
self._lock = Lock()
self._process_id = os.getpid()
# Default sampling rates (10% for all operation types)
self._sampling_rates = sampling_rates or {
"database": 0.1,
"http": 0.1,
"render": 0.1,
}
# Handle different sampling_rates types
if sampling_rates is None:
# Default to 100% sampling for all types
self._sampling_rates = {
"database": 1.0,
"http": 1.0,
"render": 1.0,
}
elif isinstance(sampling_rates, (int, float)):
# Global rate for all types
rate = float(sampling_rates)
self._sampling_rates = {
"database": rate,
"http": rate,
"render": rate,
}
else:
# Dict with per-type rates
self._sampling_rates = sampling_rates
def record(
self,
@@ -334,15 +349,15 @@ def get_buffer() -> MetricsBuffer:
try:
from flask import current_app
max_size = current_app.config.get('METRICS_BUFFER_SIZE', 1000)
sampling_rates = current_app.config.get('METRICS_SAMPLING_RATES', None)
sampling_rate = current_app.config.get('METRICS_SAMPLING_RATE', 1.0)
except (ImportError, RuntimeError):
# Flask not available or no app context
max_size = 1000
sampling_rates = None
sampling_rate = 1.0 # Default to 100%
_metrics_buffer = MetricsBuffer(
max_size=max_size,
sampling_rates=sampling_rates
sampling_rates=sampling_rate
)
return _metrics_buffer