ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

[部署篇28] 搭建OpenCode日志监控与性能告警系统:TaoToken统一Key接入Prometheus与Grafana

[部署篇28] 搭建OpenCode日志监控与性能告警系统:TaoToken统一Key接入Prometheus与Grafana 1. 为什么 OpenCode 上线后还需要一套监控告警OpenCode 这类 AI 问答服务有个特点它不像静态网站那样“要么能开要么打不开”它更多时候是“看起来能用但已经在悄悄变慢、悄悄报错、悄悄烧 Token”。用户不会主动告诉你 95 分位响应时间从 1.2 秒涨到了 6 秒他们只会默默关掉页面。等你发现的时候往往已经是磁盘写满、容器反复重启、LLM API 大面积超时。所以这一篇要解决的核心问题是给已经部署好的 OpenCode 服务装上“眼睛和耳朵”。具体来说就是把日志变成结构化数据、把运行状态变成可抓取的指标、把异常变成能主动推送的告警。做完之后你能在 Grafana 面板上实时看到问答量、响应时间、Token 消耗、API 错误率也能在服务挂掉 1 分钟内收到通知。适合谁看已经把 OpenCode 跑在服务器上、配好了反向代理但还停留在docker logs肉眼看日志阶段的同学。如果你还没部署建议先把服务跑起来再回来接监控否则没有指标可采。整条链路我按“日志 → 指标 → 抓取 → 可视化 → 告警”串起来中间会用到 TaoToken 的统一 Key 通道来管理 AI 工具链的调用凭证这样监控里涉及的模型调用错误率、Token 消耗才有统一的来源口径。下面从接入准备开始。2. 用 TaoToken 统一 Key 打通 OpenCode 的 AI 调用通道OpenCode 在运行时会调用大模型接口如果每个环境、每个工具各配一套 Key监控里根本没法归因——你看到错误率飙升却不知道是哪个 Key 被限流了。TaoToken 的作用就是把这些调用收敛到一个统一入口OpenCode 只认一个 API 地址和一个 Key日志和指标里的provider、model标签才有意义。接入本身不复杂关键是拿到 Key 之后怎么在 OpenCode 的配置里落地。我一般分三步先在控制台建 Key再写进 OpenCode 的config.toml最后用一次最小请求验证通道是通的。第一步打开控制台创建 API Key。地址是https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite登录后在 API Keys 页面新建一个复制出来先存到密码管理器里页面刷新后就看不全了。第二步把 Key 写进 OpenCode 的配置文件。OpenCode 的模型通道配置放在config.toml里核心是base_url指向 TaoToken 的 API 地址https://taotoken.net/api注意这个地址不带任何查询参数。下面是我实际在用的骨架# ~/.config/opencode/config.toml # OpenCode 模型通道配置统一走 TaoToken [provider.taotoken] name taotoken base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 从环境变量读取避免明文写进文件 wire_api chat [model.default] provider taotoken model claude-sonnet-4-20250514 max_tokens 4096 temperature 0.3这里我特意用api_key_env而不是直接把 Key 写进文件因为后面 Docker 容器里会用环境变量注入配置文件可以进 Git 仓库而不泄露凭证。环境变量这样设# 写入 shell 配置或者 Docker 的 env_file export TAOTOKEN_API_KEYsk-你的key第三步验证通道。不用启动整个 OpenCode先用 curl 打一次最小请求确认 Key 和地址都对curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: ping}], max_tokens: 16 } | head -c 400返回里能看到choices字段就说明通道通了。这一步很关键因为后面 Prometheus 抓到的opencode_api_errors_total指标如果通道本身没通错误率会一直是 100%你会误以为是监控配错了。注意base_url只写到/api不要自己拼/v1OpenCode 和 SDK 会按wire_api自动补路径。我踩过的坑就是手动写成/api/v1结果请求变成/api/v1/v1/chat/completions一直 404。Key 通道打通后OpenCode 的所有模型调用都会经过这一个入口日志里记录的providertaotoken、modelxxx就能和 TaoToken 控制台的用量对上了。接下来进入正题让 OpenCode 输出结构化日志。3. 可复制的日志与指标配置骨架监控的地基是日志格式。纯文本日志[INFO] 处理完成这种采集工具没法按字段过滤你也没法统计“平均响应时间”。所以第一步是把 OpenCode 的日志改成 JSON 行输出每行一个完整对象。3.1 结构化日志settings.json 与 logger 改造OpenCode 的运行时设置放在settings.json里我在这里控制日志级别和输出格式。下面这份是我在用的骨架生产环境开json本地开发开pretty{ log: { level: info, format: json, service_name: opencode-qa-bot, output: stdout, file: { enabled: true, dir: /app/logs, rotate_daily: true, max_days: 7 } }, metrics: { enabled: true, path: /api/metrics, collect_default: true }, server: { host: 0.0.0.0, port: 3000 } }format: json让每条日志变成一行 JSONservice_name会作为固定字段写进每条日志方便 Loki 或 Filebeat 按服务聚合。file.enabled打开后日志同时落盘Docker 日志驱动挂了也不丢。日志工具本身要支持结构化字段。核心逻辑是把timestamp、level、service、message作为固定字段业务字段sessionId、duration、intent作为扩展字段平铺进去。这样在 Grafana 里可以直接{serviceopencode-qa-bot} | json | duration 3000过滤慢请求。// src/utils/logger.ts export type LogLevel debug | info | warn | error export interface LogEntry { timestamp: string level: LogLevel service: string message: string [key: string]: unknown // 业务字段平铺 } const SERVICE_NAME process.env.SERVICE_NAME || opencode-qa-bot const JSON_FORMAT process.env.NODE_ENV production function emit(level: LogLevel, message: string, meta?: Recordstring, unknown) { const entry: LogEntry { timestamp: new Date().toISOString(), level, service: SERVICE_NAME, message, ...meta, } if (JSON_FORMAT) { process.stdout.write(JSON.stringify(entry) \n) } else { console.log([${entry.timestamp}] ${level.toUpperCase()} ${message}, meta ?? ) } } export const log { debug: (m: string, meta?: Recordstring, unknown) emit(debug, m, meta), info: (m: string, meta?: Recordstring, unknown) emit(info, m, meta), warn: (m: string, meta?: Recordstring, unknown) emit(warn, m, meta), error: (m: string, err?: Error, meta?: Recordstring, unknown) emit(error, m, { ...meta, error: err?.message, stack: err?.stack }), }在问答主流程里埋点记录耗时和意图// src/core/bot.ts async processUserInput(input: string) { const start Date.now() const sessionId await this.getCurrentSessionId() log.info(开始处理用户输入, { sessionId, inputLength: input.length }) try { const result await this.doProcess(input) log.info(处理完成, { sessionId, duration: Date.now() - start, intent: result.intent?.type, }) return result } catch (err) { log.error(处理失败, err as Error, { sessionId, duration: Date.now() - start }) throw err } }重启后docker logs opencode-qa-bot --tail 5应该看到类似这样的行{timestamp:2025-01-15T10:30:00.000Z,level:info,service:opencode-qa-bot,message:处理完成,sessionId:s-8f2a,duration:245,intent:faq}3.2 暴露 Prometheus 指标端点日志解决“发生了什么”指标解决“现在健康吗”。OpenCode 用prom-client暴露一个/api/metrics端点Prometheus 定时来抓。指标设计上我分四类问答计数、响应时间直方图、Token 消耗、API 错误。// src/services/metrics.ts import client from prom-client const register new client.Registry() client.collectDefaultMetrics({ register }) // CPU、内存、GC 等 export const questionCounter new client.Counter({ name: opencode_qa_total, help: 问答总数, labelNames: [intent, status] as const, registers: [register], }) export const responseDuration new client.Histogram({ name: opencode_qa_duration_seconds, help: 问答响应时间, labelNames: [intent] as const, buckets: [0.1, 0.5, 1, 2, 5, 10, 30], registers: [register], }) export const tokenCounter new client.Counter({ name: opencode_tokens_total, help: Token 消耗总量, labelNames: [model, type] as const, // type: input/output registers: [register], }) export const apiErrorCounter new client.Counter({ name: opencode_api_errors_total, help: LLM API 错误数, labelNames: [provider, error_type] as const, registers: [register], }) export async function metricsHandler(_req: unknown, reply: any) { reply.header(Content-Type, register.contentType) reply.send(await register.metrics()) }在 Web 服务里挂上路由// src/services/web-server.ts this.fastify.get(/api/metrics, metricsHandler)访问http://localhost:3000/api/metrics能看到opencode_qa_total、opencode_qa_duration_seconds_bucket这些行说明指标端点就绪。这一步做完OpenCode 自己该做的就做完了剩下交给 Prometheus。4. Prometheus 抓取与 Grafana 告警规则落地监控栈我用 Docker 起三个容器Prometheus 负责抓取和评估告警规则Node Exporter 采宿主机指标cAdvisor 采容器指标。Grafana 单独一个容器做可视化。4.1 Prometheus 抓取配置先写prometheus.yml把 OpenCode 的指标端点、Node Exporter、cAdvisor 都加进去# /opt/prometheus/prometheus.yml global: scrape_interval: 15s evaluation_interval: 15s rule_files: - /etc/prometheus/alerts.yml scrape_configs: - job_name: opencode-qa-bot metrics_path: /api/metrics static_configs: - targets: [host.docker.internal:3000] # 容器访问宿主机服务 - job_name: node-exporter static_configs: - targets: [host.docker.internal:9100] - job_name: cadvisor static_configs: - targets: [host.docker.internal:8080]启动三个采集容器docker run -d --name prometheus --restart always \ -p 9090:9090 \ -v /opt/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \ -v /opt/prometheus/alerts.yml:/etc/prometheus/alerts.yml \ prom/prometheus docker run -d --name node-exporter --restart always \ --nethost --pidhost \ -v /:/host:ro,rslave \ prom/node-exporter --path.rootfs/host docker run -d --name cadvisor --restart always \ -p 8080:8080 \ -v /:/rootfs:ro -v /var/run:/var/run:ro \ -v /sys:/sys:ro -v /var/lib/docker/:/var/lib/docker:ro \ google/cadvisor:latest打开http://服务器IP:9090/targets三个 job 的 State 都应该是 UP。如果opencode-qa-bot是 DOWN先确认 OpenCode 容器和 Prometheus 容器能不能互相访问——这是最常见的网络问题。4.2 告警规则文件告警规则写在alerts.yml里我配了五条覆盖服务存活、错误率、响应时间、CPU、磁盘# /opt/prometheus/alerts.yml groups: - name: opencode_alerts interval: 30s rules: - alert: OpenCodeServiceDown expr: up{jobopencode-qa-bot} 0 for: 1m labels: severity: critical annotations: summary: OpenCode 服务不可用 description: 服务已下线超过 1 分钟请立即检查容器状态。 - alert: HighErrorRate expr: | rate(opencode_qa_total{statuserror}[5m]) / rate(opencode_qa_total[5m]) 0.1 for: 5m labels: severity: warning annotations: summary: 问答错误率超过 10% description: 过去 5 分钟错误率为 {{ $value | humanizePercentage }} - alert: SlowResponse expr: | histogram_quantile(0.95, rate(opencode_qa_duration_seconds_bucket[5m])) 5 for: 5m labels: severity: warning annotations: summary: 95 分位响应时间超过 5 秒 - alert: HighCPUUsage expr: 100 - (avg(rate(node_cpu_seconds_total{modeidle}[5m])) * 100) 80 for: 10m labels: severity: warning annotations: summary: CPU 使用率超过 80% - alert: LowDiskSpace expr: (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100 85 for: 5m labels: severity: warning annotations: summary: 磁盘使用率超过 85%for字段是防误报的关键。OpenCodeServiceDown用 1 分钟因为容器重启可能只要几秒错误率和响应时间用 5 分钟避免一次偶发超时就告警。4.3 Grafana 数据源与面板启动 Grafanadocker run -d --name grafana --restart always \ -p 3001:3000 \ -v grafana-data:/var/lib/grafana \ grafana/grafana访问http://服务器IP:3001默认admin/admin。添加数据源时 URL 填http://prometheus:9090如果 Grafana 和 Prometheus 在同一 Docker 网络或者http://宿主机IP:9090。保存后点 “Save test”出现绿色提示才算通。面板我建议先建四个核心的面板PromQL用途每分钟问答数rate(opencode_qa_total[5m]) * 60看流量趋势95 分位响应时间histogram_quantile(0.95, rate(opencode_qa_duration_seconds_bucket[5m]))看慢请求API 错误率rate(opencode_api_errors_total[5m])看模型通道健康Token 消耗速率rate(opencode_tokens_total[5m])看成本4.4 告警通知接入Prometheus 自己只负责评估规则发通知要靠 Alertmanager。写一份最小配置用 Webhook 推送到飞书或钉钉机器人# /opt/prometheus/alertmanager.yml route: group_by: [alertname] group_wait: 30s group_interval: 5m repeat_interval: 4h receiver: webhook receivers: - name: webhook webhook_configs: - url: https://your-webhook-url # 替换为飞书/钉钉机器人地址 send_resolved: true启动 Alertmanagerdocker run -d --name alertmanager --restart always \ -p 9093:9093 \ -v /opt/prometheus/alertmanager.yml:/etc/alertmanager/alertmanager.yml \ prom/alertmanager然后在prometheus.yml里加上 Alertmanager 地址alerting: alertmanagers: - static_configs: - targets: [host.docker.internal:9093]重启 Prometheus 后告警规则生效。5. 验证请求与告警触发实测配置写完不算完必须验证两件事指标端点真的被抓到了告警真的能触发通知。先验证指标抓取。在 Prometheus 的 Graph 页面输入opencode_qa_total点 Execute如果能看到时间序列说明抓取成功。如果为空去 Targets 页面看opencode-qa-bot的状态和 Last Error。再验证告警触发。最直接的办法是手动停掉 OpenCode 容器模拟服务下线docker stop opencode-qa-bot等 1 分钟后打开http://服务器IP:9090/alerts应该看到OpenCodeServiceDown从 Inactive 变成 Pending再变成 Firing。同时 Alertmanager 的http://服务器IP:9093页面会出现这条告警Webhook 应该收到通知。验证完记得把容器拉起来docker start opencode-qa-bot告警恢复后Alertmanager 会发一条 resolved 通知因为配了send_resolved: true。如果想验证业务指标告警可以临时把HighErrorRate的阈值从 0.1 改成 0.001然后制造几次失败请求观察告警是否触发。验证完改回原值。提示测试告警时不要直接改生产规则文件可以复制一份alerts-test.yml单独加载避免误改。6. 本篇常见报错排查6.1 Prometheus 抓不到 OpenCode 指标报错长这样Get http://host.docker.internal:3000/api/metrics: dial tcp: connect: connection refused原因通常是 Prometheus 容器访问不到宿主机上的 OpenCode 服务。分三步查先确认 OpenCode 容器在跑docker ps | grep opencode再确认端口映射docker port opencode-qa-bot最后在 Prometheus 容器里手动 curl 一次docker exec -it prometheus wget -qO- http://host.docker.internal:3000/api/metrics | head如果容器里host.docker.internal解析不了Linux 上默认没有改用宿主机在 Docker 网桥上的 IP一般是172.17.0.1。或者干脆把 Prometheus 也加到--nethost直接用localhost:3000。6.2 Grafana 数据源连不上 Prometheus报错Data source connected, but no labels found.这通常是 URL 写错了。Grafana 容器里访问 Prometheus如果两者不在同一网络http://prometheus:9090解析不了。解决办法是把两个容器放到同一个自定义网络docker network create monitor-net docker network connect monitor-net prometheus docker network connect monitor-net grafana然后在 Grafana 数据源里用http://prometheus:9090。或者简单点直接用宿主机 IPhttp://192.168.x.x:9090。6.3 告警规则一直不触发指标明明超了阈值Alerts 页面却一直是 Inactive。先看evaluation_interval是不是设得太长默认 15s 没问题。再看for时间如果设成10m你得等满 10 分钟才会 Firing。最后检查表达式本身在 Graph 页面把expr里的 PromQL 贴进去执行一次看返回值是不是真的超过阈值。还有一个隐蔽的坑rate()函数需要至少两个采样点如果指标刚暴露不久rate会返回空告警自然不触发。等几分钟再观察。6.4 日志里中文乱码JSON 日志里的中文在某些采集链路里会变成\uXXXX。这是正常的 JSON 转义Loki 和 Grafana 解析后会自动还原。如果落盘文件里也是转义形式检查写入时有没有用Buffer.from(str, utf8)。一般不用管查询时能正常显示。7. 把监控接进日常下一步怎么走到这里OpenCode 的日志、指标、告警三条链路都通了。你可以在 Grafana 上看到实时问答量在 Prometheus 上看到告警状态在飞书里收到服务下线的通知。这套东西的价值不在于“配好了”而在于“用起来”——建议每天花两分钟扫一眼面板看看响应时间有没有缓慢上涨、Token 消耗有没有异常波动。如果你还想把模型调用侧的监控做得更细比如按模型维度看错误率、按 Key 看用量可以到模型对话页面手动发几次请求对照 TaoToken 控制台的用量记录确认指标口径一致https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite。长期跑编码类 Agent 的话Coding Plan 里可以统一管理多个工具的调用配额监控里的provider标签也能对上https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteAPI Keys 管理在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite。下一篇会把部署流程自动化从代码提交到容器更新一条流水线跑完监控告警也会作为流水线的一个验证环节接进去。
返回列表