feat(diagnostics-otel): add content capture controls

Add opt-in diagnostics OTEL content capture controls, keep raw content export default-off, and guard the content-capture tests against magic truncation bounds.
This commit is contained in:
Vincent Koc
2026-04-24 16:41:28 -07:00
committed by GitHub
parent fbf8b216c6
commit d4d4a8c14e
12 changed files with 455 additions and 9 deletions

View File

@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
- Plugins/channels: use manifest `channelConfigs` for read-only external channel discovery when no setup entry is available or setup descriptors declare runtime unnecessary. Thanks @vincentkoc.
- TUI/dependencies: remove direct `cli-highlight` usage from the OpenClaw TUI code-block renderer, keeping themed code coloring without the extra root dependency. Thanks @vincentkoc.
- Diagnostics/OTEL: export run, model-call, and tool-execution diagnostic lifecycle events as OTEL spans without retaining live span state. Thanks @vincentkoc.
- Diagnostics/OTEL: accept opt-in `diagnostics.otel.captureContent` controls for future model/tool content span attributes while keeping raw content export disabled by default. Thanks @vincentkoc.
- Providers/Anthropic Vertex: move the Vertex SDK runtime behind the bundled provider plugin so core no longer owns that provider-specific dependency. Thanks @vincentkoc.
- Plugins/activation: expose activation plan reasons and a richer plan API so callers can inspect why a plugin was selected while preserving existing id-list activation behavior. (#70943) Thanks @vincentkoc.
- Plugins/source metadata: expose normalized install-source facts on provider and channel catalogs so onboarding can explain npm pinning, integrity state, and local availability before runtime loads. (#70951) Thanks @vincentkoc.

View File

@@ -1,4 +1,4 @@
a608561acecc7cfc5f16a31b7498d7a66001f6655f5a5960a68842c59b7dcaa8 config-baseline.json
2936d2ccf0c1e6e932a0e7c617b809e4b31dbb9a7d5afefbba29b229913b9e50 config-baseline.core.json
52af51e35e05d0cbaa1a79fb415f2c2fe56ad5d52a62efa9cbb9c32489d517f5 config-baseline.json
642b4e2c9891e710790313df097b4e0db75a197ec0908e9c03bdc76f5bbdf9b0 config-baseline.core.json
22d7cd6d8279146b2d79c9531a55b80b52a2c99c81338c508104729154fdd02d config-baseline.channel.json
d47a574045a47356e513ab308d7dcad9fa0b389f50e93c5cf0f820fab858e70e config-baseline.plugin.json

View File

@@ -797,6 +797,14 @@ Notes:
logs: false,
sampleRate: 1.0,
flushIntervalMs: 5000,
captureContent: {
enabled: false,
inputMessages: false,
outputMessages: false,
toolInputs: false,
toolOutputs: false,
systemPrompt: false,
},
},
cacheTrace: {
@@ -821,6 +829,7 @@ Notes:
- `otel.traces` / `otel.metrics` / `otel.logs`: enable trace, metrics, or log export.
- `otel.sampleRate`: trace sampling rate `0``1`.
- `otel.flushIntervalMs`: periodic telemetry flush interval in ms.
- `otel.captureContent`: opt-in raw content capture for OTEL span attributes. Defaults to off. Boolean `true` captures non-system message/tool content; the object form lets you enable `inputMessages`, `outputMessages`, `toolInputs`, `toolOutputs`, and `systemPrompt` explicitly.
- `cacheTrace.enabled`: log cache trace snapshots for embedded runs (default: `false`).
- `cacheTrace.filePath`: output path for cache trace JSONL (default: `$OPENCLAW_STATE_DIR/logs/cache-trace.jsonl`).
- `cacheTrace.includeMessages` / `includePrompt` / `includeSystem`: control what is included in cache trace output (all default: `true`).

View File

@@ -279,7 +279,15 @@ works with any OpenTelemetry collector/backend that accepts OTLP/HTTP.
"metrics": true,
"logs": true,
"sampleRate": 0.2,
"flushIntervalMs": 60000
"flushIntervalMs": 60000,
"captureContent": {
"enabled": false,
"inputMessages": false,
"outputMessages": false,
"toolInputs": false,
"toolOutputs": false,
"systemPrompt": false
}
}
}
}
@@ -293,6 +301,9 @@ Notes:
counters/histograms (webhooks, queueing, session state, queue depth/wait).
- Traces/metrics can be toggled with `traces` / `metrics` (default: on). Traces
include model usage spans plus webhook/message processing spans when enabled.
- Raw model/tool content is not exported by default. Use
`diagnostics.otel.captureContent` only when your collector and retention policy
are approved for prompt, response, tool, or system prompt text.
- Set `headers` when your collector requires auth.
- Environment variables supported: `OTEL_EXPORTER_OTLP_ENDPOINT`,
`OTEL_SERVICE_NAME`, `OTEL_EXPORTER_OTLP_PROTOCOL`.
@@ -341,8 +352,17 @@ Queues + sessions:
- `openclaw.model.usage`
- `openclaw.channel`, `openclaw.provider`, `openclaw.model`
- `openclaw.sessionKey`, `openclaw.sessionId`
- `openclaw.tokens.*` (input/output/cache_read/cache_write/total)
- `openclaw.run`
- `openclaw.outcome`, `openclaw.channel`, `openclaw.provider`,
`openclaw.model`, `openclaw.errorCategory`
- `openclaw.model.call`
- `gen_ai.system`, `gen_ai.request.model`, `gen_ai.operation.name`,
`openclaw.provider`, `openclaw.model`, `openclaw.api`,
`openclaw.transport`
- `openclaw.tool.execution`
- `gen_ai.tool.name`, `openclaw.toolName`, `openclaw.errorCategory`,
`openclaw.tool.params.*`
- `openclaw.webhook.processed`
- `openclaw.channel`, `openclaw.webhook`, `openclaw.chatId`
- `openclaw.webhook.error`
@@ -350,11 +370,13 @@ Queues + sessions:
`openclaw.error`
- `openclaw.message.processed`
- `openclaw.channel`, `openclaw.outcome`, `openclaw.chatId`,
`openclaw.messageId`, `openclaw.sessionKey`, `openclaw.sessionId`,
`openclaw.reason`
`openclaw.messageId`, `openclaw.reason`
- `openclaw.session.stuck`
- `openclaw.state`, `openclaw.ageMs`, `openclaw.queueDepth`,
`openclaw.sessionKey`, `openclaw.sessionId`
- `openclaw.state`, `openclaw.ageMs`, `openclaw.queueDepth`
When content capture is explicitly enabled, model/tool spans can also include
bounded, redacted `openclaw.content.*` attributes for the specific content
classes you opted into.
### Sampling + flushing

