Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions opentelemetry-instrumentation-openclaw/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

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

## [0.1.4-beta] - 2026-05-15

### 新增

- **事件级 JSONL 输出**:与 `loongsuite-pilot` 集成的事件级数据通道。在 OTLP trace 上报之外,插件可同时把每条 LLM/工具事件按 `event_t` schema 写入本地 JSONL 文件,供 pilot 增量读取并扇出到 SLS / JSONL / HTTP。
- 配置字段:`log_enabled` / `log_dir` / `log_filename_format`(目前仅支持 `'hook'`)/ `captureMessageContent`
- 共享配置文件:`~/.openclaw/otel-config.json`(供插件与 pilot 协商;`captureMessageContent=false` 默认不写消息内容)
- 环境变量降级:`OPENCLAW_LOG_ENABLED` / `OPENCLAW_LOG_DIR` / `OPENCLAW_CAPTURE_MESSAGE_CONTENT` / `OPENCLAW_TELEMETRY_DEBUG`
- 工作模式:`endpoint` 与 `log_enabled` 至少一项必填;两者可同时启用(双模式),也可单独启用 JSONL 路径(无需 OTLP endpoint)
- 文件命名:`<log_dir>/openclaw-YYYY-MM-DD.jsonl`,与 pilot `BaseHookInput` 的 hook 模式对齐
- 新增 `src/jsonl-emitter.ts`:JSONL 写盘 + event_t 字段构造工具
- 新增 `src/jsonl-hooks.ts`:独立 hook 监听器集,与 OTLP trace 路径完全解耦

### 测试

- 新增 `test/jsonl-emitter.test.ts`(14 个用例,覆盖字段构造、写盘、错误兜底、共享 config 解析)
- 新增 `test/integration-jsonl.test.ts`(4 个端到端用例,模拟一轮 LLM + tool 调用、双触发去重、消息内容采集开关、跨轮 step 自增)

## [0.1.3-beta] - 2026-05-07

### 背景
Expand Down
52 changes: 52 additions & 0 deletions opentelemetry-instrumentation-openclaw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,58 @@ For `resourceAttributes`, config file values override environment variable value

---

## Event-level JSONL Output (loongsuite-pilot Integration)

In addition to OTLP traces, the plugin can emit each LLM/tool event as a JSONL line in the [`event_t` schema](https://code.alibaba-inc.com/yt348264/ai-agent-audit/blob/main/docs/guide/architecture.md), suitable for ingestion by [`loongsuite-pilot`](https://github.com/sls-loongsuite/loongsuite-pilot) (SLS / JSONL / HTTP fan-out).

### Modes

| `endpoint` | `log_enabled` | Behavior |
|---|---|---|
| set | unset / false | OTLP-only (existing default behavior) |
| unset | true | JSONL-only (no OTLP) |
| set | true | Dual-mode (both paths run independently) |
| unset | unset / false | Plugin refuses to activate |

### Shared config: `~/.openclaw/otel-config.json`

Used by both pilot installer and the plugin (independent of `~/.openclaw/openclaw.json`).

```json
{
"log_enabled": true,
"log_dir": "~/.loongsuite-pilot/logs/openclaw",
"log_filename_format": "hook",
"captureMessageContent": false
}
```

| Field | Type | Default | Description |
|---|---|---|---|
| `log_enabled` | boolean | `false` | Enable JSONL emission |
| `log_dir` | string | — | Output directory; supports `~` expansion |
| `log_filename_format` | string | `"hook"` | Output filename format. Currently only `"hook"` is supported, producing `<log_dir>/openclaw-YYYY-MM-DD.jsonl` |
| `captureMessageContent` | boolean | `false` | When `true`, include `gen_ai.input.messages` / `gen_ai.output.messages` in JSONL records |

Plugin-config priority for any field: `openclaw.json plugins.entries.config` > `~/.openclaw/otel-config.json` > env > default.

### Environment variables

| Variable | Equivalent |
|---|---|
| `OPENCLAW_LOG_ENABLED` | `log_enabled` |
| `OPENCLAW_LOG_DIR` | `log_dir` |
| `OPENCLAW_CAPTURE_MESSAGE_CONTENT` | `captureMessageContent` |
| `OPENCLAW_TELEMETRY_DEBUG` | `debug` (JSONL-only mode) |

### JSONL schema (per record)

Each record is one JSON object with `event.name` ∈ `{llm.request, llm.response, tool.call, tool.result}`. Required fields: `time_unix_nano`, `event.id`, `event.name`, `session.id`, `user.id`, `agent.type` (always `"openclaw"`), `turn.id`, `step.id`. Optional fields per event type include `gen_ai.provider.name`, `gen_ai.request.model`, `gen_ai.response.model`, `gen_ai.usage.*_tokens`, `gen_ai.tool.name`, `gen_ai.tool.call.id`, `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result`, `tool.result.duration`, `tool.result.status`, `error.type`, `error.message`.

Files are appended via `fs.appendFileSync`; write failures are logged at warn-level and never propagate to the gateway.

---

## Uninstall

```bash
Expand Down
2 changes: 1 addition & 1 deletion opentelemetry-instrumentation-openclaw/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@loongsuite/opentelemetry-instrumentation-openclaw",
"version": "0.1.3-beta",
"version": "0.1.4-beta",
"description": "OpenTelemetry instrumentation for OpenClaw — report AI agent execution traces to any OTLP-compatible backend",
"type": "module",
"license": "Apache-2.0",
Expand Down
97 changes: 94 additions & 3 deletions opentelemetry-instrumentation-openclaw/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import {
GEN_AI_OUTPUT_MESSAGES,
ERROR_TYPE,
} from "@loongsuite/opentelemetry-util-genai";
import { JsonlEmitter, readSharedOtelConfig } from "./jsonl-emitter.js";
import { registerJsonlHooks } from "./jsonl-hooks.js";

// ---------------------------------------------------------------------------
// Constants
Expand Down Expand Up @@ -386,7 +388,14 @@ const armsTracePlugin: OpenClawPlugin = {
activate(api: OpenClawPluginApi) {
const pluginConfig = asRecord(api.pluginConfig) || {};
const legacyConfig = getPluginEntryConfig(api.config, LEGACY_PLUGIN_ID);
const resolvedConfig = mergeDefinedValues(legacyConfig || {}, pluginConfig);
// Shared otel-config.json is the pilot integration contract: pilot installer
// writes log_dir/log_enabled there; users may also place their own settings.
// Priority: pluginConfig (openclaw.json plugins.entries) > otel-config.json > env > default.
const sharedOtelConfig = readSharedOtelConfig("~/.openclaw/otel-config.json");
const resolvedConfig = mergeDefinedValues(
mergeDefinedValues(legacyConfig || {}, sharedOtelConfig),
pluginConfig,
);
const endpoint = pluginConfig.endpoint as string | undefined;
const endpointResolved = resolvedConfig.endpoint as string | undefined;

Expand All @@ -412,9 +421,62 @@ const armsTracePlugin: OpenClawPlugin = {
);
}

if (!finalEndpoint) {
// Resolve JSONL emission settings (independent of OTLP).
const finalLogEnabled =
(resolvedConfig.log_enabled as boolean | undefined) ??
(process.env.OPENCLAW_LOG_ENABLED === "true"
|| process.env.OPENCLAW_LOG_ENABLED === "1"
|| false);
const finalLogDir =
(resolvedConfig.log_dir as string | undefined)
|| process.env.OPENCLAW_LOG_DIR
|| "";
const finalLogFilenameFormat =
((resolvedConfig.log_filename_format as string | undefined) === "hook"
? "hook"
: "hook") as "hook";
const finalCaptureMessageContent =
(resolvedConfig.captureMessageContent as boolean | undefined)
|| process.env.OPENCLAW_CAPTURE_MESSAGE_CONTENT === "true"
|| process.env.OPENCLAW_CAPTURE_MESSAGE_CONTENT === "1"
|| false;

if (!finalEndpoint && !finalLogEnabled) {
api.logger.error(
"[ArmsTrace] Missing required configuration: 'endpoint' must be provided (config or ARMS_OTLP_ENDPOINT env)",
"[ArmsTrace] Missing required configuration: at least one of 'endpoint' (OTLP) or 'log_enabled' (JSONL for pilot) must be provided",
);
return;
}

// JSONL-only mode: register JSONL hook listeners and skip OTLP setup.
if (!finalEndpoint && finalLogEnabled) {
if (!finalLogDir) {
api.logger.error(
"[ArmsTrace] log_enabled=true but 'log_dir' is missing (config or OPENCLAW_LOG_DIR env)",
);
return;
}
const finalDebugJsonlOnly =
(resolvedConfig.debug as boolean | undefined)
|| process.env.OPENCLAW_TELEMETRY_DEBUG === "true"
|| process.env.OPENCLAW_TELEMETRY_DEBUG === "1"
|| false;
const jsonlOnlyEmitter = new JsonlEmitter({
logDir: finalLogDir,
filenameFormat: finalLogFilenameFormat,
captureMessageContent: finalCaptureMessageContent,
logger: api.logger,
});
const enabledHooksList = resolvedConfig.enabledHooks as string[] | undefined;
const disposeJsonlHooks = registerJsonlHooks(api, jsonlOnlyEmitter, {
enabledHooks: enabledHooksList,
debug: finalDebugJsonlOnly,
});
api.on("gateway_stop", () => {
try { disposeJsonlHooks(); } catch { /* ignore */ }
});
api.logger.info(
`[ArmsTrace] Plugin activated in JSONL-only mode (log_dir: ${finalLogDir})`,
);
return;
}
Expand Down Expand Up @@ -466,6 +528,32 @@ const armsTracePlugin: OpenClawPlugin = {

const exporter = new ArmsExporter(api, config);

// Dual-mode: when log_enabled is true alongside OTLP, register a parallel
// JSONL hook-listener set so pilot can ingest event-level JSONL while
// OTLP traces go to the configured backend.
let disposeJsonlInDualMode: (() => void) | null = null;
if (finalLogEnabled) {
if (finalLogDir) {
const dualModeEmitter = new JsonlEmitter({
logDir: finalLogDir,
filenameFormat: finalLogFilenameFormat,
captureMessageContent: finalCaptureMessageContent,
logger: api.logger,
});
disposeJsonlInDualMode = registerJsonlHooks(api, dualModeEmitter, {
enabledHooks: config.enabledHooks,
debug: config.debug,
});
api.logger.info(
`[ArmsTrace] JSONL emission enabled (log_dir: ${finalLogDir})`,
);
} else {
api.logger.warn(
"[ArmsTrace] log_enabled=true but log_dir is missing; JSONL emission disabled",
);
}
}

if (config.enableTracePropagation) {
installPropagation({
targetUrls: config.propagationTargetUrls,
Expand Down Expand Up @@ -1347,6 +1435,9 @@ const armsTracePlugin: OpenClawPlugin = {
}
pendingToolCalls.clear();
pendingAssistantByTraceId.clear();
if (disposeJsonlInDualMode) {
try { disposeJsonlInDualMode(); } catch { /* ignore */ }
}
await exporter.dispose();
});

Expand Down
Loading
Loading