mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
feat(diagnostics-otel): add genai usage span attrs
This commit is contained in:
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
||||
to the state-managed `plugins/installs.json` ledger, with legacy config reads
|
||||
kept as a deprecated compatibility fallback. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: add the GenAI `gen_ai.client.operation.duration` histogram for model-call latency in seconds with bounded provider/model/API and error attributes. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: add GenAI usage token attributes to model-usage spans, including cache read/write input token counts without session identifiers or prompt/response content. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna.
|
||||
- Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as `openclaw.exec` spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna.
|
||||
- Diagnostics/OTEL: support `OPENCLAW_OTEL_PRELOADED=1` so the plugin can reuse an already-registered OpenTelemetry SDK while keeping OpenClaw diagnostic listeners wired. (#71450) Thanks @vincentkoc and @jlapenna.
|
||||
|
||||
@@ -740,6 +740,53 @@ describe("diagnostics-otel service", () => {
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
test("exports GenAI usage attributes on model usage spans without diagnostic identifiers", async () => {
|
||||
const service = createDiagnosticsOtelService();
|
||||
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true });
|
||||
await service.start(ctx);
|
||||
|
||||
emitDiagnosticEvent({
|
||||
type: "model.usage",
|
||||
sessionKey: "session-key",
|
||||
sessionId: "session-id",
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4.6",
|
||||
usage: {
|
||||
input: 100,
|
||||
output: 40,
|
||||
cacheRead: 30,
|
||||
cacheWrite: 20,
|
||||
promptTokens: 150,
|
||||
total: 190,
|
||||
},
|
||||
durationMs: 25,
|
||||
});
|
||||
await flushDiagnosticEvents();
|
||||
|
||||
const modelUsageCall = telemetryState.tracer.startSpan.mock.calls.find(
|
||||
(call) => call[0] === "openclaw.model.usage",
|
||||
);
|
||||
expect(modelUsageCall?.[1]).toMatchObject({
|
||||
attributes: {
|
||||
"gen_ai.usage.input_tokens": 150,
|
||||
"gen_ai.usage.output_tokens": 40,
|
||||
"gen_ai.usage.cache_read.input_tokens": 30,
|
||||
"gen_ai.usage.cache_creation.input_tokens": 20,
|
||||
},
|
||||
});
|
||||
expect(modelUsageCall?.[1]).toEqual({
|
||||
attributes: expect.not.objectContaining({
|
||||
"openclaw.sessionKey": expect.anything(),
|
||||
"openclaw.sessionId": expect.anything(),
|
||||
"gen_ai.input.messages": expect.anything(),
|
||||
"gen_ai.output.messages": expect.anything(),
|
||||
}),
|
||||
startTime: expect.any(Number),
|
||||
});
|
||||
expect(JSON.stringify(modelUsageCall)).not.toContain("session-key");
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
test("exports GenAI client operation duration histogram without diagnostic identifiers", async () => {
|
||||
const service = createDiagnosticsOtelService();
|
||||
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { metrics: true });
|
||||
|
||||
@@ -177,6 +177,21 @@ function genAiOperationName(
|
||||
return "chat";
|
||||
}
|
||||
|
||||
function positiveFiniteNumber(value: number | undefined): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function assignPositiveNumberAttr(
|
||||
attrs: Record<string, string | number>,
|
||||
key: string,
|
||||
value: number | undefined,
|
||||
): void {
|
||||
const normalized = positiveFiniteNumber(value);
|
||||
if (normalized !== undefined) {
|
||||
attrs[key] = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
function assignGenAiModelCallAttrs(
|
||||
attrs: Record<string, string | number | boolean>,
|
||||
evt: ModelCallLifecycleDiagnosticEvent,
|
||||
@@ -933,6 +948,9 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
||||
if (!tracesEnabled) {
|
||||
return;
|
||||
}
|
||||
const genAiInputTokens =
|
||||
usage.promptTokens ??
|
||||
(usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
||||
const spanAttrs: Record<string, string | number> = {
|
||||
...attrs,
|
||||
"openclaw.tokens.input": usage.input ?? 0,
|
||||
@@ -941,6 +959,18 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
||||
"openclaw.tokens.cache_write": usage.cacheWrite ?? 0,
|
||||
"openclaw.tokens.total": usage.total ?? 0,
|
||||
};
|
||||
assignPositiveNumberAttr(spanAttrs, "gen_ai.usage.input_tokens", genAiInputTokens);
|
||||
assignPositiveNumberAttr(spanAttrs, "gen_ai.usage.output_tokens", usage.output);
|
||||
assignPositiveNumberAttr(
|
||||
spanAttrs,
|
||||
"gen_ai.usage.cache_read.input_tokens",
|
||||
usage.cacheRead,
|
||||
);
|
||||
assignPositiveNumberAttr(
|
||||
spanAttrs,
|
||||
"gen_ai.usage.cache_creation.input_tokens",
|
||||
usage.cacheWrite,
|
||||
);
|
||||
|
||||
const span = spanWithDuration("openclaw.model.usage", spanAttrs, evt.durationMs, {
|
||||
parentContext: contextForTrustedDiagnosticSpanParent(evt, metadata),
|
||||
|
||||
Reference in New Issue
Block a user