Codex 日志异常怎么提前发现?本地 AI 工具观测面板 + cpolar 远程验收实战

Codex 日志异常观测面板与 cpolar 远程验收链路封面图

Codex 这类本地 AI 编程工具一旦日志写入失控,最麻烦的地方不是“报错很红”,而是它常常躲在后台慢慢写盘。等磁盘告警、项目卡顿、SSD 写入量异常再回头查,已经晚了一拍。

这篇不聊泛泛的“AI 工具治理”,直接做一个能跑起来的小面板:本机扫描 Codex 相关日志,统计文件大小、更新时间、异常关键词命中数,再用 cpolar 临时生成 HTTPS 地址,让同事或负责人远程验收排查结果。面板只读,日志只展示脱敏摘要,验收结束关闭隧道。

1 什么是 Codex 日志异常观测面板?

这里说的观测面板不是完整 APM,也不是把日志全部搬到云端。它只负责三件事:看日志有没有突然变大、看最近写入是否过于频繁、看异常关键词是否集中出现。

对个人开发者和小团队来说,这种轻量面板足够挡住一批低级事故。比如某个 AI 工具后台不断重试、插件反复写错误栈、会话日志每天膨胀到几 GB,面板会比肉眼翻目录更早暴露问题。

这篇采用 Python 标准库实现,不强绑数据库和复杂监控系统。你只要能在本机跑 Python,就能先把第一版审计页搭起来。

我建议先从“看得见”开始,而不是一上来就做自动清理。日志异常真正危险的地方,是没人知道它从哪天开始变大、哪个文件在反复写、错误关键词集中在哪一段。先把这些证据摆到页面上,后面再讨论清理策略、告警渠道和团队流程,会稳很多。

Codex 本地日志观测面板展示总大小、更新时间和异常命中数

这张图后续放本地面板首页,重点看日志总大小、最近更新时间和异常命中数。看到数字以后,排查就从“凭感觉”变成“看证据”。

2 环境准备:确认 Python、日志目录和 cpolar

先确认本机 Python 版本。macOS 和 Linux 都可以执行下面命令:

python3 --version

建议使用 Python 3.10 及以上版本。本文脚本只使用 pathlibjsonhttp.serverhtml 等标准库,不需要额外安装依赖。

再找 Codex 相关日志或配置目录。不同安装方式的日志位置会受运行环境影响,所以这里用搜索命令先把候选文件列出来:

find "$HOME" -maxdepth 5 \
  \( -path '*/.codex/*' -o -iname '*codex*.log' -o -iname '*codex*.jsonl' \) \
  -type f -print 2>/dev/null | head -50

如果输出里已经能看到 .codex 目录下的日志、会话文件或 jsonl 文件,就把目录记下来。这里别急着扫整个 home 目录,范围太大时会把浏览器缓存、包管理缓存一起卷进来,后面排错反而更乱。

本文示例把待观测目录放到环境变量里:

export CODEX_LOG_DIR="$HOME/.codex"

如果你的日志在别的位置,把右侧路径替换成搜索结果里的真实目录。

cpolar 用在后半段远程验收。本机还没安装时,macOS 可以用 Homebrew:

brew tap probezy/core && brew install cpolar
sudo cpolar service install
sudo cpolar service start

Linux 可以使用官方一键安装脚本:

curl -L https://www.cpolar.com/static/downloads/install-release-cpolar.sh | sudo bash

安装后打开本地控制台:

curl -s http://127.0.0.1:9200 >/dev/null && echo "cpolar web ui is ready"

如果这里没有输出 ready,先检查 cpolar 服务是否启动,再打开 http://127.0.0.1:9200 登录账号。命令行环境下也可以用 cpolar authtoken xxx 绑定账号。

3 编写本地只读观测脚本

新建一个目录放脚本和运行文件:

mkdir -p ~/codex-observe
cd ~/codex-observe

写入 observe_codex.py

cat > observe_codex.py <<'PY'
import html
import json
import os
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

LOG_DIR = Path(os.environ.get("CODEX_LOG_DIR", str(Path.home() / ".codex"))).expanduser()
HOST = os.environ.get("OBSERVE_HOST", "127.0.0.1")
PORT = int(os.environ.get("OBSERVE_PORT", "8765"))
KEYWORDS = ("error", "exception", "traceback", "panic", "failed", "denied", "timeout")
MAX_FILES = 200
TAIL_BYTES = 4096


def safe_read_tail(path: Path) -> str:
    with path.open("rb") as f:
        size = path.stat().st_size
        f.seek(max(0, size - TAIL_BYTES))
        return f.read().decode("utf-8", errors="replace")


