Skip to content

Commit 60c560a

Browse files
authored
Merge pull request #56 from fangxiu-wf/fix/openclaw-span-lifecycle-and-custom-spanprocessor
feat(openclaw): custom SpanProcessor injection + fix runaway ENTRY/AGENT span durations
2 parents 25c7f8b + fd60384 commit 60c560a

13 files changed

Lines changed: 1533 additions & 17 deletions

opentelemetry-instrumentation-openclaw/CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@
22

33
本文档记录 `opentelemetry-instrumentation-openclaw` 的重要变更。
44

5+
## [0.1.5-beta] - 2026-07-14
6+
7+
### 新增
8+
9+
- **自定义 SpanProcessor 注入(`spanProcessorModule`**
10+
- 新增配置项 `spanProcessorModule`(或环境变量 `ARMS_SPAN_PROCESSOR_MODULE`),指向一个默认导出 `SpanProcessor` 的 JS/MJS 模块;绝对路径原样使用,相对路径相对 `OPENCLAW_HOME``~/.openclaw`)解析
11+
- 用于注入依赖 span 类型/内容的动态属性(如按 model 判定成本档位、按工具名判定工具类别),或将 span 转发到额外后端;补足 `globalSpanAttributes` 仅支持静态值的空缺
12+
- 通过包子路径导出 helper:`@loongsuite/opentelemetry-instrumentation-openclaw/span-processor` 提供 `defineGenAiSpanProcessor`,按 GenAI span 类型分派(`onLlmEnding`/`onToolEnding`/`onAgentEnding`/`onStepEnding`/`onEntryEnding`),并屏蔽语义规范方言差异
13+
- 加载失败/校验不通过时优雅降级到内置 processor;用户回调全程 try/catch 隔离,绝不影响内置导出管道
14+
- **`openclaw.trace.close_reason` 诊断属性**:ENTRY span 上标注 trace 关闭方式(`normal` / `runid_recovered` / `stale_sweeper`),便于在 ARMS 中定位异常关闭的调用链
15+
16+
### 修复
17+
18+
- **修复跨天巨长 ENTRY/AGENT span(时长被拉到 1d+)**
19+
- 根因:`agent_end` 仅按 channel 归属 context,当其解析到的 channel 与 `llm_input` 注册的 channel 不一致时,找不到本轮 context、关不掉已开的 ENTRY/AGENT span;这些泄漏的 span 在内存中滞留,直到之后某次 `agent_end` 或会话 reset 时以 `Date.now()` 被误关,产生天级时长
20+
- 主修:`agent_end` 在 channel 解析不到可关闭 span 时,用 openclaw 附带的 `runId` 兜回本轮 context(channel 优先、runId 补充,不改动常见路径)
21+
- 加固:过期上下文清扫器(sweeper)对仍开放的 ENTRY/AGENT/STEP span 主动强制关闭,使用**有界的最后活动时间**而非 `Date.now()`,并彻底清理所有查找结构
22+
- **修复工具 span 因 channel 错配被静默丢弃**`before_tool_call``agent/` 通道无锚点时,用 `runId` 兜回 context 恢复工具 span(并对 `runId==sessionId` 且上下文正在关闭的场景加守卫,避免挂到上一轮 trace)
23+
24+
### 说明
25+
26+
- 新增单元测试:`span-processor.test.ts`(类型分派/方言容错/优雅降级/运行时隔离)、`channel-mismatch-repro.test.ts`(工具 span channel 错配恢复)、`agent-end-stale-close.test.ts``agent_end` runId 兜回 + sweeper 有界强关)
27+
- `spanProcessorModule` 会加载并执行本地任意代码,仅指向可信模块
28+
29+
---
30+
531
## [0.1.4-beta] - 2026-05-26
632

733
### 新增

