-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add API health checks and monitoring - PR-7 #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,11 @@ | ||||||
| from functools import wraps | ||||||
| from flask import request, jsonify | ||||||
|
|
||||||
| def require_api_key(f): | ||||||
| @wraps(f) | ||||||
| def decorated_function(*args, **kwargs): | ||||||
| api_key = request.headers.get('X-API-Key') | ||||||
| if api_key != 'your-secret-key': # Replace with actual key or env var | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoding secrets is a critical security vulnerability. The API key
Suggested change
|
||||||
| return jsonify({'error': 'API key required'}), 401 | ||||||
| return f(*args, **kwargs) | ||||||
|
Comment on lines
+7
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Eliminate hardcoded API key; use env var + constant-time compare. Hardcoding secrets and using plain equality is insecure. Read from env and use hmac.compare_digest. Apply within this block: - api_key = request.headers.get('X-API-Key')
- if api_key != 'your-secret-key': # Replace with actual key or env var
- return jsonify({'error': 'API key required'}), 401
+ api_key = request.headers.get('X-API-Key', '')
+ expected = os.getenv('API_KEY', '')
+ if not expected or not hmac.compare_digest(api_key, expected):
+ return jsonify({'error': 'invalid or missing API key'}), 401Add imports at top: import os, hmacπ€ Prompt for AI Agents |
||||||
| return decorated_function | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,83 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| from flask import Blueprint, jsonify, request | ||||||||||||||||||||||||||||||||||||||||||||||||||
| from health_monitor import health_monitor | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| # Create health endpoints blueprint | ||||||||||||||||||||||||||||||||||||||||||||||||||
| health_bp = Blueprint('health', __name__, url_prefix='/api/health') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| @health_bp.route('/', methods=['GET']) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| def health_check(): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Basic health check endpoint.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| summary = health_monitor.get_health_summary() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| status_code = 200 if summary["status"] in ["healthy", "warning"] else 503 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify(summary), status_code | ||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Health check failed: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "status": "error", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "message": "Health check failed" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }), 500 | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| @health_bp.route('/detailed', methods=['GET']) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| def detailed_health(): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Detailed health check with system metrics.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| health_data = health_monitor.get_system_health() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| status_code = 200 if health_data["status"] in ["healthy", "warning"] else 503 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify(health_data), status_code | ||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Detailed health check failed: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "status": "error", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "message": "Detailed health check failed" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }), 500 | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| @health_bp.route('/ready', methods=['GET']) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| def readiness_check(): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Kubernetes readiness probe endpoint.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| health_data = health_monitor.get_system_health() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if health_data["status"] in ["healthy", "warning"]: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify({"ready": True}), 200 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify({"ready": False, "reason": health_data["status"]}), 503 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Readiness check failed: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify({"ready": False, "reason": "error"}), 503 | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| @health_bp.route('/live', methods=['GET']) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| def liveness_check(): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Kubernetes liveness probe endpoint.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # Simple liveness check - just verify the service is responding | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify({"alive": True}), 200 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Liveness check failed: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify({"alive": False}), 500 | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| @health_bp.route('/metrics', methods=['GET']) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| def health_metrics(): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Health metrics endpoint for monitoring systems.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| health_data = health_monitor.get_system_health() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| metrics = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Uptime calculation in metrics endpoint may lose precision. Tracking uptime in seconds instead of converting from hours will improve metric accuracy. Suggested implementation: "uptime_seconds": health_data["uptime_seconds"]Ensure that the |
||||||||||||||||||||||||||||||||||||||||||||||||||
| "api_requests_total": health_data["process"]["request_count"], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "api_errors_total": health_data["process"]["error_count"], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "api_error_rate_percent": health_data["process"]["error_rate"], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "system_cpu_percent": health_data["system"]["cpu_percent"], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "system_memory_percent": health_data["system"]["memory_percent"], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "system_disk_percent": health_data["system"]["disk_percent"], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "uptime_seconds": health_data["uptime_hours"] * 3600 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify(metrics), 200 | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard metrics extraction when health_data indicates error. If - health_data = health_monitor.get_system_health()
- metrics = {
+ health_data = health_monitor.get_system_health()
+ if health_data.get("status") == "error":
+ return jsonify({"error": "Metrics unavailable"}), 503
+ metrics = {
"api_requests_total": health_data["process"]["request_count"],
"api_errors_total": health_data["process"]["error_count"],
"api_error_rate_percent": health_data["process"]["error_rate"],
"system_cpu_percent": health_data["system"]["cpu_percent"],
"system_memory_percent": health_data["system"]["memory_percent"],
"system_disk_percent": health_data["system"]["disk_percent"],
"uptime_seconds": health_data["uptime_hours"] * 3600
}π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Metrics collection failed: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify({"error": "Metrics collection failed"}), 500 | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def register_health_endpoints(app): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Register health endpoints with the Flask app.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| app.register_blueprint(health_bp) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("Health endpoints registered: /api/health/*") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import psutil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import datetime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Dict, Any, Optional | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class HealthMonitor: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Health monitoring system for API endpoints and system resources.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.start_time = time.time() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.request_count = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.error_count = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.last_health_check = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_system_health(self) -> Dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Get comprehensive system health metrics.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # System resource usage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cpu_percent = psutil.cpu_percent(interval=1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| memory = psutil.virtual_memory() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disk = psutil.disk_usage('/') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Process information | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process = psutil.Process() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process_memory = process.memory_info().rss / 1024 / 1024 # MB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Uptime calculation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uptime_seconds = time.time() - self.start_time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uptime_hours = uptime_seconds / 3600 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| health_data = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "status": "healthy", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "timestamp": datetime.utcnow().isoformat(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "uptime_hours": round(uptime_hours, 2), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "system": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "cpu_percent": cpu_percent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "memory_percent": memory.percent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "memory_available_gb": round(memory.available / 1024**3, 2), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "disk_percent": disk.percent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "disk_free_gb": round(disk.free / 1024**3, 2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "process": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "memory_mb": round(process_memory, 2), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "request_count": self.request_count, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error_count": self.error_count, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error_rate": round(self.error_count / max(self.request_count, 1) * 100, 2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "last_health_check": self.last_health_check | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To improve efficiency and precision, consider adding
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Determine overall health status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| health_data["status"] = "warning" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| health_data["status"] = "critical" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+58
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| health_data["status"] = "degraded" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic for determining the overall health status is flawed. The sequence of
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Status precedence bug: βdegradedβ overrides βcriticalβ. If CPU/mem/disk >95%, status becomes critical, then is overwritten to degraded on error_rate >10%. Preserve highest severity. - if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
- health_data["status"] = "warning"
- if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
- health_data["status"] = "critical"
- if self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1:
- health_data["status"] = "degraded"
+ # Determine overall health with clear precedence: critical > degraded > warning > healthy
+ status = "healthy"
+ if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
+ status = "warning"
+ if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
+ status = "critical"
+ err_rate = self.error_count / max(self.request_count, 1)
+ if status != "critical" and self.error_count > 0 and err_rate > 0.10:
+ status = "degraded"
+ health_data["status"] = statusπ Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.last_health_check = health_data["timestamp"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return health_data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Health check failed: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "status": "error", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "timestamp": datetime.utcnow().isoformat(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error": str(e) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def record_request(self, success: bool = True): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Record a request for health monitoring.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.request_count += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not success: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.error_count += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+73
to
+77
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_health_summary(self) -> Dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Get a simplified health summary for quick checks.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| health = self.get_system_health() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "status": health["status"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "uptime_hours": health["uptime_hours"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "request_count": health["process"]["request_count"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error_rate": health["process"]["error_rate"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Global health monitor instance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| health_monitor = HealthMonitor() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from collections import defaultdict | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import datetime, timedelta | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from flask import abort, current_app | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Simple rate limiter using memory (use Redis for production) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rate_limit = defaultdict(list) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def rate_limit(max_requests=100, window_minutes=1): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Decorator name 'rate_limit' conflicts with global variable. Consider renaming either the decorator or the global variable to avoid confusion and potential bugs.
Comment on lines
+6
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a name collision between the global variable
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def decorator(f): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @wraps(f) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def decorated_function(*args, **kwargs): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client_ip = request.remote_addr | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Rate limiting by client IP may not be reliable behind proxies. Behind proxies or load balancers, 'request.remote_addr' may not provide the actual client IP. Use 'X-Forwarded-For' or configure trusted proxies to ensure accurate rate limiting.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now = datetime.utcnow() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window_start = now - timedelta(minutes=window_minutes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rate_limit[client_ip] = [req_time for req_time in rate_limit[client_ip] if req_time > window_start] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(rate_limit[client_ip]) >= max_requests: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| abort(429, description="Rate limit exceeded") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rate_limit[client_ip].append(now) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return f(*args, **kwargs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rate limiting logic is not thread-safe. The global |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return decorated_function | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return decorator | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix name shadowing and missing imports causing runtime failures.
Apply: -from collections import defaultdict
-from datetime import datetime, timedelta
-from flask import abort, current_app
+from collections import defaultdict
+from datetime import datetime, timedelta
+from functools import wraps
+from flask import abort, request
@@
-# Simple rate limiter using memory (use Redis for production)
-rate_limit = defaultdict(list)
+# Simple rate limiter using memory (use Redis for production)
+_RATE_LIMIT_STORE = defaultdict(list)
@@
-def rate_limit(max_requests=100, window_minutes=1):
+def rate_limit(max_requests=100, window_minutes=1):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
client_ip = request.remote_addr
now = datetime.utcnow()
window_start = now - timedelta(minutes=window_minutes)
- rate_limit[client_ip] = [req_time for req_time in rate_limit[client_ip] if req_time > window_start]
- if len(rate_limit[client_ip]) >= max_requests:
+ _RATE_LIMIT_STORE[client_ip] = [t for t in _RATE_LIMIT_STORE[client_ip] if t > window_start]
+ if len(_RATE_LIMIT_STORE[client_ip]) >= max_requests:
abort(429, description="Rate limit exceeded")
- rate_limit[client_ip].append(now)
+ _RATE_LIMIT_STORE[client_ip].append(now)
return f(*args, **kwargs)
return decorated_function
return decoratorOptional hardening:
π Committable suggestion
Suggested change
π§° Toolsπͺ Ruff (0.12.2)8-8: Redefinition of unused (F811) 10-10: Undefined name (F821) 12-12: Undefined name (F821) π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,45 @@ | ||||||||||||||||||||||
| from fastapi import Depends, HTTPException, status | ||||||||||||||||||||||
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | ||||||||||||||||||||||
| from jose import JWTError, jwt | ||||||||||||||||||||||
| from passlib.context import CryptContext | ||||||||||||||||||||||
| from datetime import datetime, timedelta | ||||||||||||||||||||||
| from typing import Optional | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Security settings | ||||||||||||||||||||||
| SECRET_KEY = "your-secret-key" # Should be loaded from config | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π¨ issue (security): Hardcoded secret key poses a security risk. Consider loading the secret key from environment variables or a secure config system instead of hardcoding it.
Comment on lines
+7
to
+9
|
||||||||||||||||||||||
| # Security settings | |
| SECRET_KEY = "your-secret-key" # Should be loaded from config | |
| import os | |
| # Security settings | |
| SECRET_KEY = os.environ["SECRET_KEY"] # Must be set in environment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoding a secret key is a critical security vulnerability. The SECRET_KEY should be loaded from environment variables or a secure secret management system, not committed to the repository.
| SECRET_KEY = "your-secret-key" # Should be loaded from config | |
| SECRET_KEY = os.environ.get("JWT_SECRET_KEY") # Should be loaded from config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π οΈ Refactor suggestion
Replace hardcoded JWT secret and align expiry default.
Load secret from env/config and use ACCESS_TOKEN_EXPIRE_MINUTES consistently.
-SECRET_KEY = "your-secret-key" # Should be loaded from config
+import os
+SECRET_KEY = os.getenv("JWT_SECRET_KEY", "")
@@
-ACCESS_TOKEN_EXPIRE_MINUTES = 30
+ACCESS_TOKEN_EXPIRE_MINUTES = 30Ensure startup fails if SECRET_KEY is empty, or handle gracefully.
π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Security settings | |
| SECRET_KEY = "your-secret-key" # Should be loaded from config | |
| ALGORITHM = "HS256" | |
| ACCESS_TOKEN_EXPIRE_MINUTES = 30 | |
| # Security settings | |
| import os | |
| SECRET_KEY = os.getenv("JWT_SECRET_KEY", "") | |
| ALGORITHM = "HS256" | |
| ACCESS_TOKEN_EXPIRE_MINUTES = 30 |
π§° Tools
πͺ Ruff (0.12.2)
9-9: Possible hardcoded password assigned to: "SECRET_KEY"
(S105)
π€ Prompt for AI Agents
In src/security/auth.py around lines 8 to 12, the JWT secret is hardcoded and
the expiry constant may be inconsistently used; replace the literal SECRET_KEY
with a value read from configuration or environment (e.g., os.environ or your
app config loader), validate that the loaded SECRET_KEY is non-empty at startup
and raise/exit with a clear error if it is missing, and ensure
ACCESS_TOKEN_EXPIRE_MINUTES is defined as the single canonical constant used
throughout the codebase (remove duplicate literals and reference this constant
wherever token expiry is set) so expiry behavior is consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (code-quality): We've found these issues:
- Add single value to dictionary directly rather than using update() (
simplify-dictionary-update) - Inline variable that is immediately returned (
inline-immediately-returned-variable)
| to_encode.update({"exp": expire}) | |
| encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) | |
| return encoded_jwt | |
| to_encode["exp"] = expire | |
| return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)
| except JWTError: | |
| raise credentials_exception | |
| except JWTError as e: | |
| raise credentials_exception from e |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| from collections import defaultdict | ||
| from datetime import datetime, timedelta | ||
| from typing import Optional | ||
| import time | ||
|
|
||
| class RateLimiter: | ||
| def __init__(self, max_requests: int = 100, window_seconds: int = 3600): | ||
| self.max_requests = max_requests | ||
| self.window_seconds = window_seconds | ||
| self.requests = defaultdict(list) | ||
|
|
||
| def is_allowed(self, identifier: str) -> bool: | ||
| now = time.time() | ||
| window_start = now - self.window_seconds | ||
| self.requests[identifier] = [ | ||
| timestamp for timestamp in self.requests[identifier] | ||
| if timestamp > window_start | ||
| ] | ||
| if len(self.requests[identifier]) < self.max_requests: | ||
| self.requests[identifier].append(now) | ||
| return True | ||
| return False | ||
|
Comment on lines
+12
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
|
||
| def get_remaining_requests(self, identifier: str) -> int: | ||
| now = time.time() | ||
| window_start = now - self.window_seconds | ||
| self.requests[identifier] = [ | ||
| timestamp for timestamp in self.requests[identifier] | ||
| if timestamp > window_start | ||
| ] | ||
|
Comment on lines
+15
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| return max(0, self.max_requests - len(self.requests[identifier])) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||||||||||||
| from flask import Flask, jsonify | ||||||||||||||||
| from flask_cors import CORS | ||||||||||||||||
| from auth import require_api_key | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π¨ issue (security): API key is hardcoded and not configurable. Consider loading the API key from environment variables or a config file to improve security and flexibility. |
||||||||||||||||
| from rate_limiter import rate_limit | ||||||||||||||||
|
|
||||||||||||||||
| app = Flask(__name__) | ||||||||||||||||
| CORS(app) # Enable CORS for all routes | ||||||||||||||||
|
|
||||||||||||||||
| @app.route('/api/health') | ||||||||||||||||
| def health(): | ||||||||||||||||
| return jsonify({'status': 'healthy'}) | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+9
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Route duplication with health blueprint. This overlaps with -@app.route('/api/health')
-def health():
- return jsonify({'status': 'healthy'})
+# Register health blueprint instead of a separate route
+from health_endpoints import register_health_endpoints # at top-level imports
+register_health_endpoints(app)π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||
| @app.route('/api/protected', methods=['POST']) | ||||||||||||||||
| @require_api_key | ||||||||||||||||
| @rate_limit(max_requests=10, window_minutes=1) | ||||||||||||||||
| def protected(): | ||||||||||||||||
| return jsonify({'message': 'Protected endpoint'}) | ||||||||||||||||
|
|
||||||||||||||||
| if __name__ == '__main__': | ||||||||||||||||
| app.run(host='0.0.0.0', port=5000) | ||||||||||||||||
|
Comment on lines
+19
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoded API key creates a security vulnerability. Load the key from environment variables or secure configuration.