def collect():
    rows = []
    total_size = 0
    files = [p for p in LOG_DIR.rglob("*") if p.is_file()]
    files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
    for path in files[:MAX_FILES]:
        stat = path.stat()
        total_size += stat.st_size
        tail = safe_read_tail(path).lower()
        hits = sum(tail.count(word) for word in KEYWORDS)
        rows.append({
            "path": str(path.relative_to(LOG_DIR)),
            "size_mb": round(stat.st_size / 1024 / 1024, 2),
            "mtime": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(stat.st_mtime)),
            "hits": hits,
            "sample": html.escape(safe_read_tail(path)[-500:]),
        })
    return {
        "log_dir": str(LOG_DIR),
        "file_count": len(files),
        "scanned_files": len(rows),
        "total_size_mb": round(total_size / 1024 / 1024, 2),
        "rows": rows,
    }


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        data = collect()
        if self.path == "/json":
            body = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return

        rows = "".join(
            f"<tr><td>{html.escape(r['path'])}</td><td>{r['size_mb']}</td>"
            f"<td>{r['mtime']}</td><td>{r['hits']}</td><td><pre>{r['sample']}</pre></td></tr>"
            for r in data["rows"]
        )
        body = f"""<!doctype html><meta charset='utf-8'>
        <title>Codex Log Observe</title>
        <style>body{{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;margin:24px}}
        table{{border-collapse:collapse;width:100%}}td,th{{border:1px solid #ddd;padding:8px;vertical-align:top}}
        pre{{white-space:pre-wrap;max-height:120px;overflow:auto;margin:0}}</style>
        <h1>Codex Log Observe</h1>
        <p>目录:{html.escape(data['log_dir'])}</p>
        <p>文件数:{data['file_count']},扫描:{data['scanned_files']},总大小:{data['total_size_mb']} MB</p>
        <table><tr><th>文件</th><th>大小 MB</th><th>更新时间</th><th>异常命中</th><th>尾部摘要</th></tr>{rows}</table>
        """.encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


if __name__ == "__main__":
    if not LOG_DIR.exists():
        raise SystemExit(f"log dir not found: {LOG_DIR}")
    server = ThreadingHTTPServer((HOST, PORT), Handler)
    print(f"Serving http://{HOST}:{PORT}, log_dir={LOG_DIR}")
    server.serve_forever()
PY

这里有两个关键提醒。

一是默认只监听 127.0.0.1,不会直接在局域网裸露。二是页面只读,不提供删除、清空、下载原始日志的按钮,给别人验收时风险更低。

4 启动面板并做本机验收

启动脚本:

cd ~/codex-observe
export CODEX_LOG_DIR="$HOME/.codex"
python3 observe_codex.py

终端看到下面这类输出,就说明本地服务已经起来:

Serving http://127.0.0.1:8765, log_dir=/Users/yourname/.codex

另开一个终端做接口检查:

curl -s http://127.0.0.1:8765/json | python3 -m json.tool | head -40

页面检查可以直接打开:

open http://127.0.0.1:8765

Linux 桌面环境用:

xdg-open http://127.0.0.1:8765

如果页面显示文件数为 0,先回到第 2 节重新确认 CODEX_LOG_DIR。如果脚本启动时报 log dir not found,说明环境变量指向的目录不存在,直接换成搜索命令找到的真实目录。

Codex 日志异常关键词命中列表和脱敏尾部摘要

这张图后续放异常命中列表。重点不是把每一行日志都贴出来,而是让验收人看到哪些文件在持续写入、哪些文件尾部出现了错误关键词。

5 给异常设置简单判定线

面板已经能看数据,但日常使用还需要一条明确判定线。最简单的方式是加一个 shell 检查脚本,超过阈值时直接返回非 0 状态,方便接入本地定时任务。

cat > check_codex_logs.sh <<'SH'
#!/usr/bin/env bash
set -euo pipefail

LOG_DIR="${CODEX_LOG_DIR:-$HOME/.codex}"
MAX_TOTAL_MB="${MAX_TOTAL_MB:-1024}"
MAX_SINGLE_MB="${MAX_SINGLE_MB:-200}"

total_kb=$(du -sk "$LOG_DIR" | awk '{print $1}')
total_mb=$((total_kb / 1024))

echo "log_dir=$LOG_DIR total_mb=$total_mb max_total_mb=$MAX_TOTAL_MB"

if [ "$total_mb" -gt "$MAX_TOTAL_MB" ]; then
  echo "ALERT: total log size is over limit"
  exit 2
