<!-- title: 为Skill加装可观测性看板 -->

## 概述

本教程带你为自定义 Claude Code Skill 构建一套**可观测性（observability）体系**：通过原生 OpenTelemetry 与零侵入 Hook 双通路，把 Skill 的每一次激活、调用时长、Token 消耗、成功率和成本落盘成结构化数据，再聚合成 CLI 看板，最终用真实数据回答"哪个 Skill 值得用 Opus、哪个 Skill 在偷偷失败"。适合已经写过 1–2 个 Skill、想知道它们在真实会话里表现如何的开发者。

## 前置条件

- Claude Code CLI（任意 2026 版本，内置 OpenTelemetry 导出能力）
- Node.js ≥ 20（运行采集器与看板）或 Python ≥ 3.8（uv 脚本亦可）
- 一个已存在的自定义 Skill（放于 `~/.claude/skills/your-skill/SKILL.md`）
- 可选：Docker（用于 Grafana 看板）、一个 OTLP 后端（Jaeger / Grafana Alloy / 任意 OTel Collector）

## 为什么 Skill 需要可观测性

AI 编码 Agent 在一次会话里可能触发上百次工具调用，Skill 只是其中一种触发源。没有遥测时，你只能靠"感觉"判断 Skill 好不好用——它到底被激活了几次？平均跑多久？吃掉了多少上下文预算？哪几次悄悄失败了？这些问题，传统 Evals（在静态测试集上打分）回答不了，因为它衡量的是**实验室质量**，而非**线上表现**。

可观测性补齐的就是这一环：让 Skill 从一个"黑盒提示词"变成一个**可以被度量、被诊断、被优化的服务**。这与"Skill 失效诊断"（坏了才查）不同，可观测性是**主动、持续、全程**的；也与 Evals 不同，它度量的是**真实运行时的每一次调用**。

## 两条数据通路对比

Claude Code 提供两条互补的遥测通路，理解它们的分工是本教程的核心：

| 维度 | 原生 OpenTelemetry（OTel） | 自定义 Hook 采集 |
|------|--------------------------|----------------|
| 数据粒度 | 官方预定义指标与事件 | 完全自定义字段 |
| 是否含 Skill 事件 | ✅ `Skill activated event` | ✅ 可自行增强 |
| 落地难度 | 改 3 个环境变量即可 | 需写 hook 脚本 |
| 适用场景 | 团队级成本/Token 统计 | Skill 级质量分析与模型选型 |
| 存储后端 | OTel Collector / Grafana / Datadog | 本地 JSONL / SQLite |

实战中两者并用最香：**OTel 管宏观成本，Hook 管微观质量**。

## 步骤一：开启原生 OpenTelemetry（5 分钟速赢）

Claude Code 把指标作为时间序列、事件作为日志/事件协议、（可选）分布式链路作为 trace 协议导出。在 shell 里配置环境变量即可：

```bash
# 1. 打开总开关
export CLAUDE_CODE_ENABLE_TELEMETRY=1

# 2. 选择导出器（metrics 与 logs 各自可选）
export OTEL_METRICS_EXPORTER=otlp      # 可选: otlp / prometheus / console / none
export OTEL_LOGS_EXPORTER=otlp         # 可选: otlp / console / none

# 3. 指定 OTLP 端点
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

# 4. 鉴权（如需）
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer your-token"

# 5. 调试期缩短导出间隔（生产请还原）
export OTEL_METRIC_EXPORT_INTERVAL=10000   # 10 秒，默认 60s
export OTEL_LOGS_EXPORT_INTERVAL=5000      # 5 秒，默认 5s

claude
```

管理员也可在 **managed settings** 的 `env` 块里集中下发同样的变量，实现全组织统一管控。

**预期结果：** Claude Code 会周期性吐出官方指标。其中与你 Skill 最相关的是这一组——

- **Session counter** / **Active time counter**：会话数与活跃时长
- **Token counter** / **Cost counter**：Token 与成本
- **Skill activated event**：**Skill 被激活时直接触发的事件**（本教程的关键锚点）
- **Hook execution start/complete event**、**Compaction event**：钩子执行与上下文压缩

**常见问题：** 看不到数据？把 `OTEL_METRICS_EXPORTER=console` 切到控制台导出，先把 JSON 打到终端确认格式正确，再切回 otlp。企业代理环境下记得配 `OTEL_EXPORTER_OTLP_HEADERS` 做鉴权。

## 步骤二：用 Hook 零侵入捕获 Skill 调用

原生 OTel 给的是聚合指标，但要做 **Skill 级**的质量分析（每次调用单独成行），最干净的办法是挂 Hook。Hook 是独立进程，**完全不需要改动 Skill 本身**——这正是"零侵入"的含义。

在 `.claude/settings.json` 注册四类核心事件：

```json
{
  "hooks": {
    "UserPromptSubmit": [{
      "hooks": [{
        "type": "command",
        "command": "node ~/.skill-otel/hooks/on-prompt.js",
        "timeout": 10
      }]
    }],
    "PreToolUse": [{
      "hooks": [{
        "type": "command",
        "command": "node ~/.skill-otel/hooks/pre-tool.js",
        "timeout": 10
      }]
    }],
    "PostToolUse": [{
      "hooks": [{
        "type": "command",
        "command": "node ~/.skill-otel/hooks/post-tool.js",
        "timeout": 10
      }]
    }],
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "node ~/.skill-otel/hooks/on-stop.js",
        "timeout": 10
      }]
    }]
  }
}
```

这套结构与社区项目 `claude-code-telemetry` 完全一致：`UserPromptSubmit` 开一条 trace，`PreToolUse`/`PostToolUse` 包住每一次工具调用的起止耗时，`Stop` 关闭会话 span。如果你想监控 Subagent 编排，再加 `SubagentStart` / `SubagentStop` 即可（官方支持的全部 12 类事件）。

## 步骤三：读懂 Hook 的 stdin 契约

Claude Code 把一个 JSON 对象通过 **stdin** 喂给 hook 脚本。这是整个采集链路的命脉，字段在不同事件类型下略有不同：

```js
// pre-tool.js：从 stdin 读入 Claude Code 推送的事件
const fs = require('fs');
let raw = '';
process.stdin.on('data', c => raw += c);
process.stdin.on('end', () => {
  const evt = JSON.parse(raw);
  // 核心字段（按事件类型出现）：
  // evt.hook_event_name   → "PreToolUse" | "PostToolUse" | ...
  // evt.session_id        → 会话唯一 ID（trace 根）
  // evt.transcript_path   → 本次会话的 JSONL 转写文件
  // evt.cwd               → 工作目录
  // evt.tool_name         → 工具名（Skill 触发多经 Task/MCP，需结合 Skill 事件判定）
  // evt.tool_input        → 工具入参（脱敏后落盘！）
  // evt.tool_use_id       → 工具调用唯一 ID（span 关联键）
  // evt.tool_response     → 仅 PostToolUse 有，工具返回
  // evt.agent_id / evt.agent_type → Subagent 相关
  // evt.model             → 当前模型
  appendSpan(evt);            // 你的落盘逻辑
});
```

**关键约束：** hook 的 stdout 可以输出一个 JSON 来反控 Claude Code（如 `{"decision":"block","reason":"..."}` 阻断危险命令）；但**纯采集**用途下，务必让脚本在 `timeout`（这里 10 秒）内干净退出，否则会拖慢每一次工具调用。采集脚本里**不要**做网络阻塞调用，先落本地 JSONL，再异步搬运。

## 步骤四：构建 append-only JSONL 采集器

落盘格式选 **append-only JSONL + 按日期轮转**，好处是无需数据库、可 `tail -f` 实时看、损坏只丢一行。核心写入器：

```js
// lib/jsonl-exporter.js
const fs = require('fs');
const path = require('path');

class JsonlExporter {
  constructor(logDir = '~/.skill-otel/logs', maxSizeMB = 50) {
    this.logDir = logDir.replace('~', process.env.HOME);
    this.maxBytes = maxSizeMB * 1024 * 1024;
    if (!fs.existsSync(this.logDir)) fs.mkdirSync(this.logDir, { recursive: true });
  }
  file() {
    const d = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
    return path.join(this.logDir, `telemetry-${d}.jsonl`);
  }
  write(rec) {
    const f = this.file();
    // 敏感字段先脱敏：password/token/secret/key
    const safe = this.redact(rec);
    fs.appendFileSync(f, JSON.stringify(safe) + '\n');
    if (fs.statSync(f).size > this.maxBytes) this.rotate(f);
  }
  redact(o) { /* 递归把 key 匹配 /password|token|secret|key/i 的值替换为 *** */ }
  rotate(f) { fs.renameSync(f, f.replace(/\.jsonl$/, `-${Date.now()}.jsonl`)); }
}
module.exports = { JsonlExporter };
```

