From 0e7d909202e8e5bc03475001a985f6c8af867ce9 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Thu, 19 Sep 2024 09:01:40 +0530 Subject: [PATCH 01/46] chore: basic prometheus end point created --- src/routes/__init__.py | 1 + src/routes/prometheus.py | 49 ++++++++++++++++++++++++++++++++++++++++ src/utils.py | 9 +++----- 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 src/routes/prometheus.py diff --git a/src/routes/__init__.py b/src/routes/__init__.py index 5c7c545..dd77fcb 100644 --- a/src/routes/__init__.py +++ b/src/routes/__init__.py @@ -19,3 +19,4 @@ from src.routes.experimental import experimental_bp from src.routes.error_handlers import error_handlers_bp from src.routes.profile import profile_bp +from src.routes.prometheus import prometheus_bp diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py new file mode 100644 index 0000000..3a404e2 --- /dev/null +++ b/src/routes/prometheus.py @@ -0,0 +1,49 @@ +from flask import blueprints +from werkzeug.middleware.dispatcher import DispatcherMiddleware +from prometheus_client import make_wsgi_app, Counter, Gauge +import threading +import time +from src.config import app +from src.utils import _get_system_info + +# Define the Prometheus Blueprint +prometheus_bp = blueprints.Blueprint('prometheus', __name__) + +# Initialize Prometheus metrics +cpu_usage_metric = Gauge('cpu_usage_percentage', 'Current CPU usage percentage') +memory_usage_metric = Gauge('memory_usage_percentage', 'Current memory usage percentage') +disk_usage_metric = Gauge('disk_usage_percentage', 'Disk usage percentage') +network_sent_metric = Gauge('network_bytes_sent', 'Total network bytes sent') +network_recv_metric = Gauge('network_bytes_received', 'Total network bytes received') +request_count = Counter('http_requests_total', 'Total HTTP requests made') + +def collect_metrics(): + """ + Collect system metrics and update Prometheus Gauges. + Runs in a separate thread and updates metrics every 5 seconds. + """ + while True: + # Gather system information + system_info = _get_system_info() + + # Update Prometheus metrics + cpu_usage_metric.set(system_info['cpu_percent']) + memory_usage_metric.set(system_info['memory_percent']) + disk_usage_metric.set(system_info['disk_percent']) + network_sent_metric.set(system_info['network_sent']) + network_recv_metric.set(system_info['network_received']) + + # Increment HTTP request counter + request_count.inc() + + # Sleep for 5 seconds before the next collection + time.sleep(5) + +# Start the metrics collection in a background thread +metrics_thread = threading.Thread(target=collect_metrics, daemon=True) +metrics_thread.start() + +# Expose the /metrics endpoint for Prometheus to scrape metrics +app.wsgi_app = DispatcherMiddleware(app.wsgi_app, { + '/metrics': make_wsgi_app() # Serve Prometheus metrics at /metrics +}) diff --git a/src/utils.py b/src/utils.py index 14f3bb9..f9ea87d 100644 --- a/src/utils.py +++ b/src/utils.py @@ -482,11 +482,9 @@ def _get_system_info(): Returns: dict: System information dictionary with various system metrics. """ - boot_time = get_cached_value('boot_time', lambda: datetime.datetime.fromtimestamp(psutil.boot_time())) - uptime_dict = get_cached_value('uptime', lambda: format_uptime(datetime.datetime.now() - boot_time)) - disk_total = get_cached_value("disk_total", get_disk_total) - memory_available = get_cached_value("memory_available", get_memory_available) - # Gathering fresh system information + + disk_total = get_disk_total() + memory_available = get_memory_available() battery_data = check_battery_status() memory_info = psutil.virtual_memory() disk_info = psutil.disk_usage('/') @@ -518,7 +516,6 @@ def _get_system_info(): 'timestamp': datetime.datetime.now(), } # update uptime dictionary - info.update(uptime_dict) return info From aa621a5d2c2a6b070f069bcb519922a2b8cdfcf2 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Thu, 19 Sep 2024 12:30:54 +0530 Subject: [PATCH 02/46] optimzed the prometheus endpooint --- src/routes/prometheus.py | 44 +++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index 3a404e2..45864f0 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -1,13 +1,12 @@ -from flask import blueprints -from werkzeug.middleware.dispatcher import DispatcherMiddleware -from prometheus_client import make_wsgi_app, Counter, Gauge +from flask import Blueprint, Response +from prometheus_client import Counter, Gauge, generate_latest import threading import time from src.config import app from src.utils import _get_system_info # Define the Prometheus Blueprint -prometheus_bp = blueprints.Blueprint('prometheus', __name__) +prometheus_bp = Blueprint('prometheus', __name__) # Initialize Prometheus metrics cpu_usage_metric = Gauge('cpu_usage_percentage', 'Current CPU usage percentage') @@ -23,27 +22,30 @@ def collect_metrics(): Runs in a separate thread and updates metrics every 5 seconds. """ while True: - # Gather system information - system_info = _get_system_info() - - # Update Prometheus metrics - cpu_usage_metric.set(system_info['cpu_percent']) - memory_usage_metric.set(system_info['memory_percent']) - disk_usage_metric.set(system_info['disk_percent']) - network_sent_metric.set(system_info['network_sent']) - network_recv_metric.set(system_info['network_received']) - - # Increment HTTP request counter - request_count.inc() + try: + # Gather system information + system_info = _get_system_info() + + # Update Prometheus metrics + cpu_usage_metric.set(system_info['cpu_percent']) + memory_usage_metric.set(system_info['memory_percent']) + disk_usage_metric.set(system_info['disk_percent']) + network_sent_metric.set(system_info['network_sent']) + network_recv_metric.set(system_info['network_received']) + + # Increment HTTP request counter + request_count.inc() + except Exception as e: + print(f"Error collecting metrics: {e}") # Sleep for 5 seconds before the next collection - time.sleep(5) + time.sleep(10) # Start the metrics collection in a background thread metrics_thread = threading.Thread(target=collect_metrics, daemon=True) metrics_thread.start() -# Expose the /metrics endpoint for Prometheus to scrape metrics -app.wsgi_app = DispatcherMiddleware(app.wsgi_app, { - '/metrics': make_wsgi_app() # Serve Prometheus metrics at /metrics -}) +# Define a route to serve Prometheus metrics +@app.route('/metrics') +def metrics(): + return Response(generate_latest(), mimetype='text/plain') From 1ca46502852df09ab6fcbc219e111b0ca4f7d9a3 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Thu, 19 Sep 2024 13:41:01 +0530 Subject: [PATCH 03/46] Add Prometheus metrics for CPU temperature, frequency, battery percentage, and dashboard memory usage --- requirements.txt | 5 ++++- src/routes/prometheus.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e708956..8b92f8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,4 +23,7 @@ watchdog==5.0.2 requests==2.32.3 # Speedtest CLI for testing internet speed -speedtest-cli==2.1.3 \ No newline at end of file +speedtest-cli==2.1.3 + +# prometheus client for monitoring +prometheus_client==0.20.0 \ No newline at end of file diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index 45864f0..880997c 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -15,6 +15,11 @@ network_sent_metric = Gauge('network_bytes_sent', 'Total network bytes sent') network_recv_metric = Gauge('network_bytes_received', 'Total network bytes received') request_count = Counter('http_requests_total', 'Total HTTP requests made') +cpu_temp_metric = Gauge('cpu_temperature', 'Current CPU temperature') +cpu_frequency_metric = Gauge('cpu_frequency', 'Current CPU frequency') +battery_percentage_metric = Gauge('battery_percentage', 'Current battery percentage') +dashboard_memory_usage_metric = Gauge('dashboard_memory_usage_percentage', 'Current memory usage percentage') + def collect_metrics(): """ @@ -32,6 +37,11 @@ def collect_metrics(): disk_usage_metric.set(system_info['disk_percent']) network_sent_metric.set(system_info['network_sent']) network_recv_metric.set(system_info['network_received']) + cpu_temp_metric.set(system_info['current_temp']) + cpu_frequency_metric.set(system_info['cpu_frequency']) + battery_percentage_metric.set(system_info['battery_percent']) + dashboard_memory_usage_metric.set(system_info['dashboard_memory_usage']) + # Increment HTTP request counter request_count.inc() From b021d11316c98507a45ded9370540067385592b0 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Thu, 19 Sep 2024 14:14:26 +0530 Subject: [PATCH 04/46] Refactor system info logging and metrics collection - Refactor the `log_system_info` function to check if system info logging is enabled before scheduling the next log. - Add a new function `is_logging_enabled` to check if system info logging is enabled in the general settings. - Add a new function `schedule_next_log` to schedule the next logging event after a specified interval. - Update the `log_system_info_to_db` function to also update Prometheus metrics with the latest system information. - Add a new function `update_prometheus_metrics` to update Prometheus metrics with the latest system information. - Add a new function `store_system_info_in_db` to store the collected system information into the database. - Remove the unused `monitor_settings` function. Fix memory info route and template - Add the `dashboard_memory_usage` value to the `memory_info` dictionary in the `memory_usage` function. - Update the `memory_info.html` template to include the `dashboard_memory` card component. Remove unused code from Prometheus route - Remove the unused code for collecting system metrics and updating Prometheus gauges. --- src/background_task/log_system_info.py | 132 ++++++++++++++-------- src/routes/memory_info.py | 3 +- src/routes/prometheus.py | 52 +-------- src/templates/info_pages/memory_info.html | 3 + 4 files changed, 92 insertions(+), 98 deletions(-) diff --git a/src/background_task/log_system_info.py b/src/background_task/log_system_info.py index a9c47fa..1075de2 100644 --- a/src/background_task/log_system_info.py +++ b/src/background_task/log_system_info.py @@ -6,102 +6,142 @@ from src.logger import logger from src.models import GeneralSettings, SystemInformation from sqlalchemy.exc import SQLAlchemyError -from src.logger import logger +from prometheus_client import Counter, Gauge -# Flag to check if logging is already scheduled +# Flag to track if logging is already scheduled is_logging_scheduled = False +# Initialize Prometheus metrics +metrics = { + 'cpu_usage_metric': Gauge('cpu_usage_percentage', 'Current CPU usage percentage'), + 'memory_usage_metric': Gauge('memory_usage_percentage', 'Current memory usage percentage'), + 'disk_usage_metric': Gauge('disk_usage_percentage', 'Disk usage percentage'), + 'network_sent_metric': Gauge('network_bytes_sent', 'Total network bytes sent'), + 'network_recv_metric': Gauge('network_bytes_received', 'Total network bytes received'), + 'cpu_temp_metric': Gauge('cpu_temperature', 'Current CPU temperature'), + 'cpu_frequency_metric': Gauge('cpu_frequency', 'Current CPU frequency'), + 'battery_percentage_metric': Gauge('battery_percentage', 'Current battery percentage'), + 'dashboard_memory_usage_metric': Gauge('dashboard_memory_usage_percentage', 'Current memory usage percentage'), + 'request_count': Counter('http_requests_total', 'Total HTTP requests made') +} def log_system_info(): """ Logs system information at regular intervals based on the general settings. - This function checks if logging is still active before each logging event. + This function checks if logging is enabled and schedules the next log if active. """ global is_logging_scheduled with app.app_context(): try: - # Fetch the general settings to check if logging is enabled - general_settings = GeneralSettings.query.first() - is_logging_system_info = ( - general_settings.is_logging_system_info if general_settings else False - ) - - if not is_logging_system_info: + if not is_logging_enabled(): logger.info("System info logging has been stopped.") - is_logging_scheduled = False # Reset the flag if logging stops + is_logging_scheduled = False return log_system_info_to_db() logger.debug("System information logged successfully.") - - # Schedule the next log after 60 seconds - Timer(60, log_system_info).start() + schedule_next_log() except Exception as e: logger.error(f"Error during system info logging: {e}", exc_info=True) - is_logging_scheduled = False # Reset the flag in case of an error + is_logging_scheduled = False + + +def is_logging_enabled(): + """ + Checks if system info logging is enabled in the general settings. + """ + try: + general_settings = GeneralSettings.query.first() + return general_settings.is_logging_system_info if general_settings else False + except SQLAlchemyError as e: + logger.error(f"Error fetching general settings: {e}", exc_info=True) + return False + + +def schedule_next_log(interval=60): + """ + Schedules the next logging event after the specified interval (in seconds). + """ + Timer(interval, log_system_info).start() def log_system_info_to_db(): """ - Fetches system information and logs it to the database. + Fetches system information and logs it to the database and updates Prometheus metrics. """ with app.app_context(): try: system_info = _get_system_info() - system_log = SystemInformation( - cpu_percent=system_info["cpu_percent"], - memory_percent=system_info["memory_percent"], - battery_percent=system_info["battery_percent"], - network_sent=system_info["network_sent"], - network_received=system_info["network_received"], - dashboard_memory_usage=system_info["dashboard_memory_usage"], - cpu_frequency=system_info["cpu_frequency"], - current_temp=system_info["current_temp"], - timestamp=datetime.datetime.now(), - ) - db.session.add(system_log) - db.session.commit() + + # Update Prometheus metrics + update_prometheus_metrics(system_info) + + # Store system information in the database + store_system_info_in_db(system_info) logger.info("System information logged to database.") except SQLAlchemyError as db_err: - logger.error( - f"Database error while logging system info: {db_err}", exc_info=True - ) + logger.error(f"Database error while logging system info: {db_err}", exc_info=True) db.session.rollback() except Exception as e: logger.error(f"Failed to log system information: {e}", exc_info=True) + +def update_prometheus_metrics(system_info): + """ + Updates Prometheus metrics with the latest system information. + """ + metrics['cpu_usage_metric'].set(system_info['cpu_percent']) + metrics['memory_usage_metric'].set(system_info['memory_percent']) + metrics['disk_usage_metric'].set(system_info['disk_percent']) + metrics['network_sent_metric'].set(system_info['network_sent']) + metrics['network_recv_metric'].set(system_info['network_received']) + metrics['cpu_temp_metric'].set(system_info['current_temp']) + metrics['cpu_frequency_metric'].set(system_info['cpu_frequency']) + metrics['battery_percentage_metric'].set(system_info['battery_percent']) + metrics['dashboard_memory_usage_metric'].set(system_info['dashboard_memory_usage']) + metrics['request_count'].inc() + + +def store_system_info_in_db(system_info): + """ + Stores the collected system information into the database. + """ + system_log = SystemInformation( + cpu_percent=system_info["cpu_percent"], + memory_percent=system_info["memory_percent"], + battery_percent=system_info["battery_percent"], + network_sent=system_info["network_sent"], + network_received=system_info["network_received"], + dashboard_memory_usage=system_info["dashboard_memory_usage"], + cpu_frequency=system_info["cpu_frequency"], + current_temp=system_info["current_temp"], + timestamp=datetime.datetime.now(), + ) + db.session.add(system_log) + db.session.commit() + + def monitor_settings(): """ Monitors application general settings for changes and controls system logging dynamically. - This function runs periodically to check for updates to logging settings. """ global is_logging_scheduled with app.app_context(): try: - # Fetch the general settings - general_settings = GeneralSettings.query.first() - - # Check if logging should be active or not - is_logging_system_info = ( - general_settings.is_logging_system_info if general_settings else False - ) - if is_logging_system_info: + if is_logging_enabled(): logger.info("System logging enabled. Starting system info logging.") - - # Schedule logging only if not already scheduled if not is_logging_scheduled: logger.debug("Scheduling system info logging.") Timer(0, log_system_info).start() is_logging_scheduled = True else: logger.info("System logging disabled. Stopping system info logging.") - is_logging_scheduled = False # Reset the flag if logging is disabled + is_logging_scheduled = False - # Check settings periodically (every 10 seconds) + # Recheck settings every 10 seconds Timer(10, monitor_settings).start() except SQLAlchemyError as db_err: logger.error(f"Error fetching settings: {db_err}", exc_info=True) - diff --git a/src/routes/memory_info.py b/src/routes/memory_info.py index db6fb16..f6348cc 100644 --- a/src/routes/memory_info.py +++ b/src/routes/memory_info.py @@ -2,7 +2,7 @@ from flask_login import login_required from src.config import app -from src.utils import get_cached_value, get_memory_percent, get_memory_available, get_memory_used, get_swap_memory_info +from src.utils import get_cached_value, get_memory_percent, get_memory_available, get_memory_used, get_swap_memory_info, get_flask_memory_usage from src.routes.helper.common_helper import check_page_toggle memory_info_bp = blueprints.Blueprint("memory_usage", __name__) @@ -17,6 +17,7 @@ def memory_usage(): "memory_percent": get_memory_percent(), "memory_available": memory_available, "memory_used": get_memory_used(), + 'dashboard_memory_usage': get_flask_memory_usage(), } swap_info = get_swap_memory_info() diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index 880997c..4357521 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -1,60 +1,10 @@ from flask import Blueprint, Response -from prometheus_client import Counter, Gauge, generate_latest -import threading -import time +from prometheus_client import generate_latest from src.config import app -from src.utils import _get_system_info # Define the Prometheus Blueprint prometheus_bp = Blueprint('prometheus', __name__) -# Initialize Prometheus metrics -cpu_usage_metric = Gauge('cpu_usage_percentage', 'Current CPU usage percentage') -memory_usage_metric = Gauge('memory_usage_percentage', 'Current memory usage percentage') -disk_usage_metric = Gauge('disk_usage_percentage', 'Disk usage percentage') -network_sent_metric = Gauge('network_bytes_sent', 'Total network bytes sent') -network_recv_metric = Gauge('network_bytes_received', 'Total network bytes received') -request_count = Counter('http_requests_total', 'Total HTTP requests made') -cpu_temp_metric = Gauge('cpu_temperature', 'Current CPU temperature') -cpu_frequency_metric = Gauge('cpu_frequency', 'Current CPU frequency') -battery_percentage_metric = Gauge('battery_percentage', 'Current battery percentage') -dashboard_memory_usage_metric = Gauge('dashboard_memory_usage_percentage', 'Current memory usage percentage') - - -def collect_metrics(): - """ - Collect system metrics and update Prometheus Gauges. - Runs in a separate thread and updates metrics every 5 seconds. - """ - while True: - try: - # Gather system information - system_info = _get_system_info() - - # Update Prometheus metrics - cpu_usage_metric.set(system_info['cpu_percent']) - memory_usage_metric.set(system_info['memory_percent']) - disk_usage_metric.set(system_info['disk_percent']) - network_sent_metric.set(system_info['network_sent']) - network_recv_metric.set(system_info['network_received']) - cpu_temp_metric.set(system_info['current_temp']) - cpu_frequency_metric.set(system_info['cpu_frequency']) - battery_percentage_metric.set(system_info['battery_percent']) - dashboard_memory_usage_metric.set(system_info['dashboard_memory_usage']) - - - # Increment HTTP request counter - request_count.inc() - except Exception as e: - print(f"Error collecting metrics: {e}") - - # Sleep for 5 seconds before the next collection - time.sleep(10) - -# Start the metrics collection in a background thread -metrics_thread = threading.Thread(target=collect_metrics, daemon=True) -metrics_thread.start() - # Define a route to serve Prometheus metrics @app.route('/metrics') def metrics(): diff --git a/src/templates/info_pages/memory_info.html b/src/templates/info_pages/memory_info.html index bdce209..afcf94e 100644 --- a/src/templates/info_pages/memory_info.html +++ b/src/templates/info_pages/memory_info.html @@ -14,6 +14,9 @@

Memory
{% include 'card_comp/memory/used.html' %}
+
+ {% include 'card_comp/memory/dashboard_memory.html' %} +
{% include 'card_comp/memory/swap_total.html' %}
From a0def2a45cb321ef3d6eb480fe94a9d5ed303dd7 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Fri, 20 Sep 2024 11:25:51 +0530 Subject: [PATCH 05/46] feat: support for monitor external api/file added --- app.py | 9 ++-- src/background_task/__init__.py | 3 +- src/background_task/prometheus_helper.py | 52 +++++++++++++++++++++ src/config.py | 3 +- src/models/__init__.py | 1 + src/models/prometheus_model.py | 7 +++ src/routes/prometheus.py | 40 ++++++++++++++-- src/templates/prometheus/add_file_path.html | 35 ++++++++++++++ 8 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 src/background_task/prometheus_helper.py create mode 100644 src/models/prometheus_model.py create mode 100644 src/templates/prometheus/add_file_path.html diff --git a/app.py b/app.py index 3f5f623..3b43bd9 100644 --- a/app.py +++ b/app.py @@ -1,13 +1,14 @@ from src.config import app from src import routes -from src.background_task import start_website_monitoring, monitor_settings +from src.background_task import start_website_monitoring, monitor_settings, fetch_file_metrics_task import os # background thread to monitor system settings changes print("FLASK_ENV: ", os.getenv('FLASK_ENV')) -if os.getenv('FLASK_ENV') == 'production': - monitor_settings() # Starts monitoring for system logging changes - start_website_monitoring() # Starts pinging active websites +# if os.getenv('FLASK_ENV') == 'production': +start_website_monitoring() # Starts pinging active websites +fetch_file_metrics_task() +monitor_settings() # Starts monitoring for system logging changes if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/src/background_task/__init__.py b/src/background_task/__init__.py index 27d4659..b41fbf5 100644 --- a/src/background_task/__init__.py +++ b/src/background_task/__init__.py @@ -1,4 +1,5 @@ from src.background_task.monitor_website import start_website_monitoring from src.background_task.log_system_info import monitor_settings +from src.background_task.prometheus_helper import fetch_file_metrics_task -__all__ = ["start_website_monitoring", "monitor_settings"] \ No newline at end of file +__all__ = ["start_website_monitoring", "monitor_settings", "fetch_file_metrics_task"] \ No newline at end of file diff --git a/src/background_task/prometheus_helper.py b/src/background_task/prometheus_helper.py new file mode 100644 index 0000000..898ef82 --- /dev/null +++ b/src/background_task/prometheus_helper.py @@ -0,0 +1,52 @@ +from flask import Flask, Response +from prometheus_client import Gauge, generate_latest +import os +import time +from src.models import ExternalMonitornig +from src.config import app +import threading +from src.logger import logger + +# Define a Gauge metric with a label 'key' to store multiple values from the file +file_metric = Gauge('external_metrics', 'Value read from file for each key', ['key']) + +def read_file_and_update_metric(file_path: str) -> None: + """Reads a file and updates metrics based on its content.""" + if os.path.exists(file_path): + with open(file_path, 'r') as file: + for line in file: + try: + key, value = line.strip().split(':') + file_metric.labels(key.strip()).set(float(value.strip())) + except ValueError as ve: + logger.error(f"Value error processing line '{line}': {ve}") + except Exception as e: + logger.error(f"Error processing line '{line}': {e}") + else: + logger.warning(f"File {file_path} does not exist") + +def fetch_file_metrics(sleep_duration: int = 5) -> None: + """Background task to read file paths from the database and update metrics.""" + while True: + with app.app_context(): + file_paths = ExternalMonitornig.query.all() + current_keys = {sample.labels['key'] for sample in file_metric.collect()[0].samples} + new_keys = set() + + for file_path in file_paths: + logger.info(f"Reading file: {file_path.file_path}") + read_file_and_update_metric(file_path.file_path) + new_keys.update({key.strip() for line in open(file_path.file_path) for key in line.split(':')[0].strip()}) + + # Remove metrics for keys that are no longer in the database + for key in current_keys: + if key not in new_keys: + file_metric.remove(key) + + time.sleep(sleep_duration) + +def fetch_file_metrics_task() -> None: + """Starts the background task in a separate thread.""" + thread = threading.Thread(target=fetch_file_metrics) + thread.daemon = True + thread.start() diff --git a/src/config.py b/src/config.py index 6e7c5f5..25d636d 100644 --- a/src/config.py +++ b/src/config.py @@ -26,7 +26,8 @@ os.makedirs(DB_DIR, exist_ok=True) # Configure the SQLite database -app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{DB_DIR}/systemguard.db" +# app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{DB_DIR}/systemguard.db" +app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///systemguard.db" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SECRET_KEY'] = 'secret' diff --git a/src/models/__init__.py b/src/models/__init__.py index da25b54..16780a5 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -9,6 +9,7 @@ from src.models.system_information import SystemInformation from src.models.user_profile import UserProfile from src.models.monitored_website import MonitoredWebsite +from src.models.prometheus_model import ExternalMonitornig from flask_login import current_user from src.logger import logger from werkzeug.security import generate_password_hash diff --git a/src/models/prometheus_model.py b/src/models/prometheus_model.py new file mode 100644 index 0000000..087312a --- /dev/null +++ b/src/models/prometheus_model.py @@ -0,0 +1,7 @@ +from src.config import db + + +class ExternalMonitornig(db.Model): + id = db.Column(db.Integer, primary_key=True) + file_path = db.Column(db.String(100), nullable=False) + is_active = db.Column(db.Boolean, default=True) diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index 4357521..4d29a23 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -1,6 +1,9 @@ -from flask import Blueprint, Response +from flask import Blueprint, Response, request, jsonify, render_template, flash, redirect, url_for from prometheus_client import generate_latest -from src.config import app + +from src.config import app, db +from src.models import ExternalMonitornig + # Define the Prometheus Blueprint prometheus_bp = Blueprint('prometheus', __name__) @@ -8,4 +11,35 @@ # Define a route to serve Prometheus metrics @app.route('/metrics') def metrics(): - return Response(generate_latest(), mimetype='text/plain') + output = generate_latest() + output = '\n'.join([line for line in output.decode().split('\n') if not line.startswith('#') and line]) + return Response(output, mimetype='text/plain') + +# post request to add file path +@app.route('/prometheus/add_file_path', methods=['GET', 'POST']) +def add_file_path(): + if request.method == 'POST': + + file_path = request.form.get('file_path') + # save into the ExternalMonitornig table + new_task = ExternalMonitornig(file_path=file_path) + # commit the changes + db.session.add(new_task) + db.session.commit() + + # read_file_and_update_metric(file_path=file_path) + return redirect(url_for('add_file_path')) + + data = ExternalMonitornig.query.all() + return render_template('prometheus/add_file_path.html', data=data) + + +# post request to delete file path +@app.route('/prometheus/delete_file_path/', methods=['POST']) +def delete_file_path(id): + file_path = ExternalMonitornig.query.get_or_404(id) + db.session.delete(file_path) + db.session.commit() + flash('File path deleted successfully!', 'success') + return redirect(url_for('add_file_path')) + diff --git a/src/templates/prometheus/add_file_path.html b/src/templates/prometheus/add_file_path.html new file mode 100644 index 0000000..f3d7bc6 --- /dev/null +++ b/src/templates/prometheus/add_file_path.html @@ -0,0 +1,35 @@ + + + + + + + External File for Monitoring Prometheus + + + +
+

Add File Path

+
+ + + +
+
+
+ + + + {% if data %} + {% for item in data %} +
+

{{ item.file_path }}

+
+ +
+
+ {% endfor %} + {% endif %} + + + \ No newline at end of file From 5aef69da386712a6abc2ae7294e67fa32fb9b948 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Fri, 20 Sep 2024 11:44:29 +0530 Subject: [PATCH 06/46] web page improved --- src/routes/prometheus.py | 37 +++++++++++++---- src/static/css/external_monitoring.css | 41 +++++++++++++++++++ src/templates/prometheus/add_file_path.html | 35 ---------------- .../prometheus/external_monitoring.html | 41 +++++++++++++++++++ 4 files changed, 112 insertions(+), 42 deletions(-) create mode 100644 src/static/css/external_monitoring.css delete mode 100644 src/templates/prometheus/add_file_path.html create mode 100644 src/templates/prometheus/external_monitoring.html diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index 4d29a23..102e30f 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -1,13 +1,26 @@ -from flask import Blueprint, Response, request, jsonify, render_template, flash, redirect, url_for +from flask import Blueprint, Response, request, render_template, flash, redirect, url_for from prometheus_client import generate_latest +import os from src.config import app, db from src.models import ExternalMonitornig - # Define the Prometheus Blueprint prometheus_bp = Blueprint('prometheus', __name__) + +def is_valid_file(file_path: str) -> bool: + """Checks if a file is valid and have key-value pairs separated by a colon.""" + with open(file_path, 'r') as file: + for line in file: + if not line.strip(): + continue + + if ':' not in line: + return False + + return True + # Define a route to serve Prometheus metrics @app.route('/metrics') def metrics(): @@ -16,11 +29,21 @@ def metrics(): return Response(output, mimetype='text/plain') # post request to add file path -@app.route('/prometheus/add_file_path', methods=['GET', 'POST']) -def add_file_path(): +@app.route('/prometheus/external_monitoring', methods=['GET', 'POST']) +def external_monitoring(): if request.method == 'POST': file_path = request.form.get('file_path') + + if not os.path.exists(file_path): + flash('File path does not exist', 'danger') + return redirect(url_for('external_monitoring')) + + # check file path and is_valid + if not is_valid_file(file_path): + flash('Invalid file format. File should have key-value pairs separated by a colon.', 'danger') + return redirect(url_for('external_monitoring')) + # save into the ExternalMonitornig table new_task = ExternalMonitornig(file_path=file_path) # commit the changes @@ -28,10 +51,10 @@ def add_file_path(): db.session.commit() # read_file_and_update_metric(file_path=file_path) - return redirect(url_for('add_file_path')) + return redirect(url_for('external_monitoring')) data = ExternalMonitornig.query.all() - return render_template('prometheus/add_file_path.html', data=data) + return render_template('prometheus/external_monitoring.html', data=data) # post request to delete file path @@ -41,5 +64,5 @@ def delete_file_path(id): db.session.delete(file_path) db.session.commit() flash('File path deleted successfully!', 'success') - return redirect(url_for('add_file_path')) + return redirect(url_for('external_monitoring')) diff --git a/src/static/css/external_monitoring.css b/src/static/css/external_monitoring.css new file mode 100644 index 0000000..40a9575 --- /dev/null +++ b/src/static/css/external_monitoring.css @@ -0,0 +1,41 @@ +body { + background-color: #f8f9fa; +} + +.container { + max-width: 600px; + margin: auto; +} + +.card { + border: 1px solid #ced4da; + border-radius: 0.5rem; +} + +.card-title { + font-weight: bold; +} + +.message { + color: #28a745; /* Success message color */ +} + +.list-group-item { + background-color: #ffffff; + border: 1px solid #e9ecef; + border-radius: 0.5rem; +} + +.list-group-item:hover { + background-color: #f1f1f1; +} + +.btn-primary { + background-color: #007bff; + border: none; +} + +.btn-danger { + background-color: #dc3545; + border: none; +} diff --git a/src/templates/prometheus/add_file_path.html b/src/templates/prometheus/add_file_path.html deleted file mode 100644 index f3d7bc6..0000000 --- a/src/templates/prometheus/add_file_path.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - External File for Monitoring Prometheus - - - -
-

Add File Path

-
- - - -
-
-
- - - - {% if data %} - {% for item in data %} -
-

{{ item.file_path }}

-
- -
-
- {% endfor %} - {% endif %} - - - \ No newline at end of file diff --git a/src/templates/prometheus/external_monitoring.html b/src/templates/prometheus/external_monitoring.html new file mode 100644 index 0000000..a819a10 --- /dev/null +++ b/src/templates/prometheus/external_monitoring.html @@ -0,0 +1,41 @@ +{% extends "base/base.html" %} +{% block title %}External Monitoring{% endblock %} +{% block extra_head %} + +{% endblock %} +{% block content %} + +
+

External Monitoring

+ {% include 'ext/message.html' %} +
+
+
+
+ + +
+ +
+
+
+
+ + {% if data %} +
+
Monitoring File Paths
+ {% for item in data %} +
+
+ {{ item.file_path }} +
+ +
+
+
+ {% endfor %} +
+ {% endif %} +
+ +{% endblock %} From c9fb721fecb19285c6e9994ade2565288475346c Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Fri, 20 Sep 2024 17:55:13 +0530 Subject: [PATCH 07/46] UI improvement --- app.py | 10 +-- requirements.txt | 5 +- src/static/css/style.css | 10 +++ src/static/js/graphs.js | 6 +- src/templates/base/base.html | 2 +- src/templates/ext/footer.html | 6 +- src/templates/graphs/graphs.html | 101 +++++++++++++--------- src/templates/info_pages/cpu_info.html | 2 +- src/templates/info_pages/process.html | 2 +- src/templates/settings/control_panel.html | 5 +- 10 files changed, 88 insertions(+), 61 deletions(-) diff --git a/app.py b/app.py index 3b43bd9..a70b239 100644 --- a/app.py +++ b/app.py @@ -4,11 +4,11 @@ import os # background thread to monitor system settings changes -print("FLASK_ENV: ", os.getenv('FLASK_ENV')) -# if os.getenv('FLASK_ENV') == 'production': -start_website_monitoring() # Starts pinging active websites -fetch_file_metrics_task() -monitor_settings() # Starts monitoring for system logging changes +# print("FLASK_ENV: ", os.getenv('FLASK_ENV')) +# # if os.getenv('FLASK_ENV') == 'production': +# start_website_monitoring() # Starts pinging active websites +# fetch_file_metrics_task() +# monitor_settings() # Starts monitoring for system logging changes if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/requirements.txt b/requirements.txt index 8b92f8d..27d8253 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,4 +26,7 @@ requests==2.32.3 speedtest-cli==2.1.3 # prometheus client for monitoring -prometheus_client==0.20.0 \ No newline at end of file +prometheus_client==0.20.0 + +# influxdb-client==1.46.0 +influxdb-client==1.46.0 \ No newline at end of file diff --git a/src/static/css/style.css b/src/static/css/style.css index a06d4bd..57b7e11 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -46,6 +46,16 @@ body { background-position: center center; } +.content { + margin: 0 auto; /* Center the content */ + padding: 1rem 5rem; + background-color: #f9f9f9; /* Light background color */ + border-radius: 8px; /* Rounded corners */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */ + font-family: 'Arial', sans-serif; /* Font family */ + line-height: 1.6; /* Improve readability */ +} + .header { background-color: var(--color-dark); color: var(--color-white); diff --git a/src/static/js/graphs.js b/src/static/js/graphs.js index ba2f32a..e0f8035 100644 --- a/src/static/js/graphs.js +++ b/src/static/js/graphs.js @@ -1,11 +1,7 @@ // Variables to store chart instances let cpuTimeChart, memoryTimeChart, batteryTimeChart, networkTimeChart, dashboardMemoryTimeChart, cpuFrequencyTimeChart, currentTempTimeChart; -function stringToDate(dateString) { - const date = new Date(dateString); - return date; -} - +// Function to fetch data and render charts function fetchDataAndRenderCharts() { // Retrieve stored filter value from local storage or set default value const storedFilterValue = localStorage.getItem('filterValue') || 5; diff --git a/src/templates/base/base.html b/src/templates/base/base.html index 19b960a..2c4cd8f 100644 --- a/src/templates/base/base.html +++ b/src/templates/base/base.html @@ -15,7 +15,7 @@ {% include 'ext/navbar.html' %} -
+
{% block content %} {% endblock %}
diff --git a/src/templates/ext/footer.html b/src/templates/ext/footer.html index 65db537..beefb3e 100644 --- a/src/templates/ext/footer.html +++ b/src/templates/ext/footer.html @@ -1,12 +1,12 @@ -
+ -
\ No newline at end of file + \ No newline at end of file diff --git a/src/templates/graphs/graphs.html b/src/templates/graphs/graphs.html index 3615516..c0eeb73 100644 --- a/src/templates/graphs/graphs.html +++ b/src/templates/graphs/graphs.html @@ -11,11 +11,12 @@ {% block content %}