View File

@@ -123,6 +123,8 @@ const SPAN_ID = "00f067aa0ba902b7";
const CHILD_SPAN_ID = "1111111111111111";
const GRANDCHILD_SPAN_ID = "2222222222222222";
const PROTO_KEY = "__proto__";
const MAX_TEST_OTEL_CONTENT_ATTRIBUTE_CHARS = 4096;
const OTEL_TRUNCATED_SUFFIX_MAX_CHARS = 20;
function createLogger() {
return {
@@ -137,10 +139,13 @@ type OtelContextFlags = {
traces?: boolean;
metrics?: boolean;
logs?: boolean;
captureContent?: NonNullable<
NonNullable<OpenClawPluginServiceContext["config"]["diagnostics"]>["otel"]
>["captureContent"];
};
function createOtelContext(
endpoint: string,
{ traces = false, metrics = false, logs = false }: OtelContextFlags = {},
{ traces = false, metrics = false, logs = false, captureContent }: OtelContextFlags = {},
): OpenClawPluginServiceContext {
return {
config: {
@@ -153,6 +158,7 @@ function createOtelContext(
traces,
metrics,
logs,
...(captureContent !== undefined ? { captureContent } : {}),
},
},
},
@@ -723,6 +729,122 @@ describe("diagnostics-otel service", () => {
await service.stop?.(ctx);
});
test("does not export model or tool content unless capture is explicitly enabled", async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true });
await service.start(ctx);
emitDiagnosticEvent({
type: "model.call.completed",
runId: "run-1",
callId: "call-1",
provider: "openai",
model: "gpt-5.4",
durationMs: 80,
inputMessages: ["private user prompt"],
outputMessages: ["private model reply"],
systemPrompt: "private system prompt",
} as Parameters<typeof emitDiagnosticEvent>[0]);
emitDiagnosticEvent({
type: "tool.execution.completed",
runId: "run-1",
toolName: "read",
toolCallId: "tool-1",
durationMs: 20,
toolInput: "private tool input",
toolOutput: "private tool output",
} as Parameters<typeof emitDiagnosticEvent>[0]);
await flushDiagnosticEvents();
const modelCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0] === "openclaw.model.call",
);
const toolCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0] === "openclaw.tool.execution",
);
expect(modelCall?.[1]).toEqual({
attributes: expect.not.objectContaining({
"openclaw.content.input_messages": expect.anything(),
"openclaw.content.output_messages": expect.anything(),
"openclaw.content.system_prompt": expect.anything(),
}),
startTime: expect.any(Number),
});
expect(toolCall?.[1]).toEqual({
attributes: expect.not.objectContaining({
"openclaw.content.tool_input": expect.anything(),
"openclaw.content.tool_output": expect.anything(),
}),
startTime: expect.any(Number),
});
await service.stop?.(ctx);
});
test("exports bounded redacted content when capture fields are opted in", async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, {
traces: true,
metrics: true,
captureContent: {
enabled: true,
inputMessages: true,
outputMessages: true,
toolInputs: true,
toolOutputs: true,
systemPrompt: true,
},
});
await service.start(ctx);
emitDiagnosticEvent({
type: "model.call.completed",
runId: "run-1",
callId: "call-1",
provider: "openai",
model: "gpt-5.4",
durationMs: 80,
inputMessages: ["use key sk-1234567890abcdef1234567890abcdef"], // pragma: allowlist secret
outputMessages: ["model reply"],
systemPrompt: "system prompt",
} as Parameters<typeof emitDiagnosticEvent>[0]);
emitDiagnosticEvent({
type: "tool.execution.completed",
runId: "run-1",
toolName: "read",
toolCallId: "tool-1",
durationMs: 20,
toolInput: "tool input",
toolOutput: "x".repeat(6000),
} as Parameters<typeof emitDiagnosticEvent>[0]);
await flushDiagnosticEvents();
const modelCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0] === "openclaw.model.call",
);
const toolCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0] === "openclaw.tool.execution",
);
const modelAttrs = (modelCall?.[1] as { attributes?: Record<string, unknown> } | undefined)
?.attributes;
const toolAttrs = (toolCall?.[1] as { attributes?: Record<string, unknown> } | undefined)
?.attributes;
expect(modelAttrs).toMatchObject({
"openclaw.content.output_messages": "model reply",
"openclaw.content.system_prompt": "system prompt",
});
expect(String(modelAttrs?.["openclaw.content.input_messages"])).not.toContain(
"sk-1234567890abcdef1234567890abcdef", // pragma: allowlist secret
);
expect(toolAttrs).toMatchObject({
"openclaw.content.tool_input": "tool input",
});
expect(String(toolAttrs?.["openclaw.content.tool_output"]).length).toBeLessThanOrEqual(
MAX_TEST_OTEL_CONTENT_ATTRIBUTE_CHARS + OTEL_TRUNCATED_SUFFIX_MAX_CHARS,
);
await service.stop?.(ctx);
});
test("ignores invalid diagnostic event trace parents", async () => {
const service = createDiagnosticsOtelService();
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { traces: true, metrics: true });

View File

@@ -40,6 +40,8 @@ const DROPPED_OTEL_ATTRIBUTE_KEYS = new Set([
"openclaw.traceId",
]);
const LOW_CARDINALITY_VALUE_RE = /^[A-Za-z0-9_.:-]{1,120}$/u;
const MAX_OTEL_CONTENT_ATTRIBUTE_CHARS = 4 * 1024;
const MAX_OTEL_CONTENT_ARRAY_ITEMS = 16;
const MAX_OTEL_LOG_BODY_CHARS = 4 * 1024;
const MAX_OTEL_LOG_ATTRIBUTE_COUNT = 64;
const MAX_OTEL_LOG_ATTRIBUTE_VALUE_CHARS = 4 * 1024;
@@ -48,6 +50,22 @@ const OTEL_LOG_RAW_ATTRIBUTE_KEY_RE = /^[A-Za-z0-9_.:-]{1,64}$/u;
const OTEL_LOG_ATTRIBUTE_KEY_RE = /^[A-Za-z0-9_.:-]{1,96}$/u;
const BLOCKED_OTEL_LOG_ATTRIBUTE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
type OtelContentCapturePolicy = {
inputMessages: boolean;
outputMessages: boolean;
toolInputs: boolean;
toolOutputs: boolean;
systemPrompt: boolean;
};
const NO_CONTENT_CAPTURE: OtelContentCapturePolicy = {
inputMessages: false,
outputMessages: false,
toolInputs: false,
toolOutputs: false,
systemPrompt: false,
};
function normalizeEndpoint(endpoint?: string): string | undefined {
const trimmed = endpoint?.trim();
return trimmed ? trimmed.replace(/\/+$/, "") : undefined;
@@ -119,6 +137,95 @@ function normalizeOtelLogString(value: string, maxChars: number): string {
return redactSensitiveText(clampOtelLogText(value, maxChars));
}
function resolveContentCapturePolicy(value: unknown): OtelContentCapturePolicy {
if (value === true) {
return {
inputMessages: true,
outputMessages: true,
toolInputs: true,
toolOutputs: true,
systemPrompt: false,
};
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
return NO_CONTENT_CAPTURE;
}
const config = value as Record<string, unknown>;
if (config.enabled !== true) {
return NO_CONTENT_CAPTURE;
}
return {
inputMessages: config.inputMessages === true,
outputMessages: config.outputMessages === true,
toolInputs: config.toolInputs === true,
toolOutputs: config.toolOutputs === true,
systemPrompt: config.systemPrompt === true,
};
}
function normalizeOtelContentValue(value: unknown): string | undefined {
if (typeof value === "string") {
return normalizeOtelLogString(value, MAX_OTEL_CONTENT_ATTRIBUTE_CHARS);
}
if (Array.isArray(value)) {
const items: string[] = [];
for (const item of value.slice(0, MAX_OTEL_CONTENT_ARRAY_ITEMS)) {
if (typeof item === "string") {
items.push(item);
}
}
if (items.length > 0) {
return normalizeOtelLogString(items.join("\n"), MAX_OTEL_CONTENT_ATTRIBUTE_CHARS);
}
}
return undefined;
}
function assignOtelContentAttribute(
attributes: Record<string, string | number | boolean>,
key: string,
value: unknown,
): void {
const normalized = normalizeOtelContentValue(value);
if (normalized) {
attributes[key] = normalized;
}
}
function assignOtelModelContentAttributes(
attributes: Record<string, string | number | boolean>,
event: Record<string, unknown>,
policy: OtelContentCapturePolicy,
): void {
if (policy.inputMessages) {
assignOtelContentAttribute(attributes, "openclaw.content.input_messages", event.inputMessages);
}
if (policy.outputMessages) {
assignOtelContentAttribute(
attributes,
"openclaw.content.output_messages",
event.outputMessages,
);
}
if (policy.systemPrompt) {
assignOtelContentAttribute(attributes, "openclaw.content.system_prompt", event.systemPrompt);
}
}
function assignOtelToolContentAttributes(
attributes: Record<string, string | number | boolean>,
event: Record<string, unknown>,
policy: OtelContentCapturePolicy,
): void {
if (policy.toolInputs) {
assignOtelContentAttribute(attributes, "openclaw.content.tool_input", event.toolInput);
}
if (policy.toolOutputs) {
assignOtelContentAttribute(attributes, "openclaw.content.tool_output", event.toolOutput);
}
}
function assignOtelLogAttribute(
attributes: Record<string, string | number | boolean>,
key: string,
@@ -285,6 +392,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
const serviceName =
otel.serviceName?.trim() || process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE_NAME;
const sampleRate = resolveSampleRate(otel.sampleRate);
const contentCapturePolicy = resolveContentCapturePolicy(otel.captureContent);
const tracesEnabled = otel.traces !== false;
const metricsEnabled = otel.metrics !== false;
@@ -856,6 +964,11 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
if (evt.transport) {
spanAttrs["openclaw.transport"] = evt.transport;
}
assignOtelModelContentAttributes(
spanAttrs,
evt as unknown as Record<string, unknown>,
contentCapturePolicy,
);
const span = spanWithDuration("openclaw.model.call", spanAttrs, evt.durationMs, {
endTimeMs: evt.ts,
});
@@ -886,6 +999,11 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
if (evt.transport) {
spanAttrs["openclaw.transport"] = evt.transport;
}
assignOtelModelContentAttributes(
spanAttrs,
evt as unknown as Record<string, unknown>,
contentCapturePolicy,
);
const span = spanWithDuration("openclaw.model.call", spanAttrs, evt.durationMs, {
endTimeMs: evt.ts,
});
@@ -913,6 +1031,11 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
...paramsSummaryAttrs(evt.paramsSummary),
};
addRunAttrs(spanAttrs, evt);
assignOtelToolContentAttributes(
spanAttrs,
evt as unknown as Record<string, unknown>,
contentCapturePolicy,
);
const span = spanWithDuration("openclaw.tool.execution", spanAttrs, evt.durationMs, {
endTimeMs: evt.ts,
});
@@ -941,6 +1064,11 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
if (evt.errorCode) {
spanAttrs["openclaw.errorCode"] = lowCardinalityAttr(evt.errorCode, "other");
}
assignOtelToolContentAttributes(
spanAttrs,
evt as unknown as Record<string, unknown>,
contentCapturePolicy,
);
const span = spanWithDuration("openclaw.tool.execution", spanAttrs, evt.durationMs, {
endTimeMs: evt.ts,
});

View File

@@ -65,6 +65,32 @@ describe("plugins.slots.contextEngine", () => {
});
});
describe("diagnostics.otel.captureContent", () => {
it("accepts boolean and granular OTEL content capture config", () => {
for (const captureContent of [
true,
false,
{
enabled: true,
inputMessages: true,
outputMessages: true,
toolInputs: true,
toolOutputs: true,
systemPrompt: false,
},
]) {
const result = OpenClawSchema.safeParse({
diagnostics: {
otel: {
captureContent,
},
},
});
expect(result.success).toBe(true);
}
});
});
describe("auth.cooldowns auth_permanent backoff config", () => {
it("accepts auth_permanent backoff knobs", () => {
const result = OpenClawSchema.safeParse({

View File

@@ -233,6 +233,58 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
description:
"Interval in milliseconds for periodic telemetry flush from buffers to the collector. Increase to reduce export chatter, or lower for faster visibility during active incident response.",
},
captureContent: {
anyOf: [
{
type: "boolean",
},
{
type: "object",
properties: {
enabled: {
type: "boolean",
title: "OpenTelemetry Content Capture Enabled",
description:
"Master switch for granular OTEL content capture fields. Keep disabled unless your collector is approved for raw prompt, response, or tool content.",
},
inputMessages: {
type: "boolean",
title: "OpenTelemetry Input Messages Capture",
description:
"Capture model input message text on OTEL spans when content capture is enabled.",
},
outputMessages: {
type: "boolean",
title: "OpenTelemetry Output Messages Capture",
description:
"Capture model output message text on OTEL spans when content capture is enabled.",
},
toolInputs: {
type: "boolean",
title: "OpenTelemetry Tool Inputs Capture",
description:
"Capture tool input text on OTEL spans when content capture is enabled.",
},
toolOutputs: {
type: "boolean",
title: "OpenTelemetry Tool Outputs Capture",
description:
"Capture tool output text on OTEL spans when content capture is enabled.",
},
systemPrompt: {
type: "boolean",
title: "OpenTelemetry System Prompt Capture",
description:
"Capture system prompt text on OTEL spans when content capture is enabled. This remains off unless explicitly enabled.",
},
},
additionalProperties: false,
},
],
title: "OpenTelemetry Content Capture",
description:
"Opt-in OTEL span content capture. Defaults to off; boolean true captures non-system message/tool content, while the object form lets you enable specific content classes.",
},
},
additionalProperties: false,
title: "OpenTelemetry",
@@ -23386,6 +23438,41 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
help: "Interval in milliseconds for periodic telemetry flush from buffers to the collector. Increase to reduce export chatter, or lower for faster visibility during active incident response.",
tags: ["observability", "performance"],
},
"diagnostics.otel.captureContent": {
label: "OpenTelemetry Content Capture",
help: "Opt-in OTEL span content capture. Defaults to off; boolean true captures non-system message/tool content, while the object form lets you enable specific content classes.",
tags: ["observability"],
},
"diagnostics.otel.captureContent.enabled": {
label: "OpenTelemetry Content Capture Enabled",
help: "Master switch for granular OTEL content capture fields. Keep disabled unless your collector is approved for raw prompt, response, or tool content.",
tags: ["observability"],
},
"diagnostics.otel.captureContent.inputMessages": {
label: "OpenTelemetry Input Messages Capture",
help: "Capture model input message text on OTEL spans when content capture is enabled.",
tags: ["observability"],
},
"diagnostics.otel.captureContent.outputMessages": {
label: "OpenTelemetry Output Messages Capture",
help: "Capture model output message text on OTEL spans when content capture is enabled.",
tags: ["observability"],
},
"diagnostics.otel.captureContent.toolInputs": {
label: "OpenTelemetry Tool Inputs Capture",
help: "Capture tool input text on OTEL spans when content capture is enabled.",
tags: ["observability"],
},
"diagnostics.otel.captureContent.toolOutputs": {
label: "OpenTelemetry Tool Outputs Capture",
help: "Capture tool output text on OTEL spans when content capture is enabled.",
tags: ["observability"],
},
"diagnostics.otel.captureContent.systemPrompt": {
label: "OpenTelemetry System Prompt Capture",
help: "Capture system prompt text on OTEL spans when content capture is enabled. This remains off unless explicitly enabled.",
tags: ["observability"],
},
"diagnostics.cacheTrace.enabled": {
label: "Cache Trace Enabled",
help: "Log cache trace snapshots for embedded agent runs (default: false).",

View File

@@ -548,6 +548,20 @@ export const FIELD_HELP: Record<string, string> = {
"Trace sampling rate (0-1) controlling how much trace traffic is exported to observability backends. Lower rates reduce overhead/cost, while higher rates improve debugging fidelity.",
"diagnostics.otel.flushIntervalMs":
"Interval in milliseconds for periodic telemetry flush from buffers to the collector. Increase to reduce export chatter, or lower for faster visibility during active incident response.",
"diagnostics.otel.captureContent":
"Opt-in OTEL span content capture. Defaults to off; boolean true captures non-system message/tool content, while the object form lets you enable specific content classes.",
"diagnostics.otel.captureContent.enabled":
"Master switch for granular OTEL content capture fields. Keep disabled unless your collector is approved for raw prompt, response, or tool content.",
"diagnostics.otel.captureContent.inputMessages":
"Capture model input message text on OTEL spans when content capture is enabled.",
"diagnostics.otel.captureContent.outputMessages":
"Capture model output message text on OTEL spans when content capture is enabled.",
"diagnostics.otel.captureContent.toolInputs":
"Capture tool input text on OTEL spans when content capture is enabled.",
"diagnostics.otel.captureContent.toolOutputs":
"Capture tool output text on OTEL spans when content capture is enabled.",
"diagnostics.otel.captureContent.systemPrompt":
"Capture system prompt text on OTEL spans when content capture is enabled. This remains off unless explicitly enabled.",
"diagnostics.cacheTrace.enabled":
"Log cache trace snapshots for embedded agent runs (default: false).",
"diagnostics.cacheTrace.filePath":

View File

@@ -48,6 +48,13 @@ export const FIELD_LABELS: Record<string, string> = {
"diagnostics.otel.logs": "OpenTelemetry Logs Enabled",
"diagnostics.otel.sampleRate": "OpenTelemetry Trace Sample Rate",
"diagnostics.otel.flushIntervalMs": "OpenTelemetry Flush Interval (ms)",
"diagnostics.otel.captureContent": "OpenTelemetry Content Capture",
"diagnostics.otel.captureContent.enabled": "OpenTelemetry Content Capture Enabled",
"diagnostics.otel.captureContent.inputMessages": "OpenTelemetry Input Messages Capture",
"diagnostics.otel.captureContent.outputMessages": "OpenTelemetry Output Messages Capture",
"diagnostics.otel.captureContent.toolInputs": "OpenTelemetry Tool Inputs Capture",
"diagnostics.otel.captureContent.toolOutputs": "OpenTelemetry Tool Outputs Capture",
"diagnostics.otel.captureContent.systemPrompt": "OpenTelemetry System Prompt Capture",
"diagnostics.cacheTrace.enabled": "Cache Trace Enabled",
"diagnostics.cacheTrace.filePath": "Cache Trace File Path",
"diagnostics.cacheTrace.includeMessages": "Cache Trace Include Messages",

View File

@@ -244,6 +244,21 @@ export type DiagnosticsOtelConfig = {
sampleRate?: number;
/** Metric export interval (ms). */
flushIntervalMs?: number;
/**
* Opt-in raw content capture for OTEL span attributes.
* Boolean `true` captures non-system message/tool content; the object form
* can enable each content class explicitly.
*/
captureContent?:
| boolean
| {
enabled?: boolean;
inputMessages?: boolean;
outputMessages?: boolean;
toolInputs?: boolean;
toolOutputs?: boolean;
systemPrompt?: boolean;
};
};
export type DiagnosticsCacheTraceConfig = {

View File

@@ -299,6 +299,21 @@ export const OpenClawSchema = z
logs: z.boolean().optional(),
sampleRate: z.number().min(0).max(1).optional(),
flushIntervalMs: z.number().int().nonnegative().optional(),
captureContent: z
.union([
z.boolean(),
z
.object({
enabled: z.boolean().optional(),
inputMessages: z.boolean().optional(),
outputMessages: z.boolean().optional(),
toolInputs: z.boolean().optional(),
toolOutputs: z.boolean().optional(),
systemPrompt: z.boolean().optional(),
})
.strict(),
])
.optional(),
})
.strict()
.optional(),