from __future__ import annotations # 容器日志命令里需要安全拼接容器名。 import shlex from typing import Any from .compose_templates import container_name from core.config import SERVICE_LOG_EXPORT_MAX_LINES, SERVICE_LOG_PREVIEW_LINES, flatten_services, load_hosts, utc_now from .ssh_remote import run_host_script def loggable_service_records() -> list[dict[str, Any]]: # 当前日志查看只开放业务服务实例,不扩展到 nacos / mongo 这类非业务容器。 return [service for service in flatten_services(load_hosts()) if service.get("kind") in {"java", "golang"}] def find_service_record(service_name: str, host_name: str) -> dict[str, Any]: # 日志查看必须定位到单实例,避免多机部署时把不同节点的日志混到一起。 normalized_service = str(service_name or "").strip() normalized_host = str(host_name or "").strip() for service in loggable_service_records(): if service.get("service") == normalized_service and service.get("host") == normalized_host: return service raise KeyError(f"service instance not found: {normalized_service}@{normalized_host}") def normalize_export_lines(raw_value: str | int | None) -> int: # 导出条数允许用户自定义,但要限制上限,避免一次把过多日志塞进页面进程。 if raw_value in {None, ""}: return SERVICE_LOG_PREVIEW_LINES value = int(raw_value) if value <= 0: raise ValueError("lines must be greater than 0") return min(value, SERVICE_LOG_EXPORT_MAX_LINES) def fetch_service_log_text(service_name: str, host_name: str, lines: int) -> tuple[dict[str, Any], str]: # 统一执行 docker logs,并返回实例元数据给接口层复用。 service = find_service_record(service_name, host_name) container = container_name(service["host"], service["service"]) script = f"""set -eu docker logs --timestamps --tail {int(lines)} {shlex.quote(container)} 2>&1 """ result = run_host_script( service, script, command_name=f"service-log-{service['host']}-{service['service']}", timeout_seconds=120, ) if result.get("status") != "SUCCESS": raise RuntimeError(str(result.get("output") or result.get("status") or "docker logs failed").strip()) return ( { "service": service["service"], "host": service["host"], "ip": service["ip"], "kind": service["kind"], "container": container, "updatedAt": utc_now(), "lines": int(lines), }, str(result.get("output") or ""), ) def get_service_log_preview_payload(service_name: str, host_name: str) -> dict[str, Any]: # 页面预览固定最近 1000 条,避免日志面板过重。 meta, content = fetch_service_log_text(service_name, host_name, SERVICE_LOG_PREVIEW_LINES) return { "ok": True, **meta, "content": content, "previewLines": SERVICE_LOG_PREVIEW_LINES, "exportMaxLines": SERVICE_LOG_EXPORT_MAX_LINES, } def get_service_log_export_payload(service_name: str, host_name: str, raw_lines: str | int | None) -> tuple[dict[str, Any], str]: # 导出接口允许指定条数,但会按统一上限裁剪。 lines = normalize_export_lines(raw_lines) meta, content = fetch_service_log_text(service_name, host_name, lines) return meta, content