File size: 14,644 Bytes
3a660a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
"""
Crypto API Hub Self-Healing Backend Router

This module provides backend support for the self-healing crypto API hub,
including proxy endpoints, health monitoring, and automatic recovery mechanisms.
"""

from fastapi import APIRouter, HTTPException, Request, BackgroundTasks
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel, HttpUrl
from typing import Dict, List, Optional, Any
import httpx
import asyncio
from datetime import datetime, timedelta
import logging
from pathlib import Path

logger = logging.getLogger(__name__)

router = APIRouter(
    prefix="/api/crypto-hub",
    tags=["Crypto API Hub Self-Healing"]
)

# Health monitoring storage
health_status: Dict[str, Dict[str, Any]] = {}
failed_endpoints: Dict[str, Dict[str, Any]] = {}
recovery_log: List[Dict[str, Any]] = []


class ProxyRequest(BaseModel):
    """Model for proxy request"""
    url: str
    method: str = "GET"
    headers: Optional[Dict[str, str]] = {}
    body: Optional[str] = None
    timeout: Optional[int] = 10


class HealthCheckRequest(BaseModel):
    """Model for health check request"""
    endpoints: List[str]


class RecoveryRequest(BaseModel):
    """Model for manual recovery trigger"""
    endpoint: str


@router.get("/", response_class=HTMLResponse)
async def serve_crypto_hub():
    """
    Serve the crypto API hub HTML page
    """
    try:
        html_path = Path(__file__).parent.parent.parent / "static" / "crypto-api-hub-stunning.html"
        
        if not html_path.exists():
            raise HTTPException(status_code=404, detail="Crypto API Hub page not found")
        
        with open(html_path, 'r', encoding='utf-8') as f:
            html_content = f.read()
        
        # Inject self-healing script
        injection = '''
    <script src="/static/js/crypto-api-hub-self-healing.js"></script>
    <script>
        // Initialize self-healing system
        const selfHealing = new SelfHealingAPIHub({
            backendUrl: '/api/crypto-hub',
            enableAutoRecovery: true,
            enableCaching: true,
            retryAttempts: 3,
            healthCheckInterval: 60000
        });

        // Override fetch to use self-healing
        const originalFetch = window.fetch;
        window.fetch = async function(...args) {
            const url = args[0];
            const options = args[1] || {};
            
            // Use self-healing fetch for API calls
            if (url.startsWith('http://') || url.startsWith('https://')) {
                const result = await selfHealing.fetchWithRecovery(url, options);
                
                if (result.success) {
                    return {
                        ok: true,
                        json: async () => result.data,
                        headers: new Headers(),
                        status: 200
                    };
                } else {
                    throw new Error(result.error);
                }
            }
            
            // Use original fetch for non-API calls
            return originalFetch.apply(this, args);
        };

        // Add health status indicator to UI
        function addHealthIndicator() {
            const header = document.querySelector('.header-actions');
            if (header) {
                const healthBtn = document.createElement('button');
                healthBtn.className = 'btn-gradient';
                healthBtn.innerHTML = `
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
                    </svg>
                    <span id="health-status">Health</span>
                `;
                healthBtn.onclick = showHealthStatus;
                header.insertBefore(healthBtn, header.firstChild);

                // Update health status periodically
                setInterval(updateHealthIndicator, 30000);
                updateHealthIndicator();
            }
        }

        async function updateHealthIndicator() {
            const health = selfHealing.getHealthStatus();
            const statusElement = document.getElementById('health-status');
            if (statusElement) {
                statusElement.textContent = `Health: ${health.healthPercentage}%`;
            }
        }

        async function showHealthStatus() {
            const diagnostics = selfHealing.getDiagnostics();
            alert(`System Health Status\\n\\n` +
                  `Healthy: ${diagnostics.health.healthy}/${diagnostics.health.total}\\n` +
                  `Failed Endpoints: ${diagnostics.health.failedEndpoints}\\n` +
                  `Cache Entries: ${diagnostics.cache.size}\\n` +
                  `Health: ${diagnostics.health.healthPercentage}%`);
        }

        // Initialize on page load
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', addHealthIndicator);
        } else {
            addHealthIndicator();
        }
    </script>
</body>'''
        
        html_content = html_content.replace('</body>', injection)
        
        return HTMLResponse(content=html_content)
    
    except Exception as e:
        logger.error(f"Error serving crypto hub: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/proxy")
