network-check-tool 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- network_check_tool-0.0.1/LICENSE +6 -0
- network_check_tool-0.0.1/PKG-INFO +0 -0
- network_check_tool-0.0.1/README.md +0 -0
- network_check_tool-0.0.1/network_checker/__init__.py +3 -0
- network_check_tool-0.0.1/network_checker/app.py +28 -0
- network_check_tool-0.0.1/network_checker/cli.py +145 -0
- network_check_tool-0.0.1/network_checker/config/__init__.py +0 -0
- network_check_tool-0.0.1/network_checker/config/config.py +42 -0
- network_check_tool-0.0.1/network_checker/models/__init__.py +3 -0
- network_check_tool-0.0.1/network_checker/models/ping_result.py +28 -0
- network_check_tool-0.0.1/network_checker/services/__init__.py +4 -0
- network_check_tool-0.0.1/network_checker/services/ping_service.py +57 -0
- network_check_tool-0.0.1/network_checker/services/scheduler_service.py +94 -0
- network_check_tool-0.0.1/network_checker/templates/dashboard.html +276 -0
- network_check_tool-0.0.1/network_checker/views/__init__.py +3 -0
- network_check_tool-0.0.1/network_checker/views/dashboard.py +174 -0
- network_check_tool-0.0.1/pyproject.toml +47 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from flask import Flask
|
|
2
|
+
from flask_cors import CORS
|
|
3
|
+
from .models import db
|
|
4
|
+
from .config.config import config
|
|
5
|
+
from .views.dashboard import dashboard_bp
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_app(config_name=None):
|
|
10
|
+
"""Application factory pattern"""
|
|
11
|
+
if config_name is None:
|
|
12
|
+
config_name = os.environ.get('FLASK_ENV', 'development')
|
|
13
|
+
|
|
14
|
+
app = Flask(__name__)
|
|
15
|
+
app.config.from_object(config[config_name])
|
|
16
|
+
|
|
17
|
+
# Initialize extensions
|
|
18
|
+
db.init_app(app)
|
|
19
|
+
CORS(app)
|
|
20
|
+
|
|
21
|
+
# Register blueprints
|
|
22
|
+
app.register_blueprint(dashboard_bp)
|
|
23
|
+
|
|
24
|
+
# Create database tables
|
|
25
|
+
with app.app_context():
|
|
26
|
+
db.create_all()
|
|
27
|
+
|
|
28
|
+
return app
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
from .app import create_app
|
|
4
|
+
from .services import SchedulerService
|
|
5
|
+
from .config.config import Config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
def cli():
|
|
10
|
+
"""Network Checker CLI - Monitor network hosts with ping"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@cli.command('run-server')
|
|
15
|
+
@click.option('--host', '-h', default='0.0.0.0', help='Host to bind to')
|
|
16
|
+
@click.option('--port', '-p', default=9991, help='Port to bind to')
|
|
17
|
+
@click.option('--debug', is_flag=True, help='Enable debug mode')
|
|
18
|
+
def run_server(host, port, debug):
|
|
19
|
+
"""Run the Flask web application"""
|
|
20
|
+
app = create_app('development' if debug else 'production')
|
|
21
|
+
app.run(host=host, port=port, debug=debug)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@cli.command()
|
|
25
|
+
@click.argument('hosts', nargs=-1, required=True)
|
|
26
|
+
@click.option('--interval', '-i', default=60, help='Ping interval in seconds')
|
|
27
|
+
def start_monitoring(hosts, interval):
|
|
28
|
+
"""Start monitoring specified hosts"""
|
|
29
|
+
app = create_app()
|
|
30
|
+
|
|
31
|
+
with app.app_context():
|
|
32
|
+
scheduler = SchedulerService()
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
scheduler.start()
|
|
36
|
+
click.echo(f"Starting ping monitoring for: {', '.join(hosts)}")
|
|
37
|
+
click.echo(f"Ping interval: {interval} seconds")
|
|
38
|
+
|
|
39
|
+
for host in hosts:
|
|
40
|
+
scheduler.add_ping_job(host, interval)
|
|
41
|
+
click.echo(f"Added ping job for {host}")
|
|
42
|
+
|
|
43
|
+
click.echo("Monitoring started. Press Ctrl+C to stop.")
|
|
44
|
+
|
|
45
|
+
# Keep the process running
|
|
46
|
+
import time
|
|
47
|
+
while True:
|
|
48
|
+
time.sleep(1)
|
|
49
|
+
|
|
50
|
+
except KeyboardInterrupt:
|
|
51
|
+
click.echo("\nStopping monitoring...")
|
|
52
|
+
scheduler.shutdown()
|
|
53
|
+
click.echo("Monitoring stopped.")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
click.echo(f"Error: {e}")
|
|
56
|
+
scheduler.shutdown()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@cli.command()
|
|
60
|
+
@click.argument('host')
|
|
61
|
+
@click.option('--count', '-c', default=4, help='Number of pings to send')
|
|
62
|
+
def ping(host, count):
|
|
63
|
+
"""Send ping to a specific host"""
|
|
64
|
+
app = create_app()
|
|
65
|
+
|
|
66
|
+
with app.app_context():
|
|
67
|
+
from .services import PingService
|
|
68
|
+
ping_service = PingService()
|
|
69
|
+
|
|
70
|
+
click.echo(f"Pinging {host} {count} times...")
|
|
71
|
+
|
|
72
|
+
for i in range(count):
|
|
73
|
+
result = ping_service.ping_and_save(host)
|
|
74
|
+
|
|
75
|
+
if result.packet_loss:
|
|
76
|
+
click.echo(f"Ping {i+1}: Request timeout ({result.error_message})")
|
|
77
|
+
else:
|
|
78
|
+
click.echo(f"Ping {i+1}: {result.response_time:.2f}ms")
|
|
79
|
+
|
|
80
|
+
if i < count - 1:
|
|
81
|
+
import time
|
|
82
|
+
time.sleep(1)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@cli.command()
|
|
86
|
+
@click.option('--host', help='Filter by host')
|
|
87
|
+
@click.option('--limit', '-l', default=10, help='Number of recent results to show')
|
|
88
|
+
def status(host, limit):
|
|
89
|
+
"""Show recent ping status"""
|
|
90
|
+
app = create_app()
|
|
91
|
+
|
|
92
|
+
with app.app_context():
|
|
93
|
+
from .models import PingResult
|
|
94
|
+
from sqlalchemy import desc
|
|
95
|
+
|
|
96
|
+
query = PingResult.query
|
|
97
|
+
|
|
98
|
+
if host:
|
|
99
|
+
query = query.filter(PingResult.target_host == host)
|
|
100
|
+
|
|
101
|
+
results = query.order_by(desc(PingResult.timestamp)).limit(limit).all()
|
|
102
|
+
|
|
103
|
+
if not results:
|
|
104
|
+
click.echo("No ping results found.")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
click.echo(f"Recent ping results{'for ' + host if host else ''}:")
|
|
108
|
+
click.echo("-" * 70)
|
|
109
|
+
click.echo(f"{'Host':<20} {'Time':<20} {'Response':<12} {'Status'}")
|
|
110
|
+
click.echo("-" * 70)
|
|
111
|
+
|
|
112
|
+
for result in results:
|
|
113
|
+
status_str = "FAIL" if result.packet_loss else "OK"
|
|
114
|
+
response_str = f"{result.response_time:.2f}ms" if result.response_time else "timeout"
|
|
115
|
+
time_str = result.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
116
|
+
|
|
117
|
+
click.echo(f"{result.target_host:<20} {time_str:<20} {response_str:<12} {status_str}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@cli.command()
|
|
121
|
+
def init():
|
|
122
|
+
"""Initialize the database"""
|
|
123
|
+
app = create_app()
|
|
124
|
+
|
|
125
|
+
with app.app_context():
|
|
126
|
+
from .models import db
|
|
127
|
+
db.create_all()
|
|
128
|
+
click.echo("Database initialized successfully.")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@cli.command()
|
|
132
|
+
@click.confirmation_option(prompt='Are you sure you want to clear all ping data?')
|
|
133
|
+
def clear():
|
|
134
|
+
"""Clear all ping data from the database"""
|
|
135
|
+
app = create_app()
|
|
136
|
+
|
|
137
|
+
with app.app_context():
|
|
138
|
+
from .models import db, PingResult
|
|
139
|
+
db.session.query(PingResult).delete()
|
|
140
|
+
db.session.commit()
|
|
141
|
+
click.echo("All ping data cleared.")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == '__main__':
|
|
145
|
+
cli()
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
class Config:
|
|
5
|
+
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
|
|
6
|
+
|
|
7
|
+
# Database configuration
|
|
8
|
+
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///network_checker.db'
|
|
9
|
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
10
|
+
|
|
11
|
+
# Ping configuration
|
|
12
|
+
PING_TIMEOUT = float(os.environ.get('PING_TIMEOUT', '4.0'))
|
|
13
|
+
DEFAULT_PING_INTERVAL = int(os.environ.get('DEFAULT_PING_INTERVAL', '60'))
|
|
14
|
+
|
|
15
|
+
# Default hosts to monitor
|
|
16
|
+
DEFAULT_HOSTS = os.environ.get('DEFAULT_HOSTS', 'google.com,cloudflare.com').split(',')
|
|
17
|
+
|
|
18
|
+
# Application directory
|
|
19
|
+
APP_DIR = Path(__file__).parent.parent
|
|
20
|
+
INSTANCE_PATH = APP_DIR.parent / 'instance'
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def init_app(app):
|
|
24
|
+
"""Initialize application with configuration"""
|
|
25
|
+
# Ensure instance directory exists
|
|
26
|
+
Config.INSTANCE_PATH.mkdir(exist_ok=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DevelopmentConfig(Config):
|
|
30
|
+
DEBUG = True
|
|
31
|
+
SQLALCHEMY_ECHO = True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ProductionConfig(Config):
|
|
35
|
+
DEBUG = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
config = {
|
|
39
|
+
'development': DevelopmentConfig,
|
|
40
|
+
'production': ProductionConfig,
|
|
41
|
+
'default': DevelopmentConfig
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
3
|
+
|
|
4
|
+
db = SQLAlchemy()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PingResult(db.Model):
|
|
8
|
+
__tablename__ = 'ping_results'
|
|
9
|
+
|
|
10
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
11
|
+
target_host = db.Column(db.String(255), nullable=False, index=True)
|
|
12
|
+
timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
|
|
13
|
+
response_time = db.Column(db.Float, nullable=True)
|
|
14
|
+
packet_loss = db.Column(db.Boolean, default=False, nullable=False)
|
|
15
|
+
error_message = db.Column(db.Text, nullable=True)
|
|
16
|
+
|
|
17
|
+
def __repr__(self):
|
|
18
|
+
return f'<PingResult {self.target_host} at {self.timestamp}>'
|
|
19
|
+
|
|
20
|
+
def to_dict(self):
|
|
21
|
+
return {
|
|
22
|
+
'id': self.id,
|
|
23
|
+
'target_host': self.target_host,
|
|
24
|
+
'timestamp': self.timestamp.isoformat(),
|
|
25
|
+
'response_time': self.response_time,
|
|
26
|
+
'packet_loss': self.packet_loss,
|
|
27
|
+
'error_message': self.error_message
|
|
28
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import ping3
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
from ..models import db, PingResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PingService:
|
|
8
|
+
def __init__(self, timeout: float = 4.0):
|
|
9
|
+
self.timeout = timeout
|
|
10
|
+
ping3.EXCEPTIONS = True
|
|
11
|
+
|
|
12
|
+
def ping_host(self, host: str) -> Dict:
|
|
13
|
+
try:
|
|
14
|
+
response_time = ping3.ping(host, timeout=self.timeout)
|
|
15
|
+
|
|
16
|
+
if response_time is None:
|
|
17
|
+
return {
|
|
18
|
+
'host': host,
|
|
19
|
+
'timestamp': datetime.utcnow(),
|
|
20
|
+
'response_time': None,
|
|
21
|
+
'packet_loss': True,
|
|
22
|
+
'error_message': 'Request timeout'
|
|
23
|
+
}
|
|
24
|
+
else:
|
|
25
|
+
return {
|
|
26
|
+
'host': host,
|
|
27
|
+
'timestamp': datetime.utcnow(),
|
|
28
|
+
'response_time': response_time * 1000, # Convert to milliseconds
|
|
29
|
+
'packet_loss': False,
|
|
30
|
+
'error_message': None
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
except Exception as e:
|
|
34
|
+
return {
|
|
35
|
+
'host': host,
|
|
36
|
+
'timestamp': datetime.utcnow(),
|
|
37
|
+
'response_time': None,
|
|
38
|
+
'packet_loss': True,
|
|
39
|
+
'error_message': str(e)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def save_ping_result(self, result: Dict) -> PingResult:
|
|
43
|
+
ping_result = PingResult(
|
|
44
|
+
target_host=result['host'],
|
|
45
|
+
timestamp=result['timestamp'],
|
|
46
|
+
response_time=result['response_time'],
|
|
47
|
+
packet_loss=result['packet_loss'],
|
|
48
|
+
error_message=result['error_message']
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
db.session.add(ping_result)
|
|
52
|
+
db.session.commit()
|
|
53
|
+
return ping_result
|
|
54
|
+
|
|
55
|
+
def ping_and_save(self, host: str) -> PingResult:
|
|
56
|
+
result = self.ping_host(host)
|
|
57
|
+
return self.save_ping_result(result)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from apscheduler.schedulers.background import BackgroundScheduler
|
|
2
|
+
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
3
|
+
from apscheduler.executors.pool import ThreadPoolExecutor
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
import logging
|
|
6
|
+
from .ping_service import PingService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SchedulerService:
|
|
10
|
+
def __init__(self, database_url: str = 'sqlite:///network_checker.db'):
|
|
11
|
+
jobstores = {
|
|
12
|
+
'default': SQLAlchemyJobStore(url=database_url)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
executors = {
|
|
16
|
+
'default': ThreadPoolExecutor(20)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
job_defaults = {
|
|
20
|
+
'coalesce': False,
|
|
21
|
+
'max_instances': 3
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
self.scheduler = BackgroundScheduler(
|
|
25
|
+
jobstores=jobstores,
|
|
26
|
+
executors=executors,
|
|
27
|
+
job_defaults=job_defaults
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.ping_service = PingService()
|
|
31
|
+
self.is_running = False
|
|
32
|
+
|
|
33
|
+
# Configure logging
|
|
34
|
+
logging.basicConfig(level=logging.INFO)
|
|
35
|
+
self.logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
def start(self):
|
|
38
|
+
if not self.is_running:
|
|
39
|
+
self.scheduler.start()
|
|
40
|
+
self.is_running = True
|
|
41
|
+
self.logger.info("Scheduler started")
|
|
42
|
+
|
|
43
|
+
def shutdown(self):
|
|
44
|
+
if self.is_running:
|
|
45
|
+
self.scheduler.shutdown()
|
|
46
|
+
self.is_running = False
|
|
47
|
+
self.logger.info("Scheduler stopped")
|
|
48
|
+
|
|
49
|
+
def add_ping_job(self, host: str, interval: int = 60, job_id: Optional[str] = None):
|
|
50
|
+
if job_id is None:
|
|
51
|
+
job_id = f"ping_{host}"
|
|
52
|
+
|
|
53
|
+
self.scheduler.add_job(
|
|
54
|
+
func=self._ping_job,
|
|
55
|
+
trigger='interval',
|
|
56
|
+
seconds=interval,
|
|
57
|
+
id=job_id,
|
|
58
|
+
args=[host],
|
|
59
|
+
replace_existing=True
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.logger.info(f"Added ping job for {host} with {interval}s interval")
|
|
63
|
+
|
|
64
|
+
def remove_ping_job(self, job_id: str):
|
|
65
|
+
try:
|
|
66
|
+
self.scheduler.remove_job(job_id)
|
|
67
|
+
self.logger.info(f"Removed ping job: {job_id}")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
self.logger.error(f"Failed to remove job {job_id}: {e}")
|
|
70
|
+
|
|
71
|
+
def add_multiple_ping_jobs(self, hosts: List[str], interval: int = 60):
|
|
72
|
+
for host in hosts:
|
|
73
|
+
self.add_ping_job(host, interval)
|
|
74
|
+
|
|
75
|
+
def list_jobs(self):
|
|
76
|
+
return self.scheduler.get_jobs()
|
|
77
|
+
|
|
78
|
+
def _ping_job(self, host: str):
|
|
79
|
+
try:
|
|
80
|
+
result = self.ping_service.ping_and_save(host)
|
|
81
|
+
self.logger.debug(f"Ping result for {host}: {result.response_time}ms, packet_loss: {result.packet_loss}")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
self.logger.error(f"Error pinging {host}: {e}")
|
|
84
|
+
|
|
85
|
+
def get_job_status(self, job_id: str):
|
|
86
|
+
job = self.scheduler.get_job(job_id)
|
|
87
|
+
if job:
|
|
88
|
+
return {
|
|
89
|
+
'id': job.id,
|
|
90
|
+
'name': str(job.func),
|
|
91
|
+
'next_run_time': job.next_run_time.isoformat() if job.next_run_time else None,
|
|
92
|
+
'trigger': str(job.trigger)
|
|
93
|
+
}
|
|
94
|
+
return None
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Network Checker Dashboard</title>
|
|
7
|
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
background-color: #f5f5f5;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.container {
|
|
17
|
+
max-width: 1200px;
|
|
18
|
+
margin: 0 auto;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.header {
|
|
22
|
+
background: white;
|
|
23
|
+
padding: 20px;
|
|
24
|
+
border-radius: 8px;
|
|
25
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
26
|
+
margin-bottom: 20px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.controls {
|
|
30
|
+
display: flex;
|
|
31
|
+
gap: 15px;
|
|
32
|
+
margin-bottom: 20px;
|
|
33
|
+
align-items: center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
select, button {
|
|
37
|
+
padding: 8px 12px;
|
|
38
|
+
border: 1px solid #ddd;
|
|
39
|
+
border-radius: 4px;
|
|
40
|
+
font-size: 14px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
button {
|
|
44
|
+
background-color: #007bff;
|
|
45
|
+
color: white;
|
|
46
|
+
border: none;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
button:hover {
|
|
51
|
+
background-color: #0056b3;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.stats-grid {
|
|
55
|
+
display: grid;
|
|
56
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
57
|
+
gap: 15px;
|
|
58
|
+
margin-bottom: 20px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.stat-card {
|
|
62
|
+
background: white;
|
|
63
|
+
padding: 20px;
|
|
64
|
+
border-radius: 8px;
|
|
65
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
66
|
+
text-align: center;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.stat-value {
|
|
70
|
+
font-size: 2em;
|
|
71
|
+
font-weight: bold;
|
|
72
|
+
color: #007bff;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.stat-label {
|
|
76
|
+
color: #666;
|
|
77
|
+
margin-top: 5px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.chart-container {
|
|
81
|
+
background: white;
|
|
82
|
+
padding: 20px;
|
|
83
|
+
border-radius: 8px;
|
|
84
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
85
|
+
margin-bottom: 20px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.loading {
|
|
89
|
+
text-align: center;
|
|
90
|
+
padding: 50px;
|
|
91
|
+
color: #666;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.error {
|
|
95
|
+
background-color: #f8d7da;
|
|
96
|
+
color: #721c24;
|
|
97
|
+
padding: 15px;
|
|
98
|
+
border-radius: 4px;
|
|
99
|
+
margin-bottom: 20px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.success {
|
|
103
|
+
background-color: #d4edda;
|
|
104
|
+
color: #155724;
|
|
105
|
+
padding: 15px;
|
|
106
|
+
border-radius: 4px;
|
|
107
|
+
margin-bottom: 20px;
|
|
108
|
+
}
|
|
109
|
+
</style>
|
|
110
|
+
</head>
|
|
111
|
+
<body>
|
|
112
|
+
<div class="container">
|
|
113
|
+
<div class="header">
|
|
114
|
+
<h1>Network Checker Dashboard</h1>
|
|
115
|
+
<p>Monitor ping responses and packet loss for your network hosts</p>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="controls">
|
|
119
|
+
<label for="hostSelect">Host:</label>
|
|
120
|
+
<select id="hostSelect">
|
|
121
|
+
<option value="">Select a host...</option>
|
|
122
|
+
</select>
|
|
123
|
+
|
|
124
|
+
<label for="timeRange">Time Range:</label>
|
|
125
|
+
<select id="timeRange">
|
|
126
|
+
<option value="1">Last 1 hour</option>
|
|
127
|
+
<option value="6">Last 6 hours</option>
|
|
128
|
+
<option value="24" selected>Last 24 hours</option>
|
|
129
|
+
<option value="72">Last 3 days</option>
|
|
130
|
+
<option value="168">Last week</option>
|
|
131
|
+
</select>
|
|
132
|
+
|
|
133
|
+
<button onclick="refreshData()">Refresh</button>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div id="statsContainer" class="stats-grid" style="display: none;">
|
|
137
|
+
<div class="stat-card">
|
|
138
|
+
<div class="stat-value" id="totalPings">-</div>
|
|
139
|
+
<div class="stat-label">Total Pings</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="stat-card">
|
|
142
|
+
<div class="stat-value" id="successRate">-</div>
|
|
143
|
+
<div class="stat-label">Success Rate</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="stat-card">
|
|
146
|
+
<div class="stat-value" id="avgResponseTime">-</div>
|
|
147
|
+
<div class="stat-label">Avg Response Time</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="stat-card">
|
|
150
|
+
<div class="stat-value" id="packetLossRate">-</div>
|
|
151
|
+
<div class="stat-label">Packet Loss Rate</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div class="chart-container">
|
|
156
|
+
<div id="chartContainer">
|
|
157
|
+
<div class="loading">
|
|
158
|
+
Select a host to view ping data
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<script>
|
|
165
|
+
let currentHost = '';
|
|
166
|
+
let currentTimeRange = 24;
|
|
167
|
+
|
|
168
|
+
// Load hosts on page load
|
|
169
|
+
loadHosts();
|
|
170
|
+
|
|
171
|
+
// Event listeners
|
|
172
|
+
document.getElementById('hostSelect').addEventListener('change', function() {
|
|
173
|
+
currentHost = this.value;
|
|
174
|
+
if (currentHost) {
|
|
175
|
+
loadData();
|
|
176
|
+
} else {
|
|
177
|
+
document.getElementById('statsContainer').style.display = 'none';
|
|
178
|
+
document.getElementById('chartContainer').innerHTML = '<div class="loading">Select a host to view ping data</div>';
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
document.getElementById('timeRange').addEventListener('change', function() {
|
|
183
|
+
currentTimeRange = parseInt(this.value);
|
|
184
|
+
if (currentHost) {
|
|
185
|
+
loadData();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
function loadHosts() {
|
|
190
|
+
fetch('/api/hosts')
|
|
191
|
+
.then(response => response.json())
|
|
192
|
+
.then(data => {
|
|
193
|
+
const select = document.getElementById('hostSelect');
|
|
194
|
+
select.innerHTML = '<option value="">Select a host...</option>';
|
|
195
|
+
|
|
196
|
+
data.forEach(host => {
|
|
197
|
+
const option = document.createElement('option');
|
|
198
|
+
option.value = host;
|
|
199
|
+
option.textContent = host;
|
|
200
|
+
select.appendChild(option);
|
|
201
|
+
});
|
|
202
|
+
})
|
|
203
|
+
.catch(error => {
|
|
204
|
+
console.error('Error loading hosts:', error);
|
|
205
|
+
showError('Failed to load hosts');
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function loadData() {
|
|
210
|
+
if (!currentHost) return;
|
|
211
|
+
|
|
212
|
+
document.getElementById('chartContainer').innerHTML = '<div class="loading">Loading...</div>';
|
|
213
|
+
|
|
214
|
+
// Load statistics
|
|
215
|
+
fetch(`/api/stats?host=${currentHost}&hours=${currentTimeRange}`)
|
|
216
|
+
.then(response => response.json())
|
|
217
|
+
.then(data => {
|
|
218
|
+
updateStats(data);
|
|
219
|
+
})
|
|
220
|
+
.catch(error => {
|
|
221
|
+
console.error('Error loading stats:', error);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Load chart
|
|
225
|
+
fetch(`/api/ping_chart?host=${currentHost}&hours=${currentTimeRange}`)
|
|
226
|
+
.then(response => response.json())
|
|
227
|
+
.then(data => {
|
|
228
|
+
if (data.error) {
|
|
229
|
+
document.getElementById('chartContainer').innerHTML = `<div class="error">${data.error}</div>`;
|
|
230
|
+
} else {
|
|
231
|
+
Plotly.newPlot('chartContainer', data.data, data.layout, {responsive: true});
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
.catch(error => {
|
|
235
|
+
console.error('Error loading chart:', error);
|
|
236
|
+
document.getElementById('chartContainer').innerHTML = '<div class="error">Failed to load chart data</div>';
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function updateStats(data) {
|
|
241
|
+
document.getElementById('totalPings').textContent = data.total_pings;
|
|
242
|
+
document.getElementById('successRate').textContent = `${(100 - data.packet_loss_rate).toFixed(1)}%`;
|
|
243
|
+
document.getElementById('avgResponseTime').textContent = `${data.avg_response_time.toFixed(1)}ms`;
|
|
244
|
+
document.getElementById('packetLossRate').textContent = `${data.packet_loss_rate.toFixed(1)}%`;
|
|
245
|
+
|
|
246
|
+
document.getElementById('statsContainer').style.display = 'grid';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function refreshData() {
|
|
250
|
+
if (currentHost) {
|
|
251
|
+
loadData();
|
|
252
|
+
}
|
|
253
|
+
loadHosts();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function showError(message) {
|
|
257
|
+
const container = document.querySelector('.container');
|
|
258
|
+
const errorDiv = document.createElement('div');
|
|
259
|
+
errorDiv.className = 'error';
|
|
260
|
+
errorDiv.textContent = message;
|
|
261
|
+
container.insertBefore(errorDiv, container.firstChild.nextSibling);
|
|
262
|
+
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
errorDiv.remove();
|
|
265
|
+
}, 5000);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Auto-refresh every 30 seconds if a host is selected
|
|
269
|
+
setInterval(() => {
|
|
270
|
+
if (currentHost) {
|
|
271
|
+
loadData();
|
|
272
|
+
}
|
|
273
|
+
}, 30000);
|
|
274
|
+
</script>
|
|
275
|
+
</body>
|
|
276
|
+
</html>
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from flask import Blueprint, render_template, jsonify, request
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from sqlalchemy import desc, func
|
|
4
|
+
import plotly.graph_objects as go
|
|
5
|
+
import plotly.utils
|
|
6
|
+
import json
|
|
7
|
+
from ..models import db, PingResult
|
|
8
|
+
|
|
9
|
+
dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dashboard_bp.route('/')
|
|
13
|
+
def index():
|
|
14
|
+
"""Main dashboard page"""
|
|
15
|
+
return render_template('dashboard.html')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dashboard_bp.route('/api/hosts')
|
|
19
|
+
def get_hosts():
|
|
20
|
+
"""Get list of monitored hosts"""
|
|
21
|
+
hosts = db.session.query(PingResult.target_host).distinct().all()
|
|
22
|
+
return jsonify([host[0] for host in hosts])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dashboard_bp.route('/api/ping_data')
|
|
26
|
+
def get_ping_data():
|
|
27
|
+
"""Get ping data for visualization"""
|
|
28
|
+
host = request.args.get('host')
|
|
29
|
+
hours = int(request.args.get('hours', 24))
|
|
30
|
+
|
|
31
|
+
if not host:
|
|
32
|
+
return jsonify({'error': 'Host parameter is required'}), 400
|
|
33
|
+
|
|
34
|
+
# Get data for the specified time range
|
|
35
|
+
since = datetime.utcnow() - timedelta(hours=hours)
|
|
36
|
+
|
|
37
|
+
results = db.session.query(PingResult).filter(
|
|
38
|
+
PingResult.target_host == host,
|
|
39
|
+
PingResult.timestamp >= since
|
|
40
|
+
).order_by(PingResult.timestamp).all()
|
|
41
|
+
|
|
42
|
+
if not results:
|
|
43
|
+
return jsonify({'error': 'No data found for the specified host'}), 404
|
|
44
|
+
|
|
45
|
+
# Prepare data for plotting
|
|
46
|
+
timestamps = []
|
|
47
|
+
response_times = []
|
|
48
|
+
packet_losses = []
|
|
49
|
+
|
|
50
|
+
for result in results:
|
|
51
|
+
timestamps.append(result.timestamp.isoformat())
|
|
52
|
+
response_times.append(result.response_time if not result.packet_loss else None)
|
|
53
|
+
packet_losses.append(1 if result.packet_loss else 0)
|
|
54
|
+
|
|
55
|
+
return jsonify({
|
|
56
|
+
'host': host,
|
|
57
|
+
'timestamps': timestamps,
|
|
58
|
+
'response_times': response_times,
|
|
59
|
+
'packet_losses': packet_losses,
|
|
60
|
+
'total_pings': len(results),
|
|
61
|
+
'successful_pings': sum(1 for r in results if not r.packet_loss),
|
|
62
|
+
'packet_loss_rate': (sum(packet_losses) / len(packet_losses)) * 100 if packet_losses else 0
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dashboard_bp.route('/api/ping_chart')
|
|
67
|
+
def get_ping_chart():
|
|
68
|
+
"""Generate Plotly chart for ping data"""
|
|
69
|
+
host = request.args.get('host')
|
|
70
|
+
hours = int(request.args.get('hours', 24))
|
|
71
|
+
|
|
72
|
+
if not host:
|
|
73
|
+
return jsonify({'error': 'Host parameter is required'}), 400
|
|
74
|
+
|
|
75
|
+
since = datetime.utcnow() - timedelta(hours=hours)
|
|
76
|
+
|
|
77
|
+
results = db.session.query(PingResult).filter(
|
|
78
|
+
PingResult.target_host == host,
|
|
79
|
+
PingResult.timestamp >= since
|
|
80
|
+
).order_by(PingResult.timestamp).all()
|
|
81
|
+
|
|
82
|
+
if not results:
|
|
83
|
+
return jsonify({'error': 'No data found'}), 404
|
|
84
|
+
|
|
85
|
+
# Separate successful pings and packet losses
|
|
86
|
+
successful_times = []
|
|
87
|
+
successful_response_times = []
|
|
88
|
+
failed_times = []
|
|
89
|
+
|
|
90
|
+
for result in results:
|
|
91
|
+
if result.packet_loss:
|
|
92
|
+
failed_times.append(result.timestamp)
|
|
93
|
+
else:
|
|
94
|
+
successful_times.append(result.timestamp)
|
|
95
|
+
successful_response_times.append(result.response_time)
|
|
96
|
+
|
|
97
|
+
# Create plotly figure
|
|
98
|
+
fig = go.Figure()
|
|
99
|
+
|
|
100
|
+
# Add successful pings
|
|
101
|
+
if successful_times:
|
|
102
|
+
fig.add_trace(go.Scatter(
|
|
103
|
+
x=successful_times,
|
|
104
|
+
y=successful_response_times,
|
|
105
|
+
mode='lines+markers',
|
|
106
|
+
name='Response Time (ms)',
|
|
107
|
+
line=dict(color='green'),
|
|
108
|
+
marker=dict(size=4)
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
# Add packet loss markers
|
|
112
|
+
if failed_times:
|
|
113
|
+
fig.add_trace(go.Scatter(
|
|
114
|
+
x=failed_times,
|
|
115
|
+
y=[max(successful_response_times) * 1.1] * len(failed_times) if successful_response_times else [100] * len(failed_times),
|
|
116
|
+
mode='markers',
|
|
117
|
+
name='Packet Loss',
|
|
118
|
+
marker=dict(color='red', symbol='x', size=8)
|
|
119
|
+
))
|
|
120
|
+
|
|
121
|
+
fig.update_layout(
|
|
122
|
+
title=f'Ping Results for {host} (Last {hours} hours)',
|
|
123
|
+
xaxis_title='Time',
|
|
124
|
+
yaxis_title='Response Time (ms)',
|
|
125
|
+
template='plotly_white',
|
|
126
|
+
hovermode='x unified'
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return jsonify(json.loads(plotly.utils.PlotlyJSONEncoder().encode(fig)))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dashboard_bp.route('/api/stats')
|
|
133
|
+
def get_stats():
|
|
134
|
+
"""Get overall statistics"""
|
|
135
|
+
host = request.args.get('host')
|
|
136
|
+
hours = int(request.args.get('hours', 24))
|
|
137
|
+
|
|
138
|
+
since = datetime.utcnow() - timedelta(hours=hours)
|
|
139
|
+
|
|
140
|
+
if host:
|
|
141
|
+
query = db.session.query(PingResult).filter(
|
|
142
|
+
PingResult.target_host == host,
|
|
143
|
+
PingResult.timestamp >= since
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
query = db.session.query(PingResult).filter(
|
|
147
|
+
PingResult.timestamp >= since
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
results = query.all()
|
|
151
|
+
|
|
152
|
+
if not results:
|
|
153
|
+
return jsonify({
|
|
154
|
+
'total_pings': 0,
|
|
155
|
+
'successful_pings': 0,
|
|
156
|
+
'failed_pings': 0,
|
|
157
|
+
'packet_loss_rate': 0,
|
|
158
|
+
'avg_response_time': 0,
|
|
159
|
+
'min_response_time': 0,
|
|
160
|
+
'max_response_time': 0
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
successful = [r for r in results if not r.packet_loss and r.response_time is not None]
|
|
164
|
+
successful_response_times = [r.response_time for r in successful]
|
|
165
|
+
|
|
166
|
+
return jsonify({
|
|
167
|
+
'total_pings': len(results),
|
|
168
|
+
'successful_pings': len(successful),
|
|
169
|
+
'failed_pings': len(results) - len(successful),
|
|
170
|
+
'packet_loss_rate': ((len(results) - len(successful)) / len(results)) * 100,
|
|
171
|
+
'avg_response_time': sum(successful_response_times) / len(successful_response_times) if successful_response_times else 0,
|
|
172
|
+
'min_response_time': min(successful_response_times) if successful_response_times else 0,
|
|
173
|
+
'max_response_time': max(successful_response_times) if successful_response_times else 0
|
|
174
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "network-check-tool"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "A network monitoring tool with ping tracking and dashboard visualization"
|
|
5
|
+
authors = ["hardwork9047 <tomatosimple1104@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
homepage = "https://github.com/hardwork9047/network-checker"
|
|
9
|
+
repository = "https://github.com/hardwork9047/network-checker"
|
|
10
|
+
documentation = "https://github.com/hardwork9047/network-checker"
|
|
11
|
+
packages = [{ include = "network_checker" }]
|
|
12
|
+
|
|
13
|
+
include = [
|
|
14
|
+
"network_checker/config/config.py",
|
|
15
|
+
"network_checker/templates/*",
|
|
16
|
+
"network_checker/static/*",
|
|
17
|
+
"network_checker/cli.py"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.poetry.dependencies]
|
|
21
|
+
python = "^3.10"
|
|
22
|
+
flask = "^3.1.0"
|
|
23
|
+
flask-sqlalchemy = "^3.1.1"
|
|
24
|
+
pandas = "^2.2.3"
|
|
25
|
+
flask-cors = "^4.0.0"
|
|
26
|
+
click = "^8.1.3"
|
|
27
|
+
ping3 = "^4.0.4"
|
|
28
|
+
apscheduler = "^3.10.4"
|
|
29
|
+
plotly = "^5.17.0"
|
|
30
|
+
toml = "^0.10.2"
|
|
31
|
+
|
|
32
|
+
[tool.poetry.scripts]
|
|
33
|
+
network-checker = "network_checker.cli:cli"
|
|
34
|
+
|
|
35
|
+
[tool.poetry.group.dev.dependencies]
|
|
36
|
+
pytest = "^8.3.4"
|
|
37
|
+
black = "^24.0.0"
|
|
38
|
+
|
|
39
|
+
[tool.black]
|
|
40
|
+
line-length = 88
|
|
41
|
+
target-version = ['py310']
|
|
42
|
+
include = '\.pyi?$'
|
|
43
|
+
skip-string-normalization = false
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["poetry-core"]
|
|
47
|
+
build-backend = "poetry.core.masonry.api"
|