Add Linux server monitor dashboard
This commit is contained in:
1 parent
0446648ed1
commit
315cb36a21
19 files changed
+5997
-1
No files matched your search
+196
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Server Monitor entry point.
|
||||
|
||||
Usage:
|
||||
python3 monitor.py --bind 0.0.0.0 --port 8787
|
||||
python3 monitor.py --demo
|
||||
python3 monitor.py --once --demo
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from servermon import __version__
|
||||
from servermon.demo import DemoSource
|
||||
from servermon.metrics import SystemCollector
|
||||
from servermon.service import MonitorService
|
||||
from servermon.web import AuthConfig, build_server
|
||||
|
||||
logger = logging.getLogger("servermon")
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="server-monitor",
|
||||
description="Linux 服务器监控展示牌:CPU / 内存 / 负载 / 磁盘 / 网络,1024x768 自适应面板。",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--bind", default="0.0.0.0", help="监听地址")
|
||||
parser.add_argument("--port", type=int, default=8787, help="监听端口")
|
||||
parser.add_argument("--interval", type=float, default=1.0, help="采样间隔(秒,最小 0.2)")
|
||||
parser.add_argument("--demo", action="store_true", help="使用内置演示数据(可在非 Linux 上运行)")
|
||||
parser.add_argument("--once", action="store_true", help="打印一次 JSON 快照后退出")
|
||||
parser.add_argument(
|
||||
"--auth",
|
||||
default=None,
|
||||
metavar="USER:PASSWORD",
|
||||
help="可选 HTTP Basic 认证;也可用环境变量 SERVER_MONITOR_AUTH",
|
||||
)
|
||||
parser.add_argument("--web-dir", default=None, help="前端静态文件目录")
|
||||
parser.add_argument("--verbose", action="store_true", help="输出 HTTP 访问日志")
|
||||
parser.add_argument("--version", action="version", version=f"server-monitor {__version__}")
|
||||
return parser
|
||||
|
||||
|
||||
def _local_ips() -> list:
|
||||
ips = []
|
||||
try:
|
||||
host = socket.gethostname()
|
||||
for info in socket.getaddrinfo(host, None, socket.AF_INET):
|
||||
ip = info[4][0]
|
||||
if ip not in ips and not ip.startswith("127."):
|
||||
ips.append(ip)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(0.2)
|
||||
sock.connect(("1.1.1.1", 80))
|
||||
ip = sock.getsockname()[0]
|
||||
sock.close()
|
||||
if ip not in ips:
|
||||
ips.append(ip)
|
||||
except OSError:
|
||||
pass
|
||||
return ips
|
||||
|
||||
|
||||
def _print_banner(bind: str, port: int, demo: bool, auth: Optional[AuthConfig]) -> None:
|
||||
print()
|
||||
print(" ╭──────────────────────────────────────────────────────────╮")
|
||||
print(" │ SERVER MONITOR · 1024x768 · realtime metrics │")
|
||||
print(" ╰──────────────────────────────────────────────────────────╯")
|
||||
print(f" 模式 : {'DEMO 演示数据' if demo else 'LIVE 系统指标'}")
|
||||
print(f" 监听 : {bind}:{port}")
|
||||
if bind in ("0.0.0.0", "::", ""):
|
||||
print(f" 本机访问 : http://127.0.0.1:{port}/")
|
||||
for ip in _local_ips():
|
||||
print(f" 局域网 : http://{ip}:{port}/")
|
||||
elif ":" in bind:
|
||||
print(f" 访问 : http://[{bind}]:{port}/")
|
||||
else:
|
||||
print(f" 访问 : http://{bind}:{port}/")
|
||||
print(f" 认证 : {'HTTP Basic 已启用' if auth else '未启用(建议仅暴露在可信网络)'}")
|
||||
print(" 快捷键 : SPACE 暂停轮播 · ←/→ 切换 · F 全屏")
|
||||
print()
|
||||
|
||||
|
||||
def _run_once(args: argparse.Namespace) -> int:
|
||||
source = DemoSource() if args.demo else SystemCollector()
|
||||
service = MonitorService(source, interval=args.interval)
|
||||
service.bootstrap()
|
||||
if not args.demo:
|
||||
time.sleep(max(0.3, min(2.0, args.interval)))
|
||||
service.collect_once()
|
||||
snapshot = service.snapshot()
|
||||
if snapshot is None:
|
||||
print("无法生成快照", file=sys.stderr)
|
||||
return 1
|
||||
json.dump(snapshot, sys.stdout, ensure_ascii=False, indent=2)
|
||||
sys.stdout.write("\n")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: Optional[list] = None) -> int:
|
||||
args = _build_parser().parse_args(argv)
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
|
||||
if args.interval < 0.2:
|
||||
logger.error("--interval 不能小于 0.2 秒")
|
||||
return 2
|
||||
if not 1 <= args.port <= 65535:
|
||||
logger.error("--port 必须在 1-65535 之间")
|
||||
return 2
|
||||
|
||||
try:
|
||||
auth_raw = args.auth if args.auth is not None else os.environ.get("SERVER_MONITOR_AUTH")
|
||||
auth = AuthConfig.parse(auth_raw)
|
||||
except ValueError as exc:
|
||||
logger.error("认证配置错误: %s", exc)
|
||||
return 2
|
||||
|
||||
if args.once:
|
||||
return _run_once(args)
|
||||
|
||||
if not args.demo and not sys.platform.startswith("linux"):
|
||||
logger.error("实时模式仅支持 Linux。开发预览请加 --demo。")
|
||||
return 2
|
||||
|
||||
source = DemoSource() if args.demo else SystemCollector()
|
||||
service = MonitorService(source, interval=args.interval)
|
||||
service.bootstrap()
|
||||
|
||||
stop_event = threading.Event()
|
||||
web_dir = Path(args.web_dir) if args.web_dir else None
|
||||
try:
|
||||
server = build_server(
|
||||
args.bind,
|
||||
args.port,
|
||||
service,
|
||||
auth=auth,
|
||||
web_dir=web_dir,
|
||||
stop_event=stop_event,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
except (OSError, ValueError, FileNotFoundError) as exc:
|
||||
logger.error("无法启动 HTTP 服务: %s", exc)
|
||||
return 1
|
||||
|
||||
def shutdown(signum: int, _frame: object) -> None:
|
||||
logger.info("收到信号 %s,正在停止…", signum)
|
||||
stop_event.set()
|
||||
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
try:
|
||||
signal.signal(sig, shutdown)
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
server_thread = threading.Thread(target=server.serve_forever, name="http", daemon=True)
|
||||
server_thread.start()
|
||||
service.start()
|
||||
|
||||
_print_banner(args.bind, args.port, args.demo, auth)
|
||||
if auth is None and args.bind not in ("127.0.0.1", "::1", "localhost"):
|
||||
logger.warning("当前未启用认证且监听非回环地址,请用防火墙限制访问范围。")
|
||||
|
||||
try:
|
||||
while not stop_event.wait(0.5):
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
stop_event.set()
|
||||
finally:
|
||||
service.stop()
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
logger.info("已停止")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user