- Introduced a new `system_health_constants.py` file to define thresholds and constants for system health metrics. - Refactored existing system health callbacks into modular components, enhancing maintainability and clarity. - Implemented dynamic loading of time range options in `charts.py`, improving flexibility in time range selection. - Added detailed documentation for new callback functions, ensuring clarity on their purpose and usage. - Enhanced error handling and logging practices across the new modules to ensure robust monitoring and debugging capabilities. These changes significantly improve the architecture and maintainability of the system health monitoring features, aligning with project standards for modularity and performance.
234 lines
9.3 KiB
Python
234 lines
9.3 KiB
Python
from dash import Output, Input, State, html, callback_context, no_update
|
|
import dash_bootstrap_components as dbc
|
|
from utils.logger import get_logger
|
|
from database.connection import DatabaseManager
|
|
from datetime import datetime, timedelta
|
|
|
|
from dashboard.callbacks.system_health_modules.common_health_utils import _check_data_collection_service_running
|
|
from config.constants.system_health_constants import (
|
|
DATA_FRESHNESS_RECENT_MINUTES,
|
|
DATA_FRESHNESS_STALE_HOURS
|
|
)
|
|
|
|
logger = get_logger("default_logger")
|
|
|
|
|
|
def register_data_collection_callbacks(app):
|
|
"""Register data collection status and metrics callbacks."""
|
|
|
|
# Detailed Data Collection Service Status
|
|
@app.callback(
|
|
[Output('data-collection-service-status', 'children'),
|
|
Output('data-collection-metrics', 'children')],
|
|
[Input('interval-component', 'n_intervals'),
|
|
Input('refresh-data-status-btn', 'n_clicks')]
|
|
)
|
|
def update_data_collection_status(n_intervals, refresh_clicks):
|
|
"""Update detailed data collection service status and metrics."""
|
|
try:
|
|
service_status = _get_data_collection_service_status()
|
|
metrics = _get_data_collection_metrics()
|
|
|
|
return service_status, metrics
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating data collection status: {e}")
|
|
error_div = dbc.Alert(
|
|
f"Error: {str(e)}",
|
|
color="danger",
|
|
dismissable=True
|
|
)
|
|
return error_div, error_div
|
|
|
|
# Individual Collectors Status
|
|
@app.callback(
|
|
Output('individual-collectors-status', 'children'),
|
|
[Input('interval-component', 'n_intervals'),
|
|
Input('refresh-data-status-btn', 'n_clicks')]
|
|
)
|
|
def update_individual_collectors_status(n_intervals, refresh_clicks):
|
|
"""Update individual data collector health status."""
|
|
try:
|
|
return _get_individual_collectors_status()
|
|
except Exception as e:
|
|
logger.error(f"Error updating individual collectors status: {e}")
|
|
return dbc.Alert(
|
|
f"Error: {str(e)}",
|
|
color="danger",
|
|
dismissable=True
|
|
)
|
|
|
|
# Data Collection Details Modal
|
|
@app.callback(
|
|
[Output("collection-details-modal", "is_open"),
|
|
Output("collection-details-content", "children")],
|
|
[Input("view-collection-details-btn", "n_clicks")],
|
|
[State("collection-details-modal", "is_open")]
|
|
)
|
|
def toggle_collection_details_modal(n_clicks, is_open):
|
|
"""Toggle and populate the collection details modal."""
|
|
if n_clicks:
|
|
details_content = _get_collection_details_content()
|
|
return not is_open, details_content
|
|
return is_open, no_update
|
|
|
|
# Collection Logs Modal
|
|
@app.callback(
|
|
[Output("collection-logs-modal", "is_open"),
|
|
Output("collection-logs-content", "children")],
|
|
[Input("view-collection-logs-btn", "n_clicks"),
|
|
Input("refresh-logs-btn", "n_clicks")],
|
|
[State("collection-logs-modal", "is_open")],
|
|
prevent_initial_call=True
|
|
)
|
|
def toggle_collection_logs_modal(logs_clicks, refresh_clicks, is_open):
|
|
"""Toggle and populate the collection logs modal."""
|
|
ctx = callback_context
|
|
if not ctx.triggered:
|
|
return is_open, no_update
|
|
|
|
triggered_id = ctx.triggered_id
|
|
if triggered_id in ["view-collection-logs-btn", "refresh-logs-btn"]:
|
|
logs_content = _get_collection_logs_content()
|
|
return True, logs_content
|
|
|
|
return is_open, no_update
|
|
|
|
@app.callback(
|
|
Output("collection-logs-modal", "is_open", allow_duplicate=True),
|
|
Input("close-logs-modal", "n_clicks"),
|
|
State("collection-logs-modal", "is_open"),
|
|
prevent_initial_call=True
|
|
)
|
|
def close_logs_modal(n_clicks, is_open):
|
|
if n_clicks:
|
|
return not is_open
|
|
return is_open
|
|
|
|
|
|
def _get_data_collection_service_status() -> html.Div:
|
|
"""Get detailed data collection service status."""
|
|
try:
|
|
is_running = _check_data_collection_service_running()
|
|
current_time = datetime.now().strftime('%H:%M:%S')
|
|
|
|
if is_running:
|
|
status_badge = dbc.Badge("Service Running", color="success", className="me-2")
|
|
status_text = html.P("Data collection service is actively collecting market data.", className="mb-0")
|
|
details = html.Div()
|
|
else:
|
|
status_badge = dbc.Badge("Service Stopped", color="danger", className="me-2")
|
|
status_text = html.P("Data collection service is not running.", className="text-danger")
|
|
details = html.Div([
|
|
html.P("To start the service, run:", className="mt-2 mb-1"),
|
|
html.Code("python scripts/start_data_collection.py")
|
|
])
|
|
|
|
return html.Div([
|
|
dbc.Row([
|
|
dbc.Col(status_badge, width="auto"),
|
|
dbc.Col(html.P(f"Checked: {current_time}", className="text-muted mb-0"), width="auto")
|
|
], align="center", className="mb-2"),
|
|
status_text,
|
|
details
|
|
])
|
|
except Exception as e:
|
|
return dbc.Alert(f"Error checking status: {e}", color="danger")
|
|
|
|
|
|
def _get_data_collection_metrics() -> html.Div:
|
|
"""Get data collection metrics."""
|
|
try:
|
|
db_manager = DatabaseManager()
|
|
db_manager.initialize()
|
|
|
|
with db_manager.get_session() as session:
|
|
from sqlalchemy import text
|
|
candles_count = session.execute(text("SELECT COUNT(*) FROM market_data")).scalar() or 0
|
|
tickers_count = session.execute(text("SELECT COUNT(*) FROM raw_trades WHERE data_type = 'ticker'")).scalar() or 0
|
|
latest_market_data = session.execute(text("SELECT MAX(timestamp) FROM market_data")).scalar()
|
|
latest_raw_data = session.execute(text("SELECT MAX(timestamp) FROM raw_trades")).scalar()
|
|
|
|
latest_data = max(d for d in [latest_market_data, latest_raw_data] if d) if any([latest_market_data, latest_raw_data]) else None
|
|
|
|
if latest_data:
|
|
time_diff = datetime.utcnow() - (latest_data.replace(tzinfo=None) if latest_data.tzinfo else latest_data)
|
|
if time_diff < timedelta(minutes=DATA_FRESHNESS_RECENT_MINUTES):
|
|
freshness_badge = dbc.Badge(f"Fresh ({time_diff.seconds // 60}m ago)", color="success")
|
|
elif time_diff < timedelta(hours=DATA_FRESHNESS_STALE_HOURS):
|
|
freshness_badge = dbc.Badge(f"Recent ({time_diff.seconds // 60}m ago)", color="warning")
|
|
else:
|
|
freshness_badge = dbc.Badge(f"Stale ({time_diff.total_seconds() // 3600:.1f}h ago)", color="danger")
|
|
else:
|
|
freshness_badge = dbc.Badge("No data", color="secondary")
|
|
|
|
return html.Div([
|
|
dbc.Row([
|
|
dbc.Col(html.Strong("Candles:")),
|
|
dbc.Col(f"{candles_count:,}", className="text-end")
|
|
]),
|
|
dbc.Row([
|
|
dbc.Col(html.Strong("Tickers:")),
|
|
dbc.Col(f"{tickers_count:,}", className="text-end")
|
|
]),
|
|
dbc.Row([
|
|
dbc.Col(html.Strong("Data Freshness:")),
|
|
dbc.Col(freshness_badge, className="text-end")
|
|
])
|
|
])
|
|
|
|
except Exception as e:
|
|
return dbc.Alert(f"Error loading metrics: {e}", color="danger")
|
|
|
|
|
|
def _get_individual_collectors_status() -> html.Div:
|
|
"""Get individual data collector status."""
|
|
try:
|
|
return dbc.Alert([
|
|
html.P("Individual collector health data will be displayed here when the data collection service is running.", className="mb-2"),
|
|
html.Hr(),
|
|
html.P("To start monitoring, run the following command:", className="mb-1"),
|
|
html.Code("python scripts/start_data_collection.py")
|
|
], color="info")
|
|
|
|
except Exception as e:
|
|
return dbc.Alert(f"Error checking collector status: {e}", color="danger")
|
|
|
|
|
|
def _get_collection_details_content() -> html.Div:
|
|
"""Get detailed collection information for modal."""
|
|
try:
|
|
return html.Div([
|
|
html.H5("Data Collection Service Details"),
|
|
html.P("Comprehensive data collection service information would be displayed here."),
|
|
html.Hr(),
|
|
html.H6("Configuration"),
|
|
html.P("Service configuration details..."),
|
|
html.H6("Performance Metrics"),
|
|
html.P("Detailed performance analytics..."),
|
|
html.H6("Health Status"),
|
|
html.P("Individual collector health information...")
|
|
])
|
|
except Exception as e:
|
|
return dbc.Alert(f"Error loading details: {e}", color="danger")
|
|
|
|
|
|
def _get_collection_logs_content() -> str:
|
|
"""Get recent collection service logs."""
|
|
try:
|
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
return f"""[{current_time}] INFO - Data Collection Service Logs
|
|
|
|
Recent log entries would be displayed here from the data collection service.
|
|
|
|
This would include:
|
|
- Service startup/shutdown events
|
|
- Collector connection status changes
|
|
- Data collection statistics
|
|
- Error messages and warnings
|
|
- Performance metrics
|
|
|
|
To view real logs, check the logs/ directory or configure log file monitoring.
|
|
"""
|
|
except Exception as e:
|
|
return f"Error loading logs: {str(e)}" |