Configuration¶
Configure the custom hook-based OTel observability plugin via ~/.openclaw/openclaw.json.
Full Configuration Example¶
{
"plugins": {
"entries": {
"otel-observability": {
"enabled": true,
"config": {
"endpoint": "http://localhost:4318",
"protocol": "http",
"headers": {
"Authorization": "Api-Token dt0c01.xxx"
},
"serviceName": "openclaw-gateway",
"traces": true,
"metrics": true,
"logs": true,
"sampleRate": 1.0,
"metricsIntervalMs": 30000
}
}
}
}
}
Configuration Reference¶
plugins.entries.otel-observability.config¶
Plugin entry configuration.
| Option | Type | Default | Description |
|---|---|---|---|
endpoint |
string | "http://localhost:4318" |
OTLP endpoint URL |
protocol |
string | "http" |
Protocol: "http" or "grpc" |
headers |
object | {} |
Custom HTTP headers (e.g., auth tokens) |
serviceName |
string | "openclaw-gateway" |
OTel service name attribute |
traces |
boolean | true |
Enable trace export |
metrics |
boolean | true |
Enable metrics export |
logs |
boolean | true |
Enable log forwarding |
sampleRate |
number | — | Trace sampling rate, 0.0–1.0. Omit to use the SDK default (parentbased_always_on). Overrides OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG — see Trace Sampling for precedence rules. |
metricsIntervalMs |
number | 30000 |
Metrics export interval in milliseconds |
captureContent |
boolean | object | false |
Span content capture policy |
resourceAttributes |
object | {} |
Extra OpenTelemetry resource attributes |
logConfig |
object | — | Log filtering and exclusion rules |
Endpoint Configuration¶
HTTP Protocol (Default)¶
For OTLP/HTTP endpoints (port 4318):
{
"plugins": {
"entries": {
"otel-observability": {
"enabled": true,
"config": {
"endpoint": "http://localhost:4318",
"protocol": "http"
}
}
}
}
}
The endpoint auto-appends /v1/traces, /v1/metrics, /v1/logs as needed.
gRPC Protocol¶
For OTLP/gRPC endpoints (port 4317):
{
"plugins": {
"entries": {
"otel-observability": {
"enabled": true,
"config": {
"endpoint": "http://localhost:4317",
"protocol": "grpc"
}
}
}
}
}
Note: gRPC support is experimental.
Authentication¶
Bearer Token¶
{
"diagnostics": {
"enabled": true,
"otel": {
"enabled": true,
"endpoint": "https://api.example.com/otlp",
"headers": {
"Authorization": "Bearer your-token-here"
}
}
}
}
Dynatrace API Token¶
{
"diagnostics": {
"enabled": true,
"otel": {
"enabled": true,
"endpoint": "https://{env-id}.live.dynatrace.com/api/v2/otlp",
"headers": {
"Authorization": "Api-Token dt0c01.xxx..."
}
}
}
}
Basic Auth (Grafana Cloud)¶
{
"diagnostics": {
"enabled": true,
"otel": {
"enabled": true,
"endpoint": "https://otlp-gateway-prod-us-central-0.grafana.net/otlp",
"headers": {
"Authorization": "Basic base64(instanceId:apiKey)"
}
}
}
}
Sampling¶
Control trace sampling rate to reduce volume:
{
"plugins": {
"entries": {
"otel-observability": {
"enabled": true,
"config": {
"endpoint": "http://localhost:4318",
"sampleRate": 0.1
}
}
}
}
}
1.0— Sample all traces (default)0.5— Sample 50% of traces0.1— Sample 10% of traces0.0— Disable trace sampling
Selective Export¶
Enable only specific signals:
Traces Only¶
{
"diagnostics": {
"enabled": true,
"otel": {
"enabled": true,
"endpoint": "http://localhost:4318",
"traces": true,
"metrics": false,
"logs": false
}
}
}
Metrics Only¶
{
"diagnostics": {
"enabled": true,
"otel": {
"enabled": true,
"endpoint": "http://localhost:4318",
"traces": false,
"metrics": true,
"logs": false
}
}
}
Logs Only¶
{
"diagnostics": {
"enabled": true,
"otel": {
"enabled": true,
"endpoint": "http://localhost:4318",
"traces": false,
"metrics": false,
"logs": true
}
}
}
Environment Variables¶
OpenClaw also respects standard OTel environment variables as fallbacks:
| Variable | Description |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
Default OTLP endpoint |
OTEL_EXPORTER_OTLP_PROTOCOL |
Default protocol |
OTEL_SERVICE_NAME |
Default service name |
OPENCLAW_OTEL_CAPTURE_CONTENT |
Legacy single-boolean flag for Traceloop content capture. true enables prompt/completion text on LLM-client spans. See captureContent (gateway-launch setting). |
OPENCLAW_OTEL_CONTENT_POLICY |
Granular policy JSON (ISI-1000). When set, takes precedence over the legacy boolean. Same shape as the plugin's captureContent object form. |
Config file values take precedence over environment variables.
Trace Sampling¶
The plugin exports 100% of traces by default. For high-traffic gateways this can be tuned down with the sampleRate option (0.0–1.0):
{
"plugins": {
"entries": {
"otel-observability": {
"enabled": true,
"config": {
"sampleRate": 0.1
}
}
}
}
}
Semantics:
sampleRateomitted — the SDK default sampler (parentbased_always_on) is used. The plugin does not construct its own sampler; nothing sampler-related appears in the boot log.sampleRate: 1.0— the plugin explicitly constructsParentBased(TraceIdRatio(1.0)). Behaviourally equivalent to always-on, but the boot log reportssampler=parentbased_traceidratio(1)so operators can confirm the plugin took the sampling code path.0.0— drop every trace.- Any value in between — keep that fraction of root traces.
Sampling is head-based and parent-respecting: the plugin wires a ParentBasedSampler around a TraceIdRatioBasedSampler. The root span of a trace makes the sampling decision based on the trace ID; child spans inherit the parent's decision so distributed traces stay coherent and you never see "half a trace".
Invalid values (negative, > 1, NaN, non-numeric, null, plain object) are ignored and the SDK default (parentbased_always_on) is used instead. The plugin emits a [otel] Ignoring invalid sampleRate=… warn-level log line whenever it drops a present-but-invalid value, so silent typos like "sampleRate": "0.5" (string) or 1.5 (out-of-range) are visible in operator logs instead of quietly disabling head-based sampling.
Precedence vs. OTEL_TRACES_SAMPLER env vars¶
OpenTelemetry SDKs typically read the OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG environment variables to choose a default sampler. This plugin does not. When sampleRate is set in plugin config, the plugin builds the sampler directly (ParentBased(TraceIdRatio(sampleRate))) and the env vars have no effect on the plugin's tracer provider. When sampleRate is omitted, the plugin omits the sampler key entirely so the SDK's default (parentbased_always_on) applies — and even in that case, the plugin does not propagate OTEL_TRACES_SAMPLER to its provider.
Operators migrating from other OTel SDKs should configure sampling via plugins.entries.otel-observability.config.sampleRate rather than the env vars. The precedence is:
| Configuration | Effective sampler |
|---|---|
sampleRate set (any valid value) |
ParentBased(TraceIdRatio(sampleRate)) — env vars ignored |
sampleRate omitted; OTEL_TRACES_SAMPLER set |
SDK default (parentbased_always_on) — env vars ignored |
sampleRate omitted; no env vars |
SDK default (parentbased_always_on) |
sampleRate present but invalid (e.g. "0.5" string, 1.5) |
SDK default — plugin emits a logger.warn diagnostic |
captureContent (gateway-launch setting)¶
The plugin exposes a captureContent field in plugins.entries.otel-observability.config. It accepts either:
- a single boolean —
trueturns every capture category on,falseturns every category off (legacy shape, kept for backwards compatibility), or - a granular
ContentCapturePolicyobject with five independent flags:
| Flag | Span attribute(s) | What is captured |
|---|---|---|
inputMessages |
openclaw.content.input_message (request span), openclaw.content.prompt / openclaw.content.messages (agent.turn span) |
Inbound user message + the prompt and message history fed to the LLM |
outputMessages |
openclaw.content.output_message (message.sent span) |
Outbound assistant reply text |
toolInputs |
openclaw.content.tool_input (execute_tool span) |
Full tool-call input arguments (JSON-stringified, capped at 8192 UTF-16 code units) |
toolOutputs |
openclaw.content.tool_output (execute_tool span) |
Tool-call result text (text parts of the result message, capped at 8192 UTF-16 code units) |
systemPrompt |
openclaw.content.system_prompt (agent.turn span) |
System prompt text |
LLM-client spans emitted by Traceloop (@traceloop/instrumentation-anthropic, @traceloop/instrumentation-openai) still respect the legacy single-boolean Traceloop flag. The plugin derives it from the policy as inputMessages || outputMessages || systemPrompt — the three categories that map to prompt/completion text.
⚠️ Direction selection only applies to the plugin's own spans. Traceloop's
traceContentis a single boolean with no input/output distinction, so enabling any ofinputMessages,outputMessages, orsystemPromptcauses Traceloop LLM-client spans to record bothgen_ai.prompt.*.contentandgen_ai.completion.*.content. Theopenclaw.content.*attributes on the plugin's hook-surface spans do honor each flag in isolation — e.g.,{ inputMessages: true }will recordopenclaw.content.input_messagebut notopenclaw.content.output_message. If you need strict one-direction capture without completions landing on LLM-client spans, leave every LLM-content flag off and capture from the hook surface only (or filtergen_ai.completion.*.contentat the OTel Collector). See Privacy:captureContent.
Default: false (every flag off, privacy-first). See github issue #15 for the motivating report and ISI-1000 for the granular policy.
Not hot-reloadable¶
captureContent is a gateway-launch setting, not a hot-reloadable plugin option, because the ESM preload (instrumentation/preload.mjs) instantiates AnthropicInstrumentation and OpenAIInstrumentation before OpenClaw parses plugin config. Changing the value in openclaw.json mid-run has no effect until the next gateway restart.
The telemetry providers themselves are also preserved across config hot-reload. OpenClaw calls the plugin service stop() before register() during reload; this plugin treats stop() as a non-destructive drain and calls forceFlush() instead of shutdown() so the live TracerProvider/MeterProvider keep exporting spans and metrics after reload.
Because the runtime is reused, changes to telemetry-affecting fields do not take effect until a full gateway restart:
endpointheadersprotocolserviceNametracesmetricssampleRatemetricsIntervalMsresourceAttributes- preload-backed content capture (
captureContentfor Traceloop LLM-client spans)
This is intentional: stale telemetry config is preferable to dropping all post-reload spans. Restart the gateway after changing any of those fields. The log pipeline is rebuilt during service stop()/register() reloads, so logs and logConfig can take effect through the plugin reload path.
openclaw.otel.preExit¶
The plugin publishes a loose pre-exit contract for OpenClaw code paths that call process.exit() before the BatchSpanProcessor export interval fires:
const preExit = globalThis[Symbol.for("openclaw.otel.preExit")];
if (typeof preExit === "function") await preExit();
The plugin sets the symbol to a non-destructive flush function during telemetry initialization and clears it from shutdown(). CLI exit handlers own calling it before forced exit.
How to enable content capture¶
The preload reads two env vars and picks the granular one whenever it is set:
OPENCLAW_OTEL_CONTENT_POLICY— granular policy as JSON. Preferred.OPENCLAW_OTEL_CAPTURE_CONTENT— legacy single boolean. Fallback.
All-on (legacy boolean)¶
OPENCLAW_OTEL_CAPTURE_CONTENT=true \
NODE_OPTIONS="--import /path/to/openclaw-observability-plugin/instrumentation/preload.mjs" \
openclaw gateway start
{
"plugins": {
"entries": {
"otel-observability": {
"enabled": true,
"config": {
"captureContent": true
}
}
}
}
}
Granular (recommended)¶
Enable only what you actually need. For example, capture tool inputs/outputs for debugging without recording user prompts:
OPENCLAW_OTEL_CONTENT_POLICY='{"toolInputs":true,"toolOutputs":true}' \
NODE_OPTIONS="--import /path/to/openclaw-observability-plugin/instrumentation/preload.mjs" \
openclaw gateway start
{
"plugins": {
"entries": {
"otel-observability": {
"enabled": true,
"config": {
"captureContent": {
"toolInputs": true,
"toolOutputs": true
}
}
}
}
}
}
Or via systemd:
[Service]
Environment=OPENCLAW_OTEL_CONTENT_POLICY={"toolInputs":true,"toolOutputs":true}
Environment=NODE_OPTIONS=--import /path/to/openclaw-observability-plugin/instrumentation/preload.mjs
ExecStart=/usr/bin/openclaw gateway start
Mismatch warning¶
If the plugin config and the preload-time env vars disagree about whether LLM-client content capture is on, the plugin logs a warning at start():
[otel] captureContent policy resolves traceContent=true but the preload resolved
OPENCLAW_OTEL_CAPTURE_CONTENT=false at gateway launch. Traceloop LLM-client
spans will use the preload's value. Set OPENCLAW_OTEL_CONTENT_POLICY='{"inputMessages":true}'
(or OPENCLAW_OTEL_CAPTURE_CONTENT=true) in the gateway's environment before
starting (see docs/security/privacy.md).
Fix by setting the env var and restarting the gateway. The plugin's own hook-surface content attributes (openclaw.content.*) are not affected by this warning — they are evaluated against the live plugin config and so are always consistent with the running policy.
Privacy guidance¶
Leave captureContent at false unless you control the backend and understand the implications. See Privacy: captureContent for a fuller treatment.
Applying Changes¶
After modifying trace/metric provider configuration:
Plugin hot reload can apply log pipeline changes (logs and logConfig), but it does not rebuild the trace or metric providers listed above.
Troubleshooting¶
Configuration Not Applied?¶
Check the current config:
Invalid Config Errors?¶
Validate JSON syntax:
Endpoint Unreachable?¶
Test connectivity: