内容
# java-obs-agent
[](LICENSE) [](README.zh-CN.md)
一个与框架无关的 Java 可观察性边车代理 —— 只需将单个 `-javaagent` jar 包附加到任何 Java(Servlet)服务上,然后 **与其聊天**(通过内置的 Web UI、像 Claude Code 这样的 MCP 客户端或 curl),以了解服务的实时状态:请求量、延迟、慢调用、错误、日志、线程转储、方法级跟踪。
它是 **Arthas**(JVM 内的诊断,无 AI/聚合)和 **SkyWalking**(带有分布式跟踪的 APM,无对话代理)之间的产物 —— 加上 **Datadog Bits AI** 模式(与服务聊天),但自行托管并可附加到单个 JVM。
> 在真实的 Spring Boot 服务(`interview-guide` 项目)上构建并验证:附加 → 聊天 → “哪个路由最慢?” → 代理调用其工具 → 用真实指标回答,然后 `trace_request` 固定调用树中的慢方法。
---
## 功能
将代理附加到正在运行的 Java 服务(无需重启 —— 动态附加,如 Arthas)。代理在 **目标 JVM 内运行**:
- 捕获每路由请求指标(计数、p50/p95/p99、错误率)、慢调用、错误 —— 进入 JVM 内的环形缓冲区(零外部存储)。
- 捕获最近的日志(Logback 追加器)+ 线程快照(JMX)。
- 暴露一个 **MCP 服务器**(HTTP 上的 JSON-RPC)—— 任何 MCP 客户端(Claude Code、Cursor)都可以与服务聊天。
- 暴露一个 **内置聊天 Web UI**(`/chat`)—— 兼容 OpenAI 的 LLM(自带 API 密钥)协调工具以自然语言回答问题。
- 按需 **方法级诊断**:`watch_method`(捕获方法的调用)和 `trace_request`(完整方法调用树,Arthas-`trace` 风格)。
- 留下一个 **源 SPI** 接缝,以便未来的 SkyWalking/Tempo 后端无需更改工具即可插入(第 3 阶段)。
## 快速开始
### 1. 构建
```bash
cd <repo-root>
JAVA_HOME=<path-to-jdk-21> ./gradlew :obs-agent-core:shadowJar
```
生成 `obs-agent-core/build/libs/obs-agent-core-0.1.0-all.jar`(阴影 + 重新定位 ByteBuddy/Jackson;slf4j 使用宿主的)。使用 `-all` jar(具有清单 + 依赖项);不要使用普通 jar。
> JDK 21(GraalVM 21.0.11)构建 Java 17 字节码。系统默认 `java` 是 JDK 8 —— 始终为 `./gradlew` 设置 `JAVA_HOME` 为 JDK 21。
### 2. 附加到正在运行的服务(动态,无需重启)
```bash
# 查找服务 PID(例如在端口 8080 上)
PID=$(netstat -ano | grep LISTENING | grep ":8080" | head -1 | awk '{print $NF}')
# 将代理附加到正在运行的 JVM(类似 Arthas)
JAVA_HOME=<path-to-jdk-21>
JAR="<repo-root>/obs-agent-core/build/libs/obs-agent-core-0.1.0-all.jar"
ARGS='port=9911,token=tok,enableInstrumentation=true,chat.apiKey=<YOUR_LLM_KEY>,chat.base_url=<https://...>,chat.model=<glm-4|gpt-4o-mini|...>'
"$JAVA_HOME/bin/java" --add-modules jdk.attach -cp scripts Attach "$PID" "$JAR" "$ARGS"
```
代理在目标 JVM 内启动 MCP 服务器 +(可选)聊天 UI,监听端口 `9911`。它将保持运行直到 JVM 重启。
> `Attach` 助手是一个一行代码,使用 `com.sun.tools.attach.VirtualMachine.loadAgent`(`agentmain` 入口点)。它位于 `scripts/Attach.java`。要使用它,请编译一次:`"$JAVA_HOME/bin/javac" --add-modules jdk.attach -d scripts scripts/Attach.java`。
### 2b. 或在 JVM 启动时附加(`-javaagent`,跨重启持续)
```bash
java -javaagent:<repo-root>/obs-agent-core/build/libs/obs-agent-core-0.1.0-all.jar=port=9911,token=tok,enableInstrumentation=true -jar your-app.jar
```
如果您希望代理从启动时就驻留(在重启后存活),请使用此方法。动态附加(上述)用于无需重启的临时诊断。
### 3. 使用它
**聊天 Web UI**(浏览器):打开 `http://localhost:9911/chat?token=tok` —— 聊天框;输入问题,LLM 调用工具并回答。
**Claude Code**(MCP 客户端):
```bash
claude mcp add --transport http obs-agent http://localhost:9911/mcp --header "Authorization: Bearer tok"
# 然后聊天:”哪个路由最慢?最近有哪些错误?”
```
**curl**(手动工具调用):
```bash
curl -X POST http://localhost:9911/mcp \
-H "Authorization: Bearer tok" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"recent_metrics","arguments":{"route":"/api/x"}}}'
```
> 所有端点都需要 `Authorization: Bearer <token>`。聊天 **UI 页面** 也接受 `?token=<token>`(浏览器无法在 URL 导航上发送标头)。
---
## 配置
所有配置都在代理参数(逗号分隔的 `key=value`)中,可选地被环境变量覆盖(`OBSAGENT_PORT`、`OBSAGENT_HOST`、`OBSAGENT_TOKEN`)。
| 键 | 默认值 | 描述 |
|---|---|---|
| `port` | 9000 | MCP + 聊天 HTTP 端口 |
| `host` | `127.0.0.1` | 绑定的网络接口。默认为环回地址(**未**暴露)。仅在防火墙/反向代理 **和** 设置 `token` 后才设置为 `0.0.0.0`。 |
| `token` | (无 → 无身份验证) | MCP/聊天 Bearer 令牌。**设置它** 如果端口暴露在 localhost 之外。 |
| `window` | 300s | 指标/日志保留窗口 |
| `bucket` | 1s | 指标时间桶 |
| `slowThreshold` | 500ms | 慢调用阈值 |
| `logBuffer` | 2000 | 日志环形缓冲区大小 |
| `enableInstrumentation` | false | `watch_method`/`trace_request`(仪表盘层)的总开关 |
| `watchLimit` | 10 | 每个 `watch_method` 的最大捕获数 |
| `watchTtl` | 60s | `watch_method` 自动卸载 TTL |
| `scrubPatterns` | (无) | 要从工具结果中清理的正则表达式(分号分隔)(令牌/PII) |
| `chat.apiKey` | (无 → 聊天关闭) | LLM API 密钥。设置以启用内置聊天。 |
| `chat.base_url` | — | 兼容 OpenAI 的基本 URL(例如 `https://api.openai.com/v1`,`https://open.bigmodel.cn/api/paas/v4`,`https://ark.cn-beijing.volces.com/api/coding/v3`) |
| `chat.model` | — | 模型名称(例如 `gpt-4o-mini`,`glm-4`,`glm-5.2`) |
当 `chat.apiKey` 未设置时,仅暴露 MCP(聊天 UI 返回 404)。
---
## 工具
**只读**(始终可用):
| 工具 | 它执行的操作 |
|---|---|
| `get_routes` | 已知路由 + 健康状况(计数、错误率、p95) |
| `recent_metrics(route?, window_seconds)` | 每路由计数 / p50/p95/p99 / 错误率 / qps |
| `slow_calls(top)` | 最近的慢请求(如果主机设置 MDC,则带有 traceId) |
| `recent_errors(top)` | 最近的错误请求 |
| `search_logs(query, level?, traceId?)` | 搜索 JVM 内的日志环(主机 Logback) |
| `thread_dump(top, by=cpu\|blocked)` | 顶部 CPU 线程 + 死锁 |
**仪表盘层**(由 `enableInstrumentation=true` 控制,默认关闭):
| 工具 | 它执行的操作 |
|---|---|
| `watch_method(class, method, limit?, ttl_seconds?)` | **开始** 捕获方法的调用(参数/返回值/持续时间/异常)。立即返回(两阶段)。 |
| `watch_results(class, method)` | **读取** 先前 `watch_method` 的捕获。 |
| `trace_request(class_pattern, ttl_seconds?, limit?)` | **开始** 类似 Arthas-`trace` 的方法调用树跟踪(对于与正则表达式匹配的类)。立即返回。 |
| `trace_results()` | **读取** 先前 `trace_request` 的调用树。 |
**两阶段设计**:`watch_method`/`trace_request` 安装 + 立即返回;跟踪/监视在 `ttl_seconds` 内保持活动。生成流量(访问端点或依赖前端流量),然后调用 `watch_results`/`trace_results` 读取。这是关键 —— 单个阻塞调用将阻止调用者(LLM)在窗口期间生成流量(→ 空结果)。
### trace_request 用法
- `class_pattern` 是一个 **正则表达式**。使用特定类的 FQCN。
- 查看 **跨类调用树**(控制器 → 服务),跟踪 **两个** 类,中间用 `|` 分隔:例如 `class_pattern="interview.guide.modules.x.KnowledgeBaseController|interview.guide.modules.x.service.KnowledgeBaseListService"`。
- 重复的 `trace_request` 调用 **重置** 先前的跟踪,然后安装新的跟踪(不堆叠建议)。
- 示例流程(通过聊天或 MCP):
1. `trace_request("...KnowledgeBaseController|...KnowledgeBaseListService", ttl_seconds=15)`
2. 访问端点几次
3. `trace_results()` → 树:`controller.getStatistics (335ms) → service.getStatistics (333ms)`
---
## 架构
```
┌──────────────────────────────────────────────────────────┐
│ LLM 客户端(自带):聊天 UI(/chat)/ Claude Code / curl │
└──────────────────┬───────────────────────────────────────┘
│ MCP(HTTP 上的 JSON-RPC)+ /chat(POST),令牌身份验证
┌──────────────────▼───────────────────────────────────────┐
│ obs-agent-core (在目标 JVM 内,阴影 jar) │
│ ┌───────────┐ ┌────────────┐ ┌─────────────────────┐ │
│ │ MCP 服务器│ │ 聊天端点│ │ 工具注册表 │ │
│ │ (JdkHttp) │ │ (LlmClient,│ │ (10 个工具) │ │
│ │ │ │ 代理循环)│ │ │ │
│ └───────────┘ └────────────┘ └──────────┬──────────┘ │
│ ┌──────────────────────────────────────────▼──────────┐ │
│ │ 源 SPI(扩展缝隙): │ │
│ │ 指标源 / 日志源 / 跟踪源 / │ │
│ │ 拓扑源 (+ 能力标志) │ │
│ └──────────────────────────┬───────────────────────────┘ │
│ ┌──────────────────────────▼──────────────────────────┐│
│ │ 第一阶段 JVM 内实现:环形缓冲区 / Logback / JMX ││
│ └──────────────────────────────────────────────────────┘│
│ ┌──────────────────────────────────────────────────────┐│
│ │ ByteBuddy 仪表盘(阴影 + 隔离): ││
│ │ - ServletMetricsInstrumentation(javax+jakarta) ││
│ │ - MethodInstrumenter(监视) - MethodTracer(跟踪) ││
│ └──────────────────────────────────────────────────────┘│
└────────────────────────────────────────────┬──────────────┘
(未来) ┌──────────────────────▼──────────────┐
│ 第三阶段:SkyWalkingTraceSource 等 │
│ (查询 OAP,无需更改工具) │
└──────────────────────────────────────┘
```
**关键设计要点:
- **单个阴影 jar** —— ByteBuddy/Jackson 重新定位到 `io.obsagent.shaded.*`;slf4j 是 `compileOnly`(使用宿主的,因此代理可以读取宿主的 Logback/MDC)。无主机类路径污染。
- **聊天无需新依赖** —— 使用 JDK `java.net.http.HttpClient`(兼容 OpenAI)。
- **建议永远不会抛出到用户代码中** —— 所有 ByteBuddy 建议都在 try/catch + JUL 日志记录中包装。
- **建议访问的成员是 `public static`** —— ByteBuddy 将建议内联到其他包中的目标类;包私有字段/方法将出现 `IllegalAccessError`(一个真正的陷阱 - 参见故障排除)。
- **`agentmain`** —— 实现动态附加到正在运行的 JVM(不仅仅是启动时的 `-javaagent`)。
---
## 路线图
- ✅ **第一阶段** —— 核心代理:Servlet 指标 + 环形缓冲区 + 源 SPI + JVM 内源 + MCP 服务器 + 只读工具 + 清理器/防护 + 阴影 jar + 端到端烟雾测试。
- ✅ **第二阶段** —— `watch_method`(方法捕获)+ `trace_request`(调用树)+ 验证演示。
- ✅ **内置聊天** —— LlmClient(兼容 OpenAI)+ ChatEndpoint(代理循环)+ `/chat` 处的 Web UI。
- ✅ **动态附加**(`agentmain`)+ 两阶段跟踪/监视 + 跟踪重置(无堆叠)+ 跨类调用树。
- ⏳ **第三阶段**(延期)—— `SkyWalkingTraceSource`/`SkyWalkingTopologySource`(查询 OAP)→ `distributed_trace`/`service_topology` 工具 → 跨服务业务链根因分析(顺序→支付)。源 SPI 已经就位。
- ⏳ **可选尾部** —— Spring Boot 适配器(路由模板/执行器/ Micrometer 跟踪 ID);流式聊天(基于令牌的 SSE);`obs-agent-cli`;反应式/WebFlux;Java 8 字节码扩展。
---
## 已知限制
- **`trace_request` 基于模式,而不是基于 traceId** —— 主机必须设置 MDC `traceId`(例如 Micrometer Tracing),以便 `slow_calls`/`search_logs` 按 traceId 相关联。没有它,这些字段为 `null`(路由 + 延迟诊断仍然有效)。
- **广泛的 `.*` 模式** 捕获入口方法,但可能不会将跨类调用者显示为子节点 —— 跟踪 **特定类**(使用 `|`)以获取跨类树。
- **聊天是非流式的**(POST `/chat/completions`,`stream=false`)—— 在工具调用循环后返回完整答案。实时令牌流式传输是一个后续功能。
- **仅限 Servlet**(阻塞)—— 反应式/WebFlux 尚未仪表化。
- **Java 17 字节码** —— 扩展到 Java 8(附加到较旧的 JVM)是一个后续功能。
- **代理无法在运行的 JVM 中热更新** —— 重新附加新的 jar 不会重新定义已经加载的类(系统类加载器缓存它们)。要获取代码更改,请重启目标 JVM + 重新附加(或在启动时使用 `-javaagent`)。
- **`watch_method`/`trace_request` 默认关闭** —— 设置 `enableInstrumentation=true` 以使用它们。
---
## 故障排除
**MCP 客户端(Claude Code)连接失败,提示“预期对象,收到未定义”用于 `capabilities`/`serverInfo`** —— `initialize` 结果必须同时包含两者(MCP 规范)。已修复(返回 `{"protocolVersion","capabilities":{"tools":{}},"serverInfo":{"name":"obs-agent","version":"0.1.0"}}`)。
**MCP 工具返回“内容预期数组,收到对象”/内容块验证失败** —— `tools/call` 结果必须是 `{"content":[{"type":"text","text":"<json>"}],"isError":false}`(内容块数组,每个块都有一个 `type`)。已修复。
**`search_logs` 返回“无日志源” / `traceId` 始终为 null** —— slf4j 必须不被阴影化(因此代理使用宿主的 slf4j 访问 Logback + MDC)。slf4j 是 `compileOnly` + 未重新定位。如果看到此问题,请检查 shadow 配置。
**浏览器打开 `/chat` → `{"code":-32001,"message":"unauthorized"}`** —— 浏览器无法在 URL 导航上发送 `Authorization` 标头。打开 `http://localhost:9911/chat?token=tok`(接受查询参数)。
**`trace_request` 返回空 `{ "trees":[]}`** —— (1)这是 **两阶段** 的:调用 `trace_request`(安装),生成流量,然后 `trace_results`(读取)。安装调用立即返回,不会捕获。(2)确保 `enableInstrumentation=true`。(3)验证 `class_pattern` 正则表达式是否匹配(使用 FQCN;`.` 匹配任意字符,`*` 是量化器)。
**`trace_request` 显示重复/嵌套树** —— 已修复:重复的 `trace_request` 现在使用先前的转换器重置,然后在安装新的转换器。如果您仍然看到它,则您正在使用旧 jar(重新构建 + 重新附加)。
**ByteBuddy 建议无声地不执行任何操作(空捕获,无错误)** —— 建议访问跨包目标的包私有成员 → `IllegalAccessError` 被 try/catch 吞噬 → 返回 null。建议逻辑必须位于 `public static` 方法中(例如 `onEnter`/`onExit`),以便内联建议仅调用公共方法。单元测试通过(同包测试类),但实时跨包目标失败 — 始终在具有真实跨包目标的新 JVM 上验证。
**构建因 JDK 问题而失败** —— 系统 `java` 是 JDK 8;项目需要 JDK 21。始终使用 `JAVA_HOME=<path-to-jdk-21>` 运行 `./gradlew`。
**重新附加第二个代理不会应用新代码** —— 第一次附加的类已经由系统类加载器加载;`loadAgent` 不会重新定义它们。重启目标 JVM + 附加一次。
---
## 开发
```bash
# 构建
JAVA_HOME="..." ./gradlew :obs-agent-core:build
# 测试
JAVA_HOME="..." ./gradlew :obs-agent-core:test
# 端到端烟雾测试(真实的 -javaagent 附加 + MCP)
JAVA_HOME="..." ./gradlew :obs-agent-core:test --tests 'io.obsagent.EndToEndSmokeTest' -Pe2e=true
# 阴影代理 jar
JAVA_HOME="..." ./gradlew :obs-agent-core:shadowJar
```
布局:
- `obs-agent-core/src/main/java/io/obsagent/` — `AgentPremain`,`config/`,`data/`(环形缓冲区 + `model` 记录),`source/`(SPI + `injvm/` 实现),`instr/`(ByteBuddy:Servlet/监视/跟踪),`mcp/`(JdkHttpMcpServer),`tools/`(ReadOnlyTools + InstrumentingTools),`safety/`(Scrubber + Guard),`chat/`(LlmClient + ChatEndpoint + ChatPage)。
- `docs/superpowers/specs/` — 设计规范。
- `docs/superpowers/plans/` — 第一阶段/第二阶段/聊天+跟踪实现计划。
- `.superpowers/sdd/progress.md` — SDD 执行账本(gitignored)。
---
## 它是如何构建的
通过头脑风暴 → 规格 → 实现计划 → 子代理驱动开发(12 个第一阶段任务 + 4 个第二阶段/聊天跟踪任务),每个任务 = 实现子代理 + 审查 + 修复。~40 次提交,29 个主要的 + 20 个测试 Java 文件。在真正的 `interview-guide` Spring Boot 后端上进行实时验证(动态附加 → MCP 聊天 → 真实指标 → `trace_request` 调用树固定慢速方法)。
查看 `docs/superpowers/specs/` 和 `docs/superpowers/plans/` 以获取完整的设计 + 任务分解。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器