- {{ title }} Metrics

+ {{ title }} Metrics +


-
+

Current Server Time & Time Zone

@@ -26,7 +27,7 @@

Current Server Time & Time Zone


-
+
@@ -51,65 +52,79 @@

Current Server Time & Time Zone


- -
-
-

CPU Usage Over Time

- +
+
+
+

CPU Usage Over Time

+ +
+
- -
-
-
-

Memory Usage Over Time

- +
+
+

Memory Usage Over Time

+ +
+
-
-
-
-

Battery Percentage Over Time

- +
+
+
+

Battery Percentage Over Time

+ +
+
- -
-
-
-

Network Sent & Received Over Time

- +
+
+

Network Sent & Received Over Time

+ +
+
-
-
-
-

Dashboard Memory Usage Over Time

- +
+
+
+

Memory Usage Over Time(APP)

+ +
+
- -
-
-
-

CPU Frequency Over Time

- +
+
+

CPU Frequency Over Time

+ +
+
-
-
-
-

Current Temperature Over Time

- +
+
+
+

Current Temperature Over Time

+ +
+
-
{% endblock %} {% block extra_scripts %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/src/templates/info_pages/cpu_info.html b/src/templates/info_pages/cpu_info.html index dff9226..6c1358a 100644 --- a/src/templates/info_pages/cpu_info.html +++ b/src/templates/info_pages/cpu_info.html @@ -8,7 +8,7 @@

CPU Details

-
+
{% include 'card_comp/cpu/frequency.html' %} diff --git a/src/templates/info_pages/process.html b/src/templates/info_pages/process.html index 72db064..4c52a27 100644 --- a/src/templates/info_pages/process.html +++ b/src/templates/info_pages/process.html @@ -4,7 +4,7 @@ {% endblock %} {% block content %} -
+

Top Processes on System

diff --git a/src/templates/settings/control_panel.html b/src/templates/settings/control_panel.html index c673848..ea24983 100644 --- a/src/templates/settings/control_panel.html +++ b/src/templates/settings/control_panel.html @@ -137,7 +137,10 @@
More Utilities
Ping Website - + + + External API Monitoring +
From acb3843743857efcf295a9e23e83103b3546400c Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Fri, 20 Sep 2024 21:07:50 +0530 Subject: [PATCH 08/46] feat: influx db support added --- src/background_task/log_system_info.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/background_task/log_system_info.py b/src/background_task/log_system_info.py index 1075de2..dd55c45 100644 --- a/src/background_task/log_system_info.py +++ b/src/background_task/log_system_info.py @@ -8,6 +8,8 @@ from sqlalchemy.exc import SQLAlchemyError from prometheus_client import Counter, Gauge +from src.logger import logger +from src.config import influx_client, bucket, write_api # Flag to track if logging is already scheduled is_logging_scheduled = False From 16cd51c42b453d646e5fdba5dcb1c29da9af3f26 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Fri, 20 Sep 2024 21:08:04 +0530 Subject: [PATCH 09/46] feat: influx support added --- .gitignore | 2 + app.py | 6 +- install_influx.sh | 59 +++++++++++++++ src/background_task/log_system_info.py | 36 ++++++++- src/config.py | 28 ++++++- src/models/__init__.py | 61 +++++++-------- src/routes/api.py | 101 +++++++++++++++++++++++++ src/static/js/graphs.js | 2 +- 8 files changed, 258 insertions(+), 37 deletions(-) create mode 100644 install_influx.sh diff --git a/.gitignore b/.gitignore index 48e90cb..5f58245 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,5 @@ cython_debug/ predefine_user.json src/assets/predefine_user.json + +influxdb_data \ No newline at end of file diff --git a/app.py b/app.py index a70b239..fc1a6b7 100644 --- a/app.py +++ b/app.py @@ -6,9 +6,9 @@ # background thread to monitor system settings changes # print("FLASK_ENV: ", os.getenv('FLASK_ENV')) # # if os.getenv('FLASK_ENV') == 'production': -# start_website_monitoring() # Starts pinging active websites -# fetch_file_metrics_task() -# monitor_settings() # Starts monitoring for system logging changes +start_website_monitoring() # Starts pinging active websites +fetch_file_metrics_task() +monitor_settings() # Starts monitoring for system logging changes if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/install_influx.sh b/install_influx.sh new file mode 100644 index 0000000..c439d8a --- /dev/null +++ b/install_influx.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Define variables +INFLUXDB_VERSION="latest" +CONTAINER_NAME="influxdb" +NETWORK_NAME="influx_network" +DATA_DIR="./influxdb_data" +INFLUXDB_USER="admin" # Change this to your desired username +INFLUXDB_PASSWORD="admin_passwor" # Change this to your desired password +INFLUXDB_ORG="systemguard" # Change this to your desired organization name +INFLUXDB_BUCKET="system_metrics" # Change this to your desired initial bucket (database) name +INFLUXDB_TOKEN="GixPn4ZmYuPyzPtj2XivfALmtHUaatBTK85ZjSe78P8NdbM_J9426tTPoqnbQDCok_NgBreOaqzdmoGxJ3UfsA==" + +# Export the INFLUXDB_TOKEN to be used later in your app +export "INFLUXDB_TOKEN"=$INFLUXDB_TOKEN + +# Create a data directory for InfluxDB +rm -rf $DATA_DIR +mkdir -p $DATA_DIR + +# Stop and remove existing InfluxDB container if it exists +if [ "$(docker ps -aq -f name=$CONTAINER_NAME)" ]; then + echo "Stopping existing InfluxDB container..." + docker stop $CONTAINER_NAME + echo "Removing existing InfluxDB container..." + docker rm -f $CONTAINER_NAME +fi + +# Kill any process using port 8086 +if sudo lsof -i :8086; then + echo "Killing process using port 8086..." + sudo fuser -k 8086/tcp +fi + +# Create a Docker network +echo "Creating Docker network..." +docker network create $NETWORK_NAME || true # Avoid error if the network already exists + +# Pull the InfluxDB Docker image +echo "Pulling InfluxDB Docker image..." +docker pull influxdb:$INFLUXDB_VERSION + +# Run the InfluxDB container with authentication and initial setup to skip onboarding +echo "Running InfluxDB container..." +docker run -d \ + --name $CONTAINER_NAME \ + --network $NETWORK_NAME \ + -p 8086:8086 \ + -v $PWD/$DATA_DIR:/var/lib/influxdb2 \ + -e DOCKER_INFLUXDB_INIT_MODE=setup \ + -e DOCKER_INFLUXDB_INIT_USERNAME=$INFLUXDB_USER \ + -e DOCKER_INFLUXDB_INIT_PASSWORD=$INFLUXDB_PASSWORD \ + -e DOCKER_INFLUXDB_INIT_ORG=$INFLUXDB_ORG \ + -e DOCKER_INFLUXDB_INIT_BUCKET=$INFLUXDB_BUCKET \ + -e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=$INFLUXDB_TOKEN \ + influxdb:$INFLUXDB_VERSION + +# Output completion message +echo "InfluxDB setup completed! Access it at http://localhost:8086 with your credentials." diff --git a/src/background_task/log_system_info.py b/src/background_task/log_system_info.py index dd55c45..9de681a 100644 --- a/src/background_task/log_system_info.py +++ b/src/background_task/log_system_info.py @@ -7,6 +7,8 @@ from src.models import GeneralSettings, SystemInformation from sqlalchemy.exc import SQLAlchemyError from prometheus_client import Counter, Gauge +import os, time +from influxdb_client import Point, WritePrecision from src.logger import logger from src.config import influx_client, bucket, write_api @@ -78,9 +80,11 @@ def log_system_info_to_db(): # Update Prometheus metrics update_prometheus_metrics(system_info) + # Store system information in InfluxDB + store_system_info_in_influxdb(system_info) # Store system information in the database - store_system_info_in_db(system_info) + # store_system_info_in_db(system_info) logger.info("System information logged to database.") except SQLAlchemyError as db_err: @@ -124,6 +128,36 @@ def store_system_info_in_db(system_info): db.session.add(system_log) db.session.commit() +def store_system_info_in_influxdb(system_info): + """ + Stores the collected system information into the InfluxDB with proper error handling. + """ + try: + # Create a data point for system information + point = ( + Point("system_info") + .tag("host", os.environ.get("HOSTNAME", "unknown")) + .field("cpu_percent", system_info["cpu_percent"]) + .field("memory_percent", system_info["memory_percent"]) + .field("battery_percent", system_info["battery_percent"]) + .field("network_sent", system_info["network_sent"]) + .field("network_received", system_info["network_received"]) + .field("dashboard_memory_usage", system_info["dashboard_memory_usage"]) + .field("cpu_frequency", system_info["cpu_frequency"]) + .field("current_temp", system_info["current_temp"]) + .time(int(time.time() * 1_000_000_000), WritePrecision.NS) # Nanosecond precision + ) + + # Write the data point to InfluxDB + write_api.write(bucket=bucket, record=point) + logger.info("Successfully wrote system information to InfluxDB") + + except ValueError as ve: + logger.error(f"Value error while storing system info: {ve}") + except Exception as e: + logger.error(f"An unexpected error occurred: {e}", exc_info=True) + + def monitor_settings(): """ diff --git a/src/config.py b/src/config.py index 25d636d..1397f16 100644 --- a/src/config.py +++ b/src/config.py @@ -2,6 +2,10 @@ from flask import Flask, render_template from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate +import os, time +from influxdb_client import InfluxDBClient, Point, WritePrecision +from influxdb_client.client.write_api import SYNCHRONOUS + from src.logger import logger from src.helper import get_system_node_name, get_ip_address @@ -26,8 +30,8 @@ os.makedirs(DB_DIR, exist_ok=True) # Configure the SQLite database -# app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{DB_DIR}/systemguard.db" -app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///systemguard.db" +app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{DB_DIR}/systemguard.db" +# app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///systemguard.db" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SECRET_KEY'] = 'secret' @@ -35,6 +39,26 @@ db = SQLAlchemy(app) migrate = Migrate(app, db) +# influx db configuration + +INFLUXDB_TOKEN="" +print("Token: ", INFLUXDB_TOKEN) +if not INFLUXDB_TOKEN: + raise ValueError("Please set the INFLUXDB_TOKEN environment variable.") +org = "systemguard" +url = "http://localhost:8086" +bucket="_monitoring" +try: + influx_client = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) + bucket = "_monitoring" + write_api = influx_client.write_api(write_options=SYNCHRONOUS) + query_api = influx_client.query_api() + logger.info("Connected to InfluxDB successfully") + +except Exception as e: + logger.error(f"Failed to connect to InfluxDB: {e}") + raiseclient = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) + # Define global variables for templates app.jinja_env.globals.update( title=APP_NAME, diff --git a/src/models/__init__.py b/src/models/__init__.py index 16780a5..e607d3f 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -18,9 +18,39 @@ ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +# Context processor for injecting settings into templates +@app.context_processor +def inject_settings(): + if current_user.is_anonymous: + user_dashboard_settings = UserDashboardSettings(user_id=0) + card_settings = None + page_toggles_settings = None + general_settings = None + return dict( + user_dashboard_settings=user_dashboard_settings, + card_settings=card_settings, + page_toggles_settings=page_toggles_settings, + general_settings=general_settings, + ) + general_settings = GeneralSettings.query.first() + card_settings = UserCardSettings.query.filter_by(user_id=current_user.id).first() + user_dashboard_settings = UserDashboardSettings.query.filter_by( + user_id=current_user.id + ).first() # Retrieve user-specific user_dashboard_settings from DB + page_toggles_settings = PageToggleSettings.query.filter_by( + user_id=current_user.id + ).first() + all_settings = dict( + user_dashboard_settings=user_dashboard_settings, + general_settings=general_settings, + card_settings=card_settings, + page_toggles_settings=page_toggles_settings, + ) + return all_settings + with app.app_context(): # Check if tables already exist - if not db.inspect(db.engine).has_table('user_profile'): # Use an important table to check existence + if not db.inspect(db.engine).has_table('users'): # Use an important table to check existence logger.info("Creating tables") db.create_all() @@ -70,32 +100,3 @@ else: logger.info("Tables already exist. Skipping creation.") -# Context processor for injecting settings into templates -@app.context_processor -def inject_settings(): - if current_user.is_anonymous: - user_dashboard_settings = UserDashboardSettings(user_id=0) - card_settings = None - page_toggles_settings = None - general_settings = None - return dict( - user_dashboard_settings=user_dashboard_settings, - card_settings=card_settings, - page_toggles_settings=page_toggles_settings, - general_settings=general_settings, - ) - general_settings = GeneralSettings.query.first() - card_settings = UserCardSettings.query.filter_by(user_id=current_user.id).first() - user_dashboard_settings = UserDashboardSettings.query.filter_by( - user_id=current_user.id - ).first() # Retrieve user-specific user_dashboard_settings from DB - page_toggles_settings = PageToggleSettings.query.filter_by( - user_id=current_user.id - ).first() - all_settings = dict( - user_dashboard_settings=user_dashboard_settings, - general_settings=general_settings, - card_settings=card_settings, - page_toggles_settings=page_toggles_settings, - ) - return all_settings diff --git a/src/routes/api.py b/src/routes/api.py index fd9a256..c2b03e1 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -4,8 +4,10 @@ from src.models import SystemInformation, UserDashboardSettings from src.utils import _get_system_info, get_os_release_info, get_os_info, get_cached_value from datetime import datetime, timedelta +from flask import request, jsonify import gc +from src.config import query_api, bucket api_bp = blueprints.Blueprint("api", __name__) @app.route("/api/system-info", methods=["GET"]) @@ -109,6 +111,105 @@ def graph_data_api(): # Handle and log the error for debugging purposes return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 +@app.route('/api/v2/graphs_data', methods=['GET']) +@login_required +def graph_data_api_v2(): + try: + current_time = datetime.now() + # Get the time filter from query parameters + time_filter = request.args.get('filter', default='1 day') + + # Determine the start time based on the filter + time_deltas = { + '5 minutes': '-5m', + '15 minutes': '-15m', + '30 minutes': '-30m', + '1 hour': '-1h', + '3 hours': '-3h', + '6 hours': '-6h', + '12 hours': '-12h', + '1 day': '-1d', + '2 days': '-2d', + '3 days': '-3d', + '1 week': '-1w', + '1 month': '-30d', + '3 months': '-90d', + } + + # Get the start time for the query + time_range = time_deltas.get(time_filter, '-1d') + + # Build the InfluxDB query + flux_query = f""" + from(bucket: "{bucket}") + |> range(start: {time_range}) + |> filter(fn: (r) => r._measurement == "system_info") + |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") + """ + + # Execute the query + tables = query_api.query(flux_query) + + # Initialize lists for the data + time_data = [] + cpu_data = [] + memory_data = [] + battery_data = [] + network_sent_data = [] + network_received_data = [] + dashboard_memory_usage = [] + cpu_frequency = [] + current_temp = [] + + # Parse the results + for table in tables: + for record in table.records: + time_data.append(record.values.get("_time", None)) + # Extract each field by key (handle missing fields gracefully) + cpu_data.append(record.values.get("cpu_percent", None)) + memory_data.append(record.values.get("memory_percent", None)) + battery_data.append(record.values.get("battery_percent", None)) + network_sent_data.append(record.values.get("network_sent", None)) + network_received_data.append(record.values.get("network_received", None)) + dashboard_memory_usage.append(record.values.get("dashboard_memory_usage", None)) + cpu_frequency.append(record.values.get("cpu_frequency", None)) + current_temp.append(record.values.get("current_temp", None)) + + # Return the data as JSON + response = jsonify({ + "time": time_data, + "cpu": cpu_data, + "memory": memory_data, + "battery": battery_data, + "network_sent": network_sent_data, + "network_received": network_received_data, + "dashboard_memory_usage": dashboard_memory_usage, + "cpu_frequency": cpu_frequency, + "current_temp": current_temp, + "current_time": current_time + }) + + print(time_data) + # Clean up large data structures + del tables + del time_data + del cpu_data + del memory_data + del battery_data + del network_sent_data + del network_received_data + del dashboard_memory_usage + del cpu_frequency + del current_temp + + gc.collect() + + return response, 200 + + except Exception as e: + # Handle and log the error for debugging purposes + return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 + @app.route('/api/v1/refresh-interval', methods=['GET', 'POST']) @login_required def manage_refresh_interval(): diff --git a/src/static/js/graphs.js b/src/static/js/graphs.js index e0f8035..14b1fcb 100644 --- a/src/static/js/graphs.js +++ b/src/static/js/graphs.js @@ -12,7 +12,7 @@ function fetchDataAndRenderCharts() { console.log('Stored Filter Value:', storedFilterValue); // Fetch data with the selected time filter - fetch(`/api/v1/graphs_data?filter=${storedFilterValue}`) + fetch(`/api/v2/graphs_data?filter=${storedFilterValue}`) .then(response => response.json()) .then(data => { const cpuData = data.cpu; From bec922355a561f9789d5a22e6ba09ef2929d86c5 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 07:02:09 +0530 Subject: [PATCH 10/46] influx installation added --- app.py | 4 ++-- src/config.py | 4 ++-- install_influx.sh => src/scripts/install_influx.sh | 7 +++++-- 3 files changed, 9 insertions(+), 6 deletions(-) rename install_influx.sh => src/scripts/install_influx.sh (91%) diff --git a/app.py b/app.py index fc1a6b7..c057ac0 100644 --- a/app.py +++ b/app.py @@ -6,8 +6,8 @@ # background thread to monitor system settings changes # print("FLASK_ENV: ", os.getenv('FLASK_ENV')) # # if os.getenv('FLASK_ENV') == 'production': -start_website_monitoring() # Starts pinging active websites -fetch_file_metrics_task() +# start_website_monitoring() # Starts pinging active websites +# fetch_file_metrics_task() monitor_settings() # Starts monitoring for system logging changes if __name__ == "__main__": diff --git a/src/config.py b/src/config.py index 1397f16..3a5b265 100644 --- a/src/config.py +++ b/src/config.py @@ -47,10 +47,10 @@ raise ValueError("Please set the INFLUXDB_TOKEN environment variable.") org = "systemguard" url = "http://localhost:8086" -bucket="_monitoring" +bucket="system_metrics" try: influx_client = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) - bucket = "_monitoring" + bucket = "system_metrics" write_api = influx_client.write_api(write_options=SYNCHRONOUS) query_api = influx_client.query_api() logger.info("Connected to InfluxDB successfully") diff --git a/install_influx.sh b/src/scripts/install_influx.sh similarity index 91% rename from install_influx.sh rename to src/scripts/install_influx.sh index c439d8a..7c0a2df 100644 --- a/install_influx.sh +++ b/src/scripts/install_influx.sh @@ -6,10 +6,11 @@ CONTAINER_NAME="influxdb" NETWORK_NAME="influx_network" DATA_DIR="./influxdb_data" INFLUXDB_USER="admin" # Change this to your desired username -INFLUXDB_PASSWORD="admin_passwor" # Change this to your desired password +INFLUXDB_PASSWORD="admin_password" # Change this to your desired password INFLUXDB_ORG="systemguard" # Change this to your desired organization name INFLUXDB_BUCKET="system_metrics" # Change this to your desired initial bucket (database) name -INFLUXDB_TOKEN="GixPn4ZmYuPyzPtj2XivfALmtHUaatBTK85ZjSe78P8NdbM_J9426tTPoqnbQDCok_NgBreOaqzdmoGxJ3UfsA==" +# demo influxdb token | change this to your own token +INFLUXDB_TOKEN="" # Export the INFLUXDB_TOKEN to be used later in your app export "INFLUXDB_TOKEN"=$INFLUXDB_TOKEN @@ -45,6 +46,7 @@ echo "Running InfluxDB container..." docker run -d \ --name $CONTAINER_NAME \ --network $NETWORK_NAME \ + --restart=always \ -p 8086:8086 \ -v $PWD/$DATA_DIR:/var/lib/influxdb2 \ -e DOCKER_INFLUXDB_INIT_MODE=setup \ @@ -55,5 +57,6 @@ docker run -d \ -e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=$INFLUXDB_TOKEN \ influxdb:$INFLUXDB_VERSION + # Output completion message echo "InfluxDB setup completed! Access it at http://localhost:8086 with your credentials." From 1b96a8601e7882c615f0134f0d3618fb5e9952b3 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 07:14:44 +0530 Subject: [PATCH 11/46] Refactor README.md and install_influx.sh scripts --- README.md | 17 ----------------- src/scripts/install_influx.sh | 34 ++++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 79b0b91..0d86b24 100644 --- a/README.md +++ b/README.md @@ -18,23 +18,6 @@ System Guard is a Flask app designed to monitor server stats such as CPU, Memory - Role-based dashboards tailored for Developer, Admin, IT Manager, and Manager roles (upcoming feature). - Update security updates with a single click or automatically update to the latest version to simplify maintenance. -## Old Features 🚀 - -- Administrators can manage user accounts by creating, updating, or deleting users. -- Admin-level access is required for configuring settings, managing users, and adjusting security and notification - preferences. -- Historical performance data can be viewed as charts, aiding in trend analysis. -- Supports network speed testing directly from the server. -- Provides the ability to terminate resource-heavy processes with a single command. -- Real-time server metric monitoring keeps data consistently updated. -- The interface is responsive and optimized for various devices including mobile, tablets, and desktops. -- The system can automatically update to the latest version to simplify maintenance. -- Installation can be done quickly via a bash script for easy setup. -- Notifications are sent to users and admins when a process is manually terminated. -- Offers website monitoring tasks that trigger email alerts when a website becomes unavailable. -- Configurable email alerts for various actions across the server. -- Option to download historical data in CSV format for detailed analysis (upcoming feature). -- Server status monitoring with alerts for server downtime or recovery (upcoming feature). - ## Get started 🛠️ - Check the [Installation.md](/src/docs/installation.md) file for installation instructions. diff --git a/src/scripts/install_influx.sh b/src/scripts/install_influx.sh index 7c0a2df..9a698fc 100644 --- a/src/scripts/install_influx.sh +++ b/src/scripts/install_influx.sh @@ -6,18 +6,33 @@ CONTAINER_NAME="influxdb" NETWORK_NAME="influx_network" DATA_DIR="./influxdb_data" INFLUXDB_USER="admin" # Change this to your desired username -INFLUXDB_PASSWORD="admin_password" # Change this to your desired password -INFLUXDB_ORG="systemguard" # Change this to your desired organization name +INFLUXDB_PASSWORD="admin_password" # Change this to your desired password +INFLUXDB_ORG="systemguard" # Change this to your desired organization name INFLUXDB_BUCKET="system_metrics" # Change this to your desired initial bucket (database) name -# demo influxdb token | change this to your own token -INFLUXDB_TOKEN="" +INFLUXDB_TOKEN="" # Demo InfluxDB token | Change this to your own token # Export the INFLUXDB_TOKEN to be used later in your app -export "INFLUXDB_TOKEN"=$INFLUXDB_TOKEN +export INFLUXDB_TOKEN="$INFLUXDB_TOKEN" + +# Function to get the appropriate username, even when running as root +get_user_name() { + if [ "$(whoami)" = "root" ]; then + LOGNAME_USER=$(logname 2>/dev/null) + if [ $? -ne 0 ]; then + USER_NAME=$(cat /etc/passwd | grep '/home' | cut -d: -f1 | tail -n 1) + else + USER_NAME=$LOGNAME_USER + fi + else + USER_NAME=$(whoami) + fi + echo "$USER_NAME" +} + +USER_NAME=$(get_user_name) # Create a data directory for InfluxDB -rm -rf $DATA_DIR -mkdir -p $DATA_DIR +sudo -u "$USER_NAME" mkdir -p "$DATA_DIR" # Stop and remove existing InfluxDB container if it exists if [ "$(docker ps -aq -f name=$CONTAINER_NAME)" ]; then @@ -33,7 +48,7 @@ if sudo lsof -i :8086; then sudo fuser -k 8086/tcp fi -# Create a Docker network +# Create a Docker network if it doesn't exist echo "Creating Docker network..." docker network create $NETWORK_NAME || true # Avoid error if the network already exists @@ -48,7 +63,7 @@ docker run -d \ --network $NETWORK_NAME \ --restart=always \ -p 8086:8086 \ - -v $PWD/$DATA_DIR:/var/lib/influxdb2 \ + -v "$PWD/$DATA_DIR:/var/lib/influxdb2" \ -e DOCKER_INFLUXDB_INIT_MODE=setup \ -e DOCKER_INFLUXDB_INIT_USERNAME=$INFLUXDB_USER \ -e DOCKER_INFLUXDB_INIT_PASSWORD=$INFLUXDB_PASSWORD \ @@ -57,6 +72,5 @@ docker run -d \ -e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=$INFLUXDB_TOKEN \ influxdb:$INFLUXDB_VERSION - # Output completion message echo "InfluxDB setup completed! Access it at http://localhost:8086 with your credentials." From 19433945220669bb5548b7d9fb609b4dd3f6dd61 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 11:49:47 +0530 Subject: [PATCH 12/46] sqlite replaced with prometheus for time series metrics --- app.py | 9 +- prometheus_config/prometheus.yml | 7 + setup.sh | 9 +- src/background_task/log_system_info.py | 68 ++++--- src/config.py | 20 -- src/helper.py | 6 - src/influxdb_config.py | 22 ++ src/routes/api.py | 266 +++++++++++++++++-------- src/scripts/install_influx.sh | 103 +++++++--- src/scripts/prometheus.sh | 67 +++++++ src/static/js/graphs.js | 2 +- 11 files changed, 408 insertions(+), 171 deletions(-) create mode 100644 prometheus_config/prometheus.yml create mode 100644 src/influxdb_config.py create mode 100644 src/scripts/prometheus.sh diff --git a/app.py b/app.py index c057ac0..68877a7 100644 --- a/app.py +++ b/app.py @@ -4,11 +4,10 @@ import os # background thread to monitor system settings changes -# print("FLASK_ENV: ", os.getenv('FLASK_ENV')) -# # if os.getenv('FLASK_ENV') == 'production': -# start_website_monitoring() # Starts pinging active websites -# fetch_file_metrics_task() -monitor_settings() # Starts monitoring for system logging changes +if os.getenv('FLASK_ENV') == 'production': + start_website_monitoring() # Starts pinging active websites + fetch_file_metrics_task() + monitor_settings() # Starts monitoring for system logging changes if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/prometheus_config/prometheus.yml b/prometheus_config/prometheus.yml new file mode 100644 index 0000000..dac77e2 --- /dev/null +++ b/prometheus_config/prometheus.yml @@ -0,0 +1,7 @@ +global: + scrape_interval: 30s # How often Prometheus scrapes the target + +scrape_configs: + - job_name: 'flask_app_metrics' # Scraping metrics from Flask app + static_configs: + - targets: ['192.168.1.10:5050'] diff --git a/setup.sh b/setup.sh index e709310..2ebb878 100644 --- a/setup.sh +++ b/setup.sh @@ -853,10 +853,12 @@ start_server() { # Install function install() { + create_dir "$EXTRACT_DIR" + # PROMETHEUS_INSTALL_SCRIPT message_box "$APP_NAME Installer $INSATLLER_VERSION" 0 message_box "Welcome on board: $(echo "$USER_NAME" | sed 's/.*/\u&/')" 3 check_dependencies - create_dir "$EXTRACT_DIR" + message_box "Choose the installation method\nNote: Release is recommended for production use." 0 message_box "1. Release (More Stable Version)\n2. Git Repository (Pre-Release Version)\n3. Source Code (Current Directory)" 0 @@ -878,6 +880,11 @@ install() { exit 1 ;; esac + PROMETHEUS_INSTALL_SCRIPT=$(find "$EXTRACT_DIR" -name prometheus.sh) || { + log "ERROR" "Prometheus installation script not found." + exit 1 + } + sudo -u "$USER_NAME" bash "$PROMETHEUS_INSTALL_SCRIPT" start_server message_box "The $APP_NAME server is running at $HOST_URL" 0 # open_browser diff --git a/src/background_task/log_system_info.py b/src/background_task/log_system_info.py index 9de681a..0ea57bc 100644 --- a/src/background_task/log_system_info.py +++ b/src/background_task/log_system_info.py @@ -8,12 +8,13 @@ from sqlalchemy.exc import SQLAlchemyError from prometheus_client import Counter, Gauge import os, time -from influxdb_client import Point, WritePrecision from src.logger import logger -from src.config import influx_client, bucket, write_api +from src.influxdb_config import bucket, write_api +from src.utils import get_system_username # Flag to track if logging is already scheduled is_logging_scheduled = False +fetch_system_info_interval = 30 # Initialize Prometheus metrics metrics = { @@ -44,7 +45,7 @@ def log_system_info(): log_system_info_to_db() logger.debug("System information logged successfully.") - schedule_next_log() + schedule_next_log(interval=fetch_system_info_interval) except Exception as e: logger.error(f"Error during system info logging: {e}", exc_info=True) @@ -63,7 +64,7 @@ def is_logging_enabled(): return False -def schedule_next_log(interval=60): +def schedule_next_log(interval=10): """ Schedules the next logging event after the specified interval (in seconds). """ @@ -80,8 +81,9 @@ def log_system_info_to_db(): # Update Prometheus metrics update_prometheus_metrics(system_info) + # Store system information in InfluxDB - store_system_info_in_influxdb(system_info) + # store_system_info_in_influxdb(system_info) # Store system information in the database # store_system_info_in_db(system_info) @@ -128,34 +130,34 @@ def store_system_info_in_db(system_info): db.session.add(system_log) db.session.commit() -def store_system_info_in_influxdb(system_info): - """ - Stores the collected system information into the InfluxDB with proper error handling. - """ - try: - # Create a data point for system information - point = ( - Point("system_info") - .tag("host", os.environ.get("HOSTNAME", "unknown")) - .field("cpu_percent", system_info["cpu_percent"]) - .field("memory_percent", system_info["memory_percent"]) - .field("battery_percent", system_info["battery_percent"]) - .field("network_sent", system_info["network_sent"]) - .field("network_received", system_info["network_received"]) - .field("dashboard_memory_usage", system_info["dashboard_memory_usage"]) - .field("cpu_frequency", system_info["cpu_frequency"]) - .field("current_temp", system_info["current_temp"]) - .time(int(time.time() * 1_000_000_000), WritePrecision.NS) # Nanosecond precision - ) - - # Write the data point to InfluxDB - write_api.write(bucket=bucket, record=point) - logger.info("Successfully wrote system information to InfluxDB") - - except ValueError as ve: - logger.error(f"Value error while storing system info: {ve}") - except Exception as e: - logger.error(f"An unexpected error occurred: {e}", exc_info=True) +# def store_system_info_in_influxdb(system_info): +# """ +# Stores the collected system information into the InfluxDB with proper error handling. +# """ +# try: +# # Create a data point for system information +# point = ( +# Point("system_info") +# .tag("host", get_system_username()) +# .field("cpu_percent", system_info["cpu_percent"]) +# .field("memory_percent", system_info["memory_percent"]) +# .field("battery_percent", system_info["battery_percent"]) +# .field("network_sent", system_info["network_sent"]) +# .field("network_received", system_info["network_received"]) +# .field("dashboard_memory_usage", system_info["dashboard_memory_usage"]) +# .field("cpu_frequency", system_info["cpu_frequency"]) +# .field("current_temp", system_info["current_temp"]) +# .time(int(time.time() * 1_000_000_000)) +# ) + +# # Write the data point to InfluxDB +# write_api.write(bucket=bucket, record=point) +# logger.info("Successfully wrote system information to InfluxDB") + +# except ValueError as ve: +# logger.error(f"Value error while storing system info: {ve}") +# except Exception as e: +# logger.error(f"An unexpected error occurred: {e}", exc_info=True) diff --git a/src/config.py b/src/config.py index 3a5b265..e4a6048 100644 --- a/src/config.py +++ b/src/config.py @@ -39,26 +39,6 @@ db = SQLAlchemy(app) migrate = Migrate(app, db) -# influx db configuration - -INFLUXDB_TOKEN="" -print("Token: ", INFLUXDB_TOKEN) -if not INFLUXDB_TOKEN: - raise ValueError("Please set the INFLUXDB_TOKEN environment variable.") -org = "systemguard" -url = "http://localhost:8086" -bucket="system_metrics" -try: - influx_client = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) - bucket = "system_metrics" - write_api = influx_client.write_api(write_options=SYNCHRONOUS) - query_api = influx_client.query_api() - logger.info("Connected to InfluxDB successfully") - -except Exception as e: - logger.error(f"Failed to connect to InfluxDB: {e}") - raiseclient = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) - # Define global variables for templates app.jinja_env.globals.update( title=APP_NAME, diff --git a/src/helper.py b/src/helper.py index 0d36b44..33e9e33 100644 --- a/src/helper.py +++ b/src/helper.py @@ -33,12 +33,7 @@ def get_ip_address(): return ip_address except (IndexError, subprocess.CalledProcessError) as e: - print(f"Error occurred: {e}") return None - - -import os -import subprocess def check_installation_information(): # Output dictionary to store results @@ -82,7 +77,6 @@ def check_installation_information(): # Check for updates try: result = subprocess.run(["git", "status", "-uno"], capture_output=True, text=True, check=True) - print(result.stdout) if "Your branch is up to date" in result.stdout: output["update_available"] = False else: diff --git a/src/influxdb_config.py b/src/influxdb_config.py new file mode 100644 index 0000000..110fc88 --- /dev/null +++ b/src/influxdb_config.py @@ -0,0 +1,22 @@ +from influxdb_client import InfluxDBClient, Point, WritePrecision +from influxdb_client.client.write_api import SYNCHRONOUS +import os +from src.logger import logger + + +# influx db configuration +org = "systemguard" +url = "http://localhost:8086" +bucket="system_metrics" +INFLUXDB_TOKEN=os.getenv('INFLUXDB_TOKEN') +print("INFLUXDB_TOKEN: ", INFLUXDB_TOKEN) + +try: + influx_client = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) + bucket = "system_metrics" + write_api = influx_client.write_api(write_options=SYNCHRONOUS) + query_api = influx_client.query_api() + logger.info("Connected to InfluxDB successfully") +except Exception as e: + logger.error(f"Failed to connect to InfluxDB: {e}") + raiseclient = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) diff --git a/src/routes/api.py b/src/routes/api.py index c2b03e1..dab3237 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -1,4 +1,5 @@ from flask import jsonify, blueprints, request +import requests from flask_login import login_required, current_user from src.config import app, db from src.models import SystemInformation, UserDashboardSettings @@ -6,10 +7,21 @@ from datetime import datetime, timedelta from flask import request, jsonify import gc - -from src.config import query_api, bucket +from datetime import datetime, timezone api_bp = blueprints.Blueprint("api", __name__) +PROMETHEUS_URL = 'http://localhost:9090/api/v1/query_range' # Change if using a different URL or port +PROMETHEUS_METRICS = { + 'cpu': 'cpu_usage_percentage', # Adjusting to match the defined gauge + 'memory': 'memory_usage_percentage', + 'battery': 'battery_percentage', + 'network_sent': 'network_bytes_sent', + 'network_received': 'network_bytes_received', + 'dashboard_memory_usage': 'dashboard_memory_usage_percentage', + 'cpu_frequency': 'cpu_frequency', + 'current_temp': 'cpu_temperature', +} + @app.route("/api/system-info", methods=["GET"]) @login_required def system_api(): @@ -19,7 +31,7 @@ def system_api(): except Exception as e: return jsonify({"error": "An error occurred while fetching the system information", "details": str(e)}), 500 -@app.route('/api/v1/graphs_data', methods=['GET']) +@app.route('/api/v1/sqlite/graphs_data', methods=['GET']) @login_required def graph_data_api(): try: @@ -92,6 +104,19 @@ def graph_data_api(): "current_time": current_time }) + print("response", { + "time": time_data, + "cpu": cpu_data, + "memory": memory_data, + "battery": battery_data, + "network_sent": network_sent_data, + "network_received": network_received_data, + "dashboard_memory_usage": dashboard_memory_usage, + "cpu_frequency": cpu_frequency, + "current_temp": current_temp, + "current_time": current_time + }) + # Clean up large data structures del recent_system_info_entries del time_data @@ -111,105 +136,190 @@ def graph_data_api(): # Handle and log the error for debugging purposes return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 -@app.route('/api/v2/graphs_data', methods=['GET']) +# @app.route('/api/v2/influxdb/graphs_data', methods=['GET']) +# @login_required +# def graph_data_api_v2(): +# try: +# current_time = datetime.now() +# # Get the time filter from query parameters +# time_filter = request.args.get('filter', default='1 day') + +# # Determine the start time based on the filter +# time_deltas = { +# '5 minutes': '-5m', +# '15 minutes': '-15m', +# '30 minutes': '-30m', +# '1 hour': '-1h', +# '3 hours': '-3h', +# '6 hours': '-6h', +# '12 hours': '-12h', +# '1 day': '-1d', +# '2 days': '-2d', +# '3 days': '-3d', +# '1 week': '-1w', +# '1 month': '-30d', +# '3 months': '-90d', +# } + +# # Get the start time for the query +# time_range = time_deltas.get(time_filter, '-1d') + +# # Build the InfluxDB query +# flux_query = f""" +# from(bucket: "{bucket}") +# |> range(start: {time_range}) +# |> filter(fn: (r) => r._measurement == "system_info") +# |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") +# """ + +# # Execute the query +# tables = query_api.query(flux_query) + +# # Initialize lists for the data +# time_data = [] +# cpu_data = [] +# memory_data = [] +# battery_data = [] +# network_sent_data = [] +# network_received_data = [] +# dashboard_memory_usage = [] +# cpu_frequency = [] +# current_temp = [] + +# # Parse the results +# for table in tables: +# for record in table.records: +# time_data.append(record.values.get("_time", None)) +# # Extract each field by key (handle missing fields gracefully) +# cpu_data.append(record.values.get("cpu_percent", None)) +# memory_data.append(record.values.get("memory_percent", None)) +# battery_data.append(record.values.get("battery_percent", None)) +# network_sent_data.append(record.values.get("network_sent", None)) +# network_received_data.append(record.values.get("network_received", None)) +# dashboard_memory_usage.append(record.values.get("dashboard_memory_usage", None)) +# cpu_frequency.append(record.values.get("cpu_frequency", None)) +# current_temp.append(record.values.get("current_temp", None)) + +# # Return the data as JSON +# response = jsonify({ +# "time": time_data, +# "cpu": cpu_data, +# "memory": memory_data, +# "battery": battery_data, +# "network_sent": network_sent_data, +# "network_received": network_received_data, +# "dashboard_memory_usage": dashboard_memory_usage, +# "cpu_frequency": cpu_frequency, +# "current_temp": current_temp, +# "current_time": current_time +# }) + +# # Clean up large data structures +# del tables +# del time_data +# del cpu_data +# del memory_data +# del battery_data +# del network_sent_data +# del network_received_data +# del dashboard_memory_usage +# del cpu_frequency +# del current_temp + +# gc.collect() + +# return response, 200 + +# except Exception as e: +# # Handle and log the error for debugging purposes +# return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 + +@app.route('/api/v3/prometheus/graphs_data', methods=['GET']) @login_required -def graph_data_api_v2(): +def graph_data_api_v3(): try: current_time = datetime.now() + # Get the time filter from query parameters time_filter = request.args.get('filter', default='1 day') # Determine the start time based on the filter time_deltas = { - '5 minutes': '-5m', - '15 minutes': '-15m', - '30 minutes': '-30m', - '1 hour': '-1h', - '3 hours': '-3h', - '6 hours': '-6h', - '12 hours': '-12h', - '1 day': '-1d', - '2 days': '-2d', - '3 days': '-3d', - '1 week': '-1w', - '1 month': '-30d', - '3 months': '-90d', + '5 minutes': 5 * 60, + '15 minutes': 15 * 60, + '30 minutes': 30 * 60, + '1 hour': 60 * 60, + '3 hours': 3 * 60 * 60, + '6 hours': 6 * 60 * 60, + '12 hours': 12 * 60 * 60, + '1 day': 24 * 60 * 60, + '2 days': 2 * 24 * 60 * 60, + '3 days': 3 * 24 * 60 * 60, + '1 week': 7 * 24 * 60 * 60, + '1 month': 30 * 24 * 60 * 60, + '3 months': 90 * 24 * 60 * 60, } - # Get the start time for the query - time_range = time_deltas.get(time_filter, '-1d') + # Get the time range in seconds + time_range_seconds = time_deltas.get(time_filter, 24 * 60 * 60) - # Build the InfluxDB query - flux_query = f""" - from(bucket: "{bucket}") - |> range(start: {time_range}) - |> filter(fn: (r) => r._measurement == "system_info") - |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") - """ - - # Execute the query - tables = query_api.query(flux_query) + # Prepare time parameters for the Prometheus query + end_time = int(current_time.timestamp()) + start_time = end_time - time_range_seconds + step = '10s' # Initialize lists for the data time_data = [] - cpu_data = [] - memory_data = [] - battery_data = [] - network_sent_data = [] - network_received_data = [] - dashboard_memory_usage = [] - cpu_frequency = [] - current_temp = [] + metric_data = {key: [] for key in PROMETHEUS_METRICS} + + # Fetch data for each metric from Prometheus + for metric, prometheus_query in PROMETHEUS_METRICS.items(): + # Prepare Prometheus API query parameters + params = { + 'query': prometheus_query, + 'start': start_time, + 'end': end_time, + 'step': step + } + + # Send the query to Prometheus + response = requests.get(PROMETHEUS_URL, params=params) - # Parse the results - for table in tables: - for record in table.records: - time_data.append(record.values.get("_time", None)) - # Extract each field by key (handle missing fields gracefully) - cpu_data.append(record.values.get("cpu_percent", None)) - memory_data.append(record.values.get("memory_percent", None)) - battery_data.append(record.values.get("battery_percent", None)) - network_sent_data.append(record.values.get("network_sent", None)) - network_received_data.append(record.values.get("network_received", None)) - dashboard_memory_usage.append(record.values.get("dashboard_memory_usage", None)) - cpu_frequency.append(record.values.get("cpu_frequency", None)) - current_temp.append(record.values.get("current_temp", None)) + # Check if the request was successful + if response.status_code == 200: + result = response.json().get('data', {}).get('result', []) + + # Extract the time and values for the metric + if result: + for value in result[0]['values']: + timestamp = datetime.fromtimestamp(float(value[0]), tz=timezone.utc).isoformat() + if timestamp not in time_data: + time_data.append(timestamp) + metric_data[metric].append(value[1]) + else: + print(f"No data for metric: {metric}") + else: + raise Exception(f"Failed to fetch data for {metric} from Prometheus: {response.text}") + + # Ensure all metric data has the same length as time_data + for metric in metric_data: + while len(metric_data[metric]) < len(time_data): + metric_data[metric].append(None) # Return the data as JSON - response = jsonify({ + response_data = { "time": time_data, - "cpu": cpu_data, - "memory": memory_data, - "battery": battery_data, - "network_sent": network_sent_data, - "network_received": network_received_data, - "dashboard_memory_usage": dashboard_memory_usage, - "cpu_frequency": cpu_frequency, - "current_temp": current_temp, + **metric_data, "current_time": current_time - }) - - print(time_data) - # Clean up large data structures - del tables - del time_data - del cpu_data - del memory_data - del battery_data - del network_sent_data - del network_received_data - del dashboard_memory_usage - del cpu_frequency - del current_temp - - gc.collect() + } - return response, 200 + return jsonify(response_data), 200 except Exception as e: # Handle and log the error for debugging purposes return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 - + + @app.route('/api/v1/refresh-interval', methods=['GET', 'POST']) @login_required def manage_refresh_interval(): diff --git a/src/scripts/install_influx.sh b/src/scripts/install_influx.sh index 9a698fc..83f53b1 100644 --- a/src/scripts/install_influx.sh +++ b/src/scripts/install_influx.sh @@ -1,25 +1,15 @@ #!/bin/bash -# Define variables -INFLUXDB_VERSION="latest" -CONTAINER_NAME="influxdb" -NETWORK_NAME="influx_network" -DATA_DIR="./influxdb_data" -INFLUXDB_USER="admin" # Change this to your desired username -INFLUXDB_PASSWORD="admin_password" # Change this to your desired password -INFLUXDB_ORG="systemguard" # Change this to your desired organization name -INFLUXDB_BUCKET="system_metrics" # Change this to your desired initial bucket (database) name -INFLUXDB_TOKEN="" # Demo InfluxDB token | Change this to your own token - -# Export the INFLUXDB_TOKEN to be used later in your app -export INFLUXDB_TOKEN="$INFLUXDB_TOKEN" +# how to run the script | why not with the bash command +# source install_influx.sh +# as running with source update the environment variables in the current shell -# Function to get the appropriate username, even when running as root +# Function to get the username of the current user or logname get_user_name() { if [ "$(whoami)" = "root" ]; then LOGNAME_USER=$(logname 2>/dev/null) if [ $? -ne 0 ]; then - USER_NAME=$(cat /etc/passwd | grep '/home' | cut -d: -f1 | tail -n 1) + USER_NAME=$(grep '/home' /etc/passwd | cut -d: -f1 | tail -n 1) else USER_NAME=$LOGNAME_USER fi @@ -29,41 +19,81 @@ get_user_name() { echo "$USER_NAME" } +# Variables +INFLUXDB_VERSION="latest" +CONTAINER_NAME="influxdb" +NETWORK_NAME="influx_network" +DATA_DIR="./influxdb_data" +INFLUXDB_USER="admin" # Desired username +INFLUXDB_PASSWORD="password" # Desired password +INFLUXDB_ORG="systemguard" # Organization name +INFLUXDB_BUCKET="system_metrics" # Initial bucket name USER_NAME=$(get_user_name) +INFLUXDB_TOKEN=$(openssl rand -base64 48) +BAHRC_FILE="/home/$USER_NAME/.bashrc" +DATABASE_DIR="/home/$USER_NAME/.database/$DATA_DIR" + +# Check if INFLUXDB_TOKEN is already set in .bashrc, and update if necessary +if grep -q "export INFLUXDB_TOKEN=" "$BAHRC_FILE"; then + sed -i "s|export INFLUXDB_TOKEN=.*|export INFLUXDB_TOKEN=$INFLUXDB_TOKEN|" "$BAHRC_FILE" +else + echo "export INFLUXDB_TOKEN=$INFLUXDB_TOKEN" >> "$BAHRC_FILE" +fi + +# Apply changes to the current session by sourcing .bashrc +source "$BAHRC_FILE" -# Create a data directory for InfluxDB -sudo -u "$USER_NAME" mkdir -p "$DATA_DIR" +# Verify if the token was applied to the environment +if [ -z "$INFLUXDB_TOKEN" ]; then + echo "Failed to export INFLUXDB_TOKEN" + exit 1 +else + echo "INFLUXDB_TOKEN successfully set." +fi + +# Remove and recreate the data directory for InfluxDB +rm -rf "$DATABASE_DIR" +if ! sudo -u "$USER_NAME" mkdir -p "$DATABASE_DIR"; then + echo "Failed to create data directory: $DATABASE_DIR" + exit 1 +fi # Stop and remove existing InfluxDB container if it exists if [ "$(docker ps -aq -f name=$CONTAINER_NAME)" ]; then echo "Stopping existing InfluxDB container..." - docker stop $CONTAINER_NAME + docker stop $CONTAINER_NAME || { echo "Failed to stop container"; exit 1; } + echo "Removing existing InfluxDB container..." - docker rm -f $CONTAINER_NAME + docker rm -f $CONTAINER_NAME || { echo "Failed to remove container"; exit 1; } fi # Kill any process using port 8086 if sudo lsof -i :8086; then echo "Killing process using port 8086..." - sudo fuser -k 8086/tcp + sudo fuser -k 8086/tcp || { echo "Failed to kill process on port 8086"; exit 1; } fi # Create a Docker network if it doesn't exist -echo "Creating Docker network..." -docker network create $NETWORK_NAME || true # Avoid error if the network already exists +if ! docker network inspect $NETWORK_NAME >/dev/null 2>&1; then + echo "Creating Docker network..." + docker network create $NETWORK_NAME || { echo "Failed to create network"; exit 1; } +fi # Pull the InfluxDB Docker image echo "Pulling InfluxDB Docker image..." -docker pull influxdb:$INFLUXDB_VERSION +if ! docker pull influxdb:$INFLUXDB_VERSION; then + echo "Failed to pull InfluxDB image" + exit 1 +fi -# Run the InfluxDB container with authentication and initial setup to skip onboarding +# Run the InfluxDB container with setup options echo "Running InfluxDB container..." docker run -d \ --name $CONTAINER_NAME \ --network $NETWORK_NAME \ --restart=always \ -p 8086:8086 \ - -v "$PWD/$DATA_DIR:/var/lib/influxdb2" \ + -v "$DATABASE_DIR:/var/lib/influxdb2" \ -e DOCKER_INFLUXDB_INIT_MODE=setup \ -e DOCKER_INFLUXDB_INIT_USERNAME=$INFLUXDB_USER \ -e DOCKER_INFLUXDB_INIT_PASSWORD=$INFLUXDB_PASSWORD \ @@ -72,5 +102,24 @@ docker run -d \ -e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=$INFLUXDB_TOKEN \ influxdb:$INFLUXDB_VERSION -# Output completion message -echo "InfluxDB setup completed! Access it at http://localhost:8086 with your credentials." +# Check if the container is running +if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then + echo "InfluxDB container is running successfully." +else + echo "InfluxDB container failed to start." + docker logs $CONTAINER_NAME # Fetch logs to debug why it's not starting + exit 1 +fi + +# Print InfluxDB access information +echo "" +echo "InfluxDB setup completed! Access it at http://localhost:8086" +echo "" +echo "InfluxDB credentials:" +echo "--------------------------------" +echo "Username: $INFLUXDB_USER" +echo "Password: $INFLUXDB_PASSWORD" +echo "Organization: $INFLUXDB_ORG" +echo "Bucket: $INFLUXDB_BUCKET" +echo "INFLUXDB_TOKEN: $INFLUXDB_TOKEN" +echo "--------------------------------" diff --git a/src/scripts/prometheus.sh b/src/scripts/prometheus.sh new file mode 100644 index 0000000..f0814c1 --- /dev/null +++ b/src/scripts/prometheus.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Configuration +NETWORK_NAME="flask-prometheus-net" +CONTAINER_NAME="prometheus" +PROMETHEUS_IMAGE="prom/prometheus" +PROMETHEUS_PORT="9090" +PROMETHEUS_CONFIG_DIR="$(pwd)/prometheus_config" +PROMETHEUS_CONFIG_FILE="$PROMETHEUS_CONFIG_DIR/prometheus.yml" +FLASK_APP_IP=$(hostname -I | cut -d' ' -f1) +FLASK_APP_PORT="5050" + +# Logging function for better readability +log() { + echo "[INFO] $1" +} + +# Ensure the config directory exists +log "Creating Prometheus config directory if it doesn't exist." +mkdir -p "$PROMETHEUS_CONFIG_DIR" + +# Create the prometheus.yml configuration file +log "Generating prometheus.yml configuration file." +cat > "$PROMETHEUS_CONFIG_FILE" < /dev/null + docker rm "$CONTAINER_NAME" &> /dev/null +fi + +# Run Prometheus container +log "Starting Prometheus container: $CONTAINER_NAME" +docker run -d \ + --name "$CONTAINER_NAME" \ + --network "$NETWORK_NAME" \ + -p "$PROMETHEUS_PORT:$PROMETHEUS_PORT" \ + --restart unless-stopped \ + -v "$PROMETHEUS_CONFIG_FILE:/etc/prometheus/prometheus.yml" \ + "$PROMETHEUS_IMAGE" &> /dev/null + +# Check if Prometheus started successfully +if [ $? -eq 0 ]; then + log "Prometheus container $CONTAINER_NAME started successfully on port $PROMETHEUS_PORT." + log "Prometheus config file located at $PROMETHEUS_CONFIG_FILE" +else + echo "[ERROR] Failed to start Prometheus container." + exit 1 +fi diff --git a/src/static/js/graphs.js b/src/static/js/graphs.js index 14b1fcb..adb5769 100644 --- a/src/static/js/graphs.js +++ b/src/static/js/graphs.js @@ -12,7 +12,7 @@ function fetchDataAndRenderCharts() { console.log('Stored Filter Value:', storedFilterValue); // Fetch data with the selected time filter - fetch(`/api/v2/graphs_data?filter=${storedFilterValue}`) + fetch(`/api/v3/prometheus/graphs_data?filter=${storedFilterValue}`) .then(response => response.json()) .then(data => { const cpuData = data.cpu; From c5b5f0219041e63605afcb25c5fcddcc0a3be80e Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 12:35:05 +0530 Subject: [PATCH 13/46] chore: updating prometheus for central server added --- .gitignore | 5 ++++- prometheus_config/prometheus.yml | 7 ------ requirements.txt | 4 ++-- src/scripts/update_prometheus.sh | 38 ++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 10 deletions(-) delete mode 100644 prometheus_config/prometheus.yml create mode 100644 src/scripts/update_prometheus.sh diff --git a/.gitignore b/.gitignore index 5f58245..069c006 100644 --- a/.gitignore +++ b/.gitignore @@ -165,4 +165,7 @@ cython_debug/ predefine_user.json src/assets/predefine_user.json -influxdb_data \ No newline at end of file +influxdb_data +prometheus_config +*.yml +prometheus_config/prometheus.yml diff --git a/prometheus_config/prometheus.yml b/prometheus_config/prometheus.yml deleted file mode 100644 index dac77e2..0000000 --- a/prometheus_config/prometheus.yml +++ /dev/null @@ -1,7 +0,0 @@ -global: - scrape_interval: 30s # How often Prometheus scrapes the target - -scrape_configs: - - job_name: 'flask_app_metrics' # Scraping metrics from Flask app - static_configs: - - targets: ['192.168.1.10:5050'] diff --git a/requirements.txt b/requirements.txt index 27d8253..51bde8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,5 +28,5 @@ speedtest-cli==2.1.3 # prometheus client for monitoring prometheus_client==0.20.0 -# influxdb-client==1.46.0 -influxdb-client==1.46.0 \ No newline at end of file +# influxdb-client, optional for writing metrics to InfluxDB +# influxdb-client==1.46.0 \ No newline at end of file diff --git a/src/scripts/update_prometheus.sh b/src/scripts/update_prometheus.sh new file mode 100644 index 0000000..26219c7 --- /dev/null +++ b/src/scripts/update_prometheus.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Configuration +CONTAINER_NAME="prometheus" +PROMETHEUS_CONFIG="$(pwd)/prometheus.yml" +PROMETHEUS_IMAGE="prom/prometheus" # Add your image name if needed +NETWORK_NAME="flask-prometheus-net" # Specify your network name +PROMETHEUS_PORT="9090" # Specify your port + +# Logging function for better readability +log() { + echo "[INFO] $1" +} + +# Check if Prometheus container is running +if [ "$(docker ps -q -f name="$CONTAINER_NAME")" ]; then + log "Stopping container: $CONTAINER_NAME" + docker stop "$CONTAINER_NAME" + + log "Starting container: $CONTAINER_NAME with new configuration" + docker start "$CONTAINER_NAME" + + log "Reloading configuration for container: $CONTAINER_NAME" + docker exec "$CONTAINER_NAME" kill -HUP 1 # Send SIGHUP to reload config +else + log "Container $CONTAINER_NAME is not running. Starting a new container." + docker run -d \ + --name "$CONTAINER_NAME" \ + --network "$NETWORK_NAME" \ + -p "$PROMETHEUS_PORT:$PROMETHEUS_PORT" \ + --restart unless-stopped \ + -v "$PROMETHEUS_CONFIG:/etc/prometheus/prometheus.yml" \ + "$PROMETHEUS_IMAGE" &> /dev/null + + log "Prometheus container started successfully." +fi + +log "Prometheus container has been updated with the new configuration." From 10433f84edbbd78fbda35724f7c3be72df6c3ee4 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 14:11:22 +0530 Subject: [PATCH 14/46] Refactor log_system_info.py and influxdb_config.py --- src/background_task/log_system_info.py | 6 ++-- src/influxdb_config.py | 38 ++++++++++---------- src/templates/graphs/graphs.html | 49 +++++++++++++------------- 3 files changed, 46 insertions(+), 47 deletions(-) diff --git a/src/background_task/log_system_info.py b/src/background_task/log_system_info.py index 0ea57bc..a4b0018 100644 --- a/src/background_task/log_system_info.py +++ b/src/background_task/log_system_info.py @@ -7,11 +7,9 @@ from src.models import GeneralSettings, SystemInformation from sqlalchemy.exc import SQLAlchemyError from prometheus_client import Counter, Gauge -import os, time +import os from src.logger import logger -from src.influxdb_config import bucket, write_api -from src.utils import get_system_username # Flag to track if logging is already scheduled is_logging_scheduled = False fetch_system_info_interval = 30 @@ -179,7 +177,7 @@ def monitor_settings(): is_logging_scheduled = False # Recheck settings every 10 seconds - Timer(10, monitor_settings).start() + Timer(30, monitor_settings).start() except SQLAlchemyError as db_err: logger.error(f"Error fetching settings: {db_err}", exc_info=True) diff --git a/src/influxdb_config.py b/src/influxdb_config.py index 110fc88..87e68c1 100644 --- a/src/influxdb_config.py +++ b/src/influxdb_config.py @@ -1,22 +1,22 @@ -from influxdb_client import InfluxDBClient, Point, WritePrecision -from influxdb_client.client.write_api import SYNCHRONOUS -import os -from src.logger import logger +# from influxdb_client import InfluxDBClient, Point, WritePrecision +# from influxdb_client.client.write_api import SYNCHRONOUS +# import os +# from src.logger import logger -# influx db configuration -org = "systemguard" -url = "http://localhost:8086" -bucket="system_metrics" -INFLUXDB_TOKEN=os.getenv('INFLUXDB_TOKEN') -print("INFLUXDB_TOKEN: ", INFLUXDB_TOKEN) +# # influx db configuration +# org = "systemguard" +# url = "http://localhost:8086" +# bucket="system_metrics" +# INFLUXDB_TOKEN=os.getenv('INFLUXDB_TOKEN') +# print("INFLUXDB_TOKEN: ", INFLUXDB_TOKEN) -try: - influx_client = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) - bucket = "system_metrics" - write_api = influx_client.write_api(write_options=SYNCHRONOUS) - query_api = influx_client.query_api() - logger.info("Connected to InfluxDB successfully") -except Exception as e: - logger.error(f"Failed to connect to InfluxDB: {e}") - raiseclient = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) +# try: +# influx_client = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) +# bucket = "system_metrics" +# write_api = influx_client.write_api(write_options=SYNCHRONOUS) +# query_api = influx_client.query_api() +# logger.info("Connected to InfluxDB successfully") +# except Exception as e: +# logger.error(f"Failed to connect to InfluxDB: {e}") +# raiseclient = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) diff --git a/src/templates/graphs/graphs.html b/src/templates/graphs/graphs.html index c0eeb73..3333dba 100644 --- a/src/templates/graphs/graphs.html +++ b/src/templates/graphs/graphs.html @@ -55,7 +55,7 @@

Current Server Time & Time Zone

-

CPU Usage Over Time

+

CPU Usage

@@ -64,7 +64,7 @@

CPU Usage Over Time

-

Memory Usage Over Time

+

Memory Usage

@@ -72,10 +72,30 @@

Memory Usage Over Time

+
+
+
+

Current Temperature

+ +
+ +
+ +
+
+

CPU Frequency

+ +
+ +
+
+
-

Battery Percentage Over Time

+

Battery Percentage

@@ -84,7 +104,7 @@

Battery Percentage Over Time

-

Network Sent & Received Over Time

+

Network Sent & Received

@@ -95,32 +115,13 @@

Network Sent & Received Over Time

-

Memory Usage Over Time(APP)

+

Memory Usage(APP)

-
-
-

CPU Frequency Over Time

- -
- -
-
- -
-
-
-

Current Temperature Over Time

- -
- -
{% endblock %} From b526879691ee2ebf28f648e6ba99659acf23237b Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 14:39:01 +0530 Subject: [PATCH 15/46] Refactor setup.sh to fix message box display issue --- setup.sh | 2 +- src/background_task/log_system_info.py | 2 +- src/scripts/prometheus.sh | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/setup.sh b/setup.sh index 2ebb878..6332cfd 100644 --- a/setup.sh +++ b/setup.sh @@ -856,7 +856,7 @@ install() { create_dir "$EXTRACT_DIR" # PROMETHEUS_INSTALL_SCRIPT message_box "$APP_NAME Installer $INSATLLER_VERSION" 0 - message_box "Welcome on board: $(echo "$USER_NAME" | sed 's/.*/\u&/')" 3 + message_box "Welcome on board: $(echo "$USER_NAME" | sed 's/.*/\u&/')" 0 check_dependencies message_box "Choose the installation method\nNote: Release is recommended for production use." 0 diff --git a/src/background_task/log_system_info.py b/src/background_task/log_system_info.py index a4b0018..e760634 100644 --- a/src/background_task/log_system_info.py +++ b/src/background_task/log_system_info.py @@ -7,7 +7,7 @@ from src.models import GeneralSettings, SystemInformation from sqlalchemy.exc import SQLAlchemyError from prometheus_client import Counter, Gauge -import os +import os, time from src.logger import logger # Flag to track if logging is already scheduled diff --git a/src/scripts/prometheus.sh b/src/scripts/prometheus.sh index f0814c1..5089fb4 100644 --- a/src/scripts/prometheus.sh +++ b/src/scripts/prometheus.sh @@ -45,23 +45,26 @@ if docker ps -a --format '{{.Names}}' | grep -q "$CONTAINER_NAME"; then log "Container $CONTAINER_NAME already exists. Stopping and removing it." docker stop "$CONTAINER_NAME" &> /dev/null docker rm "$CONTAINER_NAME" &> /dev/null +else + log "No existing container found. Proceeding to start a new one." fi # Run Prometheus container log "Starting Prometheus container: $CONTAINER_NAME" -docker run -d \ +run_output=$(docker run -d \ --name "$CONTAINER_NAME" \ --network "$NETWORK_NAME" \ -p "$PROMETHEUS_PORT:$PROMETHEUS_PORT" \ --restart unless-stopped \ -v "$PROMETHEUS_CONFIG_FILE:/etc/prometheus/prometheus.yml" \ - "$PROMETHEUS_IMAGE" &> /dev/null + "$PROMETHEUS_IMAGE" 2>&1) # Capture both stdout and stderr # Check if Prometheus started successfully if [ $? -eq 0 ]; then log "Prometheus container $CONTAINER_NAME started successfully on port $PROMETHEUS_PORT." log "Prometheus config file located at $PROMETHEUS_CONFIG_FILE" else - echo "[ERROR] Failed to start Prometheus container." + echo "[ERROR] Failed to start Prometheus container: $run_output" + echo "[ERROR] Checking logs for container: $CONTAINER_NAME" exit 1 fi From 7374dc6d6db504243965aeb3e6ae5ca2e9adbf7e Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 15:57:16 +0530 Subject: [PATCH 16/46] Refactor Docker run command to always restart Prometheus container --- src/scripts/prometheus.sh | 2 +- src/scripts/update_prometheus.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripts/prometheus.sh b/src/scripts/prometheus.sh index 5089fb4..714019c 100644 --- a/src/scripts/prometheus.sh +++ b/src/scripts/prometheus.sh @@ -55,7 +55,7 @@ run_output=$(docker run -d \ --name "$CONTAINER_NAME" \ --network "$NETWORK_NAME" \ -p "$PROMETHEUS_PORT:$PROMETHEUS_PORT" \ - --restart unless-stopped \ + --restart always \ -v "$PROMETHEUS_CONFIG_FILE:/etc/prometheus/prometheus.yml" \ "$PROMETHEUS_IMAGE" 2>&1) # Capture both stdout and stderr diff --git a/src/scripts/update_prometheus.sh b/src/scripts/update_prometheus.sh index 26219c7..23eb308 100644 --- a/src/scripts/update_prometheus.sh +++ b/src/scripts/update_prometheus.sh @@ -28,7 +28,7 @@ else --name "$CONTAINER_NAME" \ --network "$NETWORK_NAME" \ -p "$PROMETHEUS_PORT:$PROMETHEUS_PORT" \ - --restart unless-stopped \ + --restart always \ -v "$PROMETHEUS_CONFIG:/etc/prometheus/prometheus.yml" \ "$PROMETHEUS_IMAGE" &> /dev/null From 2d68173e29adad05fb0386cad4de79331506398e Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 16:53:32 +0530 Subject: [PATCH 17/46] chore: api end point added for multiple target --- src/routes/api.py | 100 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/src/routes/api.py b/src/routes/api.py index dab3237..447ffc5 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -234,7 +234,7 @@ def graph_data_api(): # # Handle and log the error for debugging purposes # return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 -@app.route('/api/v3/prometheus/graphs_data', methods=['GET']) +@app.route('/api/v1/prometheus/graphs_data', methods=['GET']) @login_required def graph_data_api_v3(): try: @@ -319,6 +319,104 @@ def graph_data_api_v3(): # Handle and log the error for debugging purposes return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 +@app.route('/api/v1/prometheus/graphs_data/targets', methods=['GET']) +@login_required +def graph_data_api_v3_(): + try: + current_time = datetime.now() + + # Get the time filter from query parameters + time_filter = request.args.get('filter', default='1 day') + + # Determine the start time based on the filter + time_deltas = { + '5 minutes': 5 * 60, + '15 minutes': 15 * 60, + '30 minutes': 30 * 60, + '1 hour': 60 * 60, + '3 hours': 3 * 60 * 60, + '6 hours': 6 * 60 * 60, + '12 hours': 12 * 60 * 60, + '1 day': 24 * 60 * 60, + '2 days': 2 * 24 * 60 * 60, + '3 days': 3 * 24 * 60 * 60, + '1 week': 7 * 24 * 60 * 60, + '1 month': 30 * 24 * 60 * 60, + '3 months': 90 * 24 * 60 * 60, + } + + # Get the time range in seconds + time_range_seconds = time_deltas.get(time_filter, 24 * 60 * 60) + + # Prepare time parameters for the Prometheus query + end_time = int(current_time.timestamp()) + start_time = end_time - time_range_seconds + step = '10s' + + # Initialize lists for the data + time_data = [] + metric_data = {} + + # Fetch data for each metric from Prometheus + for metric, prometheus_query in PROMETHEUS_METRICS.items(): + # Prepare Prometheus API query parameters + params = { + 'query': prometheus_query, + 'start': start_time, + 'end': end_time, + 'step': step + } + + # Send the query to Prometheus + response = requests.get(PROMETHEUS_URL, params=params) + + # Check if the request was successful + if response.status_code == 200: + result = response.json().get('data', {}).get('result', []) + + if result: + # Initialize a dictionary to hold time series data for this metric + metric_data[metric] = [] + + for series in result: + # Create a new list for the time series data of this particular series + series_data = { + "metric": series.get("metric"), + "values": [] + } + + # Iterate over the values for this series + for value in series.get("values", []): + timestamp = datetime.fromtimestamp(float(value[0]), tz=timezone.utc).isoformat() + if timestamp not in time_data: + time_data.append(timestamp) + series_data["values"].append(value[1]) + + # Append the series data to the metric + metric_data[metric].append(series_data) + else: + print(f"No data for metric: {metric}") + else: + raise Exception(f"Failed to fetch data for {metric} from Prometheus: {response.text}") + + # Ensure all metric data has the same length as time_data + for metric, series_list in metric_data.items(): + for series in series_list: + while len(series["values"]) < len(time_data): + series["values"].append(None) + + # Return the data as JSON + response_data = { + "time": time_data, + **metric_data, + "current_time": current_time + } + + return jsonify(response_data), 200 + + except Exception as e: + # Handle and log the error for debugging purposes + return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 @app.route('/api/v1/refresh-interval', methods=['GET', 'POST']) @login_required From 4253df990b03ef9593fe04e02e1e6b0ca1c2c851 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 16:54:33 +0530 Subject: [PATCH 18/46] Refactor API endpoint URL in graphs.js --- src/static/js/graphs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/graphs.js b/src/static/js/graphs.js index adb5769..07e5d59 100644 --- a/src/static/js/graphs.js +++ b/src/static/js/graphs.js @@ -12,7 +12,7 @@ function fetchDataAndRenderCharts() { console.log('Stored Filter Value:', storedFilterValue); // Fetch data with the selected time filter - fetch(`/api/v3/prometheus/graphs_data?filter=${storedFilterValue}`) + fetch(`/api/v1/prometheus/graphs_data?filter=${storedFilterValue}`) .then(response => response.json()) .then(data => { const cpuData = data.cpu; From 3567b0514c392a4e03dabf851d5b8bebbc7a2485 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 19:42:30 +0530 Subject: [PATCH 19/46] Refactor API endpoint URL in graphs.js to include targets in the filter parameter --- src/static/js/graphs copy.js | 286 +++++++++++++++++++++++++++++++++++ src/static/js/graphs.js | 127 ++++++++-------- 2 files changed, 353 insertions(+), 60 deletions(-) create mode 100644 src/static/js/graphs copy.js diff --git a/src/static/js/graphs copy.js b/src/static/js/graphs copy.js new file mode 100644 index 0000000..07e5d59 --- /dev/null +++ b/src/static/js/graphs copy.js @@ -0,0 +1,286 @@ +// Variables to store chart instances +let cpuTimeChart, memoryTimeChart, batteryTimeChart, networkTimeChart, dashboardMemoryTimeChart, cpuFrequencyTimeChart, currentTempTimeChart; + +// Function to fetch data and render charts +function fetchDataAndRenderCharts() { + // Retrieve stored filter value from local storage or set default value + const storedFilterValue = localStorage.getItem('filterValue') || 5; + + // Set the filter element value to the stored filter value + document.getElementById('timeFilter').value = storedFilterValue; + + console.log('Stored Filter Value:', storedFilterValue); + + // Fetch data with the selected time filter + fetch(`/api/v1/prometheus/graphs_data?filter=${storedFilterValue}`) + .then(response => response.json()) + .then(data => { + const cpuData = data.cpu; + const memoryData = data.memory; + const batteryData = data.battery; + const networkSentData = data.network_sent; + const networkReceivedData = data.network_received; + const dashboardMemoryUsageData = data.dashboard_memory_usage; + const cpuFrequencyData = data.cpu_frequency; + const currentTempData = data.current_temp; + const currentTime = data.current_time; + const timeZoneName = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // Format the time data using the currentTime from backend + const timeData = data.time.map(time => formatDate(time, currentTime)); + + displayTimeAndTimeZone(currentTime, timeZoneName); + + createCharts(cpuData, timeData, memoryData, batteryData, networkSentData, networkReceivedData, dashboardMemoryUsageData, cpuFrequencyData, currentTempData); + }) + .catch(error => console.error('Error fetching data:', error)); +} + +// Add event listener to refresh data when filter value changes +document.getElementById('timeFilter').addEventListener('change', (event) => { + // Save the new filter value to local storage + localStorage.setItem('filterValue', event.target.value); + // Fetch data with the new filter value + fetchDataAndRenderCharts(); +}); + +// Initial fetch when the page loads +document.addEventListener('DOMContentLoaded', () => { + fetchDataAndRenderCharts(); +}); + +// Add the refresh button to fetch the data +document.getElementById('refreshData').addEventListener('click', () => { + fetchDataAndRenderCharts(); +}); + +document.getElementById('refreshCpuTime').addEventListener('click', () => { + fetchDataAndRenderCharts(); +}); + +document.getElementById('refreshMemoryTime').addEventListener('click', () => { + fetchDataAndRenderCharts(); +}); + +document.getElementById('refreshBatteryTime').addEventListener('click', () => { + fetchDataAndRenderCharts(); +}); + +document.getElementById('refreshNetworkTime').addEventListener('click', () => { + fetchDataAndRenderCharts(); +}); + +document.getElementById('refreshDashboardMemoryTime').addEventListener('click', () => { + fetchDataAndRenderCharts(); +}); + +document.getElementById('refreshCpuFrequencyTime').addEventListener('click', () => { + fetchDataAndRenderCharts(); +}); + +document.getElementById('refreshCurrentTempTime').addEventListener('click', () => { + fetchDataAndRenderCharts(); +}); + + +function formatDate(dateString, currentTime) { + const date = new Date(dateString); + const now = new Date(currentTime); // Use currentTime from backend + + // Helper function to format with leading zeros + const pad = (num) => String(num).padStart(2, '0'); + + // Manually extract UTC components + const day = pad(date.getUTCDate()); // e.g., 09 + const month = pad(date.getUTCMonth() + 1); // e.g., 04 + const year = date.getUTCFullYear(); // e.g., 2021 + const hours = pad(date.getUTCHours()); // e.g., 11 + const minutes = pad(date.getUTCMinutes()); // e.g., 33 + + + // Calculate time differences + const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); + const diffWeeks = Math.floor(diffDays / 7); + const diffMonths = now.getMonth() - date.getUTCMonth() + (12 * (now.getFullYear() - date.getUTCFullYear())); + const diffYears = now.getFullYear() - date.getUTCFullYear(); + + // Determine the label based on time differences + // // Reset the time to 12am for the date comparison + // date.setUTCHours(0, 0, 0, 0); + // now.setUTCHours(0, 0, 0, 0); + + // if (diffDays === 0) { + // return `Today ${hours}:${minutes}`; + // } else if (diffDays === 1) { + // return `Yesterday ${hours}:${minutes}`; + // } else if (diffDays <= 3) { + // return `${diffDays} Days Ago ${hours}:${minutes}`; + // } else if (diffDays <= 7) { + // return `${Math.ceil(diffDays / 7)} Week${diffDays > 7 ? 's' : ''} Ago ${hours}:${minutes}`; + // } else if (diffDays <= 30) { + // return `${Math.ceil(diffDays / 7)} Weeks Ago ${hours}:${minutes}`; + // } else if (diffMonths < 12) { + // return `${diffMonths} Month${diffMonths > 1 ? 's' : ''} Ago ${hours}:${minutes}`; + // } else if (diffYears < 2) { + // return `Last Year ${hours}:${minutes}`; + // } else { + // return `${year}/${month}/${day} ${hours}:${minutes}`; + // } + + return `${year}/${month}/${day} ${hours}:${minutes}`; +} + +function displayTimeAndTimeZone(currentTime, timeZoneName) { + // Display the current time and timezone + document.getElementById('currentTime').textContent = `Current Time: ${currentTime}`; + document.getElementById('timeZoneName').textContent = `Time Zone: ${timeZoneName}`; + // Update currentTime by 1 second every second + setInterval(() => { + const date = new Date(currentTime); + date.setSeconds(date.getSeconds() + 1); + currentTime = date.toISOString(); + document.getElementById('currentTime').textContent = `Current Time: ${currentTime}`; + }, 1000); +} + +// add the refresh button to fetch the data +document.getElementById('refreshData').addEventListener('click', () => { + fetchDataAndRenderCharts(); +}); + + +// Function to create a chart with multiple datasets +function createChart(ctx, labels, datasets, yLabel) { + if (ctx.chart) { + ctx.chart.destroy(); // Destroy the existing chart if it exists + } + + ctx.chart = new Chart(ctx, { + type: 'line', + data: { + labels: labels, // Use your timeData directly as labels + datasets: datasets + }, + options: { + scales: { + x: { + type: 'category', // Use 'category' scale to treat time as strings + title: { + display: true, + text: 'Time' + }, + ticks: { + autoSkip: true, // Automatically skip some labels to prevent overlap + maxTicksLimit: 10, // Maximum number of ticks to display + maxRotation: 20, // Prevent rotating the labels for better readability + minRotation: 0, + + } + }, + y: { + beginAtZero: true, + title: { + display: true, + text: yLabel + } + } + } + } + }); +} + + +// Function to create charts with the fetched data +function createCharts(cpuData, timeData, memoryData, batteryData, networkSentData, networkReceivedData, dashboardMemoryUsageData, cpuFrequencyData, currentTempData) { + // CPU Usage Chart + const ctxCpu = document.getElementById('cpuTimeChart').getContext('2d'); + createChart(ctxCpu, timeData, [{ + label: 'CPU Usage (%)', + data: cpuData, + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + fill: true, + tension: 0.4 + }], 'CPU Usage (%)'); + + // Memory Usage Chart + const ctxMemory = document.getElementById('memoryTimeChart').getContext('2d'); + createChart(ctxMemory, timeData, [{ + label: 'Memory Usage (%)', + data: memoryData, + borderColor: 'rgba(153, 102, 255, 1)', + backgroundColor: 'rgba(153, 102, 255, 0.2)', + fill: true, + tension: 0.4 + }], 'Memory Usage (%)'); + + // Battery Percentage Chart + const ctxBattery = document.getElementById('batteryTimeChart').getContext('2d'); + createChart(ctxBattery, timeData, [{ + label: 'Battery Percentage (%)', + data: batteryData, + borderColor: 'rgba(255, 159, 64, 1)', + backgroundColor: 'rgba(255, 159, 64, 0.2)', + fill: true, + tension: 0.4 + }], 'Battery Percentage (%)'); + + // Network Sent & Received Chart + const ctxNetwork = document.getElementById('networkTimeChart').getContext('2d'); + createChart(ctxNetwork, timeData, [ + { + label: 'Network Sent (MB)', + data: networkSentData, + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + fill: true, + tension: 0.4 + }, + { + label: 'Network Received (MB)', + data: networkReceivedData, + borderColor: 'rgba(54, 162, 235, 1)', + backgroundColor: 'rgba(54, 162, 235, 0.2)', + fill: true, + tension: 0.4 + } + ], 'Data Transferred (MB)'); + + // Dashboard Memory Usage Chart + const ctxDashboardMemory = document.getElementById('dashboardMemoryTimeChart').getContext('2d'); + createChart(ctxDashboardMemory, timeData, [{ + label: 'Dashboard Memory Usage (%)', + data: dashboardMemoryUsageData, + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + fill: true, + tension: 0.4 + }], 'Dashboard Memory Usage (%)'); + + // CPU Frequency Chart + const ctxCpuFrequency = document.getElementById('cpuFrequencyTimeChart').getContext('2d'); + createChart(ctxCpuFrequency, timeData, [{ + label: 'CPU Frequency (GHz)', + data: cpuFrequencyData, + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + fill: true, + tension: 0.4 + }], 'CPU Frequency (GHz)'); + + // Current Temperature Chart + const ctxCurrentTemp = document.getElementById('currentTempTimeChart').getContext('2d'); + createChart(ctxCurrentTemp, timeData, [{ + label: 'Current Temperature (°C)', + data: currentTempData, + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + fill: true, + tension: 0.4 + }], 'Current Temperature (°C)'); +} + +// Fetch initial data when the page loads +document.addEventListener('DOMContentLoaded', () => { + fetchDataAndRenderCharts(); +}); diff --git a/src/static/js/graphs.js b/src/static/js/graphs.js index 07e5d59..53a48f9 100644 --- a/src/static/js/graphs.js +++ b/src/static/js/graphs.js @@ -12,7 +12,7 @@ function fetchDataAndRenderCharts() { console.log('Stored Filter Value:', storedFilterValue); // Fetch data with the selected time filter - fetch(`/api/v1/prometheus/graphs_data?filter=${storedFilterValue}`) + fetch(`/api/v1/prometheus/graphs_data/targets?filter=${storedFilterValue}`) .then(response => response.json()) .then(data => { const cpuData = data.cpu; @@ -194,90 +194,97 @@ function createChart(ctx, labels, datasets, yLabel) { function createCharts(cpuData, timeData, memoryData, batteryData, networkSentData, networkReceivedData, dashboardMemoryUsageData, cpuFrequencyData, currentTempData) { // CPU Usage Chart const ctxCpu = document.getElementById('cpuTimeChart').getContext('2d'); - createChart(ctxCpu, timeData, [{ - label: 'CPU Usage (%)', - data: cpuData, - borderColor: 'rgba(75, 192, 192, 1)', - backgroundColor: 'rgba(75, 192, 192, 0.2)', - fill: true, + const cpuDatasets = cpuData.map((cpu, index) => ({ + label: `CPU Usage (%) ${cpu.metric.instance}`, + data: cpu.values, + borderColor: `rgba(${75 + index * 20}, 192, 192, 1)`, + backgroundColor: `rgba(${75 + index * 20}, 192, 192, 0.2)`, tension: 0.4 - }], 'CPU Usage (%)'); + })); + + createChart(ctxCpu, timeData, cpuDatasets, 'CPU Usage (%)'); // Memory Usage Chart const ctxMemory = document.getElementById('memoryTimeChart').getContext('2d'); - createChart(ctxMemory, timeData, [{ - label: 'Memory Usage (%)', - data: memoryData, - borderColor: 'rgba(153, 102, 255, 1)', - backgroundColor: 'rgba(153, 102, 255, 0.2)', - fill: true, + const memoryDatasets = memoryData.map((memory, index) => ({ + label: `Memory Usage (%) ${memory.metric.instance}`, + data: memory.values, + borderColor: `rgba(${75 + index * 20}, 75, 192, 1)`, + backgroundColor: `rgba(${75 + index * 20}, 75, 192, 0.2)`, tension: 0.4 - }], 'Memory Usage (%)'); + })); + + createChart(ctxMemory, timeData, memoryDatasets, 'Memory Usage (%)'); // Battery Percentage Chart const ctxBattery = document.getElementById('batteryTimeChart').getContext('2d'); - createChart(ctxBattery, timeData, [{ - label: 'Battery Percentage (%)', - data: batteryData, - borderColor: 'rgba(255, 159, 64, 1)', - backgroundColor: 'rgba(255, 159, 64, 0.2)', - fill: true, + const batteryDatasets = batteryData.map((battery, index) => ({ + label: `Battery Usage (%) ${battery.metric.instance}`, + data: battery.values, + borderColor: `rgba(${192 + index * 20}, 75, 75, 1)`, + backgroundColor: `rgba(${192 + index * 20}, 75, 75, 0.2)`, tension: 0.4 - }], 'Battery Percentage (%)'); + })); + + createChart(ctxBattery, timeData, batteryDatasets, 'Battery Percentage (%)'); // Network Sent & Received Chart const ctxNetwork = document.getElementById('networkTimeChart').getContext('2d'); - createChart(ctxNetwork, timeData, [ - { - label: 'Network Sent (MB)', - data: networkSentData, - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - fill: true, + const networkDatasets = [ + ...networkSentData.map((networkSent, index) => ({ + label: `Network Sent (MB) ${networkSent.metric.instance}`, + data: networkSent.values, + borderColor: `rgba(${75 + index * 20}, 75, 192, 1)`, + backgroundColor: `rgba(${75 + index * 20}, 75, 192, 0.2)`, tension: 0.4 - }, - { - label: 'Network Received (MB)', - data: networkReceivedData, - borderColor: 'rgba(54, 162, 235, 1)', - backgroundColor: 'rgba(54, 162, 235, 0.2)', - fill: true, + })), + ...networkReceivedData.map((networkReceived, index) => ({ + label: `Network Received (MB) ${networkReceived.metric.instance}`, + data: networkReceived.values, + borderColor: `rgba(${192 + index * 20}, 75, 75, 1)`, + backgroundColor: `rgba(${192 + index * 20}, 75, 75, 0.2)`, tension: 0.4 - } - ], 'Data Transferred (MB)'); + })) + ]; + + createChart(ctxNetwork, timeData, networkDatasets, 'Data Transferred (MB)'); // Dashboard Memory Usage Chart const ctxDashboardMemory = document.getElementById('dashboardMemoryTimeChart').getContext('2d'); - createChart(ctxDashboardMemory, timeData, [{ - label: 'Dashboard Memory Usage (%)', - data: dashboardMemoryUsageData, - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - fill: true, + const dashboardMemoryDatasets = dashboardMemoryUsageData.map((dashboardMemory, index) => ({ + label: `Dashboard Memory Usage (%) ${dashboardMemory.metric.instance}`, + data: dashboardMemory.values, + borderColor: `rgba(${75 + index * 20}, 192, 75, 1)`, + backgroundColor: `rgba(${75 + index * 20}, 192, 75, 0.2)`, tension: 0.4 - }], 'Dashboard Memory Usage (%)'); + })); + + createChart(ctxDashboardMemory, timeData, dashboardMemoryDatasets, 'Dashboard Memory Usage (%)'); // CPU Frequency Chart const ctxCpuFrequency = document.getElementById('cpuFrequencyTimeChart').getContext('2d'); - createChart(ctxCpuFrequency, timeData, [{ - label: 'CPU Frequency (GHz)', - data: cpuFrequencyData, - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - fill: true, - tension: 0.4 - }], 'CPU Frequency (GHz)'); + const cpuFrequencyDatasets = cpuFrequencyData.map((cpuFrequency, index) => ({ + label: `CPU Frequency (GHz) ${cpuFrequency.metric.instance}`, + data: cpuFrequency.values, + borderColor: `rgba(${192 + index * 20}, 192, 75, 1)`, + backgroundColor: `rgba(${192 + index * 20}, 192, 75, 0.2)`, + tension: 0.4, + + })); + + createChart(ctxCpuFrequency, timeData, cpuFrequencyDatasets, 'CPU Frequency (GHz)'); // Current Temperature Chart const ctxCurrentTemp = document.getElementById('currentTempTimeChart').getContext('2d'); - createChart(ctxCurrentTemp, timeData, [{ - label: 'Current Temperature (°C)', - data: currentTempData, - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - fill: true, + const currentTempDatasets = currentTempData.map((currentTemp, index) => ({ + label: `Current Temperature (°C) ${currentTemp.metric.instance}`, + data: currentTemp.values, + borderColor: `rgba(${75 + index * 20}, 192, 192, 1)`, + backgroundColor: `rgba(${75 + index * 20}, 192, 192, 0.2)`, tension: 0.4 - }], 'Current Temperature (°C)'); + })); + + createChart(ctxCurrentTemp, timeData, currentTempDatasets, 'Current Temperature (°C)'); } // Fetch initial data when the page loads From 6dfee0ffcfb863594a48ab5c02971542aa3f1e77 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sat, 21 Sep 2024 21:03:47 +0530 Subject: [PATCH 20/46] chore: experimental graph created --- src/routes/graphs.py | 6 + src/static/js/graphs.js | 137 ++++++++--------- ...{graphs copy.js => graphs_experimental.js} | 139 ++++++++++-------- src/templates/experimental/graphs.html | 132 +++++++++++++++++ src/templates/graphs/graphs.html | 1 + 5 files changed, 278 insertions(+), 137 deletions(-) rename src/static/js/{graphs copy.js => graphs_experimental.js} (69%) create mode 100644 src/templates/experimental/graphs.html diff --git a/src/routes/graphs.py b/src/routes/graphs.py index db82123..e007e58 100644 --- a/src/routes/graphs.py +++ b/src/routes/graphs.py @@ -8,3 +8,9 @@ @admin_required def graphs(): return render_template('graphs/graphs.html') + + +@app.route('/experimental/graphs') +@admin_required +def experimental_graphs(): + return render_template('experimental/graphs.html') diff --git a/src/static/js/graphs.js b/src/static/js/graphs.js index 53a48f9..bbf404f 100644 --- a/src/static/js/graphs.js +++ b/src/static/js/graphs.js @@ -12,7 +12,7 @@ function fetchDataAndRenderCharts() { console.log('Stored Filter Value:', storedFilterValue); // Fetch data with the selected time filter - fetch(`/api/v1/prometheus/graphs_data/targets?filter=${storedFilterValue}`) + fetch(`/api/v1/prometheus/graphs_data?filter=${storedFilterValue}`) .then(response => response.json()) .then(data => { const cpuData = data.cpu; @@ -164,17 +164,17 @@ function createChart(ctx, labels, datasets, yLabel) { options: { scales: { x: { - type: 'category', // Use 'category' scale to treat time as strings + type: 'category', + reverse: true, // Reverse the x-axis to start from the right title: { display: true, text: 'Time' }, ticks: { - autoSkip: true, // Automatically skip some labels to prevent overlap - maxTicksLimit: 10, // Maximum number of ticks to display - maxRotation: 20, // Prevent rotating the labels for better readability + autoSkip: true, + maxTicksLimit: 10, + maxRotation: 20, minRotation: 0, - } }, y: { @@ -194,97 +194,90 @@ function createChart(ctx, labels, datasets, yLabel) { function createCharts(cpuData, timeData, memoryData, batteryData, networkSentData, networkReceivedData, dashboardMemoryUsageData, cpuFrequencyData, currentTempData) { // CPU Usage Chart const ctxCpu = document.getElementById('cpuTimeChart').getContext('2d'); - const cpuDatasets = cpuData.map((cpu, index) => ({ - label: `CPU Usage (%) ${cpu.metric.instance}`, - data: cpu.values, - borderColor: `rgba(${75 + index * 20}, 192, 192, 1)`, - backgroundColor: `rgba(${75 + index * 20}, 192, 192, 0.2)`, + createChart(ctxCpu, timeData, [{ + label: 'CPU Usage (%)', + data: cpuData, + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + fill: true, tension: 0.4 - })); - - createChart(ctxCpu, timeData, cpuDatasets, 'CPU Usage (%)'); + }], 'CPU Usage (%)'); // Memory Usage Chart const ctxMemory = document.getElementById('memoryTimeChart').getContext('2d'); - const memoryDatasets = memoryData.map((memory, index) => ({ - label: `Memory Usage (%) ${memory.metric.instance}`, - data: memory.values, - borderColor: `rgba(${75 + index * 20}, 75, 192, 1)`, - backgroundColor: `rgba(${75 + index * 20}, 75, 192, 0.2)`, + createChart(ctxMemory, timeData, [{ + label: 'Memory Usage (%)', + data: memoryData, + borderColor: 'rgba(153, 102, 255, 1)', + backgroundColor: 'rgba(153, 102, 255, 0.2)', + fill: true, tension: 0.4 - })); - - createChart(ctxMemory, timeData, memoryDatasets, 'Memory Usage (%)'); + }], 'Memory Usage (%)'); // Battery Percentage Chart const ctxBattery = document.getElementById('batteryTimeChart').getContext('2d'); - const batteryDatasets = batteryData.map((battery, index) => ({ - label: `Battery Usage (%) ${battery.metric.instance}`, - data: battery.values, - borderColor: `rgba(${192 + index * 20}, 75, 75, 1)`, - backgroundColor: `rgba(${192 + index * 20}, 75, 75, 0.2)`, + createChart(ctxBattery, timeData, [{ + label: 'Battery Percentage (%)', + data: batteryData, + borderColor: 'rgba(255, 159, 64, 1)', + backgroundColor: 'rgba(255, 159, 64, 0.2)', + fill: true, tension: 0.4 - })); - - createChart(ctxBattery, timeData, batteryDatasets, 'Battery Percentage (%)'); + }], 'Battery Percentage (%)'); // Network Sent & Received Chart const ctxNetwork = document.getElementById('networkTimeChart').getContext('2d'); - const networkDatasets = [ - ...networkSentData.map((networkSent, index) => ({ - label: `Network Sent (MB) ${networkSent.metric.instance}`, - data: networkSent.values, - borderColor: `rgba(${75 + index * 20}, 75, 192, 1)`, - backgroundColor: `rgba(${75 + index * 20}, 75, 192, 0.2)`, + createChart(ctxNetwork, timeData, [ + { + label: 'Network Sent (MB)', + data: networkSentData, + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + fill: true, tension: 0.4 - })), - ...networkReceivedData.map((networkReceived, index) => ({ - label: `Network Received (MB) ${networkReceived.metric.instance}`, - data: networkReceived.values, - borderColor: `rgba(${192 + index * 20}, 75, 75, 1)`, - backgroundColor: `rgba(${192 + index * 20}, 75, 75, 0.2)`, + }, + { + label: 'Network Received (MB)', + data: networkReceivedData, + borderColor: 'rgba(54, 162, 235, 1)', + backgroundColor: 'rgba(54, 162, 235, 0.2)', + fill: true, tension: 0.4 - })) - ]; - - createChart(ctxNetwork, timeData, networkDatasets, 'Data Transferred (MB)'); + } + ], 'Data Transferred (MB)'); // Dashboard Memory Usage Chart const ctxDashboardMemory = document.getElementById('dashboardMemoryTimeChart').getContext('2d'); - const dashboardMemoryDatasets = dashboardMemoryUsageData.map((dashboardMemory, index) => ({ - label: `Dashboard Memory Usage (%) ${dashboardMemory.metric.instance}`, - data: dashboardMemory.values, - borderColor: `rgba(${75 + index * 20}, 192, 75, 1)`, - backgroundColor: `rgba(${75 + index * 20}, 192, 75, 0.2)`, + createChart(ctxDashboardMemory, timeData, [{ + label: 'Dashboard Memory Usage (%)', + data: dashboardMemoryUsageData, + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + fill: true, tension: 0.4 - })); - - createChart(ctxDashboardMemory, timeData, dashboardMemoryDatasets, 'Dashboard Memory Usage (%)'); + }], 'Dashboard Memory Usage (%)'); // CPU Frequency Chart const ctxCpuFrequency = document.getElementById('cpuFrequencyTimeChart').getContext('2d'); - const cpuFrequencyDatasets = cpuFrequencyData.map((cpuFrequency, index) => ({ - label: `CPU Frequency (GHz) ${cpuFrequency.metric.instance}`, - data: cpuFrequency.values, - borderColor: `rgba(${192 + index * 20}, 192, 75, 1)`, - backgroundColor: `rgba(${192 + index * 20}, 192, 75, 0.2)`, - tension: 0.4, - - })); - - createChart(ctxCpuFrequency, timeData, cpuFrequencyDatasets, 'CPU Frequency (GHz)'); + createChart(ctxCpuFrequency, timeData, [{ + label: 'CPU Frequency (GHz)', + data: cpuFrequencyData, + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + fill: true, + tension: 0.4 + }], 'CPU Frequency (GHz)'); // Current Temperature Chart const ctxCurrentTemp = document.getElementById('currentTempTimeChart').getContext('2d'); - const currentTempDatasets = currentTempData.map((currentTemp, index) => ({ - label: `Current Temperature (°C) ${currentTemp.metric.instance}`, - data: currentTemp.values, - borderColor: `rgba(${75 + index * 20}, 192, 192, 1)`, - backgroundColor: `rgba(${75 + index * 20}, 192, 192, 0.2)`, + createChart(ctxCurrentTemp, timeData, [{ + label: 'Current Temperature (°C)', + data: currentTempData, + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + fill: true, tension: 0.4 - })); - - createChart(ctxCurrentTemp, timeData, currentTempDatasets, 'Current Temperature (°C)'); + }], 'Current Temperature (°C)'); } // Fetch initial data when the page loads diff --git a/src/static/js/graphs copy.js b/src/static/js/graphs_experimental.js similarity index 69% rename from src/static/js/graphs copy.js rename to src/static/js/graphs_experimental.js index 07e5d59..d981898 100644 --- a/src/static/js/graphs copy.js +++ b/src/static/js/graphs_experimental.js @@ -12,7 +12,7 @@ function fetchDataAndRenderCharts() { console.log('Stored Filter Value:', storedFilterValue); // Fetch data with the selected time filter - fetch(`/api/v1/prometheus/graphs_data?filter=${storedFilterValue}`) + fetch(`/api/v1/prometheus/graphs_data/targets?filter=${storedFilterValue}`) .then(response => response.json()) .then(data => { const cpuData = data.cpu; @@ -26,6 +26,8 @@ function fetchDataAndRenderCharts() { const currentTime = data.current_time; const timeZoneName = Intl.DateTimeFormat().resolvedOptions().timeZone; + console.log('cpuFrequencyData:', cpuFrequencyData); + // Format the time data using the currentTime from backend const timeData = data.time.map(time => formatDate(time, currentTime)); @@ -164,17 +166,17 @@ function createChart(ctx, labels, datasets, yLabel) { options: { scales: { x: { - type: 'category', // Use 'category' scale to treat time as strings + type: 'category', + reverse: true, // Reverse the x-axis to start from the right title: { display: true, text: 'Time' }, ticks: { - autoSkip: true, // Automatically skip some labels to prevent overlap - maxTicksLimit: 10, // Maximum number of ticks to display - maxRotation: 20, // Prevent rotating the labels for better readability + autoSkip: true, + maxTicksLimit: 10, + maxRotation: 20, minRotation: 0, - } }, y: { @@ -194,90 +196,97 @@ function createChart(ctx, labels, datasets, yLabel) { function createCharts(cpuData, timeData, memoryData, batteryData, networkSentData, networkReceivedData, dashboardMemoryUsageData, cpuFrequencyData, currentTempData) { // CPU Usage Chart const ctxCpu = document.getElementById('cpuTimeChart').getContext('2d'); - createChart(ctxCpu, timeData, [{ - label: 'CPU Usage (%)', - data: cpuData, - borderColor: 'rgba(75, 192, 192, 1)', - backgroundColor: 'rgba(75, 192, 192, 0.2)', - fill: true, + const cpuDatasets = cpuData.map((cpu, index) => ({ + label: `CPU Usage (%) ${cpu.metric.instance}`, + data: cpu.values, + borderColor: `rgba(${75 + index * 20}, 192, 192, 1)`, + backgroundColor: `rgba(${75 + index * 20}, 192, 192, 0.2)`, tension: 0.4 - }], 'CPU Usage (%)'); + })); + + createChart(ctxCpu, timeData, cpuDatasets, 'CPU Usage (%)'); // Memory Usage Chart const ctxMemory = document.getElementById('memoryTimeChart').getContext('2d'); - createChart(ctxMemory, timeData, [{ - label: 'Memory Usage (%)', - data: memoryData, - borderColor: 'rgba(153, 102, 255, 1)', - backgroundColor: 'rgba(153, 102, 255, 0.2)', - fill: true, + const memoryDatasets = memoryData.map((memory, index) => ({ + label: `Memory Usage (%) ${memory.metric.instance}`, + data: memory.values, + borderColor: `rgba(${75 + index * 20}, 75, 192, 1)`, + backgroundColor: `rgba(${75 + index * 20}, 75, 192, 0.2)`, tension: 0.4 - }], 'Memory Usage (%)'); + })); + + createChart(ctxMemory, timeData, memoryDatasets, 'Memory Usage (%)'); // Battery Percentage Chart const ctxBattery = document.getElementById('batteryTimeChart').getContext('2d'); - createChart(ctxBattery, timeData, [{ - label: 'Battery Percentage (%)', - data: batteryData, - borderColor: 'rgba(255, 159, 64, 1)', - backgroundColor: 'rgba(255, 159, 64, 0.2)', - fill: true, + const batteryDatasets = batteryData.map((battery, index) => ({ + label: `Battery Usage (%) ${battery.metric.instance}`, + data: battery.values, + borderColor: `rgba(${192 + index * 20}, 75, 75, 1)`, + backgroundColor: `rgba(${192 + index * 20}, 75, 75, 0.2)`, tension: 0.4 - }], 'Battery Percentage (%)'); + })); + + createChart(ctxBattery, timeData, batteryDatasets, 'Battery Percentage (%)'); // Network Sent & Received Chart const ctxNetwork = document.getElementById('networkTimeChart').getContext('2d'); - createChart(ctxNetwork, timeData, [ - { - label: 'Network Sent (MB)', - data: networkSentData, - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - fill: true, + const networkDatasets = [ + ...networkSentData.map((networkSent, index) => ({ + label: `Network Sent (MB) ${networkSent.metric.instance}`, + data: networkSent.values, + borderColor: `rgba(${75 + index * 20}, 75, 192, 1)`, + backgroundColor: `rgba(${75 + index * 20}, 75, 192, 0.2)`, tension: 0.4 - }, - { - label: 'Network Received (MB)', - data: networkReceivedData, - borderColor: 'rgba(54, 162, 235, 1)', - backgroundColor: 'rgba(54, 162, 235, 0.2)', - fill: true, + })), + ...networkReceivedData.map((networkReceived, index) => ({ + label: `Network Received (MB) ${networkReceived.metric.instance}`, + data: networkReceived.values, + borderColor: `rgba(${192 + index * 20}, 75, 75, 1)`, + backgroundColor: `rgba(${192 + index * 20}, 75, 75, 0.2)`, tension: 0.4 - } - ], 'Data Transferred (MB)'); + })) + ]; + + createChart(ctxNetwork, timeData, networkDatasets, 'Data Transferred (MB)'); // Dashboard Memory Usage Chart const ctxDashboardMemory = document.getElementById('dashboardMemoryTimeChart').getContext('2d'); - createChart(ctxDashboardMemory, timeData, [{ - label: 'Dashboard Memory Usage (%)', - data: dashboardMemoryUsageData, - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - fill: true, + const dashboardMemoryDatasets = dashboardMemoryUsageData.map((dashboardMemory, index) => ({ + label: `Dashboard Memory Usage (%) ${dashboardMemory.metric.instance}`, + data: dashboardMemory.values, + borderColor: `rgba(${75 + index * 20}, 192, 75, 1)`, + backgroundColor: `rgba(${75 + index * 20}, 192, 75, 0.2)`, tension: 0.4 - }], 'Dashboard Memory Usage (%)'); + })); + + createChart(ctxDashboardMemory, timeData, dashboardMemoryDatasets, 'Dashboard Memory Usage (%)'); // CPU Frequency Chart const ctxCpuFrequency = document.getElementById('cpuFrequencyTimeChart').getContext('2d'); - createChart(ctxCpuFrequency, timeData, [{ - label: 'CPU Frequency (GHz)', - data: cpuFrequencyData, - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - fill: true, - tension: 0.4 - }], 'CPU Frequency (GHz)'); + const cpuFrequencyDatasets = cpuFrequencyData.map((cpuFrequency, index) => ({ + label: `CPU Frequency (GHz) ${cpuFrequency.metric.instance}`, + data: cpuFrequency.values, + borderColor: `rgba(${192 + index * 20}, 192, 75, 1)`, + backgroundColor: `rgba(${192 + index * 20}, 192, 75, 0.2)`, + tension: 0.4, + + })); + + createChart(ctxCpuFrequency, timeData, cpuFrequencyDatasets, 'CPU Frequency (GHz)'); // Current Temperature Chart const ctxCurrentTemp = document.getElementById('currentTempTimeChart').getContext('2d'); - createChart(ctxCurrentTemp, timeData, [{ - label: 'Current Temperature (°C)', - data: currentTempData, - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - fill: true, + const currentTempDatasets = currentTempData.map((currentTemp, index) => ({ + label: `Current Temperature (°C) ${currentTemp.metric.instance}`, + data: currentTemp.values, + borderColor: `rgba(${75 + index * 20}, 192, 192, 1)`, + backgroundColor: `rgba(${75 + index * 20}, 192, 192, 0.2)`, tension: 0.4 - }], 'Current Temperature (°C)'); + })); + + createChart(ctxCurrentTemp, timeData, currentTempDatasets, 'Current Temperature (°C)'); } // Fetch initial data when the page loads diff --git a/src/templates/experimental/graphs.html b/src/templates/experimental/graphs.html new file mode 100644 index 0000000..a712f20 --- /dev/null +++ b/src/templates/experimental/graphs.html @@ -0,0 +1,132 @@ +{% extends 'base/base.html' %} +{% block title %}Dashboard Network{% endblock %} +{% block extra_head %} + + + + + + +{% endblock %} +{% block content %} + +

+ {{ title }} Metrics (Experimental) +

+
+ + +
+
+

Current Server Time & Time Zone

+ + Exit Experimental Graphs +
+

Current Time:

+

Time Zone:

+
+
+ + +
+
+
+ + +
+
+
+
+ +
+
+
+

CPU Usage

+ +
+ +
+ +
+
+

Memory Usage

+ +
+ +
+
+ +
+
+
+

Current Temperature

+ +
+ +
+ +
+
+

CPU Frequency

+ +
+ +
+
+ +
+
+
+

Battery Percentage

+ +
+ +
+ +
+
+

Network Sent & Received

+ +
+ +
+
+ +
+
+
+

Memory Usage(APP)

+ +
+ +
+ +
+ +{% endblock %} +{% block extra_scripts %} + + +{% endblock %} \ No newline at end of file diff --git a/src/templates/graphs/graphs.html b/src/templates/graphs/graphs.html index 3333dba..a1c184f 100644 --- a/src/templates/graphs/graphs.html +++ b/src/templates/graphs/graphs.html @@ -20,6 +20,7 @@

Current Server Time & Time Zone

+ Experimental Graphs

Current Time:

Time Zone:

From f76246909fe8ec108925a7a6a015061704a7eabc Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sun, 22 Sep 2024 12:13:07 +0530 Subject: [PATCH 21/46] removed unnessarty import --- src/background_task/log_system_info.py | 2 -- src/config.py | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/background_task/log_system_info.py b/src/background_task/log_system_info.py index e760634..5249c2d 100644 --- a/src/background_task/log_system_info.py +++ b/src/background_task/log_system_info.py @@ -1,4 +1,3 @@ -import os import datetime from threading import Timer from src.config import app, db @@ -7,7 +6,6 @@ from src.models import GeneralSettings, SystemInformation from sqlalchemy.exc import SQLAlchemyError from prometheus_client import Counter, Gauge -import os, time from src.logger import logger # Flag to track if logging is already scheduled diff --git a/src/config.py b/src/config.py index e4a6048..4bfe3f4 100644 --- a/src/config.py +++ b/src/config.py @@ -2,10 +2,7 @@ from flask import Flask, render_template from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate -import os, time -from influxdb_client import InfluxDBClient, Point, WritePrecision -from influxdb_client.client.write_api import SYNCHRONOUS - +import os from src.logger import logger from src.helper import get_system_node_name, get_ip_address From 0adbfc689a545ce907cc91d64e9424e067e6c938 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sun, 22 Sep 2024 12:58:38 +0530 Subject: [PATCH 22/46] Refactor API endpoint URL in graphs.js and remove unnecessary import --- app.py | 11 +- src/background_task/__init__.py | 16 +- src/background_task/log_system_info.py | 4 +- src/background_task/thread_process.py | 248 ------------------------- src/scripts/prometheus.sh | 2 +- src/static/js/graphs.js | 1 - src/static/js/graphs_experimental.js | 1 - 7 files changed, 22 insertions(+), 261 deletions(-) delete mode 100644 src/background_task/thread_process.py diff --git a/app.py b/app.py index 68877a7..bb29e1c 100644 --- a/app.py +++ b/app.py @@ -1,13 +1,10 @@ from src.config import app from src import routes -from src.background_task import start_website_monitoring, monitor_settings, fetch_file_metrics_task -import os +from src.background_task import start_background_tasks + +# Start the background tasks +start_background_tasks() -# background thread to monitor system settings changes -if os.getenv('FLASK_ENV') == 'production': - start_website_monitoring() # Starts pinging active websites - fetch_file_metrics_task() - monitor_settings() # Starts monitoring for system logging changes if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/src/background_task/__init__.py b/src/background_task/__init__.py index b41fbf5..55abe59 100644 --- a/src/background_task/__init__.py +++ b/src/background_task/__init__.py @@ -1,5 +1,19 @@ +import os from src.background_task.monitor_website import start_website_monitoring from src.background_task.log_system_info import monitor_settings from src.background_task.prometheus_helper import fetch_file_metrics_task +from src.logger import logger -__all__ = ["start_website_monitoring", "monitor_settings", "fetch_file_metrics_task"] \ No newline at end of file + + +def start_background_tasks(): + """ + Starts the background tasks for the application. + """ + if os.getenv('FLASK_ENV') == 'production': + logger.info("Starting background tasks for production environment.") + start_website_monitoring() + fetch_file_metrics_task() + monitor_settings() + else: + logger.info("Background tasks are not started in development environment.") \ No newline at end of file diff --git a/src/background_task/log_system_info.py b/src/background_task/log_system_info.py index 5249c2d..6733073 100644 --- a/src/background_task/log_system_info.py +++ b/src/background_task/log_system_info.py @@ -10,7 +10,7 @@ from src.logger import logger # Flag to track if logging is already scheduled is_logging_scheduled = False -fetch_system_info_interval = 30 +fetch_system_info_interval = 10 # Initialize Prometheus metrics metrics = { @@ -175,7 +175,7 @@ def monitor_settings(): is_logging_scheduled = False # Recheck settings every 10 seconds - Timer(30, monitor_settings).start() + Timer(10, monitor_settings).start() except SQLAlchemyError as db_err: logger.error(f"Error fetching settings: {db_err}", exc_info=True) diff --git a/src/background_task/thread_process.py b/src/background_task/thread_process.py deleted file mode 100644 index b5108a0..0000000 --- a/src/background_task/thread_process.py +++ /dev/null @@ -1,248 +0,0 @@ -# deprecated file - -import os -import datetime -from threading import Timer -from flask import url_for -from src.config import app, db -from src.utils import _get_system_info -from src.logger import logger -from src.models import MonitoredWebsite, GeneralSettings, SystemInformation -from sqlalchemy.exc import SQLAlchemyError -import requests -from src.scripts.email_me import send_smtp_email -from src.logger import logger -from src.utils import render_template_from_file, ROOT_DIR -from src.config import get_app_info - -# Dictionary to track the last known status of each website -website_status = {} -# Flag to check if logging is already scheduled -is_logging_scheduled = False - - -def send_mail(website_name, status, email_adress, email_alerts_enabled): - """ - Dummy function to simulate sending an email. - - Args: - website_name (str): The name or URL of the website. - status (str): The status of the website, either 'DOWN' or 'UP'. - """ - # This is a dummy function, so no real email is sent. - if email_alerts_enabled: - context = { - "website_status": status, # UP/DOWN - "website_name": website_name, - "checked_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "message": f"{website_name} is now {status}", - "title": get_app_info()["title"], - } - website_status_template = os.path.join( - ROOT_DIR, "src/templates/email_templates/website_monitor_status.html" - ) - email_subject = f"{website_name} is now {status}" - email_body = render_template_from_file(website_status_template, **context) - send_smtp_email(email_adress, email_subject, email_body, is_html=True) - - -def update_website_status(website, status): - """ - Updates the status of the website and sends an email notification if the status has changed. - - Args: - website (MonitoredWebsite): The website object to update. - status (str): The new status of the website. - """ - global website_status - - if website.id not in website_status: - website_status[website.id] = "UP" # Initialize with UP status if not present - - if website_status[website.id] != status: - send_mail( - website.name, status, website.email_address, website.email_alerts_enabled - ) - website_status[website.id] = status - - -def ping_website(website): - """ - Pings a single website and updates its status in the database. - - Args: - website (MonitoredWebsite): The website object to ping. - """ - with app.app_context(): - try: - # Check if the website is still active - updated_website = MonitoredWebsite.query.get(website.id) - if not updated_website or not updated_website.is_ping_active: - logger.info( - f"Website {website.name} is no longer active. Stopping monitoring." - ) - return - - logger.info( - f"Pinging {website.name} (Interval: {website.ping_interval}s)..." - ) - response = requests.get(website.name, timeout=10) - updated_website.last_ping_time = datetime.datetime.now() - updated_website.ping_status_code = response.status_code - - new_status = "UP" if response.status_code == 200 else "DOWN" - updated_website.ping_status = new_status - - # Update the website status - db.session.commit() - logger.info(f"Website {website.name} updated successfully.") - - # Determine if an email should be sent - update_website_status(website, new_status) - - except requests.RequestException as req_err: - updated_website.ping_status = "DOWN" - logger.error(f"Failed to ping {website.name}: {req_err}", exc_info=True) - db.session.rollback() - - except SQLAlchemyError as db_err: - logger.error( - f"Database commit error for {website.name}: {db_err}", exc_info=True - ) - db.session.rollback() - - finally: - # Add more detailed logging for debugging - if db.session.new or db.session.dirty: - logger.warning( - f"Database transaction not committed properly for {website.name}." - ) - - # Schedule the next ping for this website - Timer( - updated_website.ping_interval, ping_website, args=[updated_website] - ).start() - -def start_website_monitoring(): - """ - Periodically pings monitored websites based on individual ping intervals. - """ - with app.app_context(): - try: - while True: - active_websites = MonitoredWebsite.query.filter_by( - is_ping_active=True - ).all() - if not active_websites: - logger.info("No active websites to monitor.") - else: - for website in active_websites: - # Start pinging each website individually based on its ping interval - Timer(0, ping_website, args=[website]).start() - - # Check for active websites periodically (every 30 seconds) - Timer(30, start_website_monitoring).start() - break # Break out of the loop to avoid creating a new thread infinitely - - except SQLAlchemyError as db_err: - logger.error( - f"Database error during website monitoring: {db_err}", exc_info=True - ) - except Exception as e: - logger.error(f"Error during website monitoring: {e}", exc_info=True) - -def monitor_settings(): - """ - Monitors application general settings for changes and controls system logging dynamically. - This function runs periodically to check for updates to logging settings. - """ - global is_logging_scheduled - with app.app_context(): - try: - # Fetch the general settings - general_settings = GeneralSettings.query.first() - - # Check if logging should be active or not - is_logging_system_info = ( - general_settings.is_logging_system_info if general_settings else False - ) - if is_logging_system_info: - logger.info("System logging enabled. Starting system info logging.") - - # Schedule logging only if not already scheduled - if not is_logging_scheduled: - logger.debug("Scheduling system info logging.") - Timer(0, log_system_info).start() - is_logging_scheduled = True - else: - logger.info("System logging disabled. Stopping system info logging.") - is_logging_scheduled = False # Reset the flag if logging is disabled - - # Check settings periodically (every 10 seconds) - Timer(10, monitor_settings).start() - - except SQLAlchemyError as db_err: - logger.error(f"Error fetching settings: {db_err}", exc_info=True) - - -def log_system_info(): - """ - Logs system information at regular intervals based on the general settings. - This function checks if logging is still active before each logging event. - """ - global is_logging_scheduled - with app.app_context(): - try: - # Fetch the general settings to check if logging is enabled - general_settings = GeneralSettings.query.first() - is_logging_system_info = ( - general_settings.is_logging_system_info if general_settings else False - ) - - if not is_logging_system_info: - logger.info("System info logging has been stopped.") - is_logging_scheduled = False # Reset the flag if logging stops - return - - log_system_info_to_db() - logger.debug("System information logged successfully.") - - # Schedule the next log after 60 seconds - Timer(60, log_system_info).start() - - except Exception as e: - logger.error(f"Error during system info logging: {e}", exc_info=True) - is_logging_scheduled = False # Reset the flag in case of an error - - -def log_system_info_to_db(): - """ - Fetches system information and logs it to the database. - """ - with app.app_context(): - try: - system_info = _get_system_info() - system_log = SystemInformation( - cpu_percent=system_info["cpu_percent"], - memory_percent=system_info["memory_percent"], - battery_percent=system_info["battery_percent"], - network_sent=system_info["network_sent"], - network_received=system_info["network_received"], - dashboard_memory_usage=system_info["dashboard_memory_usage"], - cpu_frequency=system_info["cpu_frequency"], - current_temp=system_info["current_temp"], - timestamp=datetime.datetime.now(), - ) - db.session.add(system_log) - db.session.commit() - logger.info("System information logged to database.") - - except SQLAlchemyError as db_err: - logger.error( - f"Database error while logging system info: {db_err}", exc_info=True - ) - db.session.rollback() - except Exception as e: - logger.error(f"Failed to log system information: {e}", exc_info=True) - - diff --git a/src/scripts/prometheus.sh b/src/scripts/prometheus.sh index 714019c..3c6fd1f 100644 --- a/src/scripts/prometheus.sh +++ b/src/scripts/prometheus.sh @@ -23,7 +23,7 @@ mkdir -p "$PROMETHEUS_CONFIG_DIR" log "Generating prometheus.yml configuration file." cat > "$PROMETHEUS_CONFIG_FILE" < Date: Sun, 22 Sep 2024 14:01:40 +0530 Subject: [PATCH 23/46] Refactor issue templates and README.md, and update prometheus.sh script --- .github/ISSUE_TEMPLATE/bug_report.md | 20 +++++-------- .github/ISSUE_TEMPLATE/feature_request.md | 4 +++ README.md | 11 +++++++ src/docs/images/SystemGuard-Architecture.jpg | Bin 0 -> 80584 bytes src/scripts/prometheus.sh | 30 +++++++++++++++++-- 5 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 src/docs/images/SystemGuard-Architecture.jpg diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..4d823bc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,31 +8,27 @@ assignees: '' --- **Describe the bug** + A clear and concise description of what the bug is. **To Reproduce** + Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error **Expected behavior** + A clear and concise description of what you expected to happen. **Screenshots** + If applicable, add screenshots to help explain your problem. -**Desktop (please complete the following information):** +**(please complete the following information):** + - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + - Systemguard version [e.g. 22] **Additional context** + Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7..f0291e0 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,13 +8,17 @@ assignees: '' --- **Is your feature request related to a problem? Please describe.** + A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** + A clear and concise description of what you want to happen. **Describe alternatives you've considered** + A clear and concise description of any alternative solutions or features you've considered. **Additional context** + Add any other context or screenshots about the feature request here. diff --git a/README.md b/README.md index 0d86b24..3760df4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ System Guard is a Flask app designed to monitor server stats such as CPU, Memory, Disk, and Network. It also provides real-time monitoring capabilities which can be useful for system administrators, developers, and DevOps engineers to keep track of their server's performance and troubleshoot issues. The app uses the `psutil` library to retrieve system stats and the `speedtest-cli` library to perform a network speed test. + ## Features 🚀 - Lightweight, open-source, and free to use with a straightforward installation process, out-of-the-box monitoring solution. @@ -18,6 +19,16 @@ System Guard is a Flask app designed to monitor server stats such as CPU, Memory - Role-based dashboards tailored for Developer, Admin, IT Manager, and Manager roles (upcoming feature). - Update security updates with a single click or automatically update to the latest version to simplify maintenance. +## Architecture 🏗️ + +![SystemGuard-Architecture](/src/docs/images/SystemGuard-Architecture.jpg) + +## Tech Stack 🛠️ + +- **Frontend**: JavaScript, Bootstrap, Chart.js, Grafana +- **Backend**: Python, Flask, SQLAlchemy, SQLite, Prometheus, InfluxDB +- **Monitoring**: psutil, speedtest-cli, nmap, netstat + ## Get started 🛠️ - Check the [Installation.md](/src/docs/installation.md) file for installation instructions. diff --git a/src/docs/images/SystemGuard-Architecture.jpg b/src/docs/images/SystemGuard-Architecture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..06556c0af611cfb2a6ed324ba9747936a65559ca GIT binary patch literal 80584 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 literal 0 HcmV?d00001 diff --git a/src/scripts/prometheus.sh b/src/scripts/prometheus.sh index 3c6fd1f..7a53480 100644 --- a/src/scripts/prometheus.sh +++ b/src/scripts/prometheus.sh @@ -1,5 +1,21 @@ #!/bin/bash +# function to get the user name +get_user_name() { + if [ "$(whoami)" = "root" ]; then + LOGNAME_USER=$(logname 2>/dev/null) # Redirect any error output to /dev/null + if [ $? -ne 0 ]; then # Check if the exit status of the last command is not 0 + USER_NAME=$(cat /etc/passwd | grep '/home' | cut -d: -f1 | tail -n 1) + else + USER_NAME=$LOGNAME_USER + fi + else + USER_NAME=$(whoami) + fi + echo "$USER_NAME" +} + +USER_NAME=$(get_user_name) # Configuration NETWORK_NAME="flask-prometheus-net" CONTAINER_NAME="prometheus" @@ -7,8 +23,11 @@ PROMETHEUS_IMAGE="prom/prometheus" PROMETHEUS_PORT="9090" PROMETHEUS_CONFIG_DIR="$(pwd)/prometheus_config" PROMETHEUS_CONFIG_FILE="$PROMETHEUS_CONFIG_DIR/prometheus.yml" +PROMETHEUS_DATA_DIR="/home/$USER_NAME/.database/prometheus" FLASK_APP_IP=$(hostname -I | cut -d' ' -f1) FLASK_APP_PORT="5050" +SCRAPING_INTERVAL="10s" +JOB_NAME='systemguard-metrics' # Logging function for better readability log() { @@ -18,18 +37,22 @@ log() { # Ensure the config directory exists log "Creating Prometheus config directory if it doesn't exist." mkdir -p "$PROMETHEUS_CONFIG_DIR" +mkdir -p "$PROMETHEUS_DATA_DIR" # Create the prometheus.yml configuration file log "Generating prometheus.yml configuration file." cat > "$PROMETHEUS_CONFIG_FILE" <&1) # Capture both stdout and stderr # Check if Prometheus started successfully From 85fcac86ae32bd1e556d0efad5c8fb4d40d3abbe Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sun, 22 Sep 2024 16:43:13 +0530 Subject: [PATCH 24/46] chore: :art: graph page improved --- src/static/js/graphs.js | 100 ++++++++++++--------------- src/static/js/graphs_experimental.js | 96 +++++++++++-------------- 2 files changed, 85 insertions(+), 111 deletions(-) diff --git a/src/static/js/graphs.js b/src/static/js/graphs.js index 81f4558..3736b22 100644 --- a/src/static/js/graphs.js +++ b/src/static/js/graphs.js @@ -23,13 +23,14 @@ function fetchDataAndRenderCharts() { const dashboardMemoryUsageData = data.dashboard_memory_usage; const cpuFrequencyData = data.cpu_frequency; const currentTempData = data.current_temp; + + // current time from backend to display const currentTime = data.current_time; const timeZoneName = Intl.DateTimeFormat().resolvedOptions().timeZone; + displayTimeAndTimeZone(currentTime, timeZoneName); // Format the time data using the currentTime from backend - const timeData = data.time.map(time => formatDate(time, currentTime)); - - displayTimeAndTimeZone(currentTime, timeZoneName); + const timeData = data.time.map(time => formatDate(time, timeZoneName)); // Use timeZoneName from displayTimeAndTimeZone function createCharts(cpuData, timeData, memoryData, batteryData, networkSentData, networkReceivedData, dashboardMemoryUsageData, cpuFrequencyData, currentTempData); }) @@ -82,52 +83,25 @@ document.getElementById('refreshCurrentTempTime').addEventListener('click', () = fetchDataAndRenderCharts(); }); - -function formatDate(dateString, currentTime) { - const date = new Date(dateString); - const now = new Date(currentTime); // Use currentTime from backend - - // Helper function to format with leading zeros - const pad = (num) => String(num).padStart(2, '0'); - - // Manually extract UTC components - const day = pad(date.getUTCDate()); // e.g., 09 - const month = pad(date.getUTCMonth() + 1); // e.g., 04 - const year = date.getUTCFullYear(); // e.g., 2021 - const hours = pad(date.getUTCHours()); // e.g., 11 - const minutes = pad(date.getUTCMinutes()); // e.g., 33 - - - // Calculate time differences - const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); - const diffWeeks = Math.floor(diffDays / 7); - const diffMonths = now.getMonth() - date.getUTCMonth() + (12 * (now.getFullYear() - date.getUTCFullYear())); - const diffYears = now.getFullYear() - date.getUTCFullYear(); - - // Determine the label based on time differences - // // Reset the time to 12am for the date comparison - // date.setUTCHours(0, 0, 0, 0); - // now.setUTCHours(0, 0, 0, 0); - - // if (diffDays === 0) { - // return `Today ${hours}:${minutes}`; - // } else if (diffDays === 1) { - // return `Yesterday ${hours}:${minutes}`; - // } else if (diffDays <= 3) { - // return `${diffDays} Days Ago ${hours}:${minutes}`; - // } else if (diffDays <= 7) { - // return `${Math.ceil(diffDays / 7)} Week${diffDays > 7 ? 's' : ''} Ago ${hours}:${minutes}`; - // } else if (diffDays <= 30) { - // return `${Math.ceil(diffDays / 7)} Weeks Ago ${hours}:${minutes}`; - // } else if (diffMonths < 12) { - // return `${diffMonths} Month${diffMonths > 1 ? 's' : ''} Ago ${hours}:${minutes}`; - // } else if (diffYears < 2) { - // return `Last Year ${hours}:${minutes}`; - // } else { - // return `${year}/${month}/${day} ${hours}:${minutes}`; - // } - - return `${year}/${month}/${day} ${hours}:${minutes}`; +function formatDate(utcTime, timeZone) { + const date = new Date(utcTime); + + // Format options can be adjusted for your needs + const options = { + timeZone: timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false // Change to true if you prefer 12-hour format + }; + + // Generate formatted string + const formattedDate = date.toLocaleString('en-US', options); + + // For better graph display, you might want just the date and hour + return formattedDate.replace(/, (\d{2}:\d{2})/, ' $1'); // Example: "09/22/2024 14:30" } function displayTimeAndTimeZone(currentTime, timeZoneName) { @@ -155,13 +129,27 @@ function createChart(ctx, labels, datasets, yLabel) { ctx.chart.destroy(); // Destroy the existing chart if it exists } + const allDataPoints = datasets.flatMap(dataset => dataset.data); + const minY = Math.min(...allDataPoints.filter(value => typeof value === 'number')); + const maxY = Math.max(...allDataPoints.filter(value => typeof value === 'number')); + ctx.chart = new Chart(ctx, { type: 'line', data: { - labels: labels, // Use your timeData directly as labels - datasets: datasets + labels: labels, + datasets: datasets.map(dataset => ({ + ...dataset, + borderWidth: 2, + fill: false, + tension: 0.3, + pointRadius: 5, + pointHoverRadius: 7, + backgroundColor: dataset.backgroundColor || 'rgba(75, 192, 192, 0.2)', + borderColor: dataset.borderColor || 'rgba(75, 192, 192, 1)', + })), }, options: { + scales: { x: { type: 'category', @@ -171,17 +159,17 @@ function createChart(ctx, labels, datasets, yLabel) { }, ticks: { autoSkip: true, - maxTicksLimit: 10, - maxRotation: 20, + maxTicksLimit: 6, + maxRotation: 0, minRotation: 0, } }, y: { - beginAtZero: true, + beginAtZero: minY < 0 ? false : true, // Adjust y-axis based on data title: { display: true, - text: yLabel - } + text: yLabel, + }, } } } diff --git a/src/static/js/graphs_experimental.js b/src/static/js/graphs_experimental.js index c2a4c45..2547d71 100644 --- a/src/static/js/graphs_experimental.js +++ b/src/static/js/graphs_experimental.js @@ -26,10 +26,8 @@ function fetchDataAndRenderCharts() { const currentTime = data.current_time; const timeZoneName = Intl.DateTimeFormat().resolvedOptions().timeZone; - console.log('cpuFrequencyData:', cpuFrequencyData); - // Format the time data using the currentTime from backend - const timeData = data.time.map(time => formatDate(time, currentTime)); + const timeData = data.time.map(time => formatDate(time, timeZoneName)); // Use timeZoneName from displayTimeAndTimeZone function displayTimeAndTimeZone(currentTime, timeZoneName); @@ -85,51 +83,25 @@ document.getElementById('refreshCurrentTempTime').addEventListener('click', () = }); -function formatDate(dateString, currentTime) { - const date = new Date(dateString); - const now = new Date(currentTime); // Use currentTime from backend - - // Helper function to format with leading zeros - const pad = (num) => String(num).padStart(2, '0'); - - // Manually extract UTC components - const day = pad(date.getUTCDate()); // e.g., 09 - const month = pad(date.getUTCMonth() + 1); // e.g., 04 - const year = date.getUTCFullYear(); // e.g., 2021 - const hours = pad(date.getUTCHours()); // e.g., 11 - const minutes = pad(date.getUTCMinutes()); // e.g., 33 - - - // Calculate time differences - const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); - const diffWeeks = Math.floor(diffDays / 7); - const diffMonths = now.getMonth() - date.getUTCMonth() + (12 * (now.getFullYear() - date.getUTCFullYear())); - const diffYears = now.getFullYear() - date.getUTCFullYear(); - - // Determine the label based on time differences - // // Reset the time to 12am for the date comparison - // date.setUTCHours(0, 0, 0, 0); - // now.setUTCHours(0, 0, 0, 0); - - // if (diffDays === 0) { - // return `Today ${hours}:${minutes}`; - // } else if (diffDays === 1) { - // return `Yesterday ${hours}:${minutes}`; - // } else if (diffDays <= 3) { - // return `${diffDays} Days Ago ${hours}:${minutes}`; - // } else if (diffDays <= 7) { - // return `${Math.ceil(diffDays / 7)} Week${diffDays > 7 ? 's' : ''} Ago ${hours}:${minutes}`; - // } else if (diffDays <= 30) { - // return `${Math.ceil(diffDays / 7)} Weeks Ago ${hours}:${minutes}`; - // } else if (diffMonths < 12) { - // return `${diffMonths} Month${diffMonths > 1 ? 's' : ''} Ago ${hours}:${minutes}`; - // } else if (diffYears < 2) { - // return `Last Year ${hours}:${minutes}`; - // } else { - // return `${year}/${month}/${day} ${hours}:${minutes}`; - // } - - return `${year}/${month}/${day} ${hours}:${minutes}`; +function formatDate(utcTime, timeZone) { + const date = new Date(utcTime); + + // Format options can be adjusted for your needs + const options = { + timeZone: timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false // Change to true if you prefer 12-hour format + }; + + // Generate formatted string + const formattedDate = date.toLocaleString('en-US', options); + + // For better graph display, you might want just the date and hour + return formattedDate.replace(/, (\d{2}:\d{2})/, ' $1'); // Example: "09/22/2024 14:30" } function displayTimeAndTimeZone(currentTime, timeZoneName) { @@ -157,13 +129,27 @@ function createChart(ctx, labels, datasets, yLabel) { ctx.chart.destroy(); // Destroy the existing chart if it exists } + const allDataPoints = datasets.flatMap(dataset => dataset.data); + const minY = Math.min(...allDataPoints.filter(value => typeof value === 'number')); + const maxY = Math.max(...allDataPoints.filter(value => typeof value === 'number')); + ctx.chart = new Chart(ctx, { type: 'line', data: { - labels: labels, // Use your timeData directly as labels - datasets: datasets + labels: labels, + datasets: datasets.map(dataset => ({ + ...dataset, + borderWidth: 2, + fill: false, + tension: 0.3, + pointRadius: 5, + pointHoverRadius: 7, + backgroundColor: dataset.backgroundColor || 'rgba(75, 192, 192, 0.2)', + borderColor: dataset.borderColor || 'rgba(75, 192, 192, 1)', + })), }, options: { + scales: { x: { type: 'category', @@ -173,17 +159,17 @@ function createChart(ctx, labels, datasets, yLabel) { }, ticks: { autoSkip: true, - maxTicksLimit: 10, - maxRotation: 20, + maxTicksLimit: 6, + maxRotation: 0, minRotation: 0, } }, y: { - beginAtZero: true, + beginAtZero: minY < 0 ? false : true, // Adjust y-axis based on data title: { display: true, - text: yLabel - } + text: yLabel, + }, } } } From 6cba83057f3da810ddaef92a8c8c440d403ff524 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sun, 22 Sep 2024 17:17:36 +0530 Subject: [PATCH 25/46] chore: improved the graph --- src/routes/api.py | 17 +-- src/static/js/graphs_experimental.js | 151 ++++++++++++++++----------- 2 files changed, 103 insertions(+), 65 deletions(-) diff --git a/src/routes/api.py b/src/routes/api.py index 447ffc5..ed31b90 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -382,7 +382,7 @@ def graph_data_api_v3_(): # Create a new list for the time series data of this particular series series_data = { "metric": series.get("metric"), - "values": [] + "values": {} } # Iterate over the values for this series @@ -390,7 +390,7 @@ def graph_data_api_v3_(): timestamp = datetime.fromtimestamp(float(value[0]), tz=timezone.utc).isoformat() if timestamp not in time_data: time_data.append(timestamp) - series_data["values"].append(value[1]) + series_data["values"][timestamp] = value[1] # Append the series data to the metric metric_data[metric].append(series_data) @@ -399,16 +399,21 @@ def graph_data_api_v3_(): else: raise Exception(f"Failed to fetch data for {metric} from Prometheus: {response.text}") - # Ensure all metric data has the same length as time_data + # Sort the time data for proper alignment + time_data.sort() + + # Ensure all metric data aligns with time_data for metric, series_list in metric_data.items(): for series in series_list: - while len(series["values"]) < len(time_data): - series["values"].append(None) + aligned_values = [] + for timestamp in time_data: + aligned_values.append(series["values"].get(timestamp, None)) + series["values"] = aligned_values # Return the data as JSON response_data = { "time": time_data, - **metric_data, + **{metric: [{"metric": s["metric"], "values": s["values"]} for s in series_list] for metric, series_list in metric_data.items()}, "current_time": current_time } diff --git a/src/static/js/graphs_experimental.js b/src/static/js/graphs_experimental.js index 2547d71..d22714a 100644 --- a/src/static/js/graphs_experimental.js +++ b/src/static/js/graphs_experimental.js @@ -149,7 +149,7 @@ function createChart(ctx, labels, datasets, yLabel) { })), }, options: { - + responsive: true, scales: { x: { type: 'category', @@ -179,97 +179,130 @@ function createChart(ctx, labels, datasets, yLabel) { // Function to create charts with the fetched data function createCharts(cpuData, timeData, memoryData, batteryData, networkSentData, networkReceivedData, dashboardMemoryUsageData, cpuFrequencyData, currentTempData) { + + // Function to generate dynamic colors based on index + function generateColor(index) { + const hue = (index * 40) % 360; // Adjust hue for unique colors + return { + borderColor: `hsl(${hue}, 70%, 50%)`, // Border color + backgroundColor: `hsl(${hue}, 70%, 80%)` // Background color + }; + } + // CPU Usage Chart const ctxCpu = document.getElementById('cpuTimeChart').getContext('2d'); - const cpuDatasets = cpuData.map((cpu, index) => ({ - label: `CPU Usage (%) ${cpu.metric.instance}`, - data: cpu.values, - borderColor: `rgba(${75 + index * 20}, 192, 192, 1)`, - backgroundColor: `rgba(${75 + index * 20}, 192, 192, 0.2)`, - tension: 0.4 - })); + const cpuDatasets = cpuData.map((cpu, index) => { + const { borderColor, backgroundColor } = generateColor(index); + return { + label: `CPU Usage (%) ${cpu.metric.instance}`, + data: cpu.values, + borderColor: borderColor, + backgroundColor: backgroundColor, + tension: 0.4 + }; + }); createChart(ctxCpu, timeData, cpuDatasets, 'CPU Usage (%)'); // Memory Usage Chart const ctxMemory = document.getElementById('memoryTimeChart').getContext('2d'); - const memoryDatasets = memoryData.map((memory, index) => ({ - label: `Memory Usage (%) ${memory.metric.instance}`, - data: memory.values, - borderColor: `rgba(${75 + index * 20}, 75, 192, 1)`, - backgroundColor: `rgba(${75 + index * 20}, 75, 192, 0.2)`, - tension: 0.4 - })); + const memoryDatasets = memoryData.map((memory, index) => { + const { borderColor, backgroundColor } = generateColor(index); + return { + label: `Memory Usage (%) ${memory.metric.instance}`, + data: memory.values, + borderColor: borderColor, + backgroundColor: backgroundColor, + tension: 0.4 + }; + }); - createChart(ctxMemory, timeData, memoryDatasets, 'Memory Usage (%)'); + createChart(ctxMemory, timeData, memoryDatasets, 'Memory Usage (%)'); // Battery Percentage Chart const ctxBattery = document.getElementById('batteryTimeChart').getContext('2d'); - const batteryDatasets = batteryData.map((battery, index) => ({ - label: `Battery Usage (%) ${battery.metric.instance}`, - data: battery.values, - borderColor: `rgba(${192 + index * 20}, 75, 75, 1)`, - backgroundColor: `rgba(${192 + index * 20}, 75, 75, 0.2)`, - tension: 0.4 - })); + const batteryDatasets = batteryData.map((battery, index) => { + const { borderColor, backgroundColor } = generateColor(index); + return { + label: `Battery Usage (%) ${battery.metric.instance}`, + data: battery.values, + borderColor: borderColor, + backgroundColor: backgroundColor, + tension: 0.4 + }; + }); createChart(ctxBattery, timeData, batteryDatasets, 'Battery Percentage (%)'); // Network Sent & Received Chart const ctxNetwork = document.getElementById('networkTimeChart').getContext('2d'); const networkDatasets = [ - ...networkSentData.map((networkSent, index) => ({ - label: `Network Sent (MB) ${networkSent.metric.instance}`, - data: networkSent.values, - borderColor: `rgba(${75 + index * 20}, 75, 192, 1)`, - backgroundColor: `rgba(${75 + index * 20}, 75, 192, 0.2)`, - tension: 0.4 - })), - ...networkReceivedData.map((networkReceived, index) => ({ - label: `Network Received (MB) ${networkReceived.metric.instance}`, - data: networkReceived.values, - borderColor: `rgba(${192 + index * 20}, 75, 75, 1)`, - backgroundColor: `rgba(${192 + index * 20}, 75, 75, 0.2)`, - tension: 0.4 - })) + ...networkSentData.map((networkSent, index) => { + const { borderColor, backgroundColor } = generateColor(index); + return { + label: `Network Sent (MB) ${networkSent.metric.instance}`, + data: networkSent.values, + borderColor: borderColor, + backgroundColor: backgroundColor, + tension: 0.4 + }; + }), + ...networkReceivedData.map((networkReceived, index) => { + const { borderColor, backgroundColor } = generateColor(index + networkSentData.length); + return { + label: `Network Received (MB) ${networkReceived.metric.instance}`, + data: networkReceived.values, + borderColor: borderColor, + backgroundColor: backgroundColor, + tension: 0.4 + }; + }) ]; createChart(ctxNetwork, timeData, networkDatasets, 'Data Transferred (MB)'); // Dashboard Memory Usage Chart const ctxDashboardMemory = document.getElementById('dashboardMemoryTimeChart').getContext('2d'); - const dashboardMemoryDatasets = dashboardMemoryUsageData.map((dashboardMemory, index) => ({ - label: `Dashboard Memory Usage (%) ${dashboardMemory.metric.instance}`, - data: dashboardMemory.values, - borderColor: `rgba(${75 + index * 20}, 192, 75, 1)`, - backgroundColor: `rgba(${75 + index * 20}, 192, 75, 0.2)`, - tension: 0.4 - })); + const dashboardMemoryDatasets = dashboardMemoryUsageData.map((dashboardMemory, index) => { + const { borderColor, backgroundColor } = generateColor(index); + return { + label: `Dashboard Memory Usage (%) ${dashboardMemory.metric.instance}`, + data: dashboardMemory.values, + borderColor: borderColor, + backgroundColor: backgroundColor, + tension: 0.4 + }; + }); createChart(ctxDashboardMemory, timeData, dashboardMemoryDatasets, 'Dashboard Memory Usage (%)'); // CPU Frequency Chart const ctxCpuFrequency = document.getElementById('cpuFrequencyTimeChart').getContext('2d'); - const cpuFrequencyDatasets = cpuFrequencyData.map((cpuFrequency, index) => ({ - label: `CPU Frequency (GHz) ${cpuFrequency.metric.instance}`, - data: cpuFrequency.values, - borderColor: `rgba(${192 + index * 20}, 192, 75, 1)`, - backgroundColor: `rgba(${192 + index * 20}, 192, 75, 0.2)`, - tension: 0.4, - - })); + const cpuFrequencyDatasets = cpuFrequencyData.map((cpuFrequency, index) => { + const { borderColor, backgroundColor } = generateColor(index); + return { + label: `CPU Frequency (GHz) ${cpuFrequency.metric.instance}`, + data: cpuFrequency.values, + borderColor: borderColor, + backgroundColor: backgroundColor, + tension: 0.4 + }; + }); createChart(ctxCpuFrequency, timeData, cpuFrequencyDatasets, 'CPU Frequency (GHz)'); // Current Temperature Chart const ctxCurrentTemp = document.getElementById('currentTempTimeChart').getContext('2d'); - const currentTempDatasets = currentTempData.map((currentTemp, index) => ({ - label: `Current Temperature (°C) ${currentTemp.metric.instance}`, - data: currentTemp.values, - borderColor: `rgba(${75 + index * 20}, 192, 192, 1)`, - backgroundColor: `rgba(${75 + index * 20}, 192, 192, 0.2)`, - tension: 0.4 - })); + const currentTempDatasets = currentTempData.map((currentTemp, index) => { + const { borderColor, backgroundColor } = generateColor(index); + return { + label: `Current Temperature (°C) ${currentTemp.metric.instance}`, + data: currentTemp.values, + borderColor: borderColor, + backgroundColor: backgroundColor, + tension: 0.4 + }; + }); createChart(ctxCurrentTemp, timeData, currentTempDatasets, 'Current Temperature (°C)'); } From 0c7fd058fcbb07ed28300cd9f9869571c13ab19a Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sun, 22 Sep 2024 19:46:19 +0530 Subject: [PATCH 26/46] Refactor prometheus.py, README.md, and dashboard_network.html --- src/docs/README.md | 4 + src/routes/api.py | 46 +++++++- src/routes/dashboard_network.py | 60 +---------- src/routes/prometheus.py | 1 - src/static/css/dashboard_network.css | 108 ++++++------------- src/static/css/targets.css | 57 ++++++++++ src/static/images/tracking.png | Bin 0 -> 228934 bytes src/static/js/targets.js | 71 ++++++++++++ src/templates/network/add_server.html | 44 -------- src/templates/network/dashboard_network.html | 60 +++++------ src/templates/network/edit_server.html | 50 --------- 11 files changed, 236 insertions(+), 265 deletions(-) create mode 100644 src/static/css/targets.css create mode 100644 src/static/images/tracking.png create mode 100644 src/static/js/targets.js delete mode 100644 src/templates/network/add_server.html delete mode 100644 src/templates/network/edit_server.html diff --git a/src/docs/README.md b/src/docs/README.md index a864371..66b76ec 100644 --- a/src/docs/README.md +++ b/src/docs/README.md @@ -23,3 +23,7 @@ ## Security Analysis ![screenshot_007](/src/static/images/screenshot_007.png) + +## Central Dashboard + +![screenshot_008](/src/static/images/tracking.png) \ No newline at end of file diff --git a/src/routes/api.py b/src/routes/api.py index ed31b90..025a8c4 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -8,9 +8,15 @@ from flask import request, jsonify import gc from datetime import datetime, timezone + +from src.routes.helper.common_helper import admin_required + api_bp = blueprints.Blueprint("api", __name__) -PROMETHEUS_URL = 'http://localhost:9090/api/v1/query_range' # Change if using a different URL or port +PROMETHEUS_URL = 'http://localhost:9090' # Change if using a different URL or port +QUERY_API_URL = f'{PROMETHEUS_URL}/api/v1/query' +TARGETS_API_URL = f'{PROMETHEUS_URL}/api/v1/targets' + PROMETHEUS_METRICS = { 'cpu': 'cpu_usage_percentage', # Adjusting to match the defined gauge 'memory': 'memory_usage_percentage', @@ -283,7 +289,7 @@ def graph_data_api_v3(): } # Send the query to Prometheus - response = requests.get(PROMETHEUS_URL, params=params) + response = requests.get(QUERY_API_URL, params=params) # Check if the request was successful if response.status_code == 200: @@ -368,7 +374,7 @@ def graph_data_api_v3_(): } # Send the query to Prometheus - response = requests.get(PROMETHEUS_URL, params=params) + response = requests.get(QUERY_API_URL, params=params) # Check if the request was successful if response.status_code == 200: @@ -423,6 +429,40 @@ def graph_data_api_v3_(): # Handle and log the error for debugging purposes return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 + +@app.route('/api/v1/targets', methods=['GET']) +@admin_required +def get_prometheus_targets(): + try: + # Query Prometheus API to get the targets + response = requests.get(TARGETS_API_URL) + + # Check if the request was successful + if response.status_code == 200: + targets_data = response.json().get('data', {}) + active_targets = targets_data.get('activeTargets', []) + dropped_targets = targets_data.get('droppedTargets', []) + + # Return the active and dropped targets as JSON + return jsonify({ + 'active_targets': active_targets, + 'dropped_targets': dropped_targets + }), 200 + else: + # Handle non-200 responses from Prometheus + return jsonify({ + 'error': 'Failed to fetch targets from Prometheus', + 'details': response.text + }), response.status_code + + except Exception as e: + # Handle exceptions + return jsonify({ + 'error': 'An error occurred while fetching Prometheus targets', + 'details': str(e) + }), 500 + + @app.route('/api/v1/refresh-interval', methods=['GET', 'POST']) @login_required def manage_refresh_interval(): diff --git a/src/routes/dashboard_network.py b/src/routes/dashboard_network.py index 5f38f1b..284979e 100644 --- a/src/routes/dashboard_network.py +++ b/src/routes/dashboard_network.py @@ -1,63 +1,11 @@ -from flask import render_template, blueprints, flash, redirect, url_for, request +from flask import render_template, blueprints -from src.config import app, db -from src.models import DashboardNetworkSettings +from src.config import app from src.routes.helper.common_helper import admin_required network_bp = blueprints.Blueprint('network', __name__) -@app.route('/network', methods=['GET']) +@app.route('/dashboard_network', methods=['GET']) @admin_required def dashboard_network(): - groups = DashboardNetworkSettings.query.all() # Fetch all dashboard groups - return render_template('network/dashboard_network.html', groups=groups) - -@app.route('/add_server', methods=['GET', 'POST']) -@admin_required -def add_server(): - if request.method == 'POST': - name = request.form.get('name') - description = request.form.get('description') - ip_address = request.form.get('ip_address') - port = request.form.get('port') - link = request.form.get('link') - - # Check if the server name already exists - existing_server = DashboardNetworkSettings.query.filter_by(name=name).first() - if existing_server: - flash('Server name already exists. Please choose a different name.', 'danger') - return redirect(url_for('add_server')) - - # Create a new server entry - new_server = DashboardNetworkSettings(name=name, description=description, ip_address=ip_address, port=port, link=link) - db.session.add(new_server) - db.session.commit() - - flash('Server added successfully!', 'success') - return redirect(url_for('dashboard_network')) - - return render_template('network/add_server.html') - -@app.route('/edit_server/', methods=['GET', 'POST']) -@admin_required -def edit_server(server_id): - server = DashboardNetworkSettings.query.get_or_404(server_id) - if request.method == 'POST': - server.name = request.form['name'] - server.description = request.form['description'] - server.ip_address = request.form['ip_address'] - server.port = request.form['port'] - server.link = request.form['link'] - db.session.commit() - flash('Server updated successfully!', 'success') - return redirect(url_for('dashboard_network')) - return render_template('network/edit_server.html', server=server) - -@app.route('/delete_server/', methods=['POST']) -@admin_required -def delete_server(server_id): - server = DashboardNetworkSettings.query.get_or_404(server_id) - db.session.delete(server) - db.session.commit() - flash('Server deleted successfully!', 'success') - return redirect(url_for('dashboard_network')) + return render_template('network/dashboard_network.html') diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index 102e30f..75f8152 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -65,4 +65,3 @@ def delete_file_path(id): db.session.commit() flash('File path deleted successfully!', 'success') return redirect(url_for('external_monitoring')) - diff --git a/src/static/css/dashboard_network.css b/src/static/css/dashboard_network.css index bb30156..d90d57a 100644 --- a/src/static/css/dashboard_network.css +++ b/src/static/css/dashboard_network.css @@ -1,101 +1,57 @@ + + .container { max-width: 1200px; - margin: 0 auto; - padding: 20px; - text-align: center; - color: #fff; -} - -.card-container { - display: flex; - flex-wrap: wrap; - gap: 20px; - justify-content: center; + margin-top: 20px; } -.card { - background-color: var(--color-username); - border: 1px solid #ddd; - border-radius: 8px; - padding: 20px; - width: calc(33.333% - 40px); /* Adjust based on gaps */ - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - text-decoration: none; +h1 { + text-align: center; color: #333; - transition: transform 0.2s; -} - -.card:hover { - transform: translateX(-10px); /* Slight lift effect */ - box-shadow: 0 10px 20px #378fe7; - transition: transform 0.3s ease, box-shadow 0.3s ease; /* Smooth transition */ } -.card h2 { - margin-top: 0; - font-size: 1.5em; - color: #fff; -} - -.card p { - margin: 5px 0; - color: #fff; -} - -.card a { - color: #fff; - text-decoration: none; -} - -.add-server-link { +.target-table { margin-top: 20px; } -.add-server-link .btn { - font-size: 1.2em; - padding: 10px 20px; - border-radius: 5px; - background-color: #007bff; - color: white; - text-decoration: none; - transition: background-color 0.3s; +table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; } -.add-server-link .btn:hover { - background-color: #0056b3; +table th, table td { + padding: 12px; + border: 1px solid #ddd; + text-align: left; } -/* Responsive layout */ -@media (max-width: 992px) { - .card { - width: calc(50% - 30px); /* Two cards per line on medium screens */ - } +table th { + background-color: #3f51b5; + color: white; } -@media (max-width: 768px) { - .card { - width: calc(100% - 40px); /* One card per line on small screens */ - } +table tr:nth-child(even) { + background-color: #f9f9f9; } -.card-actions { - margin-top: 10px; +table td.health-up { + color: green; + font-weight: bold; } -.card-actions .btn { - margin-right: 5px; - padding: 5px 10px; - font-size: 14px; +table td.health-down { + color: red; + font-weight: bold; } -.card-actions .btn-warning { - background-color: #ffc107; - border-color: #ffc107; - color: #fff; +.error-message { + color: red; + font-style: italic; } -.card-actions .btn-danger { - background-color: #dc3545; - border-color: #dc3545; - color: #fff; +.footer { + text-align: center; + color: #666; + font-size: 12px; } diff --git a/src/static/css/targets.css b/src/static/css/targets.css new file mode 100644 index 0000000..d90d57a --- /dev/null +++ b/src/static/css/targets.css @@ -0,0 +1,57 @@ + + +.container { + max-width: 1200px; + margin-top: 20px; +} + +h1 { + text-align: center; + color: #333; +} + +.target-table { + margin-top: 20px; +} + +table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +table th, table td { + padding: 12px; + border: 1px solid #ddd; + text-align: left; +} + +table th { + background-color: #3f51b5; + color: white; +} + +table tr:nth-child(even) { + background-color: #f9f9f9; +} + +table td.health-up { + color: green; + font-weight: bold; +} + +table td.health-down { + color: red; + font-weight: bold; +} + +.error-message { + color: red; + font-style: italic; +} + +.footer { + text-align: center; + color: #666; + font-size: 12px; +} diff --git a/src/static/images/tracking.png b/src/static/images/tracking.png new file mode 100644 index 0000000000000000000000000000000000000000..a6a0dccabb0a7289b0e159152e2c05bf206d4fa5 GIT binary patch literal 228934 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�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 0) { + data.active_targets.forEach(target => { + const row = document.createElement('tr'); + + const instanceCell = document.createElement('td'); + instanceCell.textContent = target.labels.instance; + row.appendChild(instanceCell); + + const jobCell = document.createElement('td'); + jobCell.textContent = target.labels.job; + row.appendChild(jobCell); + + const healthCell = document.createElement('td'); + healthCell.textContent = target.health; + healthCell.className = target.health === 'up' ? 'health-up' : 'health-down'; + row.appendChild(healthCell); + + const lastScrapeCell = document.createElement('td'); + lastScrapeCell.textContent = new Date(target.lastScrape).toLocaleString(); + row.appendChild(lastScrapeCell); + + const lastErrorCell = document.createElement('td'); + lastErrorCell.textContent = target.lastError ? target.lastError : 'No Errors'; + row.appendChild(lastErrorCell); + + const scrapeUrlCell = document.createElement('td'); + scrapeUrlCell.textContent = target.scrapeUrl; + row.appendChild(scrapeUrlCell); + + const scrapeDurationCell = document.createElement('td'); + scrapeDurationCell.textContent = target.lastScrapeDuration.toFixed(3); + row.appendChild(scrapeDurationCell); + + if (target.health === 'up') { + const dashboardCell = document.createElement('td'); + const dashboardLink = document.createElement('a'); + dashboardLink.href = target.scrapeUrl.replace(/\/metrics$/, ''); + dashboardLink.textContent = 'View Dashboard'; + dashboardLink.target = '_blank'; // Open link in a new tab + dashboardLink.rel = 'noopener noreferrer'; // Security measure for external links + dashboardCell.appendChild(dashboardLink); + row.appendChild(dashboardCell); + } else { + const emptyCell = document.createElement('td'); + row.appendChild(emptyCell); + } + + // Append the row to the table body + targetTableBody.appendChild(row); + }); + } else { + targetTableBody.innerHTML = 'No active targets found'; + } + } catch (error) { + console.error('Error fetching target data:', error); + const targetTableBody = document.getElementById('target-table-body'); + targetTableBody.innerHTML = 'Failed to fetch targets'; + } +} + +// Fetch the data when the page loads +document.addEventListener('DOMContentLoaded', fetchTargetData); \ No newline at end of file diff --git a/src/templates/network/add_server.html b/src/templates/network/add_server.html deleted file mode 100644 index 44f3e32..0000000 --- a/src/templates/network/add_server.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'base/base.html' %} -{% block title %}Add New Dashboard Network{% endblock %} -{% block extra_head %} - - -{% endblock %} -{% block content %} -

-

Add New Server

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - -
-
-{% endblock %} -{% block extra_scripts %} - -{% endblock %} \ No newline at end of file diff --git a/src/templates/network/dashboard_network.html b/src/templates/network/dashboard_network.html index 850813f..c4bf7e1 100644 --- a/src/templates/network/dashboard_network.html +++ b/src/templates/network/dashboard_network.html @@ -1,41 +1,31 @@ {% extends 'base/base.html' %} -{% block title %}Dashboard Network{% endblock %} +{% block title %}Prometheus Active Targets{% endblock %} + {% block extra_head %} {% endblock %} {% block content %} - -
-

Dashboard Groups

-
- -{% endblock %} \ No newline at end of file +{% endblock %} +{% block extra_scripts %} + +{% endblock %} diff --git a/src/templates/network/edit_server.html b/src/templates/network/edit_server.html deleted file mode 100644 index 8c226fc..0000000 --- a/src/templates/network/edit_server.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends 'base/base.html' %} -{% block title %}Edit Server{% endblock %} -{% block extra_head %} - -{% endblock %} -{% block content %} -
-

Edit Server

-
-
- - -
-
- - -
-
- - -
- -
-
- - -
-
-
-
- - -
-
- - -
-
-{% endblock %} -{% block extra_scripts %} - -{% endblock %} \ No newline at end of file From 7641ab62268b540f544056bad170111170ded4ef Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Sun, 22 Sep 2024 20:52:59 +0530 Subject: [PATCH 27/46] chore: new feature added to add and remove targets --- requirements.txt | 5 +- src/background_task/__init__.py | 2 +- ...theus_helper.py => external_monitoring.py} | 0 src/influxdb_config.py | 100 ++++++++++++++++ src/models/page_toggle_settings.py | 3 +- src/routes/api.py | 103 +---------------- src/routes/helper/__init__.py | 0 src/routes/helper/prometheus_helper.py | 40 +++++++ src/routes/prometheus.py | 107 +++++++++++++----- src/static/css/targets.css | 81 ++++++++----- src/static/js/refreshCardData.js | 2 +- src/templates/ext/navbar.html | 2 +- src/templates/other/targets.html | 53 +++++++++ 13 files changed, 337 insertions(+), 161 deletions(-) rename src/background_task/{prometheus_helper.py => external_monitoring.py} (100%) create mode 100644 src/routes/helper/__init__.py create mode 100644 src/routes/helper/prometheus_helper.py create mode 100644 src/templates/other/targets.html diff --git a/requirements.txt b/requirements.txt index 51bde8f..9c55f5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,4 +29,7 @@ speedtest-cli==2.1.3 prometheus_client==0.20.0 # influxdb-client, optional for writing metrics to InfluxDB -# influxdb-client==1.46.0 \ No newline at end of file +# influxdb-client==1.46.0 + +# pyyaml for parsing YAML configuration files +pyyaml==6.0.2 \ No newline at end of file diff --git a/src/background_task/__init__.py b/src/background_task/__init__.py index 55abe59..9007474 100644 --- a/src/background_task/__init__.py +++ b/src/background_task/__init__.py @@ -1,7 +1,7 @@ import os from src.background_task.monitor_website import start_website_monitoring from src.background_task.log_system_info import monitor_settings -from src.background_task.prometheus_helper import fetch_file_metrics_task +from src.background_task.external_monitoring import fetch_file_metrics_task from src.logger import logger diff --git a/src/background_task/prometheus_helper.py b/src/background_task/external_monitoring.py similarity index 100% rename from src/background_task/prometheus_helper.py rename to src/background_task/external_monitoring.py diff --git a/src/influxdb_config.py b/src/influxdb_config.py index 87e68c1..5ba68aa 100644 --- a/src/influxdb_config.py +++ b/src/influxdb_config.py @@ -20,3 +20,103 @@ # except Exception as e: # logger.error(f"Failed to connect to InfluxDB: {e}") # raiseclient = InfluxDBClient(url=url, token=INFLUXDB_TOKEN, org=org) + + + +# @app.route('/api/v2/influxdb/graphs_data', methods=['GET']) +# @login_required +# def graph_data_api_v2(): +# try: +# current_time = datetime.now() +# # Get the time filter from query parameters +# time_filter = request.args.get('filter', default='1 day') + +# # Determine the start time based on the filter +# time_deltas = { +# '5 minutes': '-5m', +# '15 minutes': '-15m', +# '30 minutes': '-30m', +# '1 hour': '-1h', +# '3 hours': '-3h', +# '6 hours': '-6h', +# '12 hours': '-12h', +# '1 day': '-1d', +# '2 days': '-2d', +# '3 days': '-3d', +# '1 week': '-1w', +# '1 month': '-30d', +# '3 months': '-90d', +# } + +# # Get the start time for the query +# time_range = time_deltas.get(time_filter, '-1d') + +# # Build the InfluxDB query +# flux_query = f""" +# from(bucket: "{bucket}") +# |> range(start: {time_range}) +# |> filter(fn: (r) => r._measurement == "system_info") +# |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") +# """ + +# # Execute the query +# tables = query_api.query(flux_query) + +# # Initialize lists for the data +# time_data = [] +# cpu_data = [] +# memory_data = [] +# battery_data = [] +# network_sent_data = [] +# network_received_data = [] +# dashboard_memory_usage = [] +# cpu_frequency = [] +# current_temp = [] + +# # Parse the results +# for table in tables: +# for record in table.records: +# time_data.append(record.values.get("_time", None)) +# # Extract each field by key (handle missing fields gracefully) +# cpu_data.append(record.values.get("cpu_percent", None)) +# memory_data.append(record.values.get("memory_percent", None)) +# battery_data.append(record.values.get("battery_percent", None)) +# network_sent_data.append(record.values.get("network_sent", None)) +# network_received_data.append(record.values.get("network_received", None)) +# dashboard_memory_usage.append(record.values.get("dashboard_memory_usage", None)) +# cpu_frequency.append(record.values.get("cpu_frequency", None)) +# current_temp.append(record.values.get("current_temp", None)) + +# # Return the data as JSON +# response = jsonify({ +# "time": time_data, +# "cpu": cpu_data, +# "memory": memory_data, +# "battery": battery_data, +# "network_sent": network_sent_data, +# "network_received": network_received_data, +# "dashboard_memory_usage": dashboard_memory_usage, +# "cpu_frequency": cpu_frequency, +# "current_temp": current_temp, +# "current_time": current_time +# }) + +# # Clean up large data structures +# del tables +# del time_data +# del cpu_data +# del memory_data +# del battery_data +# del network_sent_data +# del network_received_data +# del dashboard_memory_usage +# del cpu_frequency +# del current_temp + +# gc.collect() + +# return response, 200 + +# except Exception as e: +# # Handle and log the error for debugging purposes +# return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 diff --git a/src/models/page_toggle_settings.py b/src/models/page_toggle_settings.py index 987d718..2731f85 100644 --- a/src/models/page_toggle_settings.py +++ b/src/models/page_toggle_settings.py @@ -1,4 +1,3 @@ -import datetime from src.config import db class PageToggleSettings(db.Model): @@ -26,4 +25,4 @@ class PageToggleSettings(db.Model): is_disk_info_enabled = db.Column(db.Boolean, default=True) is_network_info_enabled = db.Column(db.Boolean, default=True) is_process_info_enabled = db.Column(db.Boolean, default=True) - is_dashboard_network_enabled = db.Column(db.Boolean, default=False) + is_dashboard_network_enabled = db.Column(db.Boolean, default=True) diff --git a/src/routes/api.py b/src/routes/api.py index 025a8c4..178d551 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -14,7 +14,7 @@ api_bp = blueprints.Blueprint("api", __name__) PROMETHEUS_URL = 'http://localhost:9090' # Change if using a different URL or port -QUERY_API_URL = f'{PROMETHEUS_URL}/api/v1/query' +QUERY_API_URL = f'{PROMETHEUS_URL}/api/v1/query_range' TARGETS_API_URL = f'{PROMETHEUS_URL}/api/v1/targets' PROMETHEUS_METRICS = { @@ -28,7 +28,7 @@ 'current_temp': 'cpu_temperature', } -@app.route("/api/system-info", methods=["GET"]) +@app.route("/api/v1/system-info", methods=["GET"]) @login_required def system_api(): try: @@ -142,104 +142,6 @@ def graph_data_api(): # Handle and log the error for debugging purposes return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 -# @app.route('/api/v2/influxdb/graphs_data', methods=['GET']) -# @login_required -# def graph_data_api_v2(): -# try: -# current_time = datetime.now() -# # Get the time filter from query parameters -# time_filter = request.args.get('filter', default='1 day') - -# # Determine the start time based on the filter -# time_deltas = { -# '5 minutes': '-5m', -# '15 minutes': '-15m', -# '30 minutes': '-30m', -# '1 hour': '-1h', -# '3 hours': '-3h', -# '6 hours': '-6h', -# '12 hours': '-12h', -# '1 day': '-1d', -# '2 days': '-2d', -# '3 days': '-3d', -# '1 week': '-1w', -# '1 month': '-30d', -# '3 months': '-90d', -# } - -# # Get the start time for the query -# time_range = time_deltas.get(time_filter, '-1d') - -# # Build the InfluxDB query -# flux_query = f""" -# from(bucket: "{bucket}") -# |> range(start: {time_range}) -# |> filter(fn: (r) => r._measurement == "system_info") -# |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") -# """ - -# # Execute the query -# tables = query_api.query(flux_query) - -# # Initialize lists for the data -# time_data = [] -# cpu_data = [] -# memory_data = [] -# battery_data = [] -# network_sent_data = [] -# network_received_data = [] -# dashboard_memory_usage = [] -# cpu_frequency = [] -# current_temp = [] - -# # Parse the results -# for table in tables: -# for record in table.records: -# time_data.append(record.values.get("_time", None)) -# # Extract each field by key (handle missing fields gracefully) -# cpu_data.append(record.values.get("cpu_percent", None)) -# memory_data.append(record.values.get("memory_percent", None)) -# battery_data.append(record.values.get("battery_percent", None)) -# network_sent_data.append(record.values.get("network_sent", None)) -# network_received_data.append(record.values.get("network_received", None)) -# dashboard_memory_usage.append(record.values.get("dashboard_memory_usage", None)) -# cpu_frequency.append(record.values.get("cpu_frequency", None)) -# current_temp.append(record.values.get("current_temp", None)) - -# # Return the data as JSON -# response = jsonify({ -# "time": time_data, -# "cpu": cpu_data, -# "memory": memory_data, -# "battery": battery_data, -# "network_sent": network_sent_data, -# "network_received": network_received_data, -# "dashboard_memory_usage": dashboard_memory_usage, -# "cpu_frequency": cpu_frequency, -# "current_temp": current_temp, -# "current_time": current_time -# }) - -# # Clean up large data structures -# del tables -# del time_data -# del cpu_data -# del memory_data -# del battery_data -# del network_sent_data -# del network_received_data -# del dashboard_memory_usage -# del cpu_frequency -# del current_temp - -# gc.collect() - -# return response, 200 - -# except Exception as e: -# # Handle and log the error for debugging purposes -# return jsonify({'error': 'An error occurred while fetching the graph data', 'details': str(e)}), 500 - @app.route('/api/v1/prometheus/graphs_data', methods=['GET']) @login_required def graph_data_api_v3(): @@ -509,4 +411,3 @@ def get_os_info_api(): return jsonify(os_info), 200 except Exception as e: return jsonify({"error": "An error occurred while fetching the OS information", "details": str(e)}), 500 - \ No newline at end of file diff --git a/src/routes/helper/__init__.py b/src/routes/helper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/helper/prometheus_helper.py b/src/routes/helper/prometheus_helper.py new file mode 100644 index 0000000..24f8dfe --- /dev/null +++ b/src/routes/helper/prometheus_helper.py @@ -0,0 +1,40 @@ +import os +import yaml +from src.utils import ROOT_DIR + +prometheus_yml_path = os.path.join(ROOT_DIR, 'prometheus_config/prometheus.yml') + +def is_valid_file(file_path: str) -> bool: + """Checks if a file is valid and has key-value pairs separated by a colon.""" + with open(file_path, 'r') as file: + for line in file: + if not line.strip(): + continue + if ':' not in line: + return False + return True + +def load_yaml(file_path): + """Load the YAML file.""" + with open(file_path, 'r') as file: + return yaml.safe_load(file) + +def save_yaml(data, file_path): + """Save the updated YAML data back to the file.""" + with open(file_path, 'w') as file: + yaml.dump(data, file, default_flow_style=False) + +def show_targets(): + """Show all targets for each job.""" + config = load_yaml(prometheus_yml_path) + targets_info = [] + for scrape_config in config.get('scrape_configs', []): + job_name = scrape_config['job_name'] + targets = scrape_config['static_configs'][0]['targets'] + scrape_interval = scrape_config.get('scrape_interval', '15s') + targets_info.append({ + 'job_name': job_name, + 'targets': targets, + 'scrape_interval': scrape_interval + }) + return targets_info diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index 75f8152..b985e2d 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -1,26 +1,16 @@ from flask import Blueprint, Response, request, render_template, flash, redirect, url_for from prometheus_client import generate_latest import os +import yaml from src.config import app, db from src.models import ExternalMonitornig +from src.utils import ROOT_DIR +from src.routes.helper.prometheus_helper import load_yaml, save_yaml, is_valid_file, show_targets, prometheus_yml_path # Define the Prometheus Blueprint prometheus_bp = Blueprint('prometheus', __name__) - -def is_valid_file(file_path: str) -> bool: - """Checks if a file is valid and have key-value pairs separated by a colon.""" - with open(file_path, 'r') as file: - for line in file: - if not line.strip(): - continue - - if ':' not in line: - return False - - return True - # Define a route to serve Prometheus metrics @app.route('/metrics') def metrics(): @@ -28,40 +18,105 @@ def metrics(): output = '\n'.join([line for line in output.decode().split('\n') if not line.startswith('#') and line]) return Response(output, mimetype='text/plain') -# post request to add file path -@app.route('/prometheus/external_monitoring', methods=['GET', 'POST']) +# POST request to manage file paths +@app.route('/external_monitoring', methods=['GET', 'POST']) def external_monitoring(): if request.method == 'POST': - file_path = request.form.get('file_path') if not os.path.exists(file_path): flash('File path does not exist', 'danger') return redirect(url_for('external_monitoring')) - - # check file path and is_valid + + # Check file path and validity if not is_valid_file(file_path): flash('Invalid file format. File should have key-value pairs separated by a colon.', 'danger') return redirect(url_for('external_monitoring')) - - # save into the ExternalMonitornig table + + # Save into the ExternalMonitoring table new_task = ExternalMonitornig(file_path=file_path) - # commit the changes db.session.add(new_task) db.session.commit() - # read_file_and_update_metric(file_path=file_path) return redirect(url_for('external_monitoring')) data = ExternalMonitornig.query.all() - return render_template('prometheus/external_monitoring.html', data=data) + return render_template('prometheus/external_monitoring.html', data=data) - -# post request to delete file path -@app.route('/prometheus/delete_file_path/', methods=['POST']) +# POST request to delete file path +@app.route('/external_monitoring/delete_file_path/', methods=['POST']) def delete_file_path(id): file_path = ExternalMonitornig.query.get_or_404(id) db.session.delete(file_path) db.session.commit() flash('File path deleted successfully!', 'success') return redirect(url_for('external_monitoring')) + +@app.route('/targets') +def targets(): + targets_info = show_targets() + return render_template('other/targets.html', targets_info=targets_info) + +@app.route('/targets/add_target', methods=['POST']) +def add_target(): + job_name = request.form.get('job_name') + new_target = request.form.get('new_target') + scrape_interval = request.form.get('scrape_interval', '15s') + 's' # New scrape interval + config = load_yaml(prometheus_yml_path) + + for scrape_config in config['scrape_configs']: + if scrape_config['job_name'] == job_name: + scrape_config['static_configs'][0]['targets'].append(new_target) + break + else: + new_job = { + 'job_name': job_name, + 'static_configs': [{'targets': [new_target]}], + 'scrape_interval': scrape_interval # Set the specific interval + } + config['scrape_configs'].append(new_job) + + save_yaml(config, prometheus_yml_path) + flash('Target added successfully!', 'success') + return redirect(url_for('targets')) + +@app.route('/targets/remove_target', methods=['POST']) +def remove_target(): + job_name = request.form.get('job_name') + target_to_remove = request.form.get('target_to_remove') + config = load_yaml(prometheus_yml_path) + + for scrape_config in config['scrape_configs']: + if scrape_config['job_name'] == job_name: + targets = scrape_config['static_configs'][0]['targets'] + if target_to_remove in targets: + targets.remove(target_to_remove) + flash(f'Target {target_to_remove} removed successfully!', 'success') + + # Check if this was the last target, then remove the job + if not targets: # If the list is now empty + config['scrape_configs'].remove(scrape_config) + flash(f'Job {job_name} removed because it had no targets left.', 'success') + else: + flash(f'Target {target_to_remove} not found in job {job_name}.', 'warning') + break + else: + flash(f'Job {job_name} not found.', 'warning') + + save_yaml(config, prometheus_yml_path) + return redirect(url_for('targets')) + +@app.route('/targets/change_interval', methods=['POST']) +def change_interval(): + job_name = request.form.get('job_name') + new_interval = request.form.get('new_interval') + 's' # New scrape interval + config = load_yaml(prometheus_yml_path) + + for scrape_config in config['scrape_configs']: + if scrape_config['job_name'] == job_name: + scrape_config['scrape_interval'] = new_interval + flash('Scrape interval updated successfully!', 'success') + break + + save_yaml(config, prometheus_yml_path) + return redirect(url_for('targets')) diff --git a/src/static/css/targets.css b/src/static/css/targets.css index d90d57a..62a635e 100644 --- a/src/static/css/targets.css +++ b/src/static/css/targets.css @@ -1,17 +1,20 @@ - - -.container { - max-width: 1200px; - margin-top: 20px; +* { + box-sizing: border-box; + margin: 0; + padding: 0; } -h1 { - text-align: center; +body { + font-family: 'Arial', sans-serif; + background-color: #f4f4f9; color: #333; + line-height: 1.6; + padding: 20px; } -.target-table { - margin-top: 20px; +h1, h2 { + color: #4a4a4a; + margin-bottom: 20px; } table { @@ -20,38 +23,60 @@ table { margin-bottom: 20px; } -table th, table td { +th, td { padding: 12px; - border: 1px solid #ddd; text-align: left; + border-bottom: 1px solid #ccc; } -table th { - background-color: #3f51b5; +th { + background-color: #007bff; color: white; } -table tr:nth-child(even) { - background-color: #f9f9f9; +tr:hover { + background-color: #f1f1f1; +} + +form { + display: inline; +} + +input[type="text"], input[type="submit"] { + padding: 10px; + margin: 5px; + border: 1px solid #ccc; + border-radius: 4px; + transition: border 0.3s ease; +} + +input[type="text"]:focus { + border: 1px solid #007bff; + outline: none; +} + +input[type="submit"] { + background-color: #007bff; + color: white; + cursor: pointer; } -table td.health-up { - color: green; - font-weight: bold; +input[type="submit"]:hover { + background-color: #0056b3; } -table td.health-down { - color: red; - font-weight: bold; +.flash { + padding: 10px; + margin: 10px 0; + border-radius: 4px; } -.error-message { - color: red; - font-style: italic; +.flash.success { + background-color: #d4edda; + color: #155724; } -.footer { - text-align: center; - color: #666; - font-size: 12px; +.flash.danger { + background-color: #f8d7da; + color: #721c24; } diff --git a/src/static/js/refreshCardData.js b/src/static/js/refreshCardData.js index e28743b..b86065c 100644 --- a/src/static/js/refreshCardData.js +++ b/src/static/js/refreshCardData.js @@ -174,7 +174,7 @@ async function updateBatteryIcon(iconSelector, batteryStatusKey, batteryPercentK // Refresh all card data async function refreshData() { - const data = await queueRequest('/api/system-info'); + const data = await queueRequest('/api/v1/system-info'); if (!data) return; updateCard('.bg-disk', 'disk_percent', data, '%', '.disk-bar'); diff --git a/src/templates/ext/navbar.html b/src/templates/ext/navbar.html index 3bc8e47..ac257d4 100644 --- a/src/templates/ext/navbar.html +++ b/src/templates/ext/navbar.html @@ -21,7 +21,7 @@
+ +
+

