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

Codex 这类本地 AI 编程工具一旦日志写入失控,最麻烦的地方不是“报错很红”,而是它常常躲在后台慢慢写盘。等磁盘告警、项目卡顿、SSD 写入量异常再回头查,已经晚了一拍。
这篇不聊泛泛的“AI 工具治理”,直接做一个能跑起来的小面板:本机扫描 Codex 相关日志,统计文件大小、更新时间、异常关键词命中数,再用 cpolar 临时生成 HTTPS 地址,让同事或负责人远程验收排查结果。面板只读,日志只展示脱敏摘要,验收结束关闭隧道。
1 什么是 Codex 日志异常观测面板?
这里说的观测面板不是完整 APM,也不是把日志全部搬到云端。它只负责三件事:看日志有没有突然变大、看最近写入是否过于频繁、看异常关键词是否集中出现。
对个人开发者和小团队来说,这种轻量面板足够挡住一批低级事故。比如某个 AI 工具后台不断重试、插件反复写错误栈、会话日志每天膨胀到几 GB,面板会比肉眼翻目录更早暴露问题。
这篇采用 Python 标准库实现,不强绑数据库和复杂监控系统。你只要能在本机跑 Python,就能先把第一版审计页搭起来。
我建议先从“看得见”开始,而不是一上来就做自动清理。日志异常真正危险的地方,是没人知道它从哪天开始变大、哪个文件在反复写、错误关键词集中在哪一段。先把这些证据摆到页面上,后面再讨论清理策略、告警渠道和团队流程,会稳很多。

这张图后续放本地面板首页,重点看日志总大小、最近更新时间和异常命中数。看到数字以后,排查就从“凭感觉”变成“看证据”。
2 环境准备:确认 Python、日志目录和 cpolar
先确认本机 Python 版本。macOS 和 Linux 都可以执行下面命令:
python3 --version
建议使用 Python 3.10 及以上版本。本文脚本只使用 pathlib、json、http.server、html 等标准库,不需要额外安装依赖。
再找 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,说明环境变量指向的目录不存在,直接换成搜索命令找到的真实目录。

这张图后续放异常命中列表。重点不是把每一行日志都贴出来,而是让验收人看到哪些文件在持续写入、哪些文件尾部出现了错误关键词。
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 在线隧道列表,重点标出本地端口 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 工具后台写日志这件事拉到可见处。等你能稳定看见日志增长、异常命中和远程验收结果,再把它接入团队已有告警系统,就不会从一堆黑盒现象里重新摸索。
更多推荐


所有评论(0)