`PreToolUse` 写一条 `start` 记录并记下 `tool_use_id`；`PostToolUse` 用同一个 `tool_use_id` 回填 `end` 时间与返回摘要。跨进程的 span 拼接，社区方案是给每个未闭合 span 写一个 `.pending-spans/<spanId>.json`，收到配对事件后删除——这避免了任何内存状态。

## 步骤五：计算 Skill 质量指标

有了 JSONL，聚合就是几行流式统计。围绕 Skill 定义四类核心质量指标：

```js
// dashboard/metrics.js
function summarize(events) {
  const bySkill = {};
  for (const e of events) {
    const s = bySkill[e.skill] ||= { calls: 0, ok: 0, dur: 0, tokens: 0, cost: 0 };
    if (e.kind === 'start') { s.calls++; s._start = e.ts; }
    if (e.kind === 'end') {
      if (!e.error) s.ok++;
      s.dur += e.ts - s._start;
      s.tokens += e.tokens; s.cost += e.cost;
    }
  }
  return Object.entries(bySkill).map(([skill, s]) => ({
    skill,
    calls: s.calls,
    successRate: s.calls ? (s.ok / s.calls * 100).toFixed(1) + '%' : '—',
    avgMs: s.calls ? Math.round(s.dur / s.calls) : 0,
    tokensPerCall: s.calls ? Math.round(s.tokens / s.calls) : 0,
    costPerCall: s.calls ? (s.cost / s.calls).toFixed(4) : 0,
  }));
}
```

输出形如下表，一眼看出哪个 Skill 在亏 Token、哪个在失败：

| skill | calls | successRate | avgMs | tokensPerCall | costPerCall |
|-------|-------|-------------|-------|---------------|-------------|
| architect | 42 | 97.6% | 8120 | 6200 | $0.031 |
| sql-optimizer | 118 | 84.7% | 2310 | 850 | $0.004 |
| log-rca | 9 | 100% | 14500 | 9800 | $0.052 |

**洞察示例：** `sql-optimizer` 调用最多但成功率最低——这是 Evals 看不出的"线上退化"，该回去补案例；`log-rca` 成功率满分却吃掉最多 Token，候选降级到 Sonnet。

## 步骤六：渲染 CLI 看板

无需前端，一个终端看板就能驱动决策：

```bash
# Skill 用量报告
node dashboard.js view --report skills
# 按日期 + Skill 过滤
node dashboard.js view --report skills --date 2026-06-17 --skill architect
# 单条 trace 的瀑布视图（Jaeger 风格）
node dashboard.js view --report trace --trace-id <id>
# JSON 输出，方便接 CI
node dashboard.js view --report skills --format json
```

进阶可像 `claude-code-telemetry` 那样起 `docker compose up -d`，拿到 Grafana 三张预置看板：**Skill Performance**（调用数/耗时/成功率，成功率按红黄绿阈值染色）、**Token Analysis**（Token 时序 + 上下文预算剩余 gauge）、**Model Comparison**（Skill → 推荐模型），外加一个 `/trace/:traceId` 的瀑布回溯。

## 步骤七：数据驱动的模型选型

这是可观测性的终极回报：不再凭感觉，而是用观测数据为每个 Skill 挑模型。社区项目用一套规则评分做推荐，阈值可直接借鉴：

| 推荐模型 | 单次 Token | 上下文占比 | 其他特征 |
|----------|-----------|-----------|----------|
| Opus | ≥ 5000 | ≥ 30% | 复杂度高 |
| Sonnet | 1000–5000 | 10–30% | 成功率 90–98% |
| Haiku | ≤ 1000 | ≤ 10% | 成功率 ≥ 98% |
| Gemini | ≥ 3000 | ≥ 40% | 成功率 ≥ 95% |

把步骤五的 `tokensPerCall`、`successRate` 喂进这张表，每个 Skill 自动得到一个"该用哪档模型"的建议。Token 占比低、成功率高的 Skill（如 `sql-optimizer`）从 Opus 降到 Haiku，单次成本可砍掉一个数量级——这就是数据替你省钱。

## 整体架构（Mermaid）

```mermaid
graph TB
    subgraph 采集["数据采集（零侵入）"]
      A[Claude Code 会话] -->|Skill activated event| OT[原生 OTel]
      A -->|stdin JSON| H[Hook 脚本]
      H -->|append-only| L[telemetry-YYYY-MM-DD.jsonl]
    end
    subgraph 存储["存储"]
      OT -->|OTLP gRPC| COL[OTel Collector]
      L -->|按日轮转| DIS[本地磁盘]
    end
    subgraph 分析["分析与可视化"]
      DIS --> M[质量指标聚合]
      M --> CLI[CLI 看板]
      DIS --> TR[Jaeger 式 Trace 瀑布]
      M --> REC[模型选型推荐]
      COL --> G[Grafana 成本/Token 看板]
    end
    REC -.->|反馈| A
```

## 实战案例：最小可观测性套件

把上述步骤拼成一个可跑的端到端示例：

1. **建目录**：`mkdir -p ~/.skill-otel/{hooks,lib,dashboard}`，放入 `pre-tool.js`、`post-tool.js`、`jsonl-exporter.js`、`metrics.js`、`dashboard.js`。
2. **注册 Hook**：把步骤二的 JSON 写进 `~/.claude/settings.json`，重启 Claude Code。
3. **触发 Skill**：在会话里运行你的 Skill 一次（如 `/your-skill ...`）。
4. **看数据**：`cat ~/.skill-otel/logs/telemetry-$(date +%F).jsonl | jq .` 应看到 Skill 的 start/end 成对记录。
5. **跑看板**：`node ~/.skill-otel/dashboard/dashboard.js view --report skills`，确认成功率/耗时/Token 出表。
6. **决策**：把成功率 < 90% 的 Skill 回灌 Evals 补案例；把 Token 偏低、成功率高的 Skill 在 frontmatter 里切到更便宜的模型。

整套实现控制在 ~200 行，零数据库依赖，落地一个工作日内完成。

## 常见问题

- **Hook 把工具调用拖慢了**：检查脚本是否有同步网络请求或重计算；采集一律先 `appendFile` 落盘再异步处理。把 `timeout` 设为 10s 是上限而非目标，正常应 < 100ms 退出。
- **Skill 没被识别成 Skill 调用**：Skill 激活走的是 `Skill activated event`（OTel）而非某个 `tool_name`；若用 Hook 判定，需结合 `transcript_path` 解析或直接订阅原生 Skill 事件。
- **JSONL 越堆越大**：开启按日轮转 + 大小阈值（如 50MB）双保险；老文件按月归档即可，无需实时库。
- **脱敏不彻底**：默认对 `password/token/secret/key` 字段脱敏，但 `tool_input` 里可能藏着 prompt 中的密钥——落盘前过一遍正则。
- **多并发 Agent 串号**：span 关联必须用 Claude Code 给的 `tool_use_id`/`session_id`，**不要**自造 ID；Subagent 场景额外带上 `agent_id`。

## 延伸阅读

- **Claude Code Monitoring 官方文档**：`code.claude.com/docs/en/monitoring-usage`——OTel 全量配置、指标与事件清单（含 `Skill activated event`）
- **claude-code-hooks-multi-agent-observability**（disler）：12 类 Hook 事件的全量实时 WebSocket 看板实现
- **claude-code-telemetry**（nnaveenraju）：本地优先 JSONL + Jaeger 瀑布 + 模型推荐器，本教程架构直接受其启发

## 训练营适用性评估

- **适合类型**：中高级开发者训练营（"把 AI Agent 工程化"方向）；不推荐纯入门营（需先有 Hook 与 Skill 基础）。
- **教学时长**：含动手实操 4–5 学时；其中步骤一（OTel 速赢）0.5 学时、步骤二–四（Hook+采集器）2 学时、步骤五–七（指标+看板+模型选型）2 学时。
- **评估产出**：学员需提交一个能跑出"Skill 成功率/Token/成本"三表的看板截图，并对至少一个自有 Skill 给出模型选型建议——以"用数据说话"作为结业标准。

## 参考来源

1. [Monitoring — Claude Code Docs](https://code.claude.com/docs/en/monitoring-usage) — 官方 OpenTelemetry 配置、可用指标与事件清单（含 Skill activated event、Hook execution 事件、Token/Cost counter）
2. [claude-code-telemetry — GitHub](https://github.com/nnaveenraju/claude-code-telemetry) — 本地优先的 Skill 调用遥测系统，提供 JSONL 采集、Jaeger 瀑布看板与规则式模型推荐（Opus/Sonnet/Haiku 阈值表来源）
3. [claude-code-hooks-multi-agent-observability — GitHub](https://github.com/disler/claude-code-hooks-multi-agent-observability) — 全量 12 类 Hook 事件的实时可观测性实现，含 `send_event.py` 的 stdin 契约与 settings.json 配置范例
4. [Claude Code Metrics Dashboard: Grafana Setup (2026) — Sealos](https://sealos.io/blog/claude-code-metrics/) — 用 Docker Compose + Grafana 搭建成本/Token/会话看板的实操指南

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
