diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d2962dbaee..79620828f4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 5690f60a2e6..68354495938 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -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 diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 13165ff8e66..d425412aef5 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -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`). diff --git a/docs/logging.md b/docs/logging.md index a9badcd9bfd..d9bc63f431e 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -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 diff --git a/extensions/diagnostics-otel/src/service.test.ts b/extensions/diagnostics-otel/src/service.test.ts index f89ec32b863..348c99a25b4 100644 --- a/extensions/diagnostics-otel/src/service.test.ts +++ b/extensions/diagnostics-otel/src/service.test.ts @@ -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["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[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[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[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[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 } | undefined) + ?.attributes; + const toolAttrs = (toolCall?.[1] as { attributes?: Record } | 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 }); diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index 2c17ac5c92a..ededdeec184 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -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; + 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, + key: string, + value: unknown, +): void { + const normalized = normalizeOtelContentValue(value); + if (normalized) { + attributes[key] = normalized; + } +} + +function assignOtelModelContentAttributes( + attributes: Record, + event: Record, + 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, + event: Record, + 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, 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, + 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, + 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, + 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, + contentCapturePolicy, + ); const span = spanWithDuration("openclaw.tool.execution", spanAttrs, evt.durationMs, { endTimeMs: evt.ts, }); diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index 930827b2eca..12a519c88f7 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -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({ diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 6f968f17c8f..3738f9f105f 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -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).", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index ded6b9f6efb..55962062a1e 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -548,6 +548,20 @@ export const FIELD_HELP: Record = { "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": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 8ca3b04d596..95c97369b8f 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -48,6 +48,13 @@ export const FIELD_LABELS: Record = { "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", diff --git a/src/config/types.base.ts b/src/config/types.base.ts index b76360eccf8..86b2fd3b10a 100644 --- a/src/config/types.base.ts +++ b/src/config/types.base.ts @@ -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 = { diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 58d246afadf..6ab57eed84a 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -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(),