debugging web ui

This commit is contained in:
Dobromir Popov
2025-08-05 15:58:51 +03:00
parent 622d059aae
commit bf4d43f6f7
8 changed files with 571 additions and 58 deletions

View File

@ -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
}

View File

@ -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'])
}

View File

@ -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