opentelemetry-instrumentation-openclaw/README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ When a config field is not set in `openclaw.json`, the plugin falls back to envi
102102
| `ARMS_ENABLE_TRACE_PROPAGATION` | `enableTracePropagation` | Enable W3C Trace Context propagation (`true` / `1`) |
103103
| `OTEL_RESOURCE_ATTRIBUTES` | `resourceAttributes` | Custom resource attributes (`key1=value1,key2=value2`) |
104104
| `OTEL_SPAN_ATTRIBUTES` | `globalSpanAttributes` | Global span attributes injected to all spans (`key1=value1,key2=value2`) |
105+
| `ARMS_SPAN_PROCESSOR_MODULE` | `spanProcessorModule` | Path to a module whose default export is a custom SpanProcessor |
105106

106107
Priority: **config file > environment variable > default value**
107108

@@ -196,6 +197,87 @@ For `resourceAttributes`, config file values override environment variable value
196197

197198
---
198199

200+
## Custom SpanProcessor (Dynamic Attributes)
201+
202+
`globalSpanAttributes` only supports **static** values. When you need attributes
203+
that depend on span type or content (e.g. cost tier by model, tool class by tool
204+
name), or you want to forward spans to an extra backend, inject a custom
205+
`SpanProcessor`.
206+
207+
Point the plugin at a module whose **default export** is a `SpanProcessor`:
208+
209+
```json
210+
{
211+
"config": {
212+
"endpoint": "https://your-otlp-endpoint:4318",
213+
"spanProcessorModule": "./biz-span-processor.mjs"
214+
}
215+
}
216+
```
217+
218+
- Absolute paths are used as-is; relative paths resolve against `~/.openclaw`.
219+
- Env fallback: `ARMS_SPAN_PROCESSOR_MODULE`.
220+
- If the module fails to load or is invalid, the plugin logs an error and keeps
221+
working with the built-in processor only (graceful degradation).
222+
223+
### Recommended: the helper API
224+
225+
The package exports a helper that dispatches by GenAI span type and hides both
226+
the OTel SDK details and the semantic-convention dialect. Write
227+
`~/.openclaw/biz-span-processor.mjs`:
228+
229+
```js
230+
import { defineGenAiSpanProcessor } from
231+
"@loongsuite/opentelemetry-instrumentation-openclaw/span-processor";
232+
233+
export default defineGenAiSpanProcessor({
234+
onLlmEnding(span, { model }) {
235+
span.setAttribute("business.cost_tier",
236+
model?.includes("gpt-4") ? "premium" : "standard");
237+
},
238+
onToolEnding(span, { toolName }) {
239+
span.setAttribute("business.tool_class",
240+
toolName?.startsWith("mcp_") ? "mcp" : "native");
241+
},
242+
// also available: onAgentEnding / onStepEnding / onEntryEnding
243+
});
244+
```
245+
246+
Each hook fires at **`onEnding`**, where the span is still writable and all
247+
attributes are populated.
248+
249+
### Advanced: a raw SpanProcessor
250+
251+
For full lifecycle control (e.g. forwarding to another backend), export a
252+
standard `SpanProcessor`:
253+
254+
```js
255+
export default {
256+
onStart(span, parentContext) {},
257+
onEnding(span) { span.setAttribute("business.env", process.env.BIZ_ENV ?? "prod"); },
258+
onEnd(readableSpan) { /* read-only: observe / forward */ },
259+
forceFlush() { return Promise.resolve(); },
260+
shutdown() { return Promise.resolve(); },
261+
};
262+
```
263+
264+
### Hook points
265+
266+
| Hook | Span writable | Attributes available | Use for |
267+
|---|---|---|---|
268+
| `onStart(span)` | Yes | Start-time attributes (incl. `gen_ai.span.kind`); end-time attributes not yet set | Static / env attributes |
269+
| `onEnding(span)` | Yes | **All attributes** | **Recommended**: dynamic, type-based injection |
270+
| `onEnd(readableSpan)` | No (read-only) | All attributes | Observation, logging, forwarding |
271+
272+
> Writing attributes in `onEnd` via `readableSpan.attributes[...] = ...` is an
273+
> unsupported hack that bypasses SDK/plugin truncation and validation. Always
274+
> write in `onEnding`.
275+
276+
> **Security**: `spanProcessorModule` loads and executes arbitrary local code.
277+
> Only point it at modules you trust.
278+
279+
---
280+
199281
## Uninstall
200282
201283
```bash

opentelemetry-instrumentation-openclaw/openclaw.plugin.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "opentelemetry-instrumentation-openclaw",
33
"name": "OpenClaw OTel Plugin",
4-
"version": "0.1.4-beta",
4+
"version": "0.1.5-beta",
55
"description": "Report OpenClaw AI agent execution traces to any OTLP-compatible backend via OpenTelemetry",
66
"type": "plugin",
77
"entry": "./dist/index.js",
@@ -60,6 +60,10 @@
6060
"type": "array",
6161
"items": { "type": "string" },
6262
"description": "URL substrings for outbound traceparent injection (e.g. [\"api.openai.com\"]). Injects into all HTTPS calls except the OTLP endpoint if omitted."
63+
},
64+
"spanProcessorModule": {
65+
"type": "string",
66+
"description": "Path to a JS/MJS module whose default export is a custom SpanProcessor. Absolute paths are used as-is; relative paths resolve against ~/.openclaw. Falls back to ARMS_SPAN_PROCESSOR_MODULE env var. Load failures degrade gracefully to the built-in processor."
6367
}
6468
}
6569
}

opentelemetry-instrumentation-openclaw/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

opentelemetry-instrumentation-openclaw/package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@loongsuite/opentelemetry-instrumentation-openclaw",
3-
"version": "0.1.4-beta",
3+
"version": "0.1.5-beta",
44
"description": "OpenTelemetry instrumentation for OpenClaw — report AI agent execution traces to any OTLP-compatible backend",
55
"type": "module",
66
"license": "Apache-2.0",
@@ -25,6 +25,17 @@
2525
"loongsuite"
2626
],
2727
"main": "dist/index.js",
28+
"types": "dist/index.d.ts",
29+
"exports": {
30+
".": {
31+
"types": "./dist/index.d.ts",
32+
"import": "./dist/index.js"
33+
},
34+
"./span-processor": {
35+
"types": "./dist/span-processor.d.ts",
36+
"import": "./dist/span-processor.js"
37+
}
38+
},
2839
"files": [
2940
"dist",
3041
"openclaw.plugin.json"

opentelemetry-instrumentation-openclaw/src/arms-exporter.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import {
1111
import {
1212
BasicTracerProvider,
1313
BatchSpanProcessor,
14+
type SpanProcessor,
1415
} from "@opentelemetry/sdk-trace-base";
1516
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
1617
import { resourceFromAttributes } from "@opentelemetry/resources";
1718
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
1819
import { hostname } from "node:os";
1920
import { basename } from "node:path";
21+
import { loadUserSpanProcessor } from "./span-processor-loader.js";
2022
import type {
2123
ArmsTraceConfig,
2224
OpenClawPluginApi,
@@ -100,9 +102,22 @@ export class ArmsExporter {
100102
scheduledDelayMillis: this.config.flushIntervalMs,
101103
});
102104

105+
// The built-in processor stays first so the export pipeline keeps working
106+
// even if a user-provided processor fails to load or throws at runtime.
107+
const spanProcessors: SpanProcessor[] = [spanProcessor];
108+
if (this.config.spanProcessorModule) {
109+
const userProcessor = await loadUserSpanProcessor(
110+
this.config.spanProcessorModule,
111+
this.api,
112+
);
113+
if (userProcessor) {
114+
spanProcessors.push(userProcessor);
115+
}
116+
}
117+
103118
this.provider = new BasicTracerProvider({
104119
resource,
105-
spanProcessors: [spanProcessor],
120+
spanProcessors,
106121
});
107122

108123
// Intentionally NOT calling provider.register() to avoid overriding

0 commit comments

Comments
 (0)