diff --git a/docker-compose.yml b/docker-compose.yml index 498f085..bcd8db6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -151,10 +151,19 @@ services: - MESHSAFE_ENV=prod - MESHSAFE_DB_URL=postgresql+psycopg2://meshsafe:${MESHSAFE_DB_PASSWORD:-meshsafe}@postgres:5432/meshsafe - MESHSAFE_MASTER_KEY=${MESHSAFE_MASTER_KEY:?MASTER_KEY_REQUIRED} + # cron 不做控制台认证,但 Settings.admin_password 是必填字段,缺它 get_settings() + # 就抛 ValidationError,归档与 VACUUM 全崩。给占位值,不把真实口令复制进 cron + # 容器环境(docker inspect 可见)。 + - MESHSAFE_ADMIN_PASSWORD=cron-unused-placeholder volumes: - ./data/logs:/var/log/meshsafe # 归档日志共享目录 - ./scripts:/app/scripts:ro # archive_old_tasks.py + cron_entrypoint.py entrypoint: ["python3", "/app/scripts/cron_entrypoint.py"] + # 镜像层的 HEALTHCHECK 探测 localhost:8443(API 服务端口),cron 容器不监听该端口, + # 因此状态恒为 unhealthy —— 真正调度器挂掉时这个信号完全失效。关掉它, + # 存活与否由 restart: unless-stopped + 日志判断。 + healthcheck: + disable: true # ---------- Nginx(反向代理,内网 HTTP 明文)---------- # 外部端口 44380 → 内部 80 → api:8443 diff --git a/scripts/cron_entrypoint.py b/scripts/cron_entrypoint.py index 92850cc..1b64ba2 100644 --- a/scripts/cron_entrypoint.py +++ b/scripts/cron_entrypoint.py @@ -25,6 +25,20 @@ logging.basicConfig( log = logging.getLogger("scheduler") +_SECRET_ENV_KEYS = ("MESHSAFE_DB_URL", "MESHSAFE_MASTER_KEY", "MESHSAFE_ADMIN_PASSWORD", + "MESHSAFE_API_KEY", "MESHSAFE_API_CLIENTS") + + +def _redact(text: str) -> str: + """崩溃日志里 pydantic 会把整个 settings dict(含 DB 口令)原样打出来,落盘前抹掉。""" + out = text + for key in _SECRET_ENV_KEYS: + val = os.environ.get(key, "") + if val and len(val) >= 8: + out = out.replace(val, "***") + return out + + def run(cmd: list[str], label: str) -> None: """跑一个命令,捕获输出并记日志。""" log.info(f"[{label}] 开始执行: {' '.join(cmd)}") @@ -32,10 +46,10 @@ def run(cmd: list[str], label: str) -> None: proc = subprocess.run(cmd, capture_output=True, text=True, timeout=1800) if proc.stdout: for line in proc.stdout.strip().splitlines(): - log.info(f"[{label}] {line}") + log.info(f"[{label}] {_redact(line)}") if proc.stderr: for line in proc.stderr.strip().splitlines(): - log.warning(f"[{label}] {line}") + log.warning(f"[{label}] {_redact(line)}") if proc.returncode == 0: log.info(f"[{label}] ✅ 完成") else: @@ -47,7 +61,11 @@ def run(cmd: list[str], label: str) -> None: def run_vacuum() -> None: - """VACUUM ANALYZE — 维护表统计信息。""" + """VACUUM ANALYZE — 维护表统计信息。 + + VACUUM 不能在事务块里跑(psycopg2 会抛 ActiveSqlTransaction), + 所以用 AUTOCOMMIT 连接而不是 engine.begin()。 + """ try: from app.db import engine from sqlalchemy import text @@ -57,13 +75,14 @@ def run_vacuum() -> None: "scan_findings", "idempotency_records", "policy_versions", "audit_logs", ] - with engine.begin() as conn: + with engine.connect() as raw: + conn = raw.execution_options(isolation_level="AUTOCOMMIT") for tbl in tables: log.info(f"[VACUUM] {tbl} ...") conn.execute(text(f"VACUUM ANALYZE {tbl}")) log.info("[VACUUM] ✅ 全部完成") except Exception as e: - log.error(f"[VACUUM] ❌ 异常: {e}") + log.error(f"[VACUUM] ❌ 异常: {_redact(str(e))}") def seconds_until(target_hour: int, target_minute: int, weekday: int | None = None) -> float: