Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/specs/otel-genai-alignment.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
Enrich the existing `diagnostics-otel` plugin to emit spans, metrics, and events
that follow the [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/)
so that OpenClaw telemetry works out-of-the-box with GenAI-aware backends
(Datadog, Grafana, Langfuse, Arize, etc.) while keeping the existing
(Datadog, Grafana, Orq, LangWatch, Langfuse, Arize, etc.) while keeping the existing
`openclaw.*` operational telemetry intact.

---
Expand Down
15 changes: 14 additions & 1 deletion extensions/diagnostics-otel/src/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,14 @@ describe("diagnostics-otel service", () => {
finishReasons: ["stop"],
sessionKey: "agent:main:main",
sessionId: "sess-001",
usage: { input: 100, output: 50, cacheRead: 80, cacheWrite: 0, total: 230 },
usage: {
input: 100,
output: 50,
cacheRead: 80,
cacheWrite: 0,
promptTokens: 180,
total: 230,
},
durationMs: 1500,
});

Expand All @@ -323,6 +330,12 @@ describe("diagnostics-otel service", () => {
expect(attrs["openclaw.tokens.input"]).toBe(100);
expect(attrs["openclaw.tokens.output"]).toBe(50);
expect(attrs["openclaw.tokens.cache_read"]).toBe(80);
expect(attrs["gen_ai.usage.cache_read.input_tokens"]).toBe(80);
expect(attrs["gen_ai.usage.cache_creation.input_tokens"]).toBe(0);
// gen_ai.usage.input_tokens should be promptTokens (input + cacheRead + cacheWrite)
// per OTEL semconv, not raw input which excludes cached tokens
expect(attrs["gen_ai.usage.input_tokens"]).toBe(180);
expect(attrs["gen_ai.usage.output_tokens"]).toBe(50);

await service.stop?.();
});
Expand Down
20 changes: 19 additions & 1 deletion extensions/diagnostics-otel/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,16 +720,26 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
}
if (typeof usage.output === "number") {
spanAttrs["openclaw.tokens.output"] = usage.output;
spanAttrs["gen_ai.usage.output_tokens"] = usage.output;
}
if (typeof usage.cacheRead === "number") {
spanAttrs["openclaw.tokens.cache_read"] = usage.cacheRead;
spanAttrs["gen_ai.usage.cache_read.input_tokens"] = usage.cacheRead;
}
if (typeof usage.cacheWrite === "number") {
spanAttrs["openclaw.tokens.cache_write"] = usage.cacheWrite;
spanAttrs["gen_ai.usage.cache_creation.input_tokens"] = usage.cacheWrite;
}
if (typeof usage.total === "number") {
spanAttrs["openclaw.tokens.total"] = usage.total;
}
// OTEL GenAI semconv: gen_ai.usage.input_tokens SHOULD include all input
// tokens including cached tokens. Use promptTokens (input + cacheRead +
// cacheWrite) when available, fall back to raw input.
const inputTokensForOtel = usage.promptTokens ?? usage.input;
if (typeof inputTokensForOtel === "number") {
spanAttrs["gen_ai.usage.input_tokens"] = inputTokensForOtel;
}
if (evt.responseModel) {
spanAttrs["gen_ai.response.model"] = evt.responseModel;
}
Expand Down Expand Up @@ -923,21 +933,29 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
};
if (typeof usage.input === "number") {
spanAttrs["openclaw.tokens.input"] = usage.input;
spanAttrs["gen_ai.usage.input_tokens"] = usage.input;
Comment thread
Baukebrenninkmeijer marked this conversation as resolved.
}
if (typeof usage.output === "number") {
spanAttrs["openclaw.tokens.output"] = usage.output;
spanAttrs["gen_ai.usage.output_tokens"] = usage.output;
}
if (typeof usage.cacheRead === "number") {
spanAttrs["openclaw.tokens.cache_read"] = usage.cacheRead;
spanAttrs["gen_ai.usage.cache_read.input_tokens"] = usage.cacheRead;
}
if (typeof usage.cacheWrite === "number") {
spanAttrs["openclaw.tokens.cache_write"] = usage.cacheWrite;
spanAttrs["gen_ai.usage.cache_creation.input_tokens"] = usage.cacheWrite;
}
if (typeof usage.total === "number") {
spanAttrs["openclaw.tokens.total"] = usage.total;
}
// OTEL GenAI semconv: gen_ai.usage.input_tokens SHOULD include all input
// tokens including cached tokens. Use promptTokens (input + cacheRead +
// cacheWrite) when available, fall back to raw input.
const inputTokensForOtel = usage.promptTokens ?? usage.input;
if (typeof inputTokensForOtel === "number") {
spanAttrs["gen_ai.usage.input_tokens"] = inputTokensForOtel;
}
if (evt.responseModel) {
spanAttrs["gen_ai.response.model"] = evt.responseModel;
}
Expand Down