Restart Prometheus Docker Service

+
+ +
+
+

Add New Target

diff --git a/src/templates/other/targets.html b/src/templates/other/targets.html index 650ffb1..4e52557 100644 --- a/src/templates/other/targets.html +++ b/src/templates/other/targets.html @@ -7,6 +7,7 @@ {% block content %}

SystemGuard Targets

+ {% include 'ext/message.html' %} From 58e0f4ca1c37c074591eaacd43255a44376cf3f0 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Mon, 23 Sep 2024 11:22:17 +0530 Subject: [PATCH 32/46] UI improvements --- src/routes/prometheus.py | 21 ++-- src/static/css/targets.css | 121 +++++++++++++++---- src/static/js/targets.js | 35 +++--- src/templates/network/dashboard_network.html | 7 +- src/templates/other/targets.html | 113 ++++++++++------- 5 files changed, 192 insertions(+), 105 deletions(-) diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index c2b493d..c031115 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -61,10 +61,9 @@ def delete_file_path(id): flash('File path deleted successfully!', 'success') return redirect(url_for('external_monitoring')) -@app.route('/targets') -def targets(): +@app.route('/configure_targets') +def configure_targets(): update_prometheus_config() - # update_prometheus_container() targets_info = show_targets() return render_template('other/targets.html', targets_info=targets_info) @@ -73,7 +72,7 @@ def restart_prometheus(): update_prometheus_config update_prometheus_container() flash('Prometheus container restarted successfully!', 'success') - return redirect(url_for('dashboard_network')) + return redirect(url_for('configure_targets')) @app.route('/targets/add_target', methods=['POST']) def add_target(): @@ -85,7 +84,7 @@ def add_target(): # new target should be : check if it is in the correct format if ':' not in new_target: flash('Invalid target format. It should be in the format :.', 'danger') - return redirect(url_for('dashboard_network')) + return redirect(url_for('configure_targets')) for scrape_config in config['scrape_configs']: if scrape_config['job_name'] == job_name: @@ -101,8 +100,8 @@ def add_target(): save_yaml(config, prometheus_yml_path) flash('Target added successfully!', 'success') - update_prometheus_container() - return redirect(url_for('dashboard_network')) + # update_prometheus_container() + return redirect(url_for('configure_targets')) @app.route('/targets/remove_target', methods=['POST']) def remove_target(): @@ -128,8 +127,8 @@ def remove_target(): flash(f'Job {job_name} not found.', 'warning') save_yaml(config, prometheus_yml_path) - update_prometheus_container() - return redirect(url_for('dashboard_network')) + # update_prometheus_container() + return redirect(url_for('configure_targets')) @app.route('/targets/change_interval', methods=['POST']) def change_interval(): @@ -144,5 +143,5 @@ def change_interval(): break save_yaml(config, prometheus_yml_path) - update_prometheus_container() - return redirect(url_for('targets')) + # update_prometheus_container() + return redirect(url_for('configure_targets')) diff --git a/src/static/css/targets.css b/src/static/css/targets.css index 0360c0a..793266d 100644 --- a/src/static/css/targets.css +++ b/src/static/css/targets.css @@ -1,66 +1,120 @@ - - -/* body { - font-family: 'Arial', sans-serif; - background-color: #f4f4f9; - color: #333; - line-height: 1.6; +/* General page styles */ +.container { + max-width: 1200px; + margin: 20px auto; padding: 20px; -} */ + background-color: #f4f6f9; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} h1, h2 { - color: #4a4a4a; + font-family: 'Roboto', sans-serif; + color: #333; margin-bottom: 20px; + text-align: center; } -table { +.section { + margin-top: 30px; + text-align: center; +} + +/* Table styles */ +.table-wrapper { + overflow-x: auto; +} + +.modern-table { width: 100%; border-collapse: collapse; - margin-bottom: 20px; + font-family: 'Roboto', sans-serif; } -th, td { - padding: 12px; +.modern-table th, .modern-table td { + padding: 12px 15px; text-align: left; - border-bottom: 1px solid #ccc; + border-bottom: 1px solid #ddd; } -th { +.modern-table th { background-color: #007bff; - color: white; + color: #fff; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.modern-table tr:nth-child(even) { + background-color: #f9f9f9; } -tr:hover { +.modern-table tr:hover { background-color: #f1f1f1; } -form { +.target-list { + list-style-type: none; + padding-left: 0; +} + +.target-list li { + margin-bottom: 8px; +} + +/* Form styles */ +form.inline-form { display: inline; } -input[type="text"], input[type="submit"] { - padding: 10px; +.input-field { + padding: 8px; margin: 5px; border: 1px solid #ccc; border-radius: 4px; - transition: border 0.3s ease; + width: 200px; } -input[type="text"]:focus { - border: 1px solid #007bff; +.input-field:focus { + border-color: #007bff; outline: none; + transition: border 0.2s; } -input[type="submit"] { - background-color: #007bff; +/* Button styles */ +.btn-primary, .btn-danger, .btn-warning { + padding: 10px 15px; + border: none; + border-radius: 4px; color: white; cursor: pointer; + transition: background-color 0.3s ease; } -input[type="submit"]:hover { +.btn-primary { + background-color: #007bff; +} + +.btn-primary:hover { background-color: #0056b3; } +.btn-danger { + background-color: #dc3545; +} + +.btn-danger:hover { + background-color: #c82333; +} + +.btn-warning { + background-color: #ffc107; +} + +.btn-warning:hover { + background-color: #e0a800; +} + +/* Messages */ .flash { padding: 10px; margin: 10px 0; @@ -76,3 +130,18 @@ input[type="submit"]:hover { background-color: #f8d7da; color: #721c24; } + +/* Add New Target Form */ +.add-target-form { + margin: 0 auto; + max-width: 600px; + text-align: center; +} + +.add-target-form .input-field { + width: 70%; +} + +.add-target-form input[type="submit"] { + width: 150px; +} diff --git a/src/static/js/targets.js b/src/static/js/targets.js index 662d3cd..2ae6b39 100644 --- a/src/static/js/targets.js +++ b/src/static/js/targets.js @@ -73,25 +73,22 @@ async function fetchTargetData() { jobNameInput.value = target.labels.job; removeForm.appendChild(jobNameInput); - const targetToRemoveInput = document.createElement('input'); - targetToRemoveInput.type = 'hidden'; - targetToRemoveInput.name = 'target_to_remove'; - targetToRemoveInput.value = target.labels.instance; - removeForm.appendChild(targetToRemoveInput); - - const submitButton = document.createElement('input'); - submitButton.type = 'submit'; - submitButton.value = 'Remove'; - submitButton.onclick = function () { - return confirm('Are you sure you want to remove this target?'); - }; - removeForm.appendChild(submitButton); - - removeCell.appendChild(removeForm); - row.appendChild(removeCell); - - // Append the row to the table body - targetTableBody.appendChild(row); + // const targetToRemoveInput = document.createElement('input'); + // targetToRemoveInput.type = 'hidden'; + // targetToRemoveInput.name = 'target_to_remove'; + // targetToRemoveInput.value = target.labels.instance; + // removeForm.appendChild(targetToRemoveInput); + + // const submitButton = document.createElement('input'); + // submitButton.type = 'submit'; + // submitButton.value = 'Remove'; + // submitButton.onclick = function () { + // return confirm('Are you sure you want to remove this target?'); + // }; + // removeForm.appendChild(submitButton); + + // removeCell.appendChild(removeForm); + // row.appendChild(removeCell); // Append the row to the table body targetTableBody.appendChild(row); diff --git a/src/templates/network/dashboard_network.html b/src/templates/network/dashboard_network.html index fb921c2..ca45bc1 100644 --- a/src/templates/network/dashboard_network.html +++ b/src/templates/network/dashboard_network.html @@ -20,7 +20,6 @@

