483 lines
19 KiB
Python
483 lines
19 KiB
Python
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from cli.tui.adapters.base import BaseDetailAdapter
|
|
from cli.tui.adapters.models import (
|
|
TUI_ADAPTER_MODEL_RENDERER,
|
|
DetailPageSnapshot,
|
|
DetailSectionSnapshot,
|
|
)
|
|
from cli.tui.copy import TUI_COPY
|
|
from cli.tui.diagnostics import TUI_DIAGNOSTIC_SERVICE
|
|
from cli.utils import NESTED_CLI_SUPPORT, SHELL_TEXT_FORMATTER
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OpsDetailSourcePayloads:
|
|
"""
|
|
运维详情页原始数据源快照。
|
|
|
|
:param health_payload: `ops health` 结果
|
|
:param ping_db_payload: `ops ping-db` 结果
|
|
:param ping_redis_payload: `ops ping-redis` 结果
|
|
:param deps_payload: `ops deps` 结果
|
|
:param server_payload: `ops server-info` 结果
|
|
"""
|
|
|
|
health_payload: dict[str, Any] | None
|
|
ping_db_payload: dict[str, Any] | None
|
|
ping_redis_payload: dict[str, Any] | None
|
|
deps_payload: dict[str, Any] | None
|
|
server_payload: dict[str, Any] | None
|
|
|
|
|
|
class OpsDetailSnapshotCollector:
|
|
"""
|
|
运维详情页数据采集器。
|
|
|
|
该对象负责拉取运维详情页所需的多路 CLI 原始结果,
|
|
让 `OpsDetailAdapter` 保持详情页编排职责。
|
|
"""
|
|
|
|
def collect(self, env: str) -> OpsDetailSourcePayloads:
|
|
"""
|
|
采集运维详情页所需原始结果。
|
|
|
|
:param env: 当前运行环境
|
|
:return: 运维详情页原始数据源快照
|
|
"""
|
|
return OpsDetailSourcePayloads(
|
|
health_payload=NESTED_CLI_SUPPORT.run(
|
|
'ops',
|
|
'health',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
ping_db_payload=NESTED_CLI_SUPPORT.run(
|
|
'ops',
|
|
'ping-db',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
ping_redis_payload=NESTED_CLI_SUPPORT.run(
|
|
'ops',
|
|
'ping-redis',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
deps_payload=NESTED_CLI_SUPPORT.run(
|
|
'ops',
|
|
'deps',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
server_payload=NESTED_CLI_SUPPORT.run(
|
|
'ops',
|
|
'server-info',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
)
|
|
|
|
|
|
class OpsSectionBuilder:
|
|
"""
|
|
运维详情分区构建器。
|
|
|
|
该构建器负责将运维相关 CLI 结果负载转换为 TUI 详情页分区,
|
|
使详情页适配器本体只保留采集与编排职责。
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_health_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot:
|
|
"""
|
|
构建运维健康检查分区。
|
|
|
|
:param payload: `ops health` JSON 负载
|
|
:return: 分区快照
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
return DetailSectionSnapshot(
|
|
title='健康检查',
|
|
status='fail',
|
|
lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(
|
|
payload, empty_label='健康检查', empty_value='不可用'
|
|
),
|
|
)
|
|
database = payload.get('database') if isinstance(payload.get('database'), dict) else {}
|
|
redis = payload.get('redis') if isinstance(payload.get('redis'), dict) else {}
|
|
return DetailSectionSnapshot(
|
|
title='健康检查',
|
|
status='ok' if payload.get('ok', False) else 'fail',
|
|
lines=[
|
|
'## 基础连通性',
|
|
f'环境: {payload.get("env", "-")}',
|
|
f'数据库: {"正常" if database.get("ok", False) else "异常"}',
|
|
f'> {SHELL_TEXT_FORMATTER.truncate_text(database.get("message", "-"), 64)}',
|
|
f'Redis: {"正常" if redis.get("ok", False) else "异常"}',
|
|
f'> {SHELL_TEXT_FORMATTER.truncate_text(redis.get("message", "-"), 64)}',
|
|
],
|
|
)
|
|
|
|
@staticmethod
|
|
def build_ping_db_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot:
|
|
"""
|
|
构建数据库探活结果分区。
|
|
|
|
:param payload: `ops ping-db` JSON 负载
|
|
:return: 分区快照
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
return DetailSectionSnapshot(
|
|
title='数据库探活',
|
|
status='fail',
|
|
lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(
|
|
payload, empty_label='数据库探活', empty_value='不可用'
|
|
),
|
|
)
|
|
return DetailSectionSnapshot(
|
|
title='数据库探活',
|
|
status='ok' if payload.get('ok', False) else 'fail',
|
|
lines=[
|
|
'## 探活结果',
|
|
f'数据库连接: {"正常" if payload.get("ok", False) else "异常"}',
|
|
f'结果摘要: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("message", "-"), 72)}',
|
|
],
|
|
)
|
|
|
|
@staticmethod
|
|
def build_ping_redis_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot:
|
|
"""
|
|
构建 Redis 探活结果分区。
|
|
|
|
:param payload: `ops ping-redis` JSON 负载
|
|
:return: 分区快照
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
return DetailSectionSnapshot(
|
|
title='Redis 探活',
|
|
status='fail',
|
|
lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(
|
|
payload, empty_label='Redis 探活', empty_value='不可用'
|
|
),
|
|
)
|
|
return DetailSectionSnapshot(
|
|
title='Redis 探活',
|
|
status='ok' if payload.get('ok', False) else 'fail',
|
|
lines=[
|
|
'## 探活结果',
|
|
f'Redis 连接: {"正常" if payload.get("ok", False) else "异常"}',
|
|
f'结果摘要: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("message", "-"), 72)}',
|
|
],
|
|
)
|
|
|
|
@staticmethod
|
|
def build_dependency_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot:
|
|
"""
|
|
构建运维依赖版本分区。
|
|
|
|
:param payload: `ops deps` JSON 负载
|
|
:return: 分区快照
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
return DetailSectionSnapshot(
|
|
title='依赖版本',
|
|
status='fail',
|
|
lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(payload, empty_label='依赖', empty_value='不可用'),
|
|
)
|
|
packages = payload.get('packages') if isinstance(payload.get('packages'), dict) else {}
|
|
missing_required = payload.get('missingRequired') if isinstance(payload.get('missingRequired'), list) else []
|
|
lines = [
|
|
'## 总体结论',
|
|
f'检查结果: {"通过" if payload.get("ok", False) else "异常"}',
|
|
f'缺失核心依赖: {len(missing_required)} 个',
|
|
f'> {SHELL_TEXT_FORMATTER.truncate_text(payload.get("message", "-"), 72)}',
|
|
'',
|
|
'## 关键版本',
|
|
]
|
|
for package_name in ('python', 'fastapi', 'sqlalchemy', 'redis', 'typer', 'alembic'):
|
|
package_payload = packages.get(package_name)
|
|
if not isinstance(package_payload, dict):
|
|
continue
|
|
installed = '已安装' if package_payload.get('installed', False) else '缺失'
|
|
lines.append(f'> {package_name} · {installed} · {package_payload.get("version", "-") or "-"}')
|
|
if missing_required:
|
|
lines.extend(['', '## 缺失项'])
|
|
lines.extend(f'> {item}' for item in missing_required[:8])
|
|
return DetailSectionSnapshot(
|
|
title='依赖版本',
|
|
status='ok' if payload.get('ok', False) else 'fail',
|
|
lines=lines,
|
|
)
|
|
|
|
@staticmethod
|
|
def build_server_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot:
|
|
"""
|
|
构建服务器运行概况分区。
|
|
|
|
:param payload: `ops server-info` JSON 负载
|
|
:return: 分区快照
|
|
"""
|
|
server = payload.get('server') if isinstance(payload, dict) else None
|
|
if not isinstance(server, dict):
|
|
return DetailSectionSnapshot(
|
|
title='服务器摘要',
|
|
status='fail',
|
|
lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(
|
|
payload, empty_label='服务器信息', empty_value='不可用'
|
|
),
|
|
)
|
|
|
|
sys_info = server.get('sys') if isinstance(server.get('sys'), dict) else {}
|
|
cpu_info = server.get('cpu') if isinstance(server.get('cpu'), dict) else {}
|
|
mem_info = server.get('mem') if isinstance(server.get('mem'), dict) else {}
|
|
py_info = server.get('py') if isinstance(server.get('py'), dict) else {}
|
|
return DetailSectionSnapshot(
|
|
title='服务器摘要',
|
|
status='ok' if payload.get('ok', False) else 'fail',
|
|
lines=[
|
|
'## 主机信息',
|
|
f'主机名: {sys_info.get("computerName", "-")}',
|
|
f'主机 IP: {sys_info.get("computerIp", "-")}',
|
|
f'操作系统: {SHELL_TEXT_FORMATTER.truncate_text(sys_info.get("osName", "-"), 56)}',
|
|
f'系统架构: {sys_info.get("osArch", "-")}',
|
|
'',
|
|
'## 资源负载',
|
|
f'CPU 核心: {cpu_info.get("cpuNum", "-")}',
|
|
f'CPU 使用率: {cpu_info.get("used", "-")}%',
|
|
f'内存总量: {mem_info.get("total", "-")}',
|
|
f'内存使用率: {mem_info.get("usage", "-")}%',
|
|
'',
|
|
'## Python 进程',
|
|
f'版本: {py_info.get("version", "-")}',
|
|
f'运行时长: {py_info.get("runTime", "-")}',
|
|
f'进程内存: {py_info.get("used", "-")} / {py_info.get("total", "-")}',
|
|
],
|
|
)
|
|
|
|
def build_disk_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot:
|
|
"""
|
|
构建服务器磁盘样本分区。
|
|
|
|
:param payload: `ops server-info` JSON 负载
|
|
:return: 分区快照
|
|
"""
|
|
server = payload.get('server') if isinstance(payload, dict) else None
|
|
if not isinstance(server, dict):
|
|
return DetailSectionSnapshot(
|
|
title='磁盘样本',
|
|
status='fail',
|
|
lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(
|
|
payload, empty_label='磁盘样本', empty_value='不可用'
|
|
),
|
|
)
|
|
sys_files = server.get('sysFiles') if isinstance(server.get('sysFiles'), list) else []
|
|
lines = TUI_ADAPTER_MODEL_RENDERER.build_empty_lines(
|
|
empty_label='磁盘样本',
|
|
empty_value='0 个',
|
|
detail='当前服务器信息中没有返回磁盘分区样本',
|
|
suggestion=TUI_COPY.build_refresh_page_suggestion('运维', '重新采集服务器信息'),
|
|
)
|
|
section_status = 'info'
|
|
if sys_files:
|
|
section_status = 'ok'
|
|
lines = []
|
|
for item in sys_files[:8]:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
lines.extend(
|
|
[
|
|
f'## {SHELL_TEXT_FORMATTER.truncate_text(item.get("dirName", "-"), 36)}',
|
|
(
|
|
f'> 已用 {item.get("used", "-")} / 总量 {item.get("total", "-")} '
|
|
f'| 使用率 {item.get("usage", "-")}'
|
|
),
|
|
f'> 可用 {item.get("free", "-")}',
|
|
'',
|
|
]
|
|
)
|
|
if lines and lines[-1] == '':
|
|
lines.pop()
|
|
return DetailSectionSnapshot(
|
|
title='磁盘样本',
|
|
status=section_status,
|
|
lines=lines,
|
|
)
|
|
|
|
@staticmethod
|
|
def build_prod_check_entry_section() -> DetailSectionSnapshot:
|
|
"""
|
|
构建生产巡检向导入口分区。
|
|
|
|
:return: 分区快照
|
|
"""
|
|
return DetailSectionSnapshot(
|
|
title='生产巡检入口',
|
|
status='info',
|
|
lines=TUI_COPY.build_command_hint_lines(
|
|
scenario='准备做生产环境巡检或上线前复核时,应先通过向导统一检查数据库、缓存和运行配置。',
|
|
command=TUI_COPY.build_cli_command_hint('wizard', 'prod-check', '--output=text'),
|
|
guide='向导会继续询问目标环境并输出预览结果,确认后再执行完整巡检。',
|
|
),
|
|
)
|
|
|
|
@staticmethod
|
|
def build_overview_section(
|
|
health_payload: dict[str, Any] | None,
|
|
deps_payload: dict[str, Any] | None,
|
|
server_payload: dict[str, Any] | None,
|
|
) -> DetailSectionSnapshot:
|
|
"""
|
|
构建运维页总览判断分区。
|
|
|
|
:param health_payload: `ops health` JSON 负载
|
|
:param deps_payload: `ops deps` JSON 负载
|
|
:param server_payload: `ops server-info` JSON 负载
|
|
:return: 分区快照
|
|
"""
|
|
health_ok = bool(isinstance(health_payload, dict) and health_payload.get('ok', False))
|
|
deps_ok = bool(isinstance(deps_payload, dict) and deps_payload.get('ok', False))
|
|
server_ok = bool(isinstance(server_payload, dict) and server_payload.get('ok', False))
|
|
missing_required = deps_payload.get('missingRequired') if isinstance(deps_payload, dict) else None
|
|
missing_count = len(missing_required) if isinstance(missing_required, list) else 0
|
|
server = (
|
|
server_payload.get('server')
|
|
if isinstance(server_payload, dict) and isinstance(server_payload.get('server'), dict)
|
|
else {}
|
|
)
|
|
cpu_info = server.get('cpu') if isinstance(server.get('cpu'), dict) else {}
|
|
mem_info = server.get('mem') if isinstance(server.get('mem'), dict) else {}
|
|
|
|
status = 'ok'
|
|
conclusion = '基础探活与运行依赖正常,可继续查看服务器资源与磁盘样本'
|
|
if not health_ok:
|
|
status = 'fail'
|
|
conclusion = '运维探活存在异常,优先处理数据库或 Redis 连通性问题'
|
|
elif not deps_ok or missing_count > 0:
|
|
status = 'warn'
|
|
conclusion = '运行依赖存在缺口,建议优先补齐核心依赖并确认版本兼容性'
|
|
elif not server_ok:
|
|
status = 'warn'
|
|
conclusion = '服务器信息采集异常,建议重新采集资源与磁盘状态'
|
|
|
|
return DetailSectionSnapshot(
|
|
title='总览判断',
|
|
status=status,
|
|
lines=[
|
|
'## 当前结论',
|
|
conclusion,
|
|
'',
|
|
'## 核心指标',
|
|
f'探活状态: {"正常" if health_ok else "异常"}',
|
|
f'缺失核心依赖: {missing_count} 个',
|
|
f'CPU 使用率: {cpu_info.get("used", "-")}%',
|
|
f'内存使用率: {mem_info.get("usage", "-")}%',
|
|
'',
|
|
'## 建议入口',
|
|
'优先关注:健康检查 / 依赖版本 / 服务器摘要 / 生产巡检入口',
|
|
],
|
|
)
|
|
|
|
def build_sections(
|
|
self,
|
|
*,
|
|
health_payload: dict[str, Any] | None,
|
|
ping_db_payload: dict[str, Any] | None,
|
|
ping_redis_payload: dict[str, Any] | None,
|
|
deps_payload: dict[str, Any] | None,
|
|
server_payload: dict[str, Any] | None,
|
|
) -> list[DetailSectionSnapshot]:
|
|
"""
|
|
构建运维页全部详情分区。
|
|
|
|
:param health_payload: `ops health` 结果负载
|
|
:param ping_db_payload: `ops ping-db` 结果负载
|
|
:param ping_redis_payload: `ops ping-redis` 结果负载
|
|
:param deps_payload: `ops deps` 结果负载
|
|
:param server_payload: `ops server-info` 结果负载
|
|
:return: 详情分区列表
|
|
"""
|
|
return [
|
|
self.build_overview_section(health_payload, deps_payload, server_payload),
|
|
self.build_health_section(health_payload),
|
|
self.build_ping_db_section(ping_db_payload),
|
|
self.build_ping_redis_section(ping_redis_payload),
|
|
self.build_dependency_section(deps_payload),
|
|
self.build_server_section(server_payload),
|
|
self.build_disk_section(server_payload),
|
|
self.build_prod_check_entry_section(),
|
|
]
|
|
|
|
|
|
class OpsDetailAdapter(BaseDetailAdapter):
|
|
"""
|
|
运维详情页适配器。
|
|
|
|
该适配器负责采集运维相关 CLI 结果,并委托分区构建器组装为
|
|
TUI 详情页快照。
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
section_builder: OpsSectionBuilder | None = None,
|
|
snapshot_collector: OpsDetailSnapshotCollector | None = None,
|
|
) -> None:
|
|
"""
|
|
初始化运维详情页适配器。
|
|
|
|
:param section_builder: 运维详情分区构建器
|
|
:param snapshot_collector: 运维详情页数据采集器
|
|
:return: None
|
|
"""
|
|
super().__init__(
|
|
page_title='运维',
|
|
search_view_key='ops',
|
|
default_suggestions=[
|
|
'总览判断',
|
|
'健康检查',
|
|
'数据库探活',
|
|
'Redis 探活',
|
|
'依赖版本',
|
|
'服务器摘要',
|
|
'磁盘样本',
|
|
'生产巡检入口',
|
|
],
|
|
)
|
|
self.section_builder = section_builder or OpsSectionBuilder()
|
|
self.snapshot_collector = snapshot_collector or OpsDetailSnapshotCollector()
|
|
|
|
def collect_snapshot(self, env: str, query: str = '') -> DetailPageSnapshot:
|
|
"""
|
|
采集运维状态页只读快照。
|
|
|
|
:param env: 当前运行环境
|
|
:param query: 当前搜索词
|
|
:return: 页面快照
|
|
"""
|
|
source_payloads = self.snapshot_collector.collect(env)
|
|
sections = self.section_builder.build_sections(
|
|
health_payload=source_payloads.health_payload,
|
|
ping_db_payload=source_payloads.ping_db_payload,
|
|
ping_redis_payload=source_payloads.ping_redis_payload,
|
|
deps_payload=source_payloads.deps_payload,
|
|
server_payload=source_payloads.server_payload,
|
|
)
|
|
return DetailPageSnapshot(
|
|
title='运维',
|
|
subtitle=TUI_DIAGNOSTIC_SERVICE.build_ops_diagnostic_subtitle(
|
|
source_payloads.health_payload,
|
|
source_payloads.deps_payload,
|
|
source_payloads.server_payload,
|
|
),
|
|
sections=self.filter_sections(sections, query),
|
|
search=self.resolve_search_context(query),
|
|
)
|
|
|
|
|
|
OPS_DETAIL_ADAPTER = OpsDetailAdapter()
|