fi

large_file=$(find "$LOG_DIR" -type f -size +"${MAX_SINGLE_MB}"M -print -quit 2>/dev/null)
if [ -n "$large_file" ]; then
  echo "ALERT: single log file is over limit: $large_file"
  exit 3
fi

echo "OK: codex log size is under limit"
SH

chmod +x check_codex_logs.sh
./check_codex_logs.sh

默认阈值是总日志 1024 MB、单文件 200 MB。这个值适合个人电脑先跑起来,再按团队机器的磁盘容量调整。这里别把阈值设得太小,否则正常会话也会被报成异常。

要看最近 24 小时内被改动的文件,可以加一条命令:

find "$CODEX_LOG_DIR" -type f -mtime -1 -printf '%TY-%Tm-%Td %TH:%TM %s %p\n' 2>/dev/null | sort | tail -30

macOS 默认 find 没有 -printf,用下面这条:

find "$CODEX_LOG_DIR" -type f -mtime -1 -exec stat -f '%Sm %z %N' -t '%Y-%m-%d %H:%M:%S' {} \; 2>/dev/null | sort | tail -30

这一步不是为了追求复杂告警,而是给“异常”一个可复核的标准。以后复盘时,谁都能看懂触发条件。

如果你在公司电脑上跑,建议把阈值写进团队文档里。比如个人机器用 1024 MB,构建机用 4096 MB,验收机按磁盘剩余空间单独设置。标准写清楚以后,排查时就不用反复解释“这算不算异常”。

6 用 cpolar 临时开放只读面板

本机确认没问题后,再把面板给同事远程验收。这里适合用 HTTP 隧道,目标端口是 8765

cpolar http 8765

命令运行后,终端会显示 cpolar 分配的公网访问地址。把 HTTPS 地址发给验收人即可,别把本机日志目录、账号 token、原始日志文件打包外发。

也可以在 http://127.0.0.1:9200 的 Web UI 里创建隧道:协议选择 http,本地地址填 8765,域名类型按当前套餐选择。免费随机公网地址 24 小时内会变化,用来做临时验收正合适;需要固定二级子域名时,基础套餐或以上支持。

cpolar HTTPS 隧道把本地只读观测面板开放给远程验收

这张图后续放 cpolar 在线隧道列表,重点标出本地端口 8765 和生成的 HTTPS 地址。验收人能打开页面,就说明“本地采集、面板展示、远程访问”三段链路都通了。

安全上建议按这几条执行:

  • 面板只放摘要,不展示完整敏感日志。
  • cpolar 隧道只在验收窗口开启,用完在终端按 Ctrl+C 关闭。
  • 需要多人长期访问时,再加访问口令、反向代理鉴权或固定域名。

如果公网地址打不开,按顺序查三处:本机 http://127.0.0.1:8765 能否打开,cpolar Web UI 里隧道是否在线,终端里显示的公网地址是否复制完整。

7 把检查脚本接入日常巡检

面板适合查看现场,巡检脚本适合日常兜底。macOS 可以用 crontab 每小时检查一次:

crontab -e

加入这一行:

0 * * * * CODEX_LOG_DIR="$HOME/.codex" MAX_TOTAL_MB=1024 MAX_SINGLE_MB=200 "$HOME/codex-observe/check_codex_logs.sh" >> "$HOME/codex-observe/check.log" 2>&1

Linux 也可以用同样的 crontab。执行后查看最近记录:

tail -50 ~/codex-observe/check.log

如果团队已经有 Prometheus、Grafana 或日志平台,就把这个脚本当成最小探针:先用它把异常标准跑顺,再决定是否接入更正式的监控系统。别一上来就堆组件,日志异常排查最怕工具很多、证据很少。

8 总结

现在我们已经搭好了一条很轻的 Codex 日志观测链路:本机脚本扫描日志目录,页面展示大小、更新时间和异常关键词,shell 脚本给出阈值判断,cpolar 负责把只读面板临时发给远程验收人。

  • 本地排查先确认 CODEX_LOG_DIR,再启动 python3 observe_codex.py
  • 日常巡检用 check_codex_logs.sh,把总大小和单文件大小变成明确阈值。
  • 远程验收用 cpolar http 8765,只开放摘要页,验收结束立即关闭隧道。

这套方案的价值不在于“监控大而全”,而在于把 AI 工具后台写日志这件事拉到可见处。等你能稳定看见日志增长、异常命中和远程验收结果,再把它接入团队已有告警系统,就不会从一堆黑盒现象里重新摸索。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