debugging web ui
This commit is contained in:
@ -32,4 +32,66 @@ class RateLimiter:
|
||||
|
||||
# Add current request
|
||||
self.requests[client_id].append(now)
|
||||
return True
|
||||
return True
|
||||
|
||||
def get_client_stats(self, client_id: str) -> Dict:
|
||||
"""Get rate limiting stats for a specific client"""
|
||||
now = time.time()
|
||||
minute_ago = now - 60
|
||||
|
||||
# Clean old requests
|
||||
self.requests[client_id] = [
|
||||
req_time for req_time in self.requests[client_id]
|
||||
if req_time > minute_ago
|
||||
]
|
||||
|
||||
current_requests = len(self.requests[client_id])
|
||||
remaining_tokens = max(0, self.requests_per_minute - current_requests)
|
||||
|
||||
# Calculate reset time (next minute boundary)
|
||||
reset_time = int(now) + (60 - int(now) % 60)
|
||||
|
||||
return {
|
||||
'client_id': client_id,
|
||||
'current_requests': current_requests,
|
||||
'remaining_tokens': remaining_tokens,
|
||||
'requests_per_minute': self.requests_per_minute,
|
||||
'reset_time': reset_time,
|
||||
'window_start': minute_ago,
|
||||
'window_end': now
|
||||
}
|
||||
|
||||
def get_global_stats(self) -> Dict:
|
||||
"""Get global rate limiting statistics"""
|
||||
now = time.time()
|
||||
minute_ago = now - 60
|
||||
|
||||
total_clients = len(self.requests)
|
||||
total_requests = 0
|
||||
active_clients = 0
|
||||
|
||||
for client_id in list(self.requests.keys()):
|
||||
# Clean old requests
|
||||
self.requests[client_id] = [
|
||||
req_time for req_time in self.requests[client_id]
|
||||
if req_time > minute_ago
|
||||
]
|
||||
|
||||
client_requests = len(self.requests[client_id])
|
||||
total_requests += client_requests
|
||||
|
||||
if client_requests > 0:
|
||||
active_clients += 1
|
||||
|
||||
# Remove clients with no recent requests
|
||||
if client_requests == 0:
|
||||
del self.requests[client_id]
|
||||
|
||||
return {
|
||||
'total_clients': total_clients,
|
||||
'active_clients': active_clients,
|
||||
'total_requests_last_minute': total_requests,
|
||||
'requests_per_minute_limit': self.requests_per_minute,
|
||||
'burst_size': self.burst_size,
|
||||
'window_duration': 60
|
||||
}
|
@ -9,17 +9,36 @@ from datetime import datetime
|
||||
class ResponseFormatter:
|
||||
"""Format API responses consistently"""
|
||||
|
||||
def success(self, data: Any, message: str = "Success") -> Dict[str, Any]:
|
||||
def __init__(self):
|
||||
self.stats = {
|
||||
'responses_formatted': 0,
|
||||
'errors_formatted': 0,
|
||||
'success_responses': 0,
|
||||
'created_at': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
def success(self, data: Any, message: str = "Success", metadata: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Format success response"""
|
||||
return {
|
||||
self.stats['responses_formatted'] += 1
|
||||
self.stats['success_responses'] += 1
|
||||
|
||||
response = {
|
||||
"status": "success",
|
||||
"message": message,
|
||||
"data": data,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
if metadata:
|
||||
response["metadata"] = metadata
|
||||
|
||||
return response
|
||||
|
||||
def error(self, message: str, code: str = "ERROR", details: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Format error response"""
|
||||
self.stats['responses_formatted'] += 1
|
||||
self.stats['errors_formatted'] += 1
|
||||
|
||||
response = {
|
||||
"status": "error",
|
||||
"message": message,
|
||||
@ -34,8 +53,120 @@ class ResponseFormatter:
|
||||
|
||||
def health(self, healthy: bool = True, components: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Format health check response"""
|
||||
self.stats['responses_formatted'] += 1
|
||||
|
||||
return {
|
||||
"status": "healthy" if healthy else "unhealthy",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"components": components or {}
|
||||
}
|
||||
|
||||
def rate_limit_error(self, client_stats: Dict) -> Dict[str, Any]:
|
||||
"""Format rate limit error response"""
|
||||
self.stats['responses_formatted'] += 1
|
||||
self.stats['errors_formatted'] += 1
|
||||
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Rate limit exceeded",
|
||||
"code": "RATE_LIMIT_EXCEEDED",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"details": {
|
||||
"remaining_tokens": client_stats.get('remaining_tokens', 0),
|
||||
"reset_time": client_stats.get('reset_time', 0),
|
||||
"requests_per_minute": client_stats.get('requests_per_minute', 0)
|
||||
}
|
||||
}
|
||||
|
||||
def validation_error(self, field: str, message: str) -> Dict[str, Any]:
|
||||
"""Format validation error response"""
|
||||
self.stats['responses_formatted'] += 1
|
||||
self.stats['errors_formatted'] += 1
|
||||
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Validation error: {message}",
|
||||
"code": "VALIDATION_ERROR",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"details": {
|
||||
"field": field,
|
||||
"validation_message": message
|
||||
}
|
||||
}
|
||||
|
||||
def status_response(self, data: Dict) -> Dict[str, Any]:
|
||||
"""Format status response"""
|
||||
self.stats['responses_formatted'] += 1
|
||||
self.stats['success_responses'] += 1
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "System status",
|
||||
"data": data,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
def heatmap_response(self, heatmap_data: Any, symbol: str, exchange: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Format heatmap response"""
|
||||
self.stats['responses_formatted'] += 1
|
||||
self.stats['success_responses'] += 1
|
||||
|
||||
if not heatmap_data:
|
||||
return self.error("Heatmap data not found", "HEATMAP_NOT_FOUND")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Heatmap data for {symbol}",
|
||||
"data": {
|
||||
"symbol": symbol,
|
||||
"exchange": exchange or "consolidated",
|
||||
"heatmap": heatmap_data
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
def orderbook_response(self, orderbook_data: Any, symbol: str, exchange: str) -> Dict[str, Any]:
|
||||
"""Format order book response"""
|
||||
self.stats['responses_formatted'] += 1
|
||||
self.stats['success_responses'] += 1
|
||||
|
||||
if not orderbook_data:
|
||||
return self.error("Order book data not found", "ORDERBOOK_NOT_FOUND")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Order book data for {symbol}@{exchange}",
|
||||
"data": {
|
||||
"symbol": symbol,
|
||||
"exchange": exchange,
|
||||
"orderbook": orderbook_data
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
def metrics_response(self, metrics_data: Any, symbol: str, exchange: str) -> Dict[str, Any]:
|
||||
"""Format metrics response"""
|
||||
self.stats['responses_formatted'] += 1
|
||||
self.stats['success_responses'] += 1
|
||||
|
||||
if not metrics_data:
|
||||
return self.error("Metrics data not found", "METRICS_NOT_FOUND")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Metrics data for {symbol}@{exchange}",
|
||||
"data": {
|
||||
"symbol": symbol,
|
||||
"exchange": exchange,
|
||||
"metrics": metrics_data
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get formatter statistics"""
|
||||
return {
|
||||
**self.stats,
|
||||
'uptime_seconds': (datetime.utcnow() - datetime.fromisoformat(self.stats['created_at'])).total_seconds(),
|
||||
'error_rate': self.stats['errors_formatted'] / max(1, self.stats['responses_formatted'])
|
||||
}
|
@ -2,13 +2,15 @@
|
||||
REST API server for COBY system.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request, Query, Path
|
||||
from fastapi import FastAPI, HTTPException, Request, Query, Path, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from typing import Optional, List
|
||||
import asyncio
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
try:
|
||||
from ..simple_config import config
|
||||
from ..caching.redis_manager import redis_manager
|
||||
@ -27,6 +29,43 @@ except ImportError:
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""Manage WebSocket connections for dashboard updates"""
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
logger.info(f"WebSocket client connected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
if websocket in self.active_connections:
|
||||
self.active_connections.remove(websocket)
|
||||
logger.info(f"WebSocket client disconnected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
async def send_personal_message(self, message: str, websocket: WebSocket):
|
||||
try:
|
||||
await websocket.send_text(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending personal message: {e}")
|
||||
self.disconnect(websocket)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
disconnected = []
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
await connection.send_text(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Error broadcasting to connection: {e}")
|
||||
disconnected.append(connection)
|
||||
|
||||
# Remove disconnected clients
|
||||
for connection in disconnected:
|
||||
self.disconnect(connection)
|
||||
|
||||
|
||||
def create_app(config_obj=None) -> FastAPI:
|
||||
"""Create and configure FastAPI application"""
|
||||
|
||||
@ -38,12 +77,7 @@ def create_app(config_obj=None) -> FastAPI:
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# Mount static files for web dashboard (since we removed nginx)
|
||||
static_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "web", "static")
|
||||
if os.path.exists(static_path):
|
||||
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
||||
# Serve index.html at root for dashboard
|
||||
app.mount("/", StaticFiles(directory=static_path, html=True), name="dashboard")
|
||||
# We'll mount static files AFTER defining all API routes to avoid conflicts
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
@ -60,7 +94,49 @@ def create_app(config_obj=None) -> FastAPI:
|
||||
burst_size=20
|
||||
)
|
||||
response_formatter = ResponseFormatter()
|
||||
connection_manager = ConnectionManager()
|
||||
|
||||
@app.websocket("/ws/dashboard")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket endpoint for dashboard real-time updates"""
|
||||
await connection_manager.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
# Send periodic updates
|
||||
await asyncio.sleep(5) # Update every 5 seconds
|
||||
|
||||
# Gather system status
|
||||
system_data = {
|
||||
"timestamp": time.time(),
|
||||
"performance": {
|
||||
"cpu_usage": 25.5, # Stub data
|
||||
"memory_usage": 45.2,
|
||||
"throughput": 1250,
|
||||
"avg_latency": 12.3
|
||||
},
|
||||
"exchanges": {
|
||||
"binance": "connected",
|
||||
"coinbase": "connected",
|
||||
"kraken": "disconnected",
|
||||
"bybit": "connected"
|
||||
},
|
||||
"processing": {
|
||||
"active": True,
|
||||
"total_processed": 15420
|
||||
}
|
||||
}
|
||||
|
||||
await connection_manager.send_personal_message(
|
||||
json.dumps(system_data),
|
||||
websocket
|
||||
)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
connection_manager.disconnect(websocket)
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket error: {e}")
|
||||
connection_manager.disconnect(websocket)
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
@ -127,6 +203,30 @@ def create_app(config_obj=None) -> FastAPI:
|
||||
except Exception as e:
|
||||
logger.error(f"API server shutdown error: {e}")
|
||||
|
||||
# API Health check endpoint (for dashboard)
|
||||
@app.get("/api/health")
|
||||
async def api_health_check():
|
||||
"""API Health check endpoint for dashboard"""
|
||||
try:
|
||||
# Check Redis connection
|
||||
redis_healthy = await redis_manager.ping()
|
||||
|
||||
health_data = {
|
||||
'status': 'healthy' if redis_healthy else 'degraded',
|
||||
'redis': 'connected' if redis_healthy else 'disconnected',
|
||||
'version': '1.0.0',
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
return response_formatter.status_response(health_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {e}")
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content=response_formatter.error("Service unavailable", "HEALTH_CHECK_FAILED")
|
||||
)
|
||||
|
||||
# Health check endpoint
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
@ -415,6 +515,13 @@ def create_app(config_obj=None) -> FastAPI:
|
||||
content=response_formatter.error("Internal server error", "BATCH_HEATMAPS_ERROR")
|
||||
)
|
||||
|
||||
# Mount static files for web dashboard AFTER all API routes are defined
|
||||
static_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "web", "static")
|
||||
if os.path.exists(static_path):
|
||||
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
||||
# Serve index.html at root for dashboard, but this should be last
|
||||
app.mount("/", StaticFiles(directory=static_path, html=True), name="dashboard")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user