SystemGuard Active Targets

- @@ -29,8 +28,7 @@

SystemGuard Active Targets

Job NameScrape Duration (s) Scrape Interval Go to DashboardRemove Target
- -
+ {% endblock %} {% block extra_scripts %} diff --git a/src/templates/other/targets.html b/src/templates/other/targets.html index 4e52557..2574ad8 100644 --- a/src/templates/other/targets.html +++ b/src/templates/other/targets.html @@ -6,49 +6,74 @@ {% endblock %} {% block content %} -

SystemGuard Targets

- {% include 'ext/message.html' %} - - - - - - - - {% for info in targets_info %} - - - +
Job NameTargetsScrape IntervalActions
{{ info.job_name }} -
    - {% for target in info.targets %} -
  • - {{ target }} - - - - - -
  • +
    +

    SystemGuard Targets

    + {% include 'ext/message.html' %} + +
    + + + + + + + + + + + {% for info in targets_info %} + + + + + + {% endfor %} - - - - - - {% endfor %} -
    Job NameTargetsScrape IntervalActions
    {{ info.job_name }} +
      + {% for target in info.targets %} +
    • + {{ target }} +
      + + + +
      +
    • + {% endfor %} +
    +
    {{ info.scrape_interval }} +
    + + + +
    +
    {{ info.scrape_interval }} -
    - - - -
    -
    +
