OpenTelemetry 可观测性插件 — 为 hermes-agent 生成多层 trace 树和丰富的 metrics
Project description
hermes-agent OTel 可观测性插件使用手册
版本:0.1.0
日期:2026-04-15
适用范围:hermes-agent 用户 + 运维排查 + 可视化部署
目录
第一部分:用户使用指南
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 规范,通过自定义属性 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 启动时自动发现插件,无需手动复制文件:
# 从本地源码安装(开发阶段)
cd plugins/otel_tracing/
pip install -e .
# 或者发布到 PyPI 后直接安装(生产阶段)
pip install hermes-plugin-otel-tracing
安装完成后验证:
# 确认包已安装
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 声明
# 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 依赖:
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 机制实现插件的自动发现。用户只需
pip install,hermes 启动时就能自动找到并加载插件,无需手动复制文件到任何目录。
如果需要 OTLP gRPC 导出器(可选):
pip install hermes-plugin-otel-tracing[grpc]
方式二:手动安装
如果不想通过 pip 安装,也可以手动复制插件目录并自行安装依赖:
# 1. 手动安装 OTel 依赖
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
# 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 输出:
# 不需要任何额外配置,默认就是 console 模式
hermes
启动后,每次对话都会在终端打印 JSON 格式的 span 和 metrics。
3.2 推荐:OTLP 模式(配合可视化)
export HERMES_OTEL_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_SERVICE_NAME=hermes-agent
hermes
3.3 调试模式:捕获消息内容
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 的情况下验证插件是否正常工作:
cd plugins/otel_tracing/
pip install opentelemetry-api opentelemetry-sdk
python test_plugin.py
预期输出:
- 8 个 hooks 注册成功
- 6 个 span(1 session + 1 turn + 2 API call + 2 tool)按 JSON 格式打印
- 7 个 metrics 按 JSON 格式打印
- 所有 span 共享同一个
trace_id,父子关系正确
第二部分:故障排查手册
🔧 本部分面向运维/开发人员,用于排查插件不工作或数据异常的问题。
1. 快速诊断流程图
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] 字样。
排查步骤:
# 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 初始化失败。
排查步骤:
# 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 中看不到数据。
排查步骤:
# 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
排查步骤:
# 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。
排查步骤:
# 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_requesthook 没有传递usage参数
排查步骤:
# 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. 日志级别控制
# 查看插件详细日志
export HERMES_LOG_LEVEL=DEBUG
# 或在 Python 中:
import logging
logging.getLogger("plugins.otel_tracing").setLevel(logging.DEBUG)
日志前缀说明:
| 前缀 | 含义 |
|---|---|
▶ |
Span 开始 |
■ |
Span 结束 |
✅ |
操作成功 |
❌ |
操作失败 |
4. 环境变量速查表
# 完整的环境变量配置示例
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 的数据。
架构图
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
数据流:
- hermes-agent 通过 OTLP HTTP 协议将 traces 和 metrics 发送到 OTel Collector(端口 4318)
- OTel Collector 将 traces 转发到 Jaeger,将 metrics 暴露给 Prometheus
- Grafana 同时连接 Jaeger(查看 traces)和 Prometheus(查看 metrics dashboard)
第 1 步:创建配置文件
在 plugins/otel_tracing/ 目录下创建以下文件:
1.1 OTel Collector 配置
创建 otel-collector-config.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:
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:
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:
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 步:一键启动
# 进入插件目录
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 并发送数据
# 设置环境变量
export HERMES_OTEL_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OT_SERVICE_NAME=hermes-agent
# 启动 hermes-agent
hermes
或者先用测试脚本验证数据链路:
# 修改测试脚本的 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
- 左侧 Service 下拉框选择
hermes-agent(或你设置的OTEL_SERVICE_NAME) - 点击 Find Traces
- 你会看到类似这样的 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:
# 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 验证数据到达
# 查看所有 hermes 相关的指标
{__name__=~"hermes_.*"}
如果没有数据,检查:
- OTel Collector 日志:
docker logs hermes-otel-collector - Prometheus targets 页面:http://localhost:9090/targets(确认
otel-collector状态为 UP)
第 6 步:在 Grafana 中创建 Dashboard
6.1 登录 Grafana
浏览器访问:http://localhost:3000
- 用户名:
admin - 密码:
hermes123
6.2 验证数据源
- 左侧菜单 → Connections → Data sources
- 确认
Jaeger和Prometheus两个数据源都显示绿色 ✅
6.3 一键导入 Dashboard(推荐)
我们提供了预配置好的 Dashboard JSON 文件,包含所有推荐的 Panel,一键导入即可使用:
- 登录 Grafana(
admin/hermes123) - 左侧菜单 → Dashboards → New → Import
- 点击 Upload dashboard JSON file,选择
plugins/otel_tracing/grafana-dashboard.json - 在 Prometheus 下拉框中选择
Prometheus - 点击 Import
或者通过命令行导入:
# 通过 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(可选)
如果你想自己从零创建,步骤如下:
- 左侧菜单 → Dashboards → New → New Dashboard
- 点击 Add visualization
6.5 推荐的 Panel 配置
Panel 1:LLM API 调用耗时(时序图)
- 数据源:Prometheus
- 查询:
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):
sum(rate(hermes_gen_ai_client_token_usage_sum{gen_ai_token_type="input"}[5m]))
- 查询 B(Output):
sum(rate(hermes_gen_ai_client_token_usage_sum{gen_ai_token_type="output"}[5m]))
- 标题:Token 用量速率
- 单位:tokens/s
Panel 3:工具执行耗时(表格)
- 数据源:Prometheus
- 查询:
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:
avg(rate(hermes_hermes_turn_api_call_count_sum[5m]) / rate(hermes_hermes_turn_api_call_count_count[5m]))
Legend: 平均 API 调用次数 - 查询 B:
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
- 查询:
sum(hermes_hermes_session_turn_count_total)
- 标题:总 Turn 数
6.6 保存 Dashboard
点击右上角 💾 保存按钮,命名为 Hermes Agent Observability。
💡 如果你使用了 6.3 的一键导入方式,Dashboard 已经自动保存好了,无需此步骤。
第 7 步:端到端验证清单
按以下步骤逐一验证,确保整条数据链路通畅:
# ✅ 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 步:停止和清理
# 停止所有服务(保留数据)
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 |
附录:完整环境变量配置
# ── 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)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hermes_plugin_otel_tracing-0.1.1.tar.gz.
File metadata
- Download URL: hermes_plugin_otel_tracing-0.1.1.tar.gz
- Upload date:
- Size: 33.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
683c4b8df6cbd6d8e344ebd7ff84b382132c0c414a7b6f9a93c95f689f5e6fa4
|
|
| MD5 |
820fc8ed38f05bdefbcf1935e48843c1
|
|
| BLAKE2b-256 |
755f0ae97a7e41ab2dd613921b2e940b316dcaac4593eadf7d454281c5005c53
|
File details
Details for the file hermes_plugin_otel_tracing-0.1.1-py3-none-any.whl.
File metadata
- Download URL: hermes_plugin_otel_tracing-0.1.1-py3-none-any.whl
- Upload date:
- Size: 23.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0404e84a4492d3a26a3d2e20250a707ed8a9f98dac7fcc7616c6f4383b7fbe45
|
|
| MD5 |
ec9e9a698ac04dba6f92b92971e9f61f
|
|
| BLAKE2b-256 |
d0a5e2214f15684519d4f4f2c3542007c791e9bd98aeaab6d449595cfb239be0
|