Metadata-Version: 2.4
Name: hermes-plugin-otel-tracing
Version: 0.1.1
Summary: OpenTelemetry 可观测性插件 — 为 hermes-agent 生成多层 trace 树和丰富的 metrics
Author: hermes-agent
License-Expression: MIT
Requires-Python: >=3.10
Requires-Dist: opentelemetry-api>=1.20.0
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20.0
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0
Requires-Dist: opentelemetry-sdk>=1.20.0
Description-Content-Type: text/markdown

# hermes-agent OTel 可观测性插件使用手册

> **版本**：0.1.0  
> **日期**：2026-04-15  
> **适用范围**：hermes-agent 用户 + 运维排查 + 可视化部署

---

## 目录

- [第一部分：用户使用指南](#第一部分用户使用指南)
- [第二部分：故障排查手册](#第二部分故障排查手册)
- [第三部分：可视化方案（Jaeger + Prometheus + Grafana）](#第三部分可视化方案jaeger--prometheus--grafana)

---

# 第一部分：用户使用指南

## 1. 插件简介

`otel_tracing` 是一个 hermes-agent 插件，通过 hermes 的 8 个 plugin hooks 自动生成 **多层 Trace 树** 和 **丰富的 Metrics**，将 hermes-agent 的运行状态导出到 OpenTelemetry 生态。

### Span 层级结构

```
hermes.session                              ← 整个会话（根 span）
  └── hermes.turn                           ← 一轮对话（用户发一条消息 → agent 回复）
        ├── chat deepseek-v3-0324           ← 第 1 次 LLM API 调用（返回 tool_calls）
        ├── hermes.tool.terminal            ← 工具执行：terminal
        ├── hermes.tool.read_file           ← 工具执行：read_file
        └── chat deepseek-v3-0324           ← 第 2 次 LLM API 调用（返回最终文本）
```

### AI SpanKind（`hermes.span.kind` 属性）

OTel 官方 `SpanKind` 枚举只有 5 种（INTERNAL / SERVER / CLIENT / PRODUCER / CONSUMER），不包含 AI 领域的语义。本插件参考 [OpenInference](https://github.com/Arize-ai/openinference) 规范，通过自定义属性 `hermes.span.kind` 标识 AI 操作类型：

| `hermes.span.kind` | OTel `SpanKind` | 对应 Span | 说明 |
|---------------------|-----------------|-----------|------|
| `AGENT` | INTERNAL | `hermes.session` | 整个 agent 会话，代表一个 agent 执行 |
| `CHAIN` | INTERNAL | `hermes.turn` | 一轮对话，包含多步 LLM + Tool 调用链 |
| `LLM` | CLIENT | `chat {model}` | 单次 LLM API 调用（向远程模型发请求） |
| `TOOL` | INTERNAL | `hermes.tool.{name}` | 工具执行（agent 调用外部工具） |
| `EMBEDDING` | — | 预留 | 向量化操作（未来支持） |
| `RETRIEVER` | — | 预留 | RAG 检索操作（未来支持） |
| `RERANKER` | — | 预留 | 结果重排序操作（未来支持） |

> **为什么需要两层 Kind？**  
> OTel 的 `SpanKind` 是协议级别的（描述 span 在分布式系统中的角色），而 `hermes.span.kind` 是业务级别的（描述 span 在 AI 工作流中的角色）。例如 LLM 调用同时是 `SpanKind.CLIENT`（因为它向远程 API 发请求）和 `hermes.span.kind = LLM`（因为它是一次大模型调用）。

### Metrics 列表

| 指标名称 | 类型 | 单位 | 说明 |
|----------|------|------|------|
| `gen_ai.client.operation.duration` | Histogram | 秒 | 每次 LLM API 调用的耗时 |
| `gen_ai.client.token.usage` | Histogram | token | 每次 LLM 调用的 token 用量（按 input/output 分类） |
| `hermes.tool.duration` | Histogram | 秒 | 每次工具执行的耗时 |
| `hermes.turn.duration` | Histogram | 秒 | 每轮对话的总耗时 |
| `hermes.turn.api_call_count` | Histogram | 次 | 每轮对话中 LLM API 的调用次数 |
| `hermes.turn.tool_call_count` | Histogram | 次 | 每轮对话中工具的调用次数 |
| `hermes.session.turn_count` | Counter | 次 | 每个 Session 的 turn 总数 |

## 2. 安装

有两种安装方式，推荐使用 **方式一（pip 安装）**，自动处理所有依赖。

### 方式一：pip 安装（推荐）

通过 pip 安装，OTel 依赖会自动安装，hermes 启动时自动发现插件，**无需手动复制文件**：

```bash
# 从本地源码安装（开发阶段）
cd plugins/otel_tracing/
pip install -e .

# 或者发布到 PyPI 后直接安装（生产阶段）
pip install hermes-plugin-otel-tracing
```

安装完成后验证：

```bash
# 确认包已安装
pip list | grep hermes-plugin-otel-tracing

# 确认 OTel 依赖自动安装
pip list | grep opentelemetry
# 预期至少有：
# opentelemetry-api                    1.x.x
# opentelemetry-sdk                    1.x.x
# opentelemetry-exporter-otlp-proto-http  1.x.x
```

#### pip 安装原理详解

**1. 包的源码目录结构**

```
plugins/otel_tracing/
├── pyproject.toml                              ← pip 包的构建配置
├── src/
│   └── hermes_plugin_otel_tracing/             ← pip 包的源码（安装后的 Python 包名）
│       └── __init__.py                         ← 插件核心代码（包含 register 函数）
├── __init__.py                                 ← 手动安装方式使用的同一份代码
├── plugin.yaml                                 ← 手动安装方式的插件描述文件
└── ...
```

`src/hermes_plugin_otel_tracing/` 是 pip 包的实际源码目录。执行 `pip install` 后，这个目录会被安装到 Python 的 `site-packages/hermes_plugin_otel_tracing/` 下，成为一个可 `import` 的 Python 包。

**2. `pyproject.toml` 中的 entry_points 声明**

```toml
# pyproject.toml 关键配置

[project.entry-points."hermes_agent.plugins"]
otel_tracing = "hermes_plugin_otel_tracing"
```

这行配置的含义是：
- **group** = `hermes_agent.plugins` — 这是 hermes 约定的插件 entry point 组名
- **name** = `otel_tracing` — 插件名称（hermes 用这个名字标识和去重）
- **value** = `hermes_plugin_otel_tracing` — 指向的 Python 包名（即 `src/hermes_plugin_otel_tracing/`）

pip 安装时，这个声明会被写入包的 metadata（`dist-info/entry_points.txt`），Python 的 `importlib.metadata` 模块可以在运行时读取到它。

**3. hermes 启动时的插件发现流程**

```
hermes 启动
    │
    ▼
PluginManager.discover_and_load()
    │
    ├── ① 扫描用户目录:  ~/.hermes/plugins/        ← 手动安装的插件
    ├── ② 扫描项目目录:  ./.hermes/plugins/         ← 项目级插件（需开启）
    └── ③ 扫描 entry_points:                        ← pip 安装的插件 ✅
            │
            ▼
        importlib.metadata.entry_points()
        .select(group="hermes_agent.plugins")
            │
            ▼
        找到 otel_tracing → hermes_plugin_otel_tracing
            │
            ▼
        ep.load() → import hermes_plugin_otel_tracing
            │
            ▼
        调用 hermes_plugin_otel_tracing.register(ctx)
            │
            ▼
        插件注册 8 个 hooks，OTel SDK 初始化完成 ✅
```

核心流程：hermes 的 `PluginManager` 通过 Python 标准库 `importlib.metadata.entry_points()` 扫描所有声明了 `hermes_agent.plugins` 组的 pip 包，找到后调用 `ep.load()` 导入模块，再调用模块的 `register(ctx)` 函数完成插件注册。

**4. 依赖自动安装**

`pyproject.toml` 的 `dependencies` 字段声明了 OTel 依赖：

```toml
dependencies = [
    "opentelemetry-api >= 1.20.0",
    "opentelemetry-sdk >= 1.20.0",
    "opentelemetry-exporter-otlp-proto-http >= 1.20.0",
]
```

执行 `pip install` 时，pip 会自动安装这些依赖，用户无需手动安装 OTel 相关包。

> **总结**：pip 安装方式的本质是利用 Python 的 [entry_points](https://packaging.python.org/en/latest/specifications/entry-points/) 机制实现插件的**自动发现**。用户只需 `pip install`，hermes 启动时就能自动找到并加载插件，无需手动复制文件到任何目录。

如果需要 OTLP gRPC 导出器（可选）：

```bash
pip install hermes-plugin-otel-tracing[grpc]
```

### 方式二：手动安装

如果不想通过 pip 安装，也可以手动复制插件目录并自行安装依赖：

```bash
# 1. 手动安装 OTel 依赖
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
```

```bash
# 2. 将插件目录复制到 hermes 的用户插件目录
cp -r otel_tracing ~/.hermes/plugins/otel_tracing
```

最终目录结构：

```
~/.hermes/plugins/otel_tracing/
├── plugin.yaml
├── __init__.py
└── test_plugin.py     # 可选，仅用于测试
```

或者如果你是从源码运行 hermes-agent，直接放在源码的 `plugins/` 目录下：

```
hermes-agent/
└── plugins/
    └── otel_tracing/
        ├── plugin.yaml
        ├── __init__.py
        └── test_plugin.py
```

### 两种安装方式对比

| 特性 | pip 安装（推荐） | 手动安装 |
|------|:---:|:---:|
| 自动安装 OTel 依赖 | ✅ | ❌ 需手动 `pip install` |
| hermes 自动发现 | ✅ entry_points 机制 | ✅ 目录扫描 |
| 版本管理 | ✅ `pip show` / `pip upgrade` | ❌ 手动管理 |
| 卸载 | `pip uninstall hermes-plugin-otel-tracing` | 手动删除目录 |
| 适用场景 | 生产环境、分发给用户 | 快速调试、临时使用 |

> ⚠️ **注意**：两种方式不要同时使用。如果你已经通过 pip 安装了插件，就不需要再手动复制到 `~/.hermes/plugins/`。hermes 的 `PluginManager` 会通过插件名去重，不会重复加载，但为了避免混淆建议只保留一种方式。

hermes-agent 启动时会自动发现并加载该插件。

## 3. 配置

所有配置通过 **环境变量** 控制，无需修改任何代码：

| 环境变量 | 默认值 | 说明 |
|---------|--------|------|
| `HERMES_OTEL_ENABLED` | `true` | 是否启用插件。设为 `false` 可完全禁用 |
| `HERMES_OTEL_EXPORTER` | `console` | 导出器类型：`console`（打印到终端）或 `otlp`（发送到 OTLP 端点） |
| `HERMES_OTEL_CAPTURE_CONTENT` | `false` | 是否捕获用户消息和工具参数/结果（**注意隐私**） |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318` | OTLP HTTP 端点地址 |
| `OTEL_SERVICE_NAME` | `hermes-agent` | 服务名称，会出现在 Jaeger/Grafana 中 |

### 3.1 快速开始：Console 模式

最简单的方式，直接在终端看到 span 和 metrics 输出：

```bash
# 不需要任何额外配置，默认就是 console 模式
hermes
```

启动后，每次对话都会在终端打印 JSON 格式的 span 和 metrics。

### 3.2 推荐：OTLP 模式（配合可视化）

```bash
export HERMES_OTEL_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_SERVICE_NAME=hermes-agent

hermes
```

### 3.3 调试模式：捕获消息内容

```bash
export HERMES_OTEL_CAPTURE_CONTENT=true
```

开启后，span 中会包含：
- `hermes.turn.user_message`：用户消息（截断到 500 字符）
- `hermes.tool.args`：工具调用参数（截断到 2048 字符）
- `hermes.tool.result`：工具返回结果（截断到 2048 字符）

> ⚠️ **隐私警告**：生产环境建议关闭此选项，避免敏感信息泄露到可观测性后端。

## 4. Span 属性详解

### 4.1 Session Span (`hermes.session`)

| 属性 | 类型 | 说明 |
|------|------|------|
| `hermes.session.id` | string | 会话唯一标识 |
| `gen_ai.request.model` | string | 使用的模型名称 |
| `hermes.platform` | string | 运行平台：cli / telegram / discord 等 |
| `gen_ai.system` | string | 固定值 `hermes-agent` |
| `hermes.session.completed` | bool | 会话是否正常完成 |
| `hermes.session.interrupted` | bool | 会话是否被中断 |
| `hermes.session.total_turns` | int | 会话中的 turn 总数 |

### 4.2 Turn Span (`hermes.turn`)

| 属性 | 类型 | 说明 |
|------|------|------|
| `hermes.session.id` | string | 所属会话 ID |
| `hermes.turn.index` | int | 第几轮对话（从 1 开始） |
| `hermes.is_first_turn` | bool | 是否是会话的第一轮 |
| `gen_ai.request.model` | string | 使用的模型 |
| `hermes.platform` | string | 运行平台 |
| `hermes.turn.api_call_count` | int | 本轮 API 调用次数 |
| `hermes.turn.tool_call_count` | int | 本轮工具调用次数 |
| `hermes.turn.duration_s` | float | 本轮总耗时（秒） |
| `hermes.turn.user_message` | string | 用户消息（仅 CAPTURE_CONTENT=true） |

### 4.3 API Call Span (`chat {model}`)

| 属性 | 类型 | 说明 |
|------|------|------|
| `gen_ai.operation.name` | string | 固定值 `chat` |
| `gen_ai.system` | string | 固定值 `hermes-agent` |
| `gen_ai.request.model` | string | 请求的模型名称 |
| `gen_ai.response.model` | string | 实际响应的模型名称 |
| `gen_ai.usage.input_tokens` | int | 输入 token 数 |
| `gen_ai.usage.output_tokens` | int | 输出 token 数 |
| `gen_ai.response.finish_reasons` | string[] | 结束原因：`["stop"]` 或 `["tool_calls"]` |
| `hermes.provider` | string | 提供商：deepseek / openai / anthropic 等 |
| `hermes.api_call_number` | int | 本轮中第几次 API 调用 |
| `hermes.api_mode` | string | API 模式 |
| `hermes.message_count` | int | 发送的消息数量 |
| `hermes.tool_count` | int | 可用工具数量 |
| `hermes.approx_input_tokens` | int | 预估输入 token 数 |
| `hermes.cache.read_tokens` | int | 缓存读取 token 数 |
| `hermes.cache.write_tokens` | int | 缓存写入 token 数 |
| `hermes.reasoning_tokens` | int | 推理 token 数 |
| `hermes.assistant_content_chars` | int | 助手回复字符数 |
| `hermes.assistant_tool_call_count` | int | 助手发起的工具调用数 |
| `server.address` | string | API 服务器地址 |

### 4.4 Tool Span (`hermes.tool.{name}`)

| 属性 | 类型 | 说明 |
|------|------|------|
| `hermes.tool.name` | string | 工具名称 |
| `hermes.session.id` | string | 所属会话 ID |
| `hermes.tool.args` | string | 工具参数 JSON（仅 CAPTURE_CONTENT=true） |
| `hermes.tool.result` | string | 工具结果（仅 CAPTURE_CONTENT=true） |

> 如果工具返回的 JSON 中包含 `"error"` 字段，span 状态会自动设为 `ERROR`。

## 5. 运行测试脚本

插件自带一个测试脚本，可以在不启动 hermes-agent 的情况下验证插件是否正常工作：

```bash
cd plugins/otel_tracing/
pip install opentelemetry-api opentelemetry-sdk
python test_plugin.py
```

预期输出：
1. 8 个 hooks 注册成功
2. 6 个 span（1 session + 1 turn + 2 API call + 2 tool）按 JSON 格式打印
3. 7 个 metrics 按 JSON 格式打印
4. 所有 span 共享同一个 `trace_id`，父子关系正确

---

# 第二部分：故障排查手册

> 🔧 **本部分面向运维/开发人员**，用于排查插件不工作或数据异常的问题。

## 1. 快速诊断流程图

```mermaid
flowchart TD
    A[插件不工作？] --> B{hermes 启动日志中<br>有 otel_tracing 字样吗？}
    B -->|没有| C[插件未被加载]
    B -->|有| D{日志中有 ✅ 还是 ❌？}
    
    C --> C1[检查插件目录位置]
    C1 --> C2[检查 plugin.yaml 格式]
    C2 --> C3[检查 __init__.py 有无语法错误]
    
    D -->|❌ OTel 初始化失败| E[检查 OTel SDK 依赖]
    D -->|✅ 注册完成| F{有 span 输出吗？}
    
    E --> E1[pip install opentelemetry-api opentelemetry-sdk]
    E1 --> E2[如果用 OTLP: pip install opentelemetry-exporter-otlp-proto-http]
    
    F -->|Console 模式有输出| G{OTLP 模式没数据？}
    F -->|完全没输出| H[检查 HERMES_OTEL_ENABLED]
    
    G --> G1[检查 OTLP endpoint 是否可达]
    G1 --> G2[检查 Jaeger/Collector 是否在运行]
    G2 --> G3[检查防火墙/网络]
    
    H --> H1[确认环境变量 HERMES_OTEL_ENABLED=true]
    H1 --> H2[确认 hermes 实际触发了 hooks]
```

## 2. 常见问题及解决方案

### 问题 1：插件未被加载

**症状**：hermes 启动日志中完全没有 `[otel_tracing]` 字样。

**排查步骤**：

```bash
# 1. 确认插件目录位置正确
ls -la ~/.hermes/plugins/otel_tracing/
# 或源码模式
ls -la plugins/otel_tracing/

# 2. 确认 plugin.yaml 存在且格式正确
cat plugins/otel_tracing/plugin.yaml

# 3. 确认 __init__.py 无语法错误
python -c "import plugins.otel_tracing"

# 4. 确认 register 函数存在
python -c "from plugins.otel_tracing import register; print('OK')"
```

### 问题 2：OTel SDK 初始化失败

**症状**：日志中出现 `[otel_tracing] ❌ OTel 初始化失败`。

**排查步骤**：

```bash
# 1. 检查 OTel SDK 是否安装
pip list | grep opentelemetry

# 预期至少有：
# opentelemetry-api          1.x.x
# opentelemetry-sdk          1.x.x

# 2. 如果缺少，安装：
pip install opentelemetry-api opentelemetry-sdk

# 3. 如果用 OTLP 模式，还需要：
pip install opentelemetry-exporter-otlp-proto-http

# 4. 检查版本兼容性（api 和 sdk 版本必须匹配）
python -c "
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
print('TracerProvider OK')
from opentelemetry.sdk.metrics import MeterProvider
print('MeterProvider OK')
"
```

### 问题 3：Console 模式有输出，OTLP 模式没数据

**症状**：`HERMES_OTEL_EXPORTER=console` 时能看到 span，切换到 `otlp` 后 Jaeger 中看不到数据。

**排查步骤**：

```bash
# 1. 确认环境变量设置正确
echo $HERMES_OTEL_EXPORTER      # 应该是 otlp
echo $OTEL_EXPORTER_OTLP_ENDPOINT  # 应该是 http://localhost:4318

# 2. 确认 OTLP endpoint 可达
curl -v http://localhost:4318/v1/traces
# 应该返回 HTTP 响应（即使是 405 也说明端口通了）

# 3. 确认 OTLP exporter 已安装
python -c "from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter; print('OK')"

# 4. 检查 Jaeger/Collector 容器是否在运行
docker ps | grep -E "jaeger|otel-collector"

# 5. 检查 Jaeger 容器日志
docker logs jaeger 2>&1 | tail -20
```

### 问题 4：Span 存在但父子关系断裂

**症状**：Jaeger 中能看到 span，但它们不在同一个 trace 中，或者没有父子关系。

**可能原因**：
- `on_session_start` 没有被触发（session span 缺失）
- `pre_llm_call` 没有被触发（turn span 缺失）
- hermes-agent 版本过旧，不支持某些 hooks

**排查步骤**：

```bash
# 1. 开启 debug 日志查看 hook 触发情况
export PYTHONPATH=plugins/otel_tracing
python -c "
import logging
logging.basicConfig(level=logging.DEBUG)
# 然后启动 hermes
"

# 2. 检查日志中是否有完整的 hook 触发序列：
#    ▶ Session span 开始
#      ▶ Turn #1 span 开始
#        ▶ API Call #1 span 开始
#        ■ API Call #1 span 结束
#      ■ Turn span 结束
#    ■ Session span 结束
```

### 问题 5：Metrics 不显示

**症状**：Span 正常，但 Prometheus/Grafana 中看不到 metrics。

**排查步骤**：

```bash
# 1. Console 模式下确认 metrics 有输出
export HERMES_OTEL_EXPORTER=console
# 启动 hermes 并发送一条消息，等待 5 秒后应看到 metrics JSON

# 2. OTLP 模式下检查 metrics endpoint
curl -v http://localhost:4318/v1/metrics

# 3. 检查 Prometheus 是否配置了正确的 scrape target
# 注意：本插件通过 OTLP push 模式发送 metrics，不是 Prometheus pull
# 需要 OTel Collector 作为中间层转换

# 4. 检查 metrics 导出间隔
# 默认 OTLP 模式下 60 秒导出一次，Console 模式下 5 秒
# 如果刚启动就关闭，可能来不及导出
```

### 问题 6：Token 数据为 0

**症状**：Span 中 `gen_ai.usage.input_tokens` 和 `gen_ai.usage.output_tokens` 都是 0。

**可能原因**：
- LLM 提供商没有返回 usage 信息
- hermes-agent 的 `post_api_request` hook 没有传递 `usage` 参数

**排查步骤**：

```bash
# 1. 检查 hermes-agent 版本是否支持 post_api_request 的 usage 参数
# 需要 hermes-agent 较新版本

# 2. 在 __init__.py 中临时添加日志
# 在 _on_post_api_request 函数开头加：
#   logger.warning("post_api_request usage=%s", usage)
# 查看 usage 是否为 None
```

## 3. 日志级别控制

```bash
# 查看插件详细日志
export HERMES_LOG_LEVEL=DEBUG

# 或在 Python 中：
import logging
logging.getLogger("plugins.otel_tracing").setLevel(logging.DEBUG)
```

日志前缀说明：

| 前缀 | 含义 |
|------|------|
| `▶` | Span 开始 |
| `■` | Span 结束 |
| `✅` | 操作成功 |
| `❌` | 操作失败 |

## 4. 环境变量速查表

```bash
# 完整的环境变量配置示例
export HERMES_OTEL_ENABLED=true
export HERMES_OTEL_EXPORTER=otlp
export HERMES_OTEL_CAPTURE_CONTENT=false
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_SERVICE_NAME=hermes-agent
```

---

# 第三部分：可视化方案（Jaeger + Prometheus + Grafana）

> 🎯 **目标**：用 Docker 一键启动 Jaeger（看 Traces）+ Prometheus（存 Metrics）+ Grafana（看 Dashboard），确保能在可视化界面看到 hermes-agent 的数据。

## 架构图

```mermaid
graph LR
    H[hermes-agent<br>otel_tracing 插件] -->|OTLP HTTP<br>:4318| C[OTel Collector<br>:4318]
    C -->|Jaeger proto| J[Jaeger<br>:16686 UI]
    C -->|Prometheus remote write| P[Prometheus<br>:9090]
    P --> G[Grafana<br>:3000]
    J --> G
    
    style H fill:#4CAF50,color:white
    style C fill:#FF9800,color:white
    style J fill:#2196F3,color:white
    style P fill:#E91E63,color:white
    style G fill:#9C27B0,color:white
```

数据流：
1. **hermes-agent** 通过 OTLP HTTP 协议将 traces 和 metrics 发送到 **OTel Collector**（端口 4318）
2. **OTel Collector** 将 traces 转发到 **Jaeger**，将 metrics 暴露给 **Prometheus**
3. **Grafana** 同时连接 Jaeger（查看 traces）和 Prometheus（查看 metrics dashboard）

## 第 1 步：创建配置文件

在 `plugins/otel_tracing/` 目录下创建以下文件：

### 1.1 OTel Collector 配置

创建 `otel-collector-config.yaml`：

```yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: "0.0.0.0:4318"

exporters:
  # Traces → Jaeger
  otlp/jaeger:
    endpoint: "jaeger:4317"
    tls:
      insecure: true

  # Metrics → Prometheus (Collector 自己暴露 /metrics 端点)
  prometheus:
    endpoint: "0.0.0.0:8889"
    namespace: "hermes"

  # 调试用：同时打印到日志
  debug:
    verbosity: basic

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/jaeger, debug]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus, debug]
```

### 1.2 Prometheus 配置

创建 `prometheus.yaml`：

```yaml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  # 从 OTel Collector 的 prometheus exporter 拉取 metrics
  - job_name: "otel-collector"
    static_configs:
      - targets: ["otel-collector:8889"]
        labels:
          source: "hermes-agent"
```

### 1.3 Grafana 数据源预配置

创建 `grafana-datasources.yaml`：

```yaml
apiVersion: 1

datasources:
  - name: Jaeger
    type: jaeger
    access: proxy
    url: http://jaeger:16686
    isDefault: false
    editable: true

  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: true
```

### 1.4 Docker Compose 文件

创建 `docker-compose.yaml`：

```yaml
version: "3.9"

services:
  # ── Jaeger：Trace 可视化 ──
  jaeger:
    image: jaegertracing/all-in-one:1.62
    container_name: hermes-jaeger
    ports:
      - "16686:16686"   # Jaeger UI
      - "4317:4317"     # OTLP gRPC (Collector → Jaeger)
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    restart: unless-stopped

  # ── OTel Collector：数据中转 ──
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.114.0
    container_name: hermes-otel-collector
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
    ports:
      - "4318:4318"     # OTLP HTTP (hermes-agent → Collector)
      - "8889:8889"     # Prometheus metrics endpoint
    depends_on:
      - jaeger
    restart: unless-stopped

  # ── Prometheus：Metrics 存储 ──
  prometheus:
    image: prom/prometheus:v2.54.1
    container_name: hermes-prometheus
    volumes:
      - ./prometheus.yaml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"     # Prometheus UI
    depends_on:
      - otel-collector
    restart: unless-stopped

  # ── Grafana：统一 Dashboard ──
  grafana:
    image: grafana/grafana:11.3.0
    container_name: hermes-grafana
    volumes:
      - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml:ro
      - grafana-data:/var/lib/grafana
    ports:
      - "3000:3000"     # Grafana UI
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=hermes123
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
    depends_on:
      - prometheus
      - jaeger
    restart: unless-stopped

volumes:
  prometheus-data:
  grafana-data:
```

## 第 2 步：一键启动

```bash
# 进入插件目录
cd plugins/otel_tracing/

# 启动所有服务（后台运行）
docker-compose up -d

# 查看启动状态
docker-compose ps
```

预期输出：

```
NAME                    STATUS
hermes-jaeger           running   0.0.0.0:16686->16686/tcp
hermes-otel-collector   running   0.0.0.0:4318->4318/tcp
hermes-prometheus       running   0.0.0.0:9090->9090/tcp
hermes-grafana          running   0.0.0.0:3000->3000/tcp
```

等待约 10 秒让所有服务完全启动。

## 第 3 步：配置 hermes-agent 并发送数据

```bash
# 设置环境变量
export HERMES_OTEL_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OT_SERVICE_NAME=hermes-agent

# 启动 hermes-agent
hermes
```

或者先用测试脚本验证数据链路：

```bash
# 修改测试脚本的 exporter 为 otlp
HERMES_OTEL_EXPORTER=otlp \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
OT_SERVICE_NAME=hermes-agent-test \
python test_plugin.py
```

> ⚠️ 注意：测试脚本默认使用 console exporter。要发送到 OTLP，需要通过环境变量覆盖。

## 第 4 步：在 Jaeger 中查看 Traces

### 4.1 打开 Jaeger UI

浏览器访问：**http://localhost:16686**

### 4.2 查找 Traces

1. 左侧 **Service** 下拉框选择 `hermes-agent`（或你设置的 `OTEL_SERVICE_NAME`）
2. 点击 **Find Traces**
3. 你会看到类似这样的 trace 列表：

```
hermes.session  |  6 spans  |  2.1s
```

### 4.3 查看 Trace 详情

点击某个 trace，你会看到完整的多层 span 树：

```
▼ hermes.session                    2.1s
  ▼ hermes.turn                     2.0s
    ▼ chat deepseek-v3-0324         0.85s   finish_reason=tool_calls
    ▼ hermes.tool.terminal          0.02s
    ▼ hermes.tool.read_file         0.01s
    ▼ chat deepseek-v3-0324         1.2s    finish_reason=stop
```

点击任意 span 可以查看详细属性：
- **API Call span**：token 用量、cache 命中、推理 token、provider 等
- **Tool span**：工具名称、执行耗时、是否出错
- **Turn span**：本轮 API 调用次数、工具调用次数、总耗时
- **Session span**：总 turn 数、是否正常完成

### 4.4 Jaeger 实用功能

| 功能 | 操作 |
|------|------|
| 按耗时排序 | 点击 "Longest First" |
| 按 span 数量过滤 | 设置 "Min/Max Spans" |
| 搜索特定 session | Tags 中输入 `hermes.session.id=xxx` |
| 查看错误 span | Tags 中输入 `error=true` |
| 对比两个 trace | 选中两个 trace 后点击 "Compare" |

## 第 5 步：在 Prometheus 中查看 Metrics

### 5.1 打开 Prometheus UI

浏览器访问：**http://localhost:9090**

### 5.2 常用查询（PromQL）

在查询框中输入以下 PromQL：

```promql
# LLM API 调用平均耗时（按模型分组）
rate(hermes_gen_ai_client_operation_duration_seconds_sum[5m])
/ rate(hermes_gen_ai_client_operation_duration_seconds_count[5m])

# Token 总用量（按 input/output 分组）
sum by (gen_ai_token_type) (
  rate(hermes_gen_ai_client_token_usage_sum[5m])
)

# 工具执行平均耗时（按工具名分组）
rate(hermes_hermes_tool_duration_seconds_sum[5m])
/ rate(hermes_hermes_tool_duration_seconds_count[5m])

# 每轮对话的平均 API 调用次数
rate(hermes_hermes_turn_api_call_count_sum[5m])
/ rate(hermes_hermes_turn_api_call_count_count[5m])

# Session 总 turn 数
hermes_hermes_session_turn_count_total
```

> 📝 **注意**：因为 OTel Collector 的 prometheus exporter 配置了 `namespace: "hermes"`，所有指标名称会加上 `hermes_` 前缀。

### 5.3 验证数据到达

```promql
# 查看所有 hermes 相关的指标
{__name__=~"hermes_.*"}
```

如果没有数据，检查：
1. OTel Collector 日志：`docker logs hermes-otel-collector`
2. Prometheus targets 页面：http://localhost:9090/targets（确认 `otel-collector` 状态为 UP）

## 第 6 步：在 Grafana 中创建 Dashboard

### 6.1 登录 Grafana

浏览器访问：**http://localhost:3000**

- 用户名：`admin`
- 密码：`hermes123`

### 6.2 验证数据源

1. 左侧菜单 → **Connections** → **Data sources**
2. 确认 `Jaeger` 和 `Prometheus` 两个数据源都显示绿色 ✅

### 6.3 一键导入 Dashboard（推荐）

我们提供了预配置好的 Dashboard JSON 文件，包含所有推荐的 Panel，**一键导入即可使用**：

1. 登录 Grafana（`admin` / `hermes123`）
2. 左侧菜单 → **Dashboards** → **New** → **Import**
3. 点击 **Upload dashboard JSON file**，选择 `plugins/otel_tracing/grafana-dashboard.json`
4. 在 **Prometheus** 下拉框中选择 `Prometheus`
5. 点击 **Import**

或者通过命令行导入：

```bash
# 通过 Grafana API 导入 Dashboard
curl -X POST http://admin:hermes123@localhost:3000/api/dashboards/import \
  -H "Content-Type: application/json" \
  -d "{
    \"dashboard\": $(cat plugins/otel_tracing/grafana-dashboard.json),
    \"overwrite\": true,
    \"inputs\": [
      {\"name\": \"DS_PROMETHEUS\", \"type\": \"datasource\", \"pluginId\": \"prometheus\", \"value\": \"Prometheus\"}
    ]
  }"
```

导入后的 Dashboard 包含以下 Panel：

| 区域 | Panel | 类型 | 说明 |
|------|-------|------|------|
| 📊 概览 | 总 Turn 数 | Stat | Session 中的 Turn 总数 |
| 📊 概览 | 每轮对话统计 | Stat | 平均 API / 工具调用次数 |
| 📊 概览 | Turn 平均耗时 | Stat | 每轮对话的平均总耗时 |
| 📊 概览 | API 平均延迟 | Stat | LLM API 调用的平均耗时 |
| ⏱️ LLM API 性能 | 延迟分布 | 时序图 | P50 / P95 / P99 延迟 |
| ⏱️ LLM API 性能 | 按模型耗时 | 时序图 | 各模型的平均 API 耗时 |
| 🪙 Token 用量 | Input vs Output | 柱状图 | Token 用量速率（堆叠） |
| 🪙 Token 用量 | 每次调用平均 Token | 时序图 | 单次 API 调用的平均 Token |
| 🔧 工具执行 | 工具平均耗时 | 时序图 | 各工具的平均执行耗时 |
| 🔧 工具执行 | 工具调用频率 | 柱状图 | 各工具的调用频率 |
| 💬 对话统计 | Turn 耗时分布 | 时序图 | Turn P50 / P95 / P99 |
| 💬 对话统计 | 调用次数趋势 | 时序图 | 每轮 API / 工具调用次数 |
| 🔍 Traces | Jaeger UI 链接 | Markdown | 点击链接跳转到 Jaeger UI 查看 Traces |

> 💡 Dashboard 默认每 30 秒自动刷新，时间范围为最近 1 小时。

### 6.4 手动创建 Dashboard（可选）

如果你想自己从零创建，步骤如下：

1. 左侧菜单 → **Dashboards** → **New** → **New Dashboard**
2. 点击 **Add visualization**

### 6.5 推荐的 Panel 配置

#### Panel 1：LLM API 调用耗时（时序图）

- **数据源**：Prometheus
- **查询**：
  ```promql
  histogram_quantile(0.95,
    rate(hermes_gen_ai_client_operation_duration_seconds_bucket[5m])
  )
  ```
- **标题**：LLM API P95 延迟
- **单位**：seconds (s)

#### Panel 2：Token 用量（柱状图）

- **数据源**：Prometheus
- **查询 A**（Input）：
  ```promql
  sum(rate(hermes_gen_ai_client_token_usage_sum{gen_ai_token_type="input"}[5m]))
  ```
- **查询 B**（Output）：
  ```promql
  sum(rate(hermes_gen_ai_client_token_usage_sum{gen_ai_token_type="output"}[5m]))
  ```
- **标题**：Token 用量速率
- **单位**：tokens/s

#### Panel 3：工具执行耗时（表格）

- **数据源**：Prometheus
- **查询**：
  ```promql
  rate(hermes_hermes_tool_duration_seconds_sum[5m])
  / rate(hermes_hermes_tool_duration_seconds_count[5m])
  ```
- **标题**：工具平均执行耗时
- **单位**：seconds (s)

#### Panel 4：Trace 查看器

- **数据源**：Jaeger
- **查询类型**：Search
- **Service**：hermes-agent
- **标题**：最近的 Traces

#### Panel 5：每轮对话统计（Stat）

- **数据源**：Prometheus
- **查询 A**：
  ```promql
  avg(rate(hermes_hermes_turn_api_call_count_sum[5m]) / rate(hermes_hermes_turn_api_call_count_count[5m]))
  ```
  Legend: 平均 API 调用次数
- **查询 B**：
  ```promql
  avg(rate(hermes_hermes_turn_tool_call_count_sum[5m]) / rate(hermes_hermes_turn_tool_call_count_count[5m]))
  ```
  Legend: 平均工具调用次数

#### Panel 6：Session Turn 总数（Counter）

- **数据源**：Prometheus
- **查询**：
  ```promql
  sum(hermes_hermes_session_turn_count_total)
  ```
- **标题**：总 Turn 数

### 6.6 保存 Dashboard

点击右上角 💾 保存按钮，命名为 `Hermes Agent Observability`。

> 💡 如果你使用了 6.3 的一键导入方式，Dashboard 已经自动保存好了，无需此步骤。

## 第 7 步：端到端验证清单

按以下步骤逐一验证，确保整条数据链路通畅：

```bash
# ✅ 1. 确认所有容器运行正常
docker-compose ps
# 所有 4 个容器状态应为 running

# ✅ 2. 确认 OTel Collector 接收端口可达
curl -s -o /dev/null -w "%{http_code}" http://localhost:4318/v1/traces
# 应返回 200 或 405

# ✅ 3. 确认 Prometheus 能抓到 OTel Collector
curl -s http://localhost:9090/api/v1/targets | python3 -m json.tool | grep -A2 "otel-collector"
# 应显示 "health": "up"

# ✅ 4. 发送测试数据
cd plugins/otel_tracing/
HERMES_OTEL_EXPORTER=otlp python test_plugin.py

# ✅ 5. 在 Jaeger 中查看
open http://localhost:16686
# Service 选 hermes-agent-plugin-test → Find Traces → 应看到 1 个 trace，6 个 span

# ✅ 6. 在 Prometheus 中查看
open http://localhost:9090
# 查询 {__name__=~"hermes_.*"} → 应看到 7 个指标

# ✅ 7. 在 Grafana 中查看
open http://localhost:3000
# 登录 admin/hermes123 → Data sources → 两个数据源都应为绿色
```

## 第 8 步：停止和清理

```bash
# 停止所有服务（保留数据）
docker-compose stop

# 停止并删除容器（保留数据卷）
docker-compose down

# 停止并删除所有数据（完全清理）
docker-compose down -v
```

---

## 附录：端口速查表

| 服务 | 端口 | 用途 |
|------|------|------|
| OTel Collector | 4318 | OTLP HTTP 接收端（hermes-agent → Collector） |
| OTel Collector | 8889 | Prometheus metrics 暴露端 |
| Jaeger | 16686 | Jaeger Web UI |
| Jaeger | 4317 | OTLP gRPC（Collector → Jaeger） |
| Prometheus | 9090 | Prometheus Web UI + API |
| Grafana | 3000 | Grafana Web UI |

## 附录：完整环境变量配置

```bash
# ── hermes-agent OTel 插件配置 ──
export HERMES_OTEL_ENABLED=true
export HERMES_OTEL_EXPORTER=otlp
export OTEL_RESOURCE_ATTRIBUTES="token=fspbWnBJxURffqeNdNJO,host.name=vllm-test"
export HERMES_OTEL_CAPTURE_CONTENT=false    # 生产环境建议 false
#export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_EXPORTER_OTLP_ENDPOINT=http://30.171.27.189:8080
export OTEL_SERVICE_NAME=hermes-agent

# ── 启动可视化后端 ──
cd plugins/otel_tracing/
docker-compose up -d

# ── 启动 hermes-agent ──
hermes

# ── 访问可视化界面 ──
# Jaeger:     http://localhost:16686
# Prometheus: http://localhost:9090
# Grafana:    http://localhost:3000  (admin / hermes123)
```
