hy-app-monitor/monitor/http_app.py

130 lines
4.8 KiB
Python

from __future__ import annotations
# JSON 用于构造统一接口响应。
import json
# HTTP 状态码常量。
from http import HTTPStatus
# 标准库 HTTP Server 足够承载这个轻量页面。
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
# 类型提示用于方法签名。
from typing import Any
# urlparse 只解析 path。
from urllib.parse import urlparse
# 读取应用配置和静态目录。
from .config import APP_BIND, APP_PORT, STATIC_DIR, utc_now
# 统一构造监控页 API 载荷。
from .dashboard import build_monitor_payload
def response_json(payload: dict[str, Any] | list[Any]) -> bytes:
# 统一把 JSON 编码成 UTF-8 文本。
return (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
class MonitorHandler(BaseHTTPRequestHandler):
# 自定义服务名,便于 curl 时识别来源。
server_version = "HyAppMonitor/2.0"
def log_message(self, format: str, *args: Any) -> None: # noqa: A003
# 关闭默认访问日志,避免轮询刷屏。
return
def do_GET(self) -> None: # noqa: N802
# GET 请求需要响应头和响应体。
self.dispatch(send_body=True)
def do_HEAD(self) -> None: # noqa: N802
# HEAD 请求只返回响应头。
self.dispatch(send_body=False)
def dispatch(self, send_body: bool) -> None:
# 只解析 URL 的 path 段。
parsed = urlparse(self.path)
# 首页返回静态 HTML。
if parsed.path in {"/", "/index.html"}:
return self.serve_static("index.html", "text/html; charset=utf-8", send_body=send_body)
# CSS 文件。
if parsed.path == "/app.css":
return self.serve_static("app.css", "text/css; charset=utf-8", send_body=send_body)
# 前端业务 JS。
if parsed.path == "/app.js":
return self.serve_static("app.js", "application/javascript; charset=utf-8", send_body=send_body)
# 本地 Vue 运行时。
if parsed.path == "/vue.js":
return self.serve_static("vue.js", "application/javascript; charset=utf-8", send_body=send_body)
# 轻量健康检查。
if parsed.path == "/api/health":
return self.send_payload(
HTTPStatus.OK,
response_json({"status": "ok", "time": utc_now()}),
"application/json; charset=utf-8",
send_body=send_body,
)
# 主监控接口。
if parsed.path == "/api/monitor/services":
return self.send_payload(
HTTPStatus.OK,
response_json(build_monitor_payload()),
"application/json; charset=utf-8",
send_body=send_body,
)
# favicon 直接返回空。
if parsed.path == "/favicon.ico":
return self.send_payload(HTTPStatus.NO_CONTENT, b"", "image/x-icon", send_body=send_body)
# 其他路径统一 404。
return self.send_error_payload(HTTPStatus.NOT_FOUND, "not found", send_body=send_body)
def serve_static(self, name: str, content_type: str, send_body: bool) -> None:
# 计算静态文件路径。
path = STATIC_DIR / name
# 文件不存在时返回 JSON 错误。
if not path.exists():
return self.send_error_payload(HTTPStatus.NOT_FOUND, "static file missing", send_body=send_body)
# 文件存在时返回原始二进制内容。
self.send_payload(HTTPStatus.OK, path.read_bytes(), content_type, send_body=send_body)
def send_error_payload(self, status: HTTPStatus, message: str, send_body: bool) -> None:
# 错误响应也统一走 JSON。
self.send_payload(
status,
response_json({"status": int(status), "error": message}),
"application/json; charset=utf-8",
send_body=send_body,
)
def send_payload(self, status: HTTPStatus, body: bytes, content_type: str, send_body: bool) -> None:
# 写状态码。
self.send_response(status)
# 写内容类型。
self.send_header("Content-Type", content_type)
# 页面和接口都禁用缓存。
self.send_header("Cache-Control", "no-store")
# 写入长度,便于 HEAD 请求工作正常。
self.send_header("Content-Length", str(len(body)))
# 结束响应头。
self.end_headers()
# 需要响应体时再写内容。
if send_body and body:
self.wfile.write(body)
def main() -> None:
# 创建线程型 HTTP 服务。
server = ThreadingHTTPServer((APP_BIND, APP_PORT), MonitorHandler)
# 打印监听地址便于排查。
print(f"hy-app-monitor listening on http://{APP_BIND}:{APP_PORT}", flush=True)
# 进入永久监听。
server.serve_forever()