+
+ +
+

Restart Prometheus Docker Service

+
+ +
+
-

Add New Target

-
- - - - -
-{% endblock %} \ No newline at end of file +
+

Add New Target

+
+ + + +
+ +
+
+

+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} From ae2c54f7dd856ba2edc6abc4872342f839ded9c3 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Mon, 23 Sep 2024 12:20:45 +0530 Subject: [PATCH 33/46] chore: updated the web page --- src/scripts/update_prometheus.sh | 18 ++++++++++++++++++ src/templates/network/dashboard_network.html | 2 +- src/templates/other/targets.html | 14 ++++++++------ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/scripts/update_prometheus.sh b/src/scripts/update_prometheus.sh index eb8aaa4..c8054f2 100644 --- a/src/scripts/update_prometheus.sh +++ b/src/scripts/update_prometheus.sh @@ -1,5 +1,21 @@ #!/bin/bash +# function to get the user name +get_user_name() { + if [ "$(whoami)" = "root" ]; then + LOGNAME_USER=$(logname 2>/dev/null) # Redirect any error output to /dev/null + if [ $? -ne 0 ]; then # Check if the exit status of the last command is not 0 + USER_NAME=$(cat /etc/passwd | grep '/home' | cut -d: -f1 | tail -n 1) + else + USER_NAME=$LOGNAME_USER + fi + else + USER_NAME=$(whoami) + fi + echo "$USER_NAME" +} + +USER_NAME=$(get_user_name) # Configuration CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" CURRENT_DIR="$CURRENT_SCRIPT_DIR" @@ -7,6 +23,7 @@ ROOT_DIR="$(dirname "$(dirname "$CURRENT_DIR")")" PROMETHEUS_CONFIG_DIR="$ROOT_DIR/prometheus_config" CONTAINER_NAME="prometheus" PROMETHEUS_CONFIG="$PROMETHEUS_CONFIG_DIR/prometheus.yml" +PROMETHEUS_DATA_DIR="/home/$USER_NAME/.database/prometheus" PROMETHEUS_IMAGE="prom/prometheus" # Add your image name if needed NETWORK_NAME="flask-prometheus-net" # Specify your network name PROMETHEUS_PORT="9090" # Specify your port @@ -34,6 +51,7 @@ else -p "$PROMETHEUS_PORT:$PROMETHEUS_PORT" \ --restart always \ -v "$PROMETHEUS_CONFIG:/etc/prometheus/prometheus.yml" \ + -v "PROMETHEUS_DATA_DIR:/prometheus" \ "$PROMETHEUS_IMAGE" &> /dev/null log "Prometheus container started successfully." diff --git a/src/templates/network/dashboard_network.html b/src/templates/network/dashboard_network.html index ca45bc1..f1df9bd 100644 --- a/src/templates/network/dashboard_network.html +++ b/src/templates/network/dashboard_network.html @@ -11,7 +11,7 @@