async def proxy_request(request: ProxyRequest):
    """
    Proxy endpoint for API requests with automatic retry and fallback
    """
    try:
        async with httpx.AsyncClient(timeout=request.timeout) as client:
            # Build request
            kwargs = {
                "method": request.method,
                "url": request.url,
                "headers": request.headers or {}
            }
            
            if request.body and request.method in ["POST", "PUT", "PATCH"]:
                kwargs["content"] = request.body
            
            # Make request with retry logic
            max_retries = 3
            last_error = None
            
            for attempt in range(max_retries):
                try:
                    response = await client.request(**kwargs)
                    
                    if response.status_code < 400:
                        return {
                            "success": True,
                            "status_code": response.status_code,
                            "data": response.json() if response.content else {},
                            "headers": dict(response.headers),
                            "source": "proxy",
                            "attempt": attempt + 1
                        }
                    
                    last_error = f"HTTP {response.status_code}"
                    
                except httpx.TimeoutException:
                    last_error = "Request timeout"
                    logger.warning(f"Proxy timeout (attempt {attempt + 1}): {request.url}")
                    
                except httpx.RequestError as e:
                    last_error = str(e)
                    logger.warning(f"Proxy error (attempt {attempt + 1}): {request.url} - {e}")
                
                # Exponential backoff
                if attempt < max_retries - 1:
                    await asyncio.sleep(2 ** attempt)
            
            # All attempts failed
            record_failure(request.url, last_error)
            
            return {
                "success": False,
                "error": last_error,
                "url": request.url,
                "attempts": max_retries
            }
    
    except Exception as e:
        logger.error(f"Proxy error: {e}")
        return {
            "success": False,
            "error": str(e),
            "url": request.url
        }


@router.post("/health-check")
async def health_check(request: HealthCheckRequest, background_tasks: BackgroundTasks):
    """
    Perform health checks on multiple endpoints
    """
    results = {}
    
    for endpoint in request.endpoints:
        background_tasks.add_task(check_endpoint_health, endpoint)
        
        # Return cached status if available
        if endpoint in health_status:
            results[endpoint] = health_status[endpoint]
        else:
            results[endpoint] = {
                "status": "checking",
                "message": "Health check in progress"
            }
    
    return {
        "success": True,
        "results": results,
        "timestamp": datetime.utcnow().isoformat()
    }


@router.get("/health-status")
async def get_health_status():
    """
    Get current health status of all monitored endpoints
    """
    total = len(health_status)
    healthy = sum(1 for s in health_status.values() if s.get("status") == "healthy")
    degraded = sum(1 for s in health_status.values() if s.get("status") == "degraded")
    unhealthy = sum(1 for s in health_status.values() if s.get("status") == "unhealthy")
    
    return {
        "total": total,
        "healthy": healthy,
        "degraded": degraded,
        "unhealthy": unhealthy,
        "health_percentage": round((healthy / total * 100)) if total > 0 else 0,
        "failed_endpoints": len(failed_endpoints),
        "endpoints": health_status,
        "timestamp": datetime.utcnow().isoformat()
    }


@router.post("/recover")
async def trigger_recovery(request: RecoveryRequest):
    """
    Manually trigger recovery for a specific endpoint
    """
    try:
        logger.info(f"Manual recovery triggered for: {request.endpoint}")
        
        # Check endpoint health
        is_healthy = await check_endpoint_health(request.endpoint)
        
        if is_healthy:
            # Remove from failed endpoints
            if request.endpoint in failed_endpoints:
                del failed_endpoints[request.endpoint]
            
            # Log recovery
            recovery_log.append({
                "endpoint": request.endpoint,
                "timestamp": datetime.utcnow().isoformat(),
                "type": "manual",
                "success": True
            })
            
            return {
                "success": True,
                "message": "Endpoint recovered successfully",
                "endpoint": request.endpoint
            }
        else:
            return {
                "success": False,
                "message": "Endpoint still unhealthy",
                "endpoint": request.endpoint
            }
    
    except Exception as e:
        logger.error(f"Recovery error: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/diagnostics")
async def get_diagnostics():
    """
    Get comprehensive diagnostics information
    """
    return {
        "health": await get_health_status(),
        "failed_endpoints": [
            {
                "url": url,
                **details
            }
            for url, details in failed_endpoints.items()
        ],
        "recovery_log": recovery_log[-50:],  # Last 50 recovery attempts
        "timestamp": datetime.utcnow().isoformat()
    }


@router.get("/recovery-log")
async def get_recovery_log(limit: int = 50):
    """
    Get recovery log
    """
    return {
        "log": recovery_log[-limit:],
        "total": len(recovery_log),
        "timestamp": datetime.utcnow().isoformat()
    }


@router.delete("/clear-failures")
async def clear_failures():
    """
    Clear all failure records (admin function)
    """
    global failed_endpoints, recovery_log
    
    cleared = len(failed_endpoints)
    failed_endpoints.clear()
    recovery_log.clear()
    
    return {
        "success": True,
        "cleared": cleared,
        "message": f"Cleared {cleared} failure records"
    }


# Helper functions

async def check_endpoint_health(endpoint: str) -> bool:
    """
    Check health of a specific endpoint
    """
    try:
        async with httpx.AsyncClient(timeout=5.0) as client:
            response = await client.head(endpoint)
            
            is_healthy = response.status_code < 400
            
            health_status[endpoint] = {
                "status": "healthy" if is_healthy else "degraded",
                "status_code": response.status_code,
                "last_check": datetime.utcnow().isoformat(),
                "response_time": response.elapsed.total_seconds()
            }
            
            return is_healthy
    
    except Exception as e:
        health_status[endpoint] = {
            "status": "unhealthy",
            "last_check": datetime.utcnow().isoformat(),
            "error": str(e)
        }
        
        record_failure(endpoint, str(e))
        return False


def record_failure(endpoint: str, error: str):
    """
    Record endpoint failure
    """
    if endpoint not in failed_endpoints:
        failed_endpoints[endpoint] = {
            "count": 0,
            "first_failure": datetime.utcnow().isoformat(),
            "errors": []
        }
    
    record = failed_endpoints[endpoint]
    record["count"] += 1
    record["last_failure"] = datetime.utcnow().isoformat()
    record["errors"].append({
        "timestamp": datetime.utcnow().isoformat(),
        "message": error
    })
    
    # Keep only last 10 errors
    if len(record["errors"]) > 10:
        record["errors"] = record["errors"][-10:]
    
    logger.error(f"Endpoint failure recorded: {endpoint} ({record['count']} failures)")


# Background task for continuous monitoring
async def continuous_monitoring():
    """
    Background task for continuous endpoint monitoring
    """
    while True:
        try:
            # Check all registered endpoints
            for endpoint in list(health_status.keys()):
                await check_endpoint_health(endpoint)
            
            # Clean up old failures (older than 1 hour)
            current_time = datetime.utcnow()
            to_remove = []
            
            for endpoint, record in failed_endpoints.items():
                last_failure = datetime.fromisoformat(record["last_failure"])
                if current_time - last_failure > timedelta(hours=1):
                    to_remove.append(endpoint)
            
            for endpoint in to_remove:
                del failed_endpoints[endpoint]
                logger.info(f"Cleaned up old failure record: {endpoint}")
            
            # Wait before next check
            await asyncio.sleep(60)  # Check every minute
        
        except Exception as e:
            logger.error(f"Monitoring error: {e}")
            await asyncio.sleep(60)