| """ |
| Cryptocurrency API Monitor - Gradio Application |
| Production-ready monitoring dashboard for Hugging Face Spaces |
| """ |
|
|
| import gradio as gr |
| import pandas as pd |
| import plotly.graph_objects as go |
| import plotly.express as px |
| from datetime import datetime, timedelta |
| import asyncio |
| import time |
| import logging |
| from typing import List, Dict, Optional |
| import json |
|
|
| |
| from config import config |
| from monitor import APIMonitor, HealthStatus, HealthCheckResult |
| from database import Database |
| from scheduler import BackgroundScheduler |
|
|
| |
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
| |
| db = Database() |
| monitor = APIMonitor(config) |
| scheduler = BackgroundScheduler(monitor, db, interval_minutes=5) |
|
|
| |
| current_results = [] |
| last_check_time = None |
|
|
|
|
| |
| |
| |
|
|
| def refresh_dashboard(category_filter="All", status_filter="All", tier_filter="All"): |
| """Refresh the main dashboard with filters""" |
| global current_results, last_check_time |
|
|
| try: |
| |
| logger.info("Running health checks...") |
| current_results = asyncio.run(monitor.check_all()) |
| last_check_time = datetime.now() |
|
|
| |
| db.save_health_checks(current_results) |
|
|
| |
| filtered_results = current_results |
|
|
| if category_filter != "All": |
| filtered_results = [r for r in filtered_results if r.category == category_filter] |
|
|
| if status_filter != "All": |
| filtered_results = [r for r in filtered_results if r.status.value == status_filter.lower()] |
|
|
| if tier_filter != "All": |
| tier_num = int(tier_filter.split()[1]) |
| tier_resources = config.get_by_tier(tier_num) |
| tier_names = [r['name'] for r in tier_resources] |
| filtered_results = [r for r in filtered_results if r.provider_name in tier_names] |
|
|
| |
| df_data = [] |
| for result in filtered_results: |
| df_data.append({ |
| 'Status': f"{result.get_badge()} {result.status.value.upper()}", |
| 'Provider': result.provider_name, |
| 'Category': result.category, |
| 'Response Time': f"{result.response_time:.0f} ms", |
| 'Last Check': datetime.fromtimestamp(result.timestamp).strftime('%H:%M:%S'), |
| 'Code': result.status_code or 'N/A' |
| }) |
|
|
| df = pd.DataFrame(df_data) |
|
|
| |
| stats = monitor.get_summary_stats(current_results) |
|
|
| |
| summary_html = f""" |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px;"> |
| <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 10px; color: white;"> |
| <h3 style="margin: 0;">📊 Total APIs</h3> |
| <p style="font-size: 32px; margin: 10px 0 0 0; font-weight: bold;">{stats['total']}</p> |
| </div> |
| <div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); padding: 20px; border-radius: 10px; color: white;"> |
| <h3 style="margin: 0;">✅ Online %</h3> |
| <p style="font-size: 32px; margin: 10px 0 0 0; font-weight: bold;">{stats['online_percentage']}%</p> |
| </div> |
| <div style="background: linear-gradient(135deg, #ee0979 0%, #ff6a00 100%); padding: 20px; border-radius: 10px; color: white;"> |
| <h3 style="margin: 0;">⚠️ Critical Issues</h3> |
| <p style="font-size: 32px; margin: 10px 0 0 0; font-weight: bold;">{stats['critical_issues']}</p> |
| </div> |
| <div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); padding: 20px; border-radius: 10px; color: white;"> |
| <h3 style="margin: 0;">⚡ Avg Response</h3> |
| <p style="font-size: 32px; margin: 10px 0 0 0; font-weight: bold;">{stats['avg_response_time']:.0f} ms</p> |
| </div> |
| </div> |
| <p style="text-align: center; color: #666;">Last updated: {last_check_time.strftime('%Y-%m-%d %H:%M:%S')}</p> |
| """ |
|
|
| return df, summary_html |
|
|
| except Exception as e: |
| logger.error(f"Error refreshing dashboard: {e}") |
| return pd.DataFrame(), f"<p style='color: red;'>Error: {str(e)}</p>" |
|
|
|
|
| def export_current_status(): |
| """Export current status to CSV""" |
| global current_results |
|
|
| if not current_results: |
| return None |
|
|
| try: |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') |
| filename = f"api_status_{timestamp}.csv" |
| filepath = f"data/{filename}" |
|
|
| df_data = [] |
| for result in current_results: |
| df_data.append({ |
| 'Provider': result.provider_name, |
| 'Category': result.category, |
| 'Status': result.status.value, |
| 'Response_Time_ms': result.response_time, |
| 'Status_Code': result.status_code, |
| 'Error': result.error_message or '', |
| 'Timestamp': datetime.fromtimestamp(result.timestamp).isoformat() |
| }) |
|
|
| df = pd.DataFrame(df_data) |
| df.to_csv(filepath, index=False) |
|
|
| return filepath |
|
|
| except Exception as e: |
| logger.error(f"Error exporting: {e}") |
| return None |
|
|
|
|
| |
| |
| |
|
|
| def get_category_overview(): |
| """Get overview of all categories""" |
| global current_results |
|
|
| if not current_results: |
| return "No data available. Please refresh the dashboard first." |
|
|
| category_stats = monitor.get_category_stats(current_results) |
|
|
| html_output = "<div style='padding: 20px;'>" |
|
|
| for category, stats in category_stats.items(): |
| online_pct = stats['online_percentage'] |
|
|
| |
| if online_pct >= 80: |
| color = "#4CAF50" |
| elif online_pct >= 50: |
| color = "#FF9800" |
| else: |
| color = "#F44336" |
|
|
| html_output += f""" |
| <div style="margin-bottom: 30px; border: 2px solid {color}; border-radius: 10px; padding: 20px; background: #f9f9f9;"> |
| <h2 style="margin-top: 0; color: {color};">📁 {category}</h2> |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px;"> |
| <div> |
| <strong>Total:</strong> {stats['total']} |
| </div> |
| <div> |
| <strong>🟢 Online:</strong> {stats['online']} |
| </div> |
| <div> |
| <strong>🟡 Degraded:</strong> {stats['degraded']} |
| </div> |
| <div> |
| <strong>🔴 Offline:</strong> {stats['offline']} |
| </div> |
| <div> |
| <strong>Availability:</strong> {online_pct}% |
| </div> |
| <div> |
| <strong>Avg Response:</strong> {stats['avg_response_time']:.0f} ms |
| </div> |
| </div> |
| <div style="margin-top: 15px; background: #e0e0e0; border-radius: 5px; height: 25px; overflow: hidden;"> |
| <div style="background: {color}; height: 100%; width: {online_pct}%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;"> |
| {online_pct}% |
| </div> |
| </div> |
| </div> |
| """ |
|
|
| html_output += "</div>" |
|
|
| return html_output |
|
|
|
|
| def get_category_chart(): |
| """Create category availability chart""" |
| global current_results |
|
|
| if not current_results: |
| return go.Figure() |
|
|
| category_stats = monitor.get_category_stats(current_results) |
|
|
| categories = list(category_stats.keys()) |
| online_pcts = [stats['online_percentage'] for stats in category_stats.values()] |
| avg_times = [stats['avg_response_time'] for stats in category_stats.values()] |
|
|
| fig = go.Figure() |
|
|
| fig.add_trace(go.Bar( |
| name='Availability %', |
| x=categories, |
| y=online_pcts, |
| marker_color='lightblue', |
| text=[f"{pct:.1f}%" for pct in online_pcts], |
| textposition='auto', |
| yaxis='y1' |
| )) |
|
|
| fig.add_trace(go.Scatter( |
| name='Avg Response Time (ms)', |
| x=categories, |
| y=avg_times, |
| mode='lines+markers', |
| marker=dict(size=10, color='red'), |
| line=dict(width=2, color='red'), |
| yaxis='y2' |
| )) |
|
|
| fig.update_layout( |
| title='Category Health Overview', |
| xaxis=dict(title='Category'), |
| yaxis=dict(title='Availability %', side='left', range=[0, 100]), |
| yaxis2=dict(title='Response Time (ms)', side='right', overlaying='y'), |
| hovermode='x unified', |
| template='plotly_white', |
| height=500 |
| ) |
|
|
| return fig |
|
|
|
|
| |
| |
| |
|
|
| def get_uptime_chart(provider_name=None, hours=24): |
| """Get uptime chart for provider(s)""" |
| try: |
| |
| status_data = db.get_recent_status(provider_name=provider_name, hours=hours) |
|
|
| if not status_data: |
| fig = go.Figure() |
| fig.add_annotation( |
| text="No historical data available. Data will accumulate over time.", |
| xref="paper", yref="paper", |
| x=0.5, y=0.5, showarrow=False, |
| font=dict(size=16) |
| ) |
| return fig |
|
|
| |
| df = pd.DataFrame(status_data) |
| df['timestamp'] = pd.to_datetime(df['timestamp'], unit='s') |
| df['uptime_value'] = df['status'].apply(lambda x: 100 if x == 'online' else 0) |
|
|
| |
| if provider_name: |
| providers = [provider_name] |
| else: |
| providers = df['provider_name'].unique()[:10] |
|
|
| fig = go.Figure() |
|
|
| for provider in providers: |
| provider_df = df[df['provider_name'] == provider] |
|
|
| |
| provider_df = provider_df.set_index('timestamp') |
| resampled = provider_df['uptime_value'].resample('1H').mean() |
|
|
| fig.add_trace(go.Scatter( |
| name=provider, |
| x=resampled.index, |
| y=resampled.values, |
| mode='lines+markers', |
| line=dict(width=2), |
| marker=dict(size=6) |
| )) |
|
|
| fig.update_layout( |
| title=f'Uptime History - Last {hours} Hours', |
| xaxis_title='Time', |
| yaxis_title='Uptime %', |
| hovermode='x unified', |
| template='plotly_white', |
| height=500, |
| yaxis=dict(range=[0, 105]) |
| ) |
|
|
| return fig |
|
|
| except Exception as e: |
| logger.error(f"Error creating uptime chart: {e}") |
| fig = go.Figure() |
| fig.add_annotation( |
| text=f"Error: {str(e)}", |
| xref="paper", yref="paper", |
| x=0.5, y=0.5, showarrow=False |
| ) |
| return fig |
|
|
|
|
| def get_response_time_chart(provider_name=None, hours=24): |
| """Get response time trends""" |
| try: |
| status_data = db.get_recent_status(provider_name=provider_name, hours=hours) |
|
|
| if not status_data: |
| return go.Figure() |
|
|
| df = pd.DataFrame(status_data) |
| df['timestamp'] = pd.to_datetime(df['timestamp'], unit='s') |
|
|
| if provider_name: |
| providers = [provider_name] |
| else: |
| providers = df['provider_name'].unique()[:10] |
|
|
| fig = go.Figure() |
|
|
| for provider in providers: |
| provider_df = df[df['provider_name'] == provider] |
|
|
| fig.add_trace(go.Scatter( |
| name=provider, |
| x=provider_df['timestamp'], |
| y=provider_df['response_time'], |
| mode='lines', |
| line=dict(width=2) |
| )) |
|
|
| fig.update_layout( |
| title=f'Response Time Trends - Last {hours} Hours', |
| xaxis_title='Time', |
| yaxis_title='Response Time (ms)', |
| hovermode='x unified', |
| template='plotly_white', |
| height=500 |
| ) |
|
|
| return fig |
|
|
| except Exception as e: |
| logger.error(f"Error creating response time chart: {e}") |
| return go.Figure() |
|
|
|
|
| def get_incident_log(hours=24): |
| """Get incident log""" |
| try: |
| incidents = db.get_incident_history(hours=hours) |
|
|
| if not incidents: |
| return pd.DataFrame({'Message': ['No incidents in the selected period']}) |
|
|
| df_data = [] |
| for incident in incidents: |
| df_data.append({ |
| 'Timestamp': incident['start_time'], |
| 'Provider': incident['provider_name'], |
| 'Category': incident['category'], |
| 'Type': incident['incident_type'], |
| 'Severity': incident['severity'], |
| 'Description': incident['description'], |
| 'Duration': f"{incident.get('duration_seconds', 0)} sec" if incident.get('resolved') else 'Ongoing', |
| 'Status': '✅ Resolved' if incident.get('resolved') else '⚠️ Active' |
| }) |
|
|
| return pd.DataFrame(df_data) |
|
|
| except Exception as e: |
| logger.error(f"Error getting incident log: {e}") |
| return pd.DataFrame({'Error': [str(e)]}) |
|
|
|
|
| |
| |
| |
|
|
| def test_endpoint(provider_name, custom_endpoint="", use_proxy=False): |
| """Test a specific endpoint""" |
| try: |
| resources = config.get_all_resources() |
| resource = next((r for r in resources if r['name'] == provider_name), None) |
|
|
| if not resource: |
| return "Provider not found", "" |
|
|
| |
| if custom_endpoint: |
| resource = resource.copy() |
| resource['endpoint'] = custom_endpoint |
|
|
| |
| result = asyncio.run(monitor.check_endpoint(resource, use_proxy=use_proxy)) |
|
|
| |
| status_emoji = result.get_badge() |
| status_text = f""" |
| ## Test Results |
| |
| **Provider:** {result.provider_name} |
| **Status:** {status_emoji} {result.status.value.upper()} |
| **Response Time:** {result.response_time:.2f} ms |
| **Status Code:** {result.status_code or 'N/A'} |
| **Endpoint:** `{result.endpoint_tested}` |
| |
| ### Details |
| """ |
|
|
| if result.error_message: |
| status_text += f"\n**Error:** {result.error_message}\n" |
| else: |
| status_text += "\n✅ Request successful\n" |
|
|
| |
| if result.status != HealthStatus.ONLINE: |
| status_text += "\n### Troubleshooting Hints\n" |
| if result.status_code == 403: |
| status_text += "- Check API key validity\n- Verify rate limits\n- Try using CORS proxy\n" |
| elif result.status_code == 429: |
| status_text += "- Rate limit exceeded\n- Wait before retrying\n- Consider using backup provider\n" |
| elif result.error_message and "timeout" in result.error_message.lower(): |
| status_text += "- Connection timeout\n- Service may be slow or down\n- Try increasing timeout\n" |
| else: |
| status_text += "- Verify endpoint URL\n- Check network connectivity\n- Review API documentation\n" |
|
|
| return status_text, json.dumps(result.to_dict(), indent=2) |
|
|
| except Exception as e: |
| return f"Error testing endpoint: {str(e)}", "" |
|
|
|
|
| def get_example_query(provider_name): |
| """Get example query for a provider""" |
| resources = config.get_all_resources() |
| resource = next((r for r in resources if r['name'] == provider_name), None) |
|
|
| if not resource: |
| return "" |
|
|
| example = resource.get('example', '') |
| if example: |
| return f"Example:\n{example}" |
|
|
| |
| endpoint = resource.get('endpoint', '') |
| url = resource.get('url', '') |
|
|
| if endpoint: |
| return f"Example URL:\n{url}{endpoint}" |
|
|
| return f"Base URL:\n{url}" |
|
|
|
|
| |
| |
| |
|
|
| def update_refresh_interval(interval_minutes): |
| """Update background refresh interval""" |
| try: |
| scheduler.update_interval(interval_minutes) |
| return f"✅ Refresh interval updated to {interval_minutes} minutes" |
| except Exception as e: |
| return f"❌ Error: {str(e)}" |
|
|
|
|
| def clear_all_cache(): |
| """Clear all caches""" |
| try: |
| monitor.clear_cache() |
| return "✅ Cache cleared successfully" |
| except Exception as e: |
| return f"❌ Error: {str(e)}" |
|
|
|
|
| def get_config_info(): |
| """Get configuration information""" |
| stats = config.stats() |
|
|
| info = f""" |
| ## Configuration Overview |
| |
| **Total API Resources:** {stats['total_resources']} |
| **Categories:** {stats['total_categories']} |
| **Free Resources:** {stats['free_resources']} |
| **Tier 1 (Critical):** {stats['tier1_count']} |
| **Tier 2 (Important):** {stats['tier2_count']} |
| **Tier 3 (Others):** {stats['tier3_count']} |
| **Configured API Keys:** {stats['api_keys_count']} |
| **CORS Proxies:** {stats['cors_proxies_count']} |
| |
| ### Categories |
| {', '.join(stats['categories'])} |
| |
| ### Scheduler Status |
| **Running:** {scheduler.is_running()} |
| **Interval:** {scheduler.interval_minutes} minutes |
| **Last Run:** {scheduler.last_run_time.strftime('%Y-%m-%d %H:%M:%S') if scheduler.last_run_time else 'Never'} |
| """ |
|
|
| return info |
|
|
|
|
| |
| |
| |
|
|
| def build_interface(): |
| """Build the complete Gradio interface""" |
|
|
| with gr.Blocks( |
| theme=gr.themes.Soft(primary_hue="purple", secondary_hue="blue"), |
| title="Crypto API Monitor", |
| css=""" |
| .gradio-container { |
| max-width: 1400px !important; |
| } |
| """ |
| ) as app: |
|
|
| gr.Markdown(""" |
| # 📊 Cryptocurrency API Monitor |
| ### Real-time health monitoring for 162+ crypto API endpoints |
| *Production-ready | Auto-refreshing | Persistent metrics | Multi-tier monitoring* |
| """) |
|
|
| |
| with gr.Tab("📊 Real-Time Dashboard"): |
| with gr.Row(): |
| refresh_btn = gr.Button("🔄 Refresh Now", variant="primary", size="lg") |
| export_btn = gr.Button("💾 Export CSV", size="lg") |
|
|
| with gr.Row(): |
| category_filter = gr.Dropdown( |
| choices=["All"] + config.get_categories(), |
| value="All", |
| label="Filter by Category" |
| ) |
| status_filter = gr.Dropdown( |
| choices=["All", "Online", "Degraded", "Offline"], |
| value="All", |
| label="Filter by Status" |
| ) |
| tier_filter = gr.Dropdown( |
| choices=["All", "Tier 1", "Tier 2", "Tier 3"], |
| value="All", |
| label="Filter by Tier" |
| ) |
|
|
| summary_cards = gr.HTML() |
| status_table = gr.DataFrame( |
| headers=["Status", "Provider", "Category", "Response Time", "Last Check", "Code"], |
| wrap=True |
| ) |
| download_file = gr.File(label="Download CSV", visible=False) |
|
|
| refresh_btn.click( |
| fn=refresh_dashboard, |
| inputs=[category_filter, status_filter, tier_filter], |
| outputs=[status_table, summary_cards] |
| ) |
|
|
| export_btn.click( |
| fn=export_current_status, |
| outputs=download_file |
| ) |
|
|
| |
| with gr.Tab("📁 Category View"): |
| gr.Markdown("### API Resources by Category") |
|
|
| with gr.Row(): |
| refresh_cat_btn = gr.Button("🔄 Refresh Categories", variant="primary") |
|
|
| category_overview = gr.HTML() |
| category_chart = gr.Plot() |
|
|
| refresh_cat_btn.click( |
| fn=get_category_overview, |
| outputs=category_overview |
| ) |
|
|
| refresh_cat_btn.click( |
| fn=get_category_chart, |
| outputs=category_chart |
| ) |
|
|
| |
| with gr.Tab("📈 Health History"): |
| gr.Markdown("### Historical Performance & Incidents") |
|
|
| with gr.Row(): |
| history_provider = gr.Dropdown( |
| choices=["All"] + [r['name'] for r in config.get_all_resources()], |
| value="All", |
| label="Select Provider" |
| ) |
| history_hours = gr.Slider( |
| minimum=1, |
| maximum=168, |
| value=24, |
| step=1, |
| label="Time Range (hours)" |
| ) |
| refresh_history_btn = gr.Button("🔄 Refresh", variant="primary") |
|
|
| uptime_chart = gr.Plot(label="Uptime History") |
| response_chart = gr.Plot(label="Response Time Trends") |
| incident_table = gr.DataFrame(label="Incident Log") |
|
|
| def update_history(provider, hours): |
| prov = None if provider == "All" else provider |
| uptime = get_uptime_chart(prov, hours) |
| response = get_response_time_chart(prov, hours) |
| incidents = get_incident_log(hours) |
| return uptime, response, incidents |
|
|
| refresh_history_btn.click( |
| fn=update_history, |
| inputs=[history_provider, history_hours], |
| outputs=[uptime_chart, response_chart, incident_table] |
| ) |
|
|
| |
| with gr.Tab("🔧 Test Endpoint"): |
| gr.Markdown("### Test Individual API Endpoints") |
|
|
| with gr.Row(): |
| test_provider = gr.Dropdown( |
| choices=[r['name'] for r in config.get_all_resources()], |
| label="Select Provider" |
| ) |
| test_btn = gr.Button("▶️ Run Test", variant="primary") |
|
|
| with gr.Row(): |
| custom_endpoint = gr.Textbox( |
| label="Custom Endpoint (optional)", |
| placeholder="/api/endpoint" |
| ) |
| use_proxy_check = gr.Checkbox(label="Use CORS Proxy", value=False) |
|
|
| example_query = gr.Markdown() |
| test_result = gr.Markdown() |
| test_json = gr.Code(label="JSON Response", language="json") |
|
|
| test_provider.change( |
| fn=get_example_query, |
| inputs=test_provider, |
| outputs=example_query |
| ) |
|
|
| test_btn.click( |
| fn=test_endpoint, |
| inputs=[test_provider, custom_endpoint, use_proxy_check], |
| outputs=[test_result, test_json] |
| ) |
|
|
| |
| with gr.Tab("⚙️ Configuration"): |
| gr.Markdown("### System Configuration & Settings") |
|
|
| config_info = gr.Markdown() |
|
|
| with gr.Row(): |
| refresh_interval = gr.Slider( |
| minimum=1, |
| maximum=60, |
| value=5, |
| step=1, |
| label="Auto-refresh Interval (minutes)" |
| ) |
| update_interval_btn = gr.Button("💾 Update Interval") |
|
|
| interval_status = gr.Textbox(label="Status", interactive=False) |
|
|
| with gr.Row(): |
| clear_cache_btn = gr.Button("🗑️ Clear Cache") |
| cache_status = gr.Textbox(label="Cache Status", interactive=False) |
|
|
| gr.Markdown("### API Keys Management") |
| gr.Markdown(""" |
| API keys are loaded from environment variables in Hugging Face Spaces. |
| Go to **Settings > Repository secrets** to add keys: |
| - `ETHERSCAN_KEY` |
| - `BSCSCAN_KEY` |
| - `TRONSCAN_KEY` |
| - `CMC_KEY` (CoinMarketCap) |
| - `CRYPTOCOMPARE_KEY` |
| """) |
|
|
| |
| app.load(fn=get_config_info, outputs=config_info) |
|
|
| update_interval_btn.click( |
| fn=update_refresh_interval, |
| inputs=refresh_interval, |
| outputs=interval_status |
| ) |
|
|
| clear_cache_btn.click( |
| fn=clear_all_cache, |
| outputs=cache_status |
| ) |
|
|
| |
| app.load( |
| fn=refresh_dashboard, |
| inputs=[category_filter, status_filter, tier_filter], |
| outputs=[status_table, summary_cards] |
| ) |
|
|
| return app |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| logger.info("Starting Crypto API Monitor...") |
|
|
| |
| scheduler.start() |
|
|
| |
| app = build_interface() |
|
|
| |
| app.launch( |
| server_name="0.0.0.0", |
| server_port=7860, |
| share=False, |
| show_error=True |
| ) |
|
|