1. 项目概述:一个为AI Agent量身打造的全栈日志监控系统
如果你正在开发或管理一个基于OpenClaw(或其他任何AI Agent框架)的智能体集群,那么你肯定遇到过这样的场景:Agent在后台默默运行,突然某个任务失败了,你只能去翻看服务器上零散的日志文件,或者更糟,Agent直接“失联”了,你完全不知道它卡在了哪一步。传统的日志方案,无论是console.log还是winston、pino,在应对AI Agent这种高并发、多会话、长流程且严重依赖外部API(如GPT、Claude)的应用时,显得力不从心。它们缺乏统一的视图、实时的聚合能力,以及对关键业务指标(如Token消耗、API成本、错误率)的洞察。
这正是openclaw-logging-system-nuxt-convex这个项目要解决的问题。它是一个专门为AI Agent设计的、开箱即用的实时日志与监控仪表盘。核心价值在于,它将分散的日志事件、会话数据、API调用和错误信息,通过一个现代化的全栈技术栈(Nuxt 4 + Convex)聚合起来,并以一个直观的Web仪表盘呈现给你。你可以把它理解为AI Agent领域的“Sentry”或“Datadog Lite”,但更轻量、更聚焦,并且与你的Agent代码无缝集成。
简单来说,这个系统能让你:
- 实时看到每个Agent在干什么,输出什么日志。
- 全局掌握所有会话的状态、耗时和资源消耗。
- 快速定位突发的错误和性能瓶颈。
- 精确核算不同AI模型的使用成本和效率。
它适合任何需要深度观测其AI Agent应用状态的开发者、运维人员或团队负责人。无论你是独立开发者管理着几个爬虫Agent,还是团队在运行一个复杂的自动化工作流系统,这个仪表盘都能显著提升你的可观测性和排错效率。
2. 技术栈选型背后的深度考量
选择Nuxt 4和Convex这套组合拳,绝非偶然,而是针对AI Agent监控场景的精准匹配。我们来拆解一下每个选择背后的“为什么”。
2.1 前端:为什么是Nuxt 4 + Nuxt UI?
前端需要快速构建一个数据密集、交互复杂的后台管理界面,同时要保证开发体验和性能。
- Nuxt 4 (Vue 3): 首先,Vue 3的Composition API非常适合处理这种状态复杂、逻辑可复用的仪表盘应用。相比Options API,Composition API能让我们把与日志流、图表数据、过滤条件相关的逻辑更好地封装成可复用的Composable。而Nuxt 4作为全栈框架,提供了开箱即用的文件路由、服务器端渲染(SSR)、自动导入等能力。对于监控仪表盘,SSR意味着首屏加载更快,对SEO也更友好(虽然后台系统对SEO要求不高,但快总是好的)。更重要的是,Nuxt的模块化生态系统,让我们能轻松集成UI库、图表库等。
- @nuxt/ui v4 (基于Reka UI): 自己从零搭建一套美观且功能完备的UI组件(表格、卡片、模态框、表单)是极其耗时且容易不一致的。
@nuxt/ui是Nuxt官方的UI套件,与框架深度集成,组件自动导入,样式基于Tailwind CSS v4,主题定制非常方便。选择它,意味着我们能用最少的时间,获得一套设计语言统一、响应式、可访问性良好的UI基础,从而把精力完全集中在业务逻辑(如日志渲染、图表配置)上。 - Chart.js + vue-chartjs: 监控离不开图表。Chart.js是久经考验的轻量级图表库,社区活跃,图表类型丰富。
vue-chartjs提供了Vue 3的封装,让我们能以Vue组件的方式使用Chart.js,数据响应式更新变得非常简单,非常适合需要实时刷新数据的监控图表。
实操心得:在早期原型阶段,我曾尝试过纯客户端渲染(CSR)和更轻量的UI库,但发现当日志数据量瞬间增大时,页面渲染会偶发卡顿。切换到Nuxt 4的SSR模式后,首屏的关键摘要信息(如今日错误数、活跃会话)由服务端直接渲染成HTML,体验明显更顺滑。
@nuxt/ui的UTable组件对大量日志行的虚拟滚动支持也更好,这是自己实现需要踩很多坑的细节。
2.2 后端与实时同步:为什么是Convex?
这是整个系统的“灵魂”所在。AI Agent的日志是典型的实时、高频、结构化的数据流。传统方案(如REST API + 自建数据库 + WebSocket)在开发效率和实时性上挑战很大。
- 真正的实时数据库:Convex首先是一个实时数据库。它内置了从数据库到前端的高效数据同步机制。当你的Agent通过
ConvexLogger写入一条日志时,这条记录会立刻出现在所有已打开仪表盘的浏览器中,无需你手动处理WebSocket连接、订阅和差分更新。这对于“日志流”视图是至关重要的体验。 - TypeScript全栈:Convex的后端函数(他们叫
queries和mutations)直接用TypeScript编写,并与前端共享类型定义。这意味着,你在convex/schema.ts中定义好数据库表结构后,前后端都能获得完全一致的、强类型的数据库模型。这极大地减少了前后端联调的沟通成本,也避免了因字段名拼写错误导致的bug。 - 无服务器函数集成:Convex的后端函数部署简单(一句
pnpm convex:deploy),并且自动处理了扩展性。我们写的logs.ts、sessions.ts等文件,每个都导出一些函数,这些函数就是我们的API。它们运行在Convex的托管环境中,我们无需关心服务器运维。 - 内置的调度与订阅模型:
cronRuns(监控定时任务)和rateLimits(速率限制事件)这类功能的实现,在Convex中非常自然。你可以用internal.cron来定义定时任务,其执行记录会自动存入数据库,并实时同步到前端。对于处理API速率限制,你可以在Agent的日志逻辑中触发一个rateLimits表的插入操作,仪表盘就能立刻告警。
对比传统方案:如果自己用Express + PostgreSQL + Socket.io来实现,你需要:1) 设计REST API;2) 设计数据库Schema并迁移;3) 搭建WebSocket服务处理房间订阅;4) 处理数据库变更的触发器以向WebSocket推送消息。而在Convex中,你只需要定义Schema和操作数据的函数,实时同步是免费的。
注意事项:Convex有免费额度,但对于日志量极大的生产环境,需要注意其读写单元(RU)和存储的消耗。建议在Agent端对日志进行适当的采样或聚合后再发送,特别是
DEBUG级别的详细日志。本项目中的ConvexLogger包装器已经考虑了分级控制,但你需要根据自身业务调整默认级别。
2.3 开发工具链:PNPM、Vitest与类型安全
- PNPM:相比npm/yarn,PNPM的磁盘空间效率和安装速度优势明显,尤其对于Monorepo项目。它严格的
node_modules结构也能更好地避免幽灵依赖问题。 - Vitest:作为Vite原生的测试框架,Vitest的启动和热更新速度极快,与Vue组件测试的集成体验很好。我们用它来测试前端的Composables和组件,以及Convex的后端函数(Convex函数本身也可以在本地开发服务器中运行和测试)。
- 全面的类型检查:
pnpm typecheck命令会运行TypeScript的全项目类型检查。在这样一个前后端深度依赖TypeScript的项目中,严格的类型检查是保证代码质量、减少运行时错误的最有力工具。务必在提交代码前运行它。
3. 系统核心模块深度解析与实操
这个日志系统的强大,源于其精心设计的六大核心数据模块。它们不仅仅是六张数据库表,更是对AI Agent运行状态进行建模的六种维度。
3.1 日志流 (logs表):事件的基石
这是最基础的表,记录每一条原始的日志事件。
// convex/schema.ts 节选 logs: defineTable({ // 关联到某个具体的会话 sessionKey: v.string(), // 日志级别:DEBUG, INFO, WARN, ERROR, CRITICAL level: v.string(), // 日志内容 message: v.string(), // 可选的JSON格式的额外上下文,如函数参数、中间结果 data: v.optional(v.any()), // 时间戳(Unix毫秒) timestamp: v.number(), // 产生日志的Agent或服务标识 source: v.string(), // 日志产生时的代码路径或模块名 module: v.optional(v.string()), })实操要点:
sessionKey的设计:这是链接日志与会话的纽带,格式为{agentId}-{date}-{shortId}。例如,email-analyzer-20240520-abc123。在Agent脚本中,需要在会话开始时生成一个唯一的sessionKey,并在该会话的所有日志中传递它。这样,在仪表盘的“会话详情”页,就能过滤出所有属于该会话的日志,完整重现任务流程。data字段的灵活运用:不要把所有信息都塞进message。message应该简洁可读,而结构化的详细信息(如一个API请求的完整请求体和响应头)应该放在data字段中。前端可以将其格式化为可折叠的JSON树状视图,保持界面整洁。- 日志级别策略:建议在生产环境将默认级别设为
INFO。DEBUG日志可用于开发阶段追踪复杂逻辑,但上线前应考虑是否关闭或采样记录,以避免海量数据冲击数据库。
3.2 会话管理 (sessions表):聚合的视角
会话表是对一个独立任务单元(例如“处理用户A的查询”)的摘要。它通过聚合该sessionKey下的所有日志来计算得出。
sessions: defineTable({ sessionKey: v.id(‘logs’), // 关联logs表 agentId: v.string(), startedAt: v.number(), endedAt: v.optional(v.number()), status: v.union(v.literal(‘running’), v.literal(‘completed’), v.literal(‘failed’), v.literal(‘timeout’)), // 聚合统计 totalTokens: v.number(), // 消耗的总Token数 estimatedCost: v.number(), // 估算的成本(美元) toolCalls: v.number(), // 工具调用次数 errorCount: v.number(), // 错误数 })关键实现逻辑: 会话的创建和更新通常不是由Agent直接完成的,那样会引入状态管理的复杂性。更优雅的做法是在后台异步计算。我们可以创建一个Convex的internal.cron任务,每分钟运行一次,扫描过去一段时间内新产生的日志,根据sessionKey进行分组聚合,然后更新或创建sessions表中的记录。计算totalTokens和estimatedCost需要关联modelUsage表的数据。
3.3 错误追踪 (errors表):从记录到解决
错误不是简单的ERROR级别日志。errors表旨在跟踪一个需要被关注和解决的故障实例。
errors: defineTable({ sessionKey: v.string(), logId: v.id(‘logs’), // 关联到触发错误的原始日志 title: v.string(), // 错误摘要 stackTrace: v.optional(v.string()), // 错误解决状态 status: v.union(v.literal(‘open’), v.literal(‘investigating’), v.literal(‘resolved’), v.literal(‘ignored’)), assignedTo: v.optional(v.string()), // 分配给谁处理 resolvedAt: v.optional(v.number()), resolutionNote: v.optional(v.string()), // 解决备注 })工作流设计:
- 自动创建:当一个
level为ERROR或CRITICAL的日志被插入时,可以触发一个Convexmutation(或由上述的cron任务)来检查并可能在errors表中创建一条新记录。 - 去重:简单的去重逻辑可以是:相同的
title和sessionKey在短时间内(如1小时)只创建一条错误记录,后续错误只增加计数。更复杂的可以去重堆栈相似性。 - 人工处理:仪表盘上的“错误看板”应允许运维人员更改错误状态、添加备注、分配负责人。这实现了简单的工单系统功能。
3.4 模型用量与成本 (modelUsage表):精打细算
这是AI项目财务和性能优化的核心。每次调用OpenAI、Anthropic等API时,都应记录一条数据。
modelUsage: defineTable({ sessionKey: v.string(), provider: v.string(), // “openai”, “anthropic”, “google” model: v.string(), // “gpt-4-turbo”, “claude-3-opus” operation: v.string(), // “chatCompletion”, “embedding” promptTokens: v.number(), completionTokens: v.number(), totalTokens: v.number(), latencyMs: v.number(), // 请求耗时 estimatedCostUsd: v.number(), // 根据官方定价计算 calledAt: v.number(), })成本计算实操: 成本计算必须在服务端(Convex函数)或Agent端进行,绝不能相信不可靠的客户端。你需要维护一个内部的价格映射表。
// 一个简化的成本计算函数示例 (convex/modelUsage.ts) const MODEL_PRICES: Record<string, { prompt: number; completion: number }> = { ‘openai:gpt-4-turbo’: { prompt: 0.01 / 1000, completion: 0.03 / 1000 }, // 每千Token价格 ‘openai:gpt-3.5-turbo’: { prompt: 0.001 / 1000, completion: 0.002 / 1000 }, }; export const logModelUsage = mutation(async ({ db }, usage: Omit<Doc<‘modelUsage’>, ‘_id’ | ‘_creationTime’ | ‘estimatedCostUsd’>) => { const key = `${usage.provider}:${usage.model}`; const price = MODEL_PRICES[key]; if (!price) { console.warn(`Unknown price for model: ${key}`); } const cost = usage.promptTokens * price.prompt + usage.completionTokens * price.completion; await db.insert(‘modelUsage’, { …usage, estimatedCostUsd: cost, }); });前端图表就可以基于这个表,绘制出按模型、按提供商、按时间段的成本和Token消耗趋势图,一目了然。
3.5 定时任务监控 (cronRuns表) 与速率限制追踪 (rateLimits表)
这两个模块是针对特定运维场景的“特种兵”。
cronRuns:记录每一个后台定时任务的执行情况(开始时间、结束时间、状态、输出)。Convex自己的internal.cron钩子就能很方便地写入此表。你也能用它来监控系统外部(如服务器Cron)触发的任务,只需在任务脚本开始和结束时调用对应的日志接口。rateLimits:当你的Agent收到API提供商(如OpenAI)的429(请求过多)速率限制响应时,立即记录一条事件,包含provider、model、retryAfter秒数等信息。这个仪表盘视图能帮你直观发现哪些接口调用过于频繁,是否需要调整策略或购买更高限额。
3.6 聚合表 (logCounters,dailyStats):为图表性能而生
直接在前端对海量日志表进行count、sum、group by操作是非常低效的,会给Convex数据库带来巨大压力。预聚合是解决之道。
logCounters表:可以是一个简单的键值对,用于实时更新全局计数,如totalLogs、errorsLastHour。每当插入新日志时,在一个原子事务中同时更新计数。dailyStats表:每天一条记录,存储该日的汇总数据,如各日志级别数量、总成本、会话数、错误数等。这可以通过一个每日运行的Convex cron任务,扫描前一天的原始数据计算得出。
前端图表在展示“近30天趋势”时,直接查询dailyStats表,性能极佳,体验流畅。
4. 从零到一的完整部署与集成指南
了解了架构,我们来看如何把它用起来。假设你有一个现有的OpenClaw项目,你需要将日志接入这个新系统。
4.1 第一步:克隆与初始化日志系统
# 1. 克隆仓库 git clone https://github.com/jake-101/openclaw-logging-system-nuxt-convex.git cd openclaw-logging-system-nuxt-convex # 2. 安装依赖 (推荐使用pnpm,速度更快且节省磁盘空间) pnpm install # 3. 配置环境变量 cp .env.example .env接下来编辑.env文件。你需要一个Convex部署。可以注册Convex Cloud(有免费套餐),也可以 自托管Convex后端 。
# .env 示例 CONVEX_DEPLOYMENT=https://your-deployment.convex.cloud # 从 Convex 仪表板获取 CONVEX_URL=“你的部署URL” NODE_ENV=“development”然后,在项目根目录初始化Convex并部署数据库Schema和函数:
# 登录Convex (如果是Cloud版本) npx convex auth # 初始化Convex项目,这会引导你创建新项目或关联已有项目 npx convex init # 启动本地Convex开发环境(可选,用于本地测试) pnpm convex:dev # 将Schema和Functions部署到你的Convex项目 pnpm convex:deploy部署成功后,Convex仪表板会给你一个部署URL和必要的密钥。
4.2 第二步:在前端仪表盘中配置Convex客户端
前端需要连接到你的Convex后端。修改app/plugins/convex.client.ts(如果存在)或直接在app.vue中初始化。
// app/plugins/convex.client.ts import { ConvexClient } from ‘convex/browser’; import { defineNuxtPlugin } from ‘#app’; export default defineNuxtPlugin((nuxtApp) => { const convex = new ConvexClient(process.env.CONVEX_URL || ‘'); nuxtApp.provide(‘convex’, convex); });现在运行pnpm dev,你应该能看到前端界面,但因为没有数据,所以是空的。
4.3 第三步:将OpenClaw Agent与日志系统集成
这是最关键的一步。你需要在你的OpenClaw Agent代码中,用提供的ConvexLogger替换掉原来的日志工具。
方法A:使用TypeScript/JS包装器(推荐)项目提供了ConvexLogger包装器,源码通常在agent-scripts/或一个独立的npm包中。你需要将其复制到你的Agent项目中。
// 在你的Agent项目中,例如 `lib/convex-logger.ts` import { ConvexHttpClient } from ‘convex/browser’; // 从你部署的Convex项目导入mutation函数类型 import type { api } from ‘./convex/_generated/api’; // 注意:这个路径需要指向你生成的类型 export class ConvexLogger { private convex: ConvexHttpClient; private sessionKey: string; private defaultLevel: ‘DEBUG’ | ‘INFO’ | ‘WARN’ | ‘ERROR’ = ‘INFO’; constructor(convexUrl: string, agentId: string) { this.convex = new ConvexHttpClient(convexUrl); // 生成一个唯一的会话Key,例如使用当前日期和随机字符串 const date = new Date().toISOString().split(‘T’)[0].replace(/-/g, ‘’); const shortId = Math.random().toString(36).substring(2, 8); this.sessionKey = `${agentId}-${date}-${shortId}`; } async log(level: ‘DEBUG’ | ‘INFO’ | ‘WARN’ | ‘ERROR’, message: string, data?: any) { if (this.shouldLog(level)) { try { // 调用Convex后端定义的 `insertLog` mutation await this.convex.mutation(api.logs.insertLog, { sessionKey: this.sessionKey, level, message, data, timestamp: Date.now(), source: ‘my-ai-agent’, module: ‘agent.core’, }); } catch (error) { // 如果网络失败,降级到控制台输出,避免影响主流程 console.error(`[ConvexLogger Failed] ${level}: ${message}`, error); } } } // 工具方法 info(msg: string, data?: any) { return this.log(‘INFO’, msg, data); } error(msg: string, error?: Error, data?: any) { return this.log(‘ERROR’, msg, { …data, stackTrace: error?.stack }); } // … 其他级别方法 private shouldLog(level: string): boolean { const levels = [‘DEBUG’, ‘INFO’, ‘WARN’, ‘ERROR’]; return levels.indexOf(level) >= levels.indexOf(this.defaultLevel); } } // 在你的主Agent文件中 import { ConvexLogger } from ‘./lib/convex-logger’; const logger = new ConvexLogger(process.env.CONVEX_URL!, ‘email-processor’); async function main() { logger.info(‘Agent started’, { userId: ‘123’ }); try { // … 你的业务逻辑 logger.info(‘Calling OpenAI API…’); // 在调用AI API后,记录用量 // await logModelUsage({…}); // 需要调用另一个mutation } catch (error) { logger.error(‘Task failed’, error as Error, { step: ‘processing’ }); } }方法B:使用Python同步脚本(异步批处理)对于非Node.js环境(如纯Python的OpenClaw Agent),项目提供了agent-scripts/目录下的Python脚本。其原理是:你的Agent将日志先写入本地文件或队列(如Redis),然后由一个独立的同步脚本定期读取这些文件,并通过Convex的HTTP API批量发送到后端。
# 1. 在你的Python Agent中,将日志写入一个结构化文件(如JSONL格式) # 2. 配置并运行同步脚本 cd agent-scripts pip install -r requirements.txt python sync_logs.py --convex-url $CONVEX_URL --log-file /path/to/your/agent.logs.jsonl这种方式牺牲了部分实时性(取决于同步频率),但降低了与Agent主程序的耦合度,更适合异构环境。
4.4 第四步:生成并查看数据
完成集成后,启动你的Agent执行任务。稍等片刻,刷新前端仪表盘(http://localhost:3000),你应该能看到:
- 日志查看器:实时滚动的日志流。
- 会话浏览器:列出了所有任务会话,可以看到状态、耗时和Token消耗。
- 错误看板:所有未解决的错误会集中显示。
- 分析图表:侧边栏或顶部有图表展示日志量、错误率、成本随时间的变化。
5. 常见问题排查与性能优化实战
在实际使用中,你可能会遇到以下问题。这里是我踩过坑后总结的排查清单。
5.1 前端仪表盘看不到数据
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 页面空白或一直加载 | Convex客户端连接失败 | 1. 检查浏览器控制台(F12)网络标签,查看对CONVEX_URL的WebSocket连接是否成功。2. 检查 .env文件中的CONVEX_URL是否正确,确保前端构建时环境变量已注入。3. 在Convex仪表板查看部署状态和日志。 |
| 有连接但表格为空 | Agent没有发送日志,或数据未同步 | 1. 在Convex Dashboard的Data部分,手动查询logs表,看是否有数据。2. 检查Agent端的 ConvexLogger是否被正确初始化,sessionKey是否生成。3. 检查Agent端网络,确保能访问Convex部署的URL。 |
| 图表不显示 | 聚合表(dailyStats)无数据 | 1. 检查用于生成聚合数据的Convex Cron任务是否已部署且正常运行(pnpm convex:deploy)。2. 检查Cron任务的执行日志(在Convex Dashboard的 Functions->Cron中查看)。 |
5.2 Agent端日志发送失败
- 错误:
Invalid URL或网络错误- 原因:
CONVEX_URL配置错误,或Agent运行环境无法访问外网(某些服务器环境)。 - 解决:确认URL正确。如果是网络问题,考虑在Agent端增加重试机制和更完善的降级处理(如失败时写入本地文件)。
- 原因:
- 错误:
Mutation requires authentication- 原因:Convex函数默认是公开的,但如果你配置了认证,则需要传递认证token。
- 解决:对于后端服务(如Agent),可以使用Convex的
service role密钥来认证。在Convex Dashboard的Settings->Keys中创建密钥,然后在Agent端初始化客户端时传入。
const convex = new ConvexHttpClient(CONVEX_URL, { useUrl: true }); // 或者,如果使用服务端SDK(如Node.js) import { ConvexHttpClient } from ‘convex/server’; const convex = new ConvexHttpClient(CONVEX_URL, { auth: process.env.CONVEX_SERVICE_KEY });
5.3 数据库性能与成本顾虑
Convex的免费套餐有额度限制,日志系统可能产生大量数据。
- 问题:数据量增长太快,费用激增
- 优化1:日志采样与级别控制。在生产环境,将默认日志级别从
DEBUG提升到INFO或WARN,能过滤掉大量调试信息。对于DEBUG日志,可以实现采样率(如只记录10%)。 - 优化2:设置数据保留策略。日志数据不需要永久保存。可以在Convex中编写一个定期(如每周)运行的Cron函数,删除超过30天的旧日志。务必先备份或汇总重要统计信息到
dailyStats表后再删除。 - 优化3:前端查询优化。避免在前端进行全表扫描或未加索引的复杂查询。确保对
timestamp、sessionKey、level等常用过滤字段建立了索引(在schema.ts中定义)。
- 优化1:日志采样与级别控制。在生产环境,将默认日志级别从
- 问题:实时日志流导致前端卡顿
- 解决:前端需要做虚拟滚动。
@nuxt/ui的UTable组件支持虚拟滚动,对于日志查看器页面务必开启。同时,可以提供一个“暂停”按钮,让用户暂停实时更新以查看特定时刻的日志。
- 解决:前端需要做虚拟滚动。
5.4 自定义扩展与高级用法
这个系统是一个优秀的起点,你可以根据需求扩展它。
- 添加告警功能:在Convex中创建一个函数,监控
errors表或rateLimits表。当发现新的高优先级错误或频繁的速率限制时,调用一个Webhook(如发送到Slack、钉钉或邮件服务)。 - 集成更多数据源:除了OpenClaw,你的系统可能还有其他服务(如数据库、第三方API)。你可以为它们创建类似的日志包装器,统一发送到同一个Convex后端,实现全栈监控。
- 构建自定义仪表盘:项目中的图表是基于
vue-chartjs的,你可以轻松修改app/components/下的图表组件,增加新的图表类型(如饼图显示模型使用分布,热力图显示错误时间分布)。
最后,关于自托管,如果你对数据主权有要求,Convex是开源的,可以部署在自己的基础设施上。这需要一些运维工作,但能给你完全的控制权。项目的docs/setup-guide.md里应该有更详细的指引,按照步骤操作即可。