SystemGuard Active Targets

- + diff --git a/src/templates/other/targets.html b/src/templates/other/targets.html index 2574ad8..b2ab7d1 100644 --- a/src/templates/other/targets.html +++ b/src/templates/other/targets.html @@ -14,10 +14,10 @@

SystemGuard Targets

JobGroup Instance Health Last Scrape
- - - - + + + + @@ -32,7 +32,9 @@

SystemGuard Targets

- + {% endfor %} @@ -62,7 +64,7 @@

Restart Prometheus Docker Service

Add New Target

- +
From c2e5241349586e9cf2cf4a2e6e5302a1181aa2a1 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Mon, 23 Sep 2024 13:21:58 +0530 Subject: [PATCH 34/46] Refactor chart creation and add download and refresh buttons This commit refactors the createChart function in graphs_experimental.js to improve code readability and maintainability. It also adds download and refresh buttons to the charts for better user experience. --- src/static/js/graphs_experimental.js | 97 +++++++++++++------------- src/templates/experimental/graphs.html | 15 ---- 2 files changed, 47 insertions(+), 65 deletions(-) diff --git a/src/static/js/graphs_experimental.js b/src/static/js/graphs_experimental.js index d22714a..e6cdd08 100644 --- a/src/static/js/graphs_experimental.js +++ b/src/static/js/graphs_experimental.js @@ -38,51 +38,10 @@ function fetchDataAndRenderCharts() { // Add event listener to refresh data when filter value changes document.getElementById('timeFilter').addEventListener('change', (event) => { - // Save the new filter value to local storage localStorage.setItem('filterValue', event.target.value); - // Fetch data with the new filter value fetchDataAndRenderCharts(); }); -// Initial fetch when the page loads -document.addEventListener('DOMContentLoaded', () => { - fetchDataAndRenderCharts(); -}); - -// Add the refresh button to fetch the data -document.getElementById('refreshData').addEventListener('click', () => { - fetchDataAndRenderCharts(); -}); - -document.getElementById('refreshCpuTime').addEventListener('click', () => { - fetchDataAndRenderCharts(); -}); - -document.getElementById('refreshMemoryTime').addEventListener('click', () => { - fetchDataAndRenderCharts(); -}); - -document.getElementById('refreshBatteryTime').addEventListener('click', () => { - fetchDataAndRenderCharts(); -}); - -document.getElementById('refreshNetworkTime').addEventListener('click', () => { - fetchDataAndRenderCharts(); -}); - -document.getElementById('refreshDashboardMemoryTime').addEventListener('click', () => { - fetchDataAndRenderCharts(); -}); - -document.getElementById('refreshCpuFrequencyTime').addEventListener('click', () => { - fetchDataAndRenderCharts(); -}); - -document.getElementById('refreshCurrentTempTime').addEventListener('click', () => { - fetchDataAndRenderCharts(); -}); - - function formatDate(utcTime, timeZone) { const date = new Date(utcTime); @@ -129,6 +88,24 @@ function createChart(ctx, labels, datasets, yLabel) { ctx.chart.destroy(); // Destroy the existing chart if it exists } + // Ensure the parent element is positioned relatively + ctx.canvas.parentNode.style.position = 'relative'; + + // Create or update download button + getOrCreateButton(ctx.canvas.parentNode, 'Download Chart', 'download-button', (e) => { + const fileName = `${yLabel.replace(/\s+/g, '_')}_chart.png`; // Dynamic filename + console.log('Download button clicked'); + const link = document.createElement('a'); + link.href = ctx.chart.toBase64Image(); + link.download = fileName; + link.click(); + }, { top: '10px', right: '10px' }); + + // Create or update refresh button + getOrCreateButton(ctx.canvas.parentNode, 'Refresh Data', 'refresh-button', () => { + fetchDataAndRenderCharts(); + }, { top: '10px', right: '200px' }); + const allDataPoints = datasets.flatMap(dataset => dataset.data); const minY = Math.min(...allDataPoints.filter(value => typeof value === 'number')); const maxY = Math.max(...allDataPoints.filter(value => typeof value === 'number')); @@ -152,20 +129,25 @@ function createChart(ctx, labels, datasets, yLabel) { responsive: true, scales: { x: { - type: 'category', - title: { - display: true, - text: 'Time' - }, + type: 'category', + // title: { + // display: true, + // text: 'Time', + // font: { + // size: 16, + // weight: 'bold' + // }, + // padding: { top: 10, left: 0, right: 0, bottom: 0 } + // }, ticks: { - autoSkip: true, - maxTicksLimit: 6, - maxRotation: 0, + autoSkip: true, + maxTicksLimit: 6, + maxRotation: 0, minRotation: 0, } }, y: { - beginAtZero: minY < 0 ? false : true, // Adjust y-axis based on data + beginAtZero: minY < 0 ? false : true, title: { display: true, text: yLabel, @@ -176,6 +158,21 @@ function createChart(ctx, labels, datasets, yLabel) { }); } +// Helper function to create or retrieve a button +function getOrCreateButton(parent, text, className, onClick, position) { + let button = parent.querySelector(`.${className}`); + if (!button) { + button = document.createElement('button'); + button.classList.add(className); + button.textContent = text; + button.style.position = 'absolute'; + button.style.zIndex = '5'; + Object.assign(button.style, position); // Apply positioning styles + parent.appendChild(button); + } + button.onclick = onClick; // Update the click handler + return button; +} // Function to create charts with the fetched data function createCharts(cpuData, timeData, memoryData, batteryData, networkSentData, networkReceivedData, dashboardMemoryUsageData, cpuFrequencyData, currentTempData) { diff --git a/src/templates/experimental/graphs.html b/src/templates/experimental/graphs.html index a712f20..a176142 100644 --- a/src/templates/experimental/graphs.html +++ b/src/templates/experimental/graphs.html @@ -57,8 +57,6 @@

Current Server Time & Time Zone

CPU Usage

-
@@ -66,8 +64,6 @@

CPU Usage

Memory Usage

-
@@ -77,8 +73,6 @@

Memory Usage

Current Temperature

-
@@ -86,8 +80,6 @@

Current Temperature

CPU Frequency

-
@@ -97,8 +89,6 @@

CPU Frequency

Battery Percentage

-
@@ -106,8 +96,6 @@

Battery Percentage

Network Sent & Received

-
@@ -117,12 +105,9 @@

Network Sent & Received

Memory Usage(APP)

-
-
{% endblock %} From 14c3ff128b7e48cd29d1d72798583ed8812e54ef Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Mon, 23 Sep 2024 15:00:25 +0530 Subject: [PATCH 35/46] auth added --- src/routes/prometheus.py | 15 +++- src/scripts/prometheus.sh | 4 + src/templates/other/targets.html | 141 ++++++++++++++++--------------- 3 files changed, 93 insertions(+), 67 deletions(-) diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index c031115..19f836a 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -6,6 +6,7 @@ from src.config import app, db from src.models import ExternalMonitornig from src.utils import ROOT_DIR +from src.routes.helper.common_helper import admin_required from src.routes.helper.prometheus_helper import ( load_yaml, save_yaml, @@ -16,19 +17,26 @@ update_prometheus_config) - # Define the Prometheus Blueprint prometheus_bp = Blueprint('prometheus', __name__) +# todo, find a better way to store the username and password +username = 'admin' +password = 'admin' + # Define a route to serve Prometheus metrics @app.route('/metrics') def metrics(): + auth = request.authorization + if not auth or not (auth.username == username and auth.password == password): + return Response('Could not verify', 401, {'WWW-Authenticate': 'Basic realm="Login required"'}) output = generate_latest() output = '\n'.join([line for line in output.decode().split('\n') if not line.startswith('#') and line]) return Response(output, mimetype='text/plain') # POST request to manage file paths @app.route('/external_monitoring', methods=['GET', 'POST']) +@admin_required def external_monitoring(): if request.method == 'POST': file_path = request.form.get('file_path') @@ -54,6 +62,7 @@ def external_monitoring(): # POST request to delete file path @app.route('/external_monitoring/delete_file_path/', methods=['POST']) +@admin_required def delete_file_path(id): file_path = ExternalMonitornig.query.get_or_404(id) db.session.delete(file_path) @@ -62,12 +71,14 @@ def delete_file_path(id): return redirect(url_for('external_monitoring')) @app.route('/configure_targets') +@admin_required def configure_targets(): update_prometheus_config() targets_info = show_targets() return render_template('other/targets.html', targets_info=targets_info) @app.route('/targets/restart_prometheus') +@admin_required def restart_prometheus(): update_prometheus_config update_prometheus_container() @@ -104,6 +115,7 @@ def add_target(): return redirect(url_for('configure_targets')) @app.route('/targets/remove_target', methods=['POST']) +@admin_required def remove_target(): job_name = request.form.get('job_name') target_to_remove = request.form.get('target_to_remove') @@ -131,6 +143,7 @@ def remove_target(): return redirect(url_for('configure_targets')) @app.route('/targets/change_interval', methods=['POST']) +@admin_required def change_interval(): job_name = request.form.get('job_name') new_interval = request.form.get('new_interval') + 's' # New scrape interval diff --git a/src/scripts/prometheus.sh b/src/scripts/prometheus.sh index ecd895c..4337b2e 100644 --- a/src/scripts/prometheus.sh +++ b/src/scripts/prometheus.sh @@ -48,6 +48,10 @@ scrape_configs: static_configs: - targets: - '$FLASK_APP_IP:$FLASK_APP_PORT' + basic_auth: + username: admin + password: admin + EOL # Check if Docker network exists diff --git a/src/templates/other/targets.html b/src/templates/other/targets.html index b2ab7d1..a738c1e 100644 --- a/src/templates/other/targets.html +++ b/src/templates/other/targets.html @@ -6,76 +6,85 @@ {% endblock %} {% block content %} -
-

SystemGuard Targets

- {% include 'ext/message.html' %} +
+

SystemGuard Targets

+ {% include 'ext/message.html' %} -
-
Job NameTargetsScrape IntervalActionsGroup Name Targets Scrape Interval New Interval
- - - - - - - - - - {% for info in targets_info %} - - - - - - - {% endfor %} - -
Group Name Targets Scrape Interval New Interval
{{ info.job_name }} -
    - {% for target in info.targets %} -
  • - {{ target }} - - - - - -
  • - {% endfor %} -
-
{{ info.scrape_interval }} -
- - - -
-
-
- -
-

Restart Prometheus Docker Service

-
- -
-
+
+ + + + + + + + + + + {% for info in targets_info %} + + + + + + + {% endfor %} + +
Group Name Targets Scrape Interval New Interval +
{{ info.job_name }} +
    + {% for target in info.targets %} +
  • + {{ target }} +
    + + + +
    +
  • + {% endfor %} +
+
{{ info.scrape_interval }} +
+ + + +
+
+
-
-

Add New Target

-
- - - -
- -
-
+
+

Restart Prometheus Docker Service

+
+ +
+
+
+

Add New Target

+
+ + + + + + + +
+ +
+ +
{% endblock %} {% block extra_scripts %} -{% endblock %} +{% endblock %} \ No newline at end of file From ad2e53a65823506cf979195fef5f088c55080804 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Mon, 23 Sep 2024 15:53:56 +0530 Subject: [PATCH 36/46] Refactor Prometheus config update function and improve authentication security --- src/routes/helper/prometheus_helper.py | 27 +++++++++++++++++++++----- src/routes/prometheus.py | 6 +++--- src/scripts/prometheus.sh | 4 ++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/routes/helper/prometheus_helper.py b/src/routes/helper/prometheus_helper.py index 6530c1b..9e46d61 100644 --- a/src/routes/helper/prometheus_helper.py +++ b/src/routes/helper/prometheus_helper.py @@ -43,22 +43,39 @@ def show_targets(): def update_prometheus_config(): """ - update the first target whenever the function called - as the network change the ip address will also change - so 1st ip address in the list is the ip adddress of the machine - with the job localhost + Update the first target whenever the function is called. + If the network changes, the IP address will also change. + This updates the first IP address in the list, assuming it's the machine with the 'localhost' job. """ print("Updating Prometheus config...") + + # Get the machine's IP address ipv4_address = subprocess.run(['hostname', '-I'], capture_output=True, text=True, check=True).stdout.split()[0] + + # Define the path to Prometheus config file prometheus_config_path = os.path.join(ROOT_DIR, 'prometheus_config', 'prometheus.yml') + + # Load the existing config config = load_yaml(prometheus_config_path) - # fetch the localhost job + # Fetch the 'localhost' job localhost_job = next((job for job in config['scrape_configs'] if job['job_name'] == 'localhost'), None) + if localhost_job: + # Update the IP address for the 'localhost' job target localhost_job['static_configs'][0]['targets'][0] = f'{ipv4_address}:5050' + + # localhost_job['basic_auth'] = { + # 'username': "username", + # 'password': "password" + # } + + # Save the updated config save_yaml(config, prometheus_config_path) + + print("Prometheus config updated successfully.") return True + print("No 'localhost' job found in Prometheus config.") return False def update_prometheus_container(): diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index 19f836a..c86be27 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -21,8 +21,8 @@ prometheus_bp = Blueprint('prometheus', __name__) # todo, find a better way to store the username and password -username = 'admin' -password = 'admin' +username = 'prometheus_admin' +password = 'prometheus_password' # Define a route to serve Prometheus metrics @app.route('/metrics') @@ -73,7 +73,7 @@ def delete_file_path(id): @app.route('/configure_targets') @admin_required def configure_targets(): - update_prometheus_config() + # update_prometheus_config() targets_info = show_targets() return render_template('other/targets.html', targets_info=targets_info) diff --git a/src/scripts/prometheus.sh b/src/scripts/prometheus.sh index 4337b2e..66b2b92 100644 --- a/src/scripts/prometheus.sh +++ b/src/scripts/prometheus.sh @@ -49,8 +49,8 @@ scrape_configs: - targets: - '$FLASK_APP_IP:$FLASK_APP_PORT' basic_auth: - username: admin - password: admin + username: prometheus_admin + password: prometheus_password EOL From 8092574f982b56bdeb3d780738e96c2dce820d78 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Mon, 23 Sep 2024 18:09:48 +0530 Subject: [PATCH 37/46] Refactor Prometheus config update function and improve authentication security --- src/routes/helper/prometheus_helper.py | 114 +++++++++++++++---------- src/routes/prometheus.py | 59 ++++++++++--- src/scripts/prometheus.sh | 8 +- 3 files changed, 123 insertions(+), 58 deletions(-) diff --git a/src/routes/helper/prometheus_helper.py b/src/routes/helper/prometheus_helper.py index 9e46d61..3da3624 100644 --- a/src/routes/helper/prometheus_helper.py +++ b/src/routes/helper/prometheus_helper.py @@ -1,6 +1,7 @@ import os import yaml import subprocess +from collections import OrderedDict from src.utils import ROOT_DIR prometheus_yml_path = os.path.join(ROOT_DIR, 'prometheus_config/prometheus.yml') @@ -16,82 +17,103 @@ def is_valid_file(file_path: str) -> bool: return False return True + +class OrderedDumper(yaml.SafeDumper): + """Custom YAML dumper that preserves order of keys.""" + pass + +def dict_representer(dumper, data): + return dumper.represent_dict(data.items()) + +OrderedDumper.add_representer(OrderedDict, dict_representer) + def load_yaml(file_path): - """Load the YAML file.""" + """Load the YAML file and keep the order of dictionaries.""" with open(file_path, 'r') as file: - return yaml.safe_load(file) + return yaml.load(file, Loader=yaml.SafeLoader) def save_yaml(data, file_path): - """Save the updated YAML data back to the file.""" + """Save the updated YAML data back to the file, preserving order.""" with open(file_path, 'w') as file: - yaml.dump(data, file, default_flow_style=False) - -def show_targets(): - """Show all targets for each job.""" - config = load_yaml(prometheus_yml_path) - targets_info = [] - for scrape_config in config.get('scrape_configs', []): - job_name = scrape_config['job_name'] - targets = scrape_config['static_configs'][0]['targets'] - scrape_interval = scrape_config.get('scrape_interval', '15s') - targets_info.append({ - 'job_name': job_name, - 'targets': targets, - 'scrape_interval': scrape_interval - }) - return targets_info + yaml.dump(data, file, Dumper=OrderedDumper, default_flow_style=False) def update_prometheus_config(): - """ - Update the first target whenever the function is called. - If the network changes, the IP address will also change. - This updates the first IP address in the list, assuming it's the machine with the 'localhost' job. - """ + """Update the first target with the machine's IP address.""" print("Updating Prometheus config...") # Get the machine's IP address - ipv4_address = subprocess.run(['hostname', '-I'], capture_output=True, text=True, check=True).stdout.split()[0] - - # Define the path to Prometheus config file - prometheus_config_path = os.path.join(ROOT_DIR, 'prometheus_config', 'prometheus.yml') - + try: + ipv4_address = subprocess.run(['hostname', '-I'], capture_output=True, text=True, check=True).stdout.split()[0] + except subprocess.CalledProcessError as e: + print(f"Error getting IP address: {e}") + return False + # Load the existing config - config = load_yaml(prometheus_config_path) + try: + config = load_yaml(prometheus_yml_path) + except Exception as e: + print(f"Error loading YAML config: {e}") + return False + # Fetch the 'localhost' job - localhost_job = next((job for job in config['scrape_configs'] if job['job_name'] == 'localhost'), None) + localhost_job = next((job for job in config.get('scrape_configs', []) if job.get('job_name') == 'localhost'), None) if localhost_job: # Update the IP address for the 'localhost' job target localhost_job['static_configs'][0]['targets'][0] = f'{ipv4_address}:5050' - # localhost_job['basic_auth'] = { - # 'username': "username", - # 'password': "password" - # } - - # Save the updated config - save_yaml(config, prometheus_config_path) + # Create a new OrderedDict to maintain the correct order + updated_job = OrderedDict() + updated_job['job_name'] = localhost_job['job_name'] + updated_job['scrape_interval'] = localhost_job.get('scrape_interval', '10s') + updated_job['static_configs'] = localhost_job['static_configs'] - print("Prometheus config updated successfully.") - return True + # Add basic_auth last to maintain order + if 'basic_auth' in localhost_job: + updated_job['basic_auth'] = localhost_job['basic_auth'] + + # Replace the old job with the updated one + for index, job in enumerate(config['scrape_configs']): + if job['job_name'] == 'localhost': + config['scrape_configs'][index] = updated_job + break + # Save the updated config + try: + save_yaml(config, prometheus_yml_path) + print("Prometheus config updated successfully.") + return True + except Exception as e: + print(f"Error saving YAML config: {e}") + return False + print("No 'localhost' job found in Prometheus config.") return False +def show_targets(): + """Show all targets for each job.""" + config = load_yaml(prometheus_yml_path) + targets_info = [] + for scrape_config in config.get('scrape_configs', []): + job_name = scrape_config['job_name'] + targets = scrape_config.get('static_configs', [{}])[0].get('targets', []) + scrape_interval = scrape_config.get('scrape_interval', '15s') + targets_info.append({ + 'job_name': job_name, + 'targets': targets, + 'scrape_interval': scrape_interval + }) + return targets_info + def update_prometheus_container(): """Update the Prometheus container.""" - # Define the path to your shell script try: - # Use subprocess.run to execute the shell script result = subprocess.run(['bash', update_prometheus_path], check=True, text=True, capture_output=True) - - # Print the output of the script print("Output:") print(result.stdout) - # Print any errors (if any) if result.stderr: print("Errors:") print(result.stderr) except subprocess.CalledProcessError as e: - print(f"An error occurred: {e}") + print(f"An error occurred while updating Prometheus container: {e}") \ No newline at end of file diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index c86be27..dacad1a 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -2,6 +2,7 @@ from prometheus_client import generate_latest import os import yaml +from collections import OrderedDict from src.config import app, db from src.models import ExternalMonitornig @@ -73,14 +74,13 @@ def delete_file_path(id): @app.route('/configure_targets') @admin_required def configure_targets(): - # update_prometheus_config() + update_prometheus_config() targets_info = show_targets() return render_template('other/targets.html', targets_info=targets_info) @app.route('/targets/restart_prometheus') @admin_required def restart_prometheus(): - update_prometheus_config update_prometheus_container() flash('Prometheus container restarted successfully!', 'success') return redirect(url_for('configure_targets')) @@ -89,26 +89,63 @@ def restart_prometheus(): def add_target(): job_name = request.form.get('job_name') new_target = request.form.get('new_target') + username = request.form.get('username') + password = request.form.get('password') scrape_interval = request.form.get('scrape_interval', '15s') + 's' # New scrape interval config = load_yaml(prometheus_yml_path) - # new target should be : check if it is in the correct format + # Validate target format if ':' not in new_target: flash('Invalid target format. It should be in the format :.', 'danger') return redirect(url_for('configure_targets')) - + + job_found = False + for scrape_config in config['scrape_configs']: if scrape_config['job_name'] == job_name: + # Append new target scrape_config['static_configs'][0]['targets'].append(new_target) + job_found = True + + # Update scrape interval + scrape_config['scrape_interval'] = scrape_interval + + # Prepare the updated job dictionary to maintain order + updated_job = OrderedDict() + updated_job['job_name'] = scrape_config['job_name'] + updated_job['static_configs'] = scrape_config['static_configs'] + updated_job['scrape_interval'] = scrape_config['scrape_interval'] + + # Update basic_auth if provided + if username and password: + updated_job['basic_auth'] = { + 'username': username, + 'password': password + } + + # Replace the existing job with the updated one + index = config['scrape_configs'].index(scrape_config) + config['scrape_configs'][index] = updated_job + break - else: - new_job = { - 'job_name': job_name, - 'static_configs': [{'targets': [new_target]}], - 'scrape_interval': scrape_interval # Set the specific interval - } - config['scrape_configs'].append(new_job) + + # if not job_found: + # # Create new job entry + # new_job = OrderedDict() + # new_job['job_name'] = job_name + # new_job['static_configs'] = [{'targets': [new_target]}] + # new_job['scrape_interval'] = scrape_interval + # # Add basic_auth if provided + # if username and password: + # new_job['basic_auth'] = { + # 'username': username, + # 'password': password + # } + # # Append the new job to scrape_configs + # config['scrape_configs'].append(new_job) + + # Save the updated config save_yaml(config, prometheus_yml_path) flash('Target added successfully!', 'success') # update_prometheus_container() diff --git a/src/scripts/prometheus.sh b/src/scripts/prometheus.sh index 66b2b92..a0f1f97 100644 --- a/src/scripts/prometheus.sh +++ b/src/scripts/prometheus.sh @@ -27,7 +27,7 @@ PROMETHEUS_DATA_DIR="/home/$USER_NAME/.database/prometheus" FLASK_APP_IP=$(hostname -I | cut -d' ' -f1) FLASK_APP_PORT="5050" SCRAPING_INTERVAL="10s" -JOB_NAME='systemguard-metrics' +monitor='systemguard-metrics' # Logging function for better readability log() { @@ -42,6 +42,12 @@ mkdir -p "$PROMETHEUS_DATA_DIR" # Create the prometheus.yml configuration file log "Generating prometheus.yml configuration file." cat > "$PROMETHEUS_CONFIG_FILE" < Date: Tue, 24 Sep 2024 07:31:13 +0530 Subject: [PATCH 38/46] chore: about page updated --- src/static/js/update.js | 23 +++++++++ src/templates/other/about.html | 62 +++++++---------------- src/templates/settings/control_panel.html | 24 ++++++++- 3 files changed, 65 insertions(+), 44 deletions(-) create mode 100644 src/static/js/update.js diff --git a/src/static/js/update.js b/src/static/js/update.js new file mode 100644 index 0000000..749d8e1 --- /dev/null +++ b/src/static/js/update.js @@ -0,0 +1,23 @@ +document.getElementById('updateButton').addEventListener('click', function (event) { + event.preventDefault(); // Prevent the default link behavior + + fetch('{{ url_for("update_git_version") }}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Add any additional headers if needed + }, + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + alert('Update successful: ' + data.message); + } else { + alert('Update failed: ' + data.message); + } + }) + .catch(error => { + console.error('Error:', error); + alert('An error occurred while updating.'); + }); +}); \ No newline at end of file diff --git a/src/templates/other/about.html b/src/templates/other/about.html index 8fd8852..5cb5550 100644 --- a/src/templates/other/about.html +++ b/src/templates/other/about.html @@ -32,24 +32,24 @@
Year
Installation Method
{% if installation_info["sg_installation_method"] == "git" %} -

Git Repository

-
Git Branch
-

{{ installation_info["git_branch"] }}

- - {% if installation_info["update_available"] %} -
Update Available
-

A new version is available. Please update to the latest version.

- - Update Now - - {% else %} -
Update Status
-

You are up-to-date with the latest version.

- {% endif %} +

Git Repository

+
Git Branch
+

{{ installation_info["git_branch"] }}

+ + {% if installation_info["update_available"] %} +
Update Available
+

A new version is available. Please update to the latest version.

+ + Update Now + + {% else %} +
Update Status
+

You are up-to-date with the latest version.

+ {% endif %} {% elif installation_info["sg_installation_method"] == "release" %} -

Installed via Release

+

Installed via Release

{% elif installation_info["sg_installation_method"] == "source" %} -

Installed via Source Code

+

Installed via Source Code

{% endif %}

@@ -69,31 +69,7 @@
Contact
- - - +{% endblock %} +{% block extra_scripts %} + {% endblock %} \ No newline at end of file diff --git a/src/templates/settings/control_panel.html b/src/templates/settings/control_panel.html index ea24983..a8dd004 100644 --- a/src/templates/settings/control_panel.html +++ b/src/templates/settings/control_panel.html @@ -19,6 +19,13 @@

{{ title } Users + +

+ +
@@ -137,7 +160,6 @@
More Utilities
Ping Website - External API Monitoring From fd7bc88cfe4da31374a082e63765228311024e86 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Tue, 24 Sep 2024 09:24:26 +0530 Subject: [PATCH 39/46] UI improvement --- src/routes/other.py | 2 +- src/static/css/graphs.css | 8 +++ src/static/css/os_info.css | 50 +++++++++++------- src/static/css/style.css | 4 +- src/static/js/cardChart.js | 62 +++++++++++++++++++++++ src/static/js/graphs_experimental.js | 21 ++++++++ src/templates/card_comp/cpu/usages.html | 28 ++++++---- src/templates/card_comp/memory/usage.html | 32 ++++++++---- src/templates/dashboard/developer.html | 3 ++ src/templates/experimental/graphs.html | 14 ++--- 10 files changed, 176 insertions(+), 48 deletions(-) create mode 100644 src/static/js/cardChart.js diff --git a/src/routes/other.py b/src/routes/other.py index ae1542f..7d2aeff 100644 --- a/src/routes/other.py +++ b/src/routes/other.py @@ -1,6 +1,6 @@ import os import subprocess -from flask import render_template, request, jsonify, flash, blueprints, redirect, url_for +from flask import render_template, request, jsonify, flash, blueprints, redirect, url_for, session from flask_login import login_required from src.models import GeneralSettings diff --git a/src/static/css/graphs.css b/src/static/css/graphs.css index 258b39d..219ae42 100644 --- a/src/static/css/graphs.css +++ b/src/static/css/graphs.css @@ -278,3 +278,11 @@ button i.fas { #refreshCpuFrequencyTime:hover, #refreshCurrentTempTime:hover { background-color: #2980b9; /* Darker blue on hover for specific buttons */ } + + +.graph { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; +} \ No newline at end of file diff --git a/src/static/css/os_info.css b/src/static/css/os_info.css index 950d07b..72d8a72 100644 --- a/src/static/css/os_info.css +++ b/src/static/css/os_info.css @@ -1,46 +1,62 @@ -/* - +/* General container styles */ .container { - background-color: #ffffff; - padding: 20px; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); -} */ + background-color: #f8f9fa; + padding: 30px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} +/* Title styling */ h1 { - font-size: 1.75rem; - font-weight: 600; - color: #333; + font-size: 2.25rem; + font-weight: 700; + color: #495057; + text-align: center; + margin-bottom: 30px; } +/* Table styles */ .table { + background-color: #ffffff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); margin-top: 20px; border: none; } -.table th { - background-color: #343a40; +/* Header of the table */ +.table thead th { + background-color: #6c757d; color: #fff; - border: none; text-transform: uppercase; font-size: 0.875rem; + letter-spacing: 0.05em; + border-bottom: none; + padding: 15px; + text-align: left; } -.table td { +/* Table body cells */ +.table tbody td { + padding: 15px; vertical-align: middle; font-size: 0.875rem; border-color: #dee2e6; } -.table td i { +/* Table icons */ +.table tbody td i { margin-right: 8px; color: #007bff; /* Icon color */ } +/* Hover effect for table rows */ .table-hover tbody tr:hover { - background-color: #f1f1f1; /* Hover effect */ + background-color: #f1f1f1; } +/* Alternating row colors for table */ .table-striped tbody tr:nth-of-type(odd) { - background-color: #f9f9f9; /* Light gray for alternating rows */ + background-color: #f9f9f9; } diff --git a/src/static/css/style.css b/src/static/css/style.css index 57b7e11..6ae24b6 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -48,10 +48,8 @@ body { .content { margin: 0 auto; /* Center the content */ - padding: 1rem 5rem; - background-color: #f9f9f9; /* Light background color */ + padding: 1rem 4rem; border-radius: 8px; /* Rounded corners */ - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */ font-family: 'Arial', sans-serif; /* Font family */ line-height: 1.6; /* Improve readability */ } diff --git a/src/static/js/cardChart.js b/src/static/js/cardChart.js new file mode 100644 index 0000000..56e6595 --- /dev/null +++ b/src/static/js/cardChart.js @@ -0,0 +1,62 @@ +const maxDataPoints = 20; // Number of data points to show on the chart + +// Generalized function to create and update a line chart +function createLineChart(canvasId, label, dataStorage, updateFunc) { + console.log(dataStorage); + const ctx = document.getElementById(canvasId).getContext('2d'); + const chart = new Chart(ctx, { + type: 'line', + data: { + labels: Array(maxDataPoints).fill(''), // Empty labels for time intervals + datasets: [{ + label: label, + data: dataStorage, + borderColor: 'red', + borderWidth: 2, + fill: true, + tension: 0.6, // Smooth line + pointRadius: 0 // Removes the round tip (data points) on the line + }] + }, + options: { + scales: { + x: { + display: false // Hide the x-axis labels and grid + }, + y: { + display: false, // Hide the y-axis labels and grid + beginAtZero: true, + max: 100 // Assuming max value is 100 for CPU and memory usage + } + }, + plugins: { + legend: { + display: false // Hide the legend + } + }, + animation: false, // Disable animation for smooth updates + responsive: true + } + }); + + // Function to update the chart with new data + function updateChart(newUsage) { + // Add the new data point + dataStorage.push(newUsage); + + // Keep the data array length within the maxDataPoints + if (dataStorage.length > maxDataPoints) { + dataStorage.shift(); + } + + // Update the chart + chart.update(); + } + + // Set interval to fetch and update data every 2 seconds + setInterval(() => { + const newUsage = updateFunc(); // Call the update function to get the current usage + console.log(`${label} Usage:`, newUsage); + updateChart(newUsage); + }, 2000); +} diff --git a/src/static/js/graphs_experimental.js b/src/static/js/graphs_experimental.js index e6cdd08..1d5b34d 100644 --- a/src/static/js/graphs_experimental.js +++ b/src/static/js/graphs_experimental.js @@ -91,6 +91,17 @@ function createChart(ctx, labels, datasets, yLabel) { // Ensure the parent element is positioned relatively ctx.canvas.parentNode.style.position = 'relative'; + // //

CPU Usage

+ // // add h2 element to the parent node + // const h2 = document.createElement('h2'); + // h2.innerHTML = ` ${yLabel}`; + // //css top and left + // h2.style.position = 'absolute'; + // h2.style.top = '10px'; + // h2.style.left = '10px'; + // ctx.canvas.parentNode.insertBefore(h2, ctx.canvas); + + // Create or update download button getOrCreateButton(ctx.canvas.parentNode, 'Download Chart', 'download-button', (e) => { const fileName = `${yLabel.replace(/\s+/g, '_')}_chart.png`; // Dynamic filename @@ -144,6 +155,12 @@ function createChart(ctx, labels, datasets, yLabel) { maxTicksLimit: 6, maxRotation: 0, minRotation: 0, + padding: 10, + font: { + size: 12, + weight: 'bold' + } + } }, y: { @@ -151,6 +168,10 @@ function createChart(ctx, labels, datasets, yLabel) { title: { display: true, text: yLabel, + font: { + size: 16, + weight: 'bold' + }, }, } } diff --git a/src/templates/card_comp/cpu/usages.html b/src/templates/card_comp/cpu/usages.html index a1a3641..04d56be 100644 --- a/src/templates/card_comp/cpu/usages.html +++ b/src/templates/card_comp/cpu/usages.html @@ -1,13 +1,23 @@ {% if card_settings.is_cpu_usage_card_enabled %} -
-
-
CPU Usage
-

- {{ system_info['cpu_percent'] }}% -

-
-
-
+
+
+
CPU Usage
+

+ {{ system_info['cpu_percent'] }}% +

+
+
+ +
+
{% endif %} + diff --git a/src/templates/card_comp/memory/usage.html b/src/templates/card_comp/memory/usage.html index 4c960a8..30a45bf 100644 --- a/src/templates/card_comp/memory/usage.html +++ b/src/templates/card_comp/memory/usage.html @@ -1,15 +1,25 @@ {% if card_settings.is_memory_usage_card_enabled %} -
-
-
Memory Usage
-

- {{ system_info['memory_percent'] }}% -

-
-
-
- +
+
+
Memory Usage
+

+ {{ system_info['memory_percent'] }}% +

+
+
+ +
+
{% endif %} + + \ No newline at end of file diff --git a/src/templates/dashboard/developer.html b/src/templates/dashboard/developer.html index 1630ea7..8656189 100644 --- a/src/templates/dashboard/developer.html +++ b/src/templates/dashboard/developer.html @@ -2,6 +2,8 @@ {% block title %}{{ system_info['system_username'] }}@{{ system_info['nodename'] }} - SystemGuard{% endblock %} {% block extra_head %} + + {% endblock %} {% block content %} {% include 'card_comp/selector/refresh_button.html' %} @@ -56,4 +58,5 @@ {% block extra_scripts %} + {% endblock %} \ No newline at end of file diff --git a/src/templates/experimental/graphs.html b/src/templates/experimental/graphs.html index a176142..74878e3 100644 --- a/src/templates/experimental/graphs.html +++ b/src/templates/experimental/graphs.html @@ -58,14 +58,14 @@

Current Server Time & Time Zone

CPU Usage

- +

Memory Usage

- +
@@ -74,14 +74,14 @@

Memory Usage

Current Temperature

- +

CPU Frequency

- +
@@ -90,14 +90,14 @@

CPU Frequency

Battery Percentage

- +

Network Sent & Received

- +
@@ -106,7 +106,7 @@

Network Sent & Received

Memory Usage(APP)

- +
From 265ef60502bf9a7a2a56738b88fdbe3a6a6a47a9 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Tue, 24 Sep 2024 09:58:20 +0530 Subject: [PATCH 40/46] more line chart added --- src/static/js/cardChart.js | 5 ++- src/templates/card_comp/cpu/current_temp.html | 10 +++++ src/templates/card_comp/cpu/frequency.html | 38 ++++++++++++------- src/templates/card_comp/disk/usage.html | 11 +++++- src/templates/dashboard/developer.html | 8 ++-- src/templates/info_pages/cpu_info.html | 2 + src/templates/info_pages/disk_info.html | 3 ++ src/templates/info_pages/memory_info.html | 2 + 8 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/static/js/cardChart.js b/src/static/js/cardChart.js index 56e6595..9838dd5 100644 --- a/src/static/js/cardChart.js +++ b/src/static/js/cardChart.js @@ -1,4 +1,4 @@ -const maxDataPoints = 20; // Number of data points to show on the chart +const maxDataPoints = 500; // Number of data points to show on the chart // Generalized function to create and update a line chart function createLineChart(canvasId, label, dataStorage, updateFunc) { @@ -14,6 +14,7 @@ function createLineChart(canvasId, label, dataStorage, updateFunc) { borderColor: 'red', borderWidth: 2, fill: true, + opacity: 0.5, tension: 0.6, // Smooth line pointRadius: 0 // Removes the round tip (data points) on the line }] @@ -58,5 +59,5 @@ function createLineChart(canvasId, label, dataStorage, updateFunc) { const newUsage = updateFunc(); // Call the update function to get the current usage console.log(`${label} Usage:`, newUsage); updateChart(newUsage); - }, 2000); + }, 300); } diff --git a/src/templates/card_comp/cpu/current_temp.html b/src/templates/card_comp/cpu/current_temp.html index 0a2b394..e6035ff 100644 --- a/src/templates/card_comp/cpu/current_temp.html +++ b/src/templates/card_comp/cpu/current_temp.html @@ -8,6 +8,16 @@
CPU Temperature
+
{% endif %} + + + diff --git a/src/templates/card_comp/cpu/frequency.html b/src/templates/card_comp/cpu/frequency.html index b50c8df..8833ca1 100644 --- a/src/templates/card_comp/cpu/frequency.html +++ b/src/templates/card_comp/cpu/frequency.html @@ -1,16 +1,26 @@ -
-
-
CPU Frequency
-

- {{ system_info['cpu_frequency'] }} -

-
-
-
+
+
+
CPU Frequency
+

+ {{ system_info['cpu_frequency'] }} +

+
+
-
-
\ No newline at end of file + + +
+
+ + diff --git a/src/templates/card_comp/disk/usage.html b/src/templates/card_comp/disk/usage.html index a2d43ba..4cb8c28 100644 --- a/src/templates/card_comp/disk/usage.html +++ b/src/templates/card_comp/disk/usage.html @@ -2,13 +2,22 @@
Disk Usage
-

+

{{ system_info['disk_percent'] }}%

+
{% endif %} + + diff --git a/src/templates/dashboard/developer.html b/src/templates/dashboard/developer.html index 8656189..0f46d3f 100644 --- a/src/templates/dashboard/developer.html +++ b/src/templates/dashboard/developer.html @@ -28,19 +28,19 @@ {% include 'card_comp/battery/percentage.html' %}
-
+
{% include 'card_comp/cpu/usages.html' %}
-
+
{% include 'card_comp/memory/usage.html' %}
{% include 'card_comp/disk/usage.html' %}
-
+
{% include 'card_comp/cpu/current_temp.html' %}
-
+
{% include 'card_comp/cpu/frequency.html' %}
diff --git a/src/templates/info_pages/cpu_info.html b/src/templates/info_pages/cpu_info.html index 6c1358a..63d54d3 100644 --- a/src/templates/info_pages/cpu_info.html +++ b/src/templates/info_pages/cpu_info.html @@ -2,6 +2,8 @@ {% block extra_head %} + + {% endblock %} {% block content %} {% include 'card_comp/selector/refresh_button.html' %} diff --git a/src/templates/info_pages/disk_info.html b/src/templates/info_pages/disk_info.html index de597d9..7123115 100644 --- a/src/templates/info_pages/disk_info.html +++ b/src/templates/info_pages/disk_info.html @@ -1,6 +1,9 @@ {% extends 'base/base.html' %}{% block title %}Disk Usage Details{% endblock %} {% block extra_head %} + + + {% endblock %} {% block content %} {% include 'card_comp/selector/refresh_button.html' %}

Disk diff --git a/src/templates/info_pages/memory_info.html b/src/templates/info_pages/memory_info.html index afcf94e..246e440 100644 --- a/src/templates/info_pages/memory_info.html +++ b/src/templates/info_pages/memory_info.html @@ -1,6 +1,8 @@ {% extends 'base/base.html' %}{% block title %}Memory Usage Details{% endblock %} {% block extra_head %} + + {% endblock %} {% block content %} {% include 'card_comp/selector/refresh_button.html' %} From 74216dfe7c3a48dcf4497f4554e1aa18fac519f9 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Tue, 24 Sep 2024 13:50:46 +0530 Subject: [PATCH 41/46] systemguard target improve --- src/routes/prometheus.py | 9 ++------- src/templates/other/targets.html | 12 +++++------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index dacad1a..119a331 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -101,6 +101,7 @@ def add_target(): job_found = False + # if job name already exists, add new target to the job for scrape_config in config['scrape_configs']: if scrape_config['job_name'] == job_name: # Append new target @@ -115,13 +116,7 @@ def add_target(): updated_job['job_name'] = scrape_config['job_name'] updated_job['static_configs'] = scrape_config['static_configs'] updated_job['scrape_interval'] = scrape_config['scrape_interval'] - - # Update basic_auth if provided - if username and password: - updated_job['basic_auth'] = { - 'username': username, - 'password': password - } + updated_job['basic_auth'] = scrape_config.get('basic_auth', None) # Replace the existing job with the updated one index = config['scrape_configs'].index(scrape_config) diff --git a/src/templates/other/targets.html b/src/templates/other/targets.html index a738c1e..d46539a 100644 --- a/src/templates/other/targets.html +++ b/src/templates/other/targets.html @@ -68,13 +68,11 @@

Restart Prometheus Docker Service

Add New Target

- - - - - - - + + + + +

diff --git a/src/templates/experimental/graphs.html b/src/templates/experimental/graphs.html index 74878e3..ff5c449 100644 --- a/src/templates/experimental/graphs.html +++ b/src/templates/experimental/graphs.html @@ -24,6 +24,7 @@

Current Server Time & Time Zone

Current Time:

Time Zone:

+

Data Retention: 15 Days


@@ -44,8 +45,7 @@

Current Server Time & Time Zone

- - +
@@ -55,57 +55,43 @@

Current Server Time & Time Zone

-
-

CPU Usage

-
+

-
-

Memory Usage

-
+

-
-

Current Temperature

-
+

-
-

CPU Frequency

-
+

-
-

Battery Percentage

-
+

-
-

Network Sent & Received

-
+

-
-

Memory Usage(APP)

-
+

From e74e84ea2598d4db786f0f23804194402d35ecd7 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Tue, 24 Sep 2024 15:40:43 +0530 Subject: [PATCH 43/46] prometheus config function updated --- src/routes/helper/prometheus_helper.py | 41 +++++++++++++------------- src/routes/prometheus.py | 35 +++++++++++++--------- src/templates/other/targets.html | 19 ++++++++++-- 3 files changed, 58 insertions(+), 37 deletions(-) diff --git a/src/routes/helper/prometheus_helper.py b/src/routes/helper/prometheus_helper.py index 3da3624..f398d9a 100644 --- a/src/routes/helper/prometheus_helper.py +++ b/src/routes/helper/prometheus_helper.py @@ -43,7 +43,9 @@ def update_prometheus_config(): # Get the machine's IP address try: - ipv4_address = subprocess.run(['hostname', '-I'], capture_output=True, text=True, check=True).stdout.split()[0] + ipv4_address = subprocess.run( + ['hostname', '-I'], capture_output=True, text=True, check=True + ).stdout.split()[0] except subprocess.CalledProcessError as e: print(f"Error getting IP address: {e}") return False @@ -54,29 +56,28 @@ def update_prometheus_config(): except Exception as e: print(f"Error loading YAML config: {e}") return False - - # Fetch the 'localhost' job - localhost_job = next((job for job in config.get('scrape_configs', []) if job.get('job_name') == 'localhost'), None) - if localhost_job: - # Update the IP address for the 'localhost' job target - localhost_job['static_configs'][0]['targets'][0] = f'{ipv4_address}:5050' - - # Create a new OrderedDict to maintain the correct order - updated_job = OrderedDict() - updated_job['job_name'] = localhost_job['job_name'] - updated_job['scrape_interval'] = localhost_job.get('scrape_interval', '10s') - updated_job['static_configs'] = localhost_job['static_configs'] - - # Add basic_auth last to maintain order - if 'basic_auth' in localhost_job: - updated_job['basic_auth'] = localhost_job['basic_auth'] + for job in config.get('scrape_configs', []): + if job['job_name'] == 'localhost': + # Update the target for 'localhost' job + job['static_configs'][0]['targets'][0] = f'{ipv4_address}:5050' + + # Create a new OrderedDict to ensure the correct order + updated_job = OrderedDict() + updated_job['job_name'] = job['job_name'] + updated_job['scrape_interval'] = job.get('scrape_interval', '10s') + updated_job['static_configs'] = job['static_configs'] + + # Add basic_auth at the end if it exists + if 'basic_auth' in job: + updated_job['basic_auth'] = job['basic_auth'] # Replace the old job with the updated one - for index, job in enumerate(config['scrape_configs']): - if job['job_name'] == 'localhost': + for index, j in enumerate(config['scrape_configs']): + if j['job_name'] == 'localhost': config['scrape_configs'][index] = updated_job - break + else: + config['scrape_configs'][index] = OrderedDict(j) # Save the updated config try: diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index 119a331..b612d81 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -124,21 +124,24 @@ def add_target(): break - # if not job_found: - # # Create new job entry - # new_job = OrderedDict() - # new_job['job_name'] = job_name - # new_job['static_configs'] = [{'targets': [new_target]}] - # new_job['scrape_interval'] = scrape_interval + if not job_found: + # Create new job entry + new_job = OrderedDict() + new_job['job_name'] = job_name + new_job['static_configs'] = [{'targets': [new_target]}] + new_job['scrape_interval'] = scrape_interval - # # Add basic_auth if provided - # if username and password: - # new_job['basic_auth'] = { - # 'username': username, - # 'password': password - # } - # # Append the new job to scrape_configs - # config['scrape_configs'].append(new_job) + # Add basic_auth if provided + if username and password: + new_job['basic_auth'] = { + 'username': username, + 'password': password + } + # Append the new job to scrape_configs + config['scrape_configs'].append(new_job) + + for index, j in enumerate(config['scrape_configs']): + config['scrape_configs'][index] = OrderedDict(j) # Save the updated config save_yaml(config, prometheus_yml_path) @@ -167,6 +170,10 @@ def remove_target(): else: flash(f'Target {target_to_remove} not found in job {job_name}.', 'warning') break + + for index, j in enumerate(config['scrape_configs']): + config['scrape_configs'][index] = OrderedDict(j) + else: flash(f'Job {job_name} not found.', 'warning') diff --git a/src/templates/other/targets.html b/src/templates/other/targets.html index d46539a..95e1259 100644 --- a/src/templates/other/targets.html +++ b/src/templates/other/targets.html @@ -68,7 +68,7 @@

Restart Prometheus Docker Service

Add New Target

- + @@ -79,10 +79,23 @@

Add New Target

-
-{% endblock %} + +{% endblock %} {% block extra_scripts %} {% endblock %} \ No newline at end of file From f9c665b0cf268bc9fdc66af863b8212a2e5b1a38 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Tue, 24 Sep 2024 15:42:45 +0530 Subject: [PATCH 44/46] Refactor route for graphs to use experimental template --- src/routes/graphs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/graphs.py b/src/routes/graphs.py index e007e58..c5852a1 100644 --- a/src/routes/graphs.py +++ b/src/routes/graphs.py @@ -7,7 +7,7 @@ @app.route('/graphs') @admin_required def graphs(): - return render_template('graphs/graphs.html') + return render_template('experimental/graphs.html') @app.route('/experimental/graphs') From de01e78c0b90d854ad8f6118b836d42638431c63 Mon Sep 17 00:00:00 2001 From: deepak-linux Date: Tue, 24 Sep 2024 15:59:42 +0530 Subject: [PATCH 45/46] fixed bug for change interval --- src/routes/graphs.py | 2 ++ src/routes/prometheus.py | 3 +++ src/static/css/targets.css | 2 -- src/templates/other/targets.html | 31 +++++++++++++++++-------------- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/routes/graphs.py b/src/routes/graphs.py index c5852a1..7c8cba9 100644 --- a/src/routes/graphs.py +++ b/src/routes/graphs.py @@ -1,3 +1,4 @@ +import subprocess from flask import render_template, blueprints from src.config import app from src.routes.helper.common_helper import admin_required @@ -7,6 +8,7 @@ @app.route('/graphs') @admin_required def graphs(): + return render_template('experimental/graphs.html') diff --git a/src/routes/prometheus.py b/src/routes/prometheus.py index b612d81..8c29a94 100644 --- a/src/routes/prometheus.py +++ b/src/routes/prometheus.py @@ -193,6 +193,9 @@ def change_interval(): scrape_config['scrape_interval'] = new_interval flash('Scrape interval updated successfully!', 'success') break + + for index, j in enumerate(config['scrape_configs']): + config['scrape_configs'][index] = OrderedDict(j) save_yaml(config, prometheus_yml_path) # update_prometheus_container() diff --git a/src/static/css/targets.css b/src/static/css/targets.css index 793266d..5310c6c 100644 --- a/src/static/css/targets.css +++ b/src/static/css/targets.css @@ -3,9 +3,7 @@ max-width: 1200px; margin: 20px auto; padding: 20px; - background-color: #f4f6f9; border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } h1, h2 { diff --git a/src/templates/other/targets.html b/src/templates/other/targets.html index 95e1259..6f8f053 100644 --- a/src/templates/other/targets.html +++ b/src/templates/other/targets.html @@ -7,7 +7,9 @@ {% block content %}
-

SystemGuard Targets

+

SystemGuard/Prometheus Targets

+

SystemGuard uses Prometheus to monitor the services. You can add, remove, and change the scrape interval of the + targets here.

{% include 'ext/message.html' %}
@@ -33,13 +35,15 @@

SystemGuard Targets

{% for target in info.targets %}
  • {{ target }} -
    - - - + {% if not system_ip_address in target %} +
    + + + + {% endif %}
  • {% endfor %} @@ -58,13 +62,6 @@

    SystemGuard Targets

    - -
    -

    Restart Prometheus Docker Service

    -
    - -
    -

    Add New Target

    @@ -78,6 +75,12 @@

    Add New Target

    Add Target
    + +
    +
    + +
    +