Telemetry · Traces

Traces — one trace per conversation turn

Span hierarchy follows the OTel GenAI semantic conventions wherever a spec equivalent exists; Nio-specific extensions use the nio.* prefix. Three span shapes — turn root, tool call, subagent task — covered below with their full attribute schemas.

Resource attributes

Every Nio provider (tracer / logger / meter) constructs an OTel Resource with three identity attributes that flow onto every span, log record, and metric data point that provider emits. Backends surface them as top-level service selectors / filter columns.

AttributeValue
service.namenio-<platform> — one independent service per agent runtime: nio-claude-code, nio-codex, nio-hermes, nio-openclaw
nio.platformRaw platform string, same value as the suffix of service.name (offered separately so backends that don't expose service.name as a queryable attribute still have a filter handle)
gen_ai.agent.nameOperator-set agent_name from ~/.nio/config.yaml; absent on the resource when unconfigured (turn span carries a platform-default fallback as a span attribute instead — see the turn-span table below)

Behaviour change in v2.4.2. Earlier releases set service.name="nio" for every platform and put nio.platform only on individual spans. Existing dashboards filtered on service.name="nio" will not match new data — re-target to service.name=nio-* (wildcard) or filter on nio.platform. Historical traces / logs / metrics keep their original service.name=nio.

Span hierarchy

Trace: invoke_agent UserPromptSubmit  (root, opens at 1st PreToolUse, ends at Stop / SubagentStop)
  ├─ Span: execute_tool <name>   (PreToolUse → PostToolUse)
  ├─ Span: execute_tool <name>   (...)
  └─ Span: task:execute          (TaskCreated → TaskCompleted, or OpenClaw subagent_spawning → subagent_ended)

Span: invoke_agent UserPromptSubmit — turn root

One per conversation turn. Carries the turn-level metadata: conversation id, accumulated token usage, agent identity, and the redacted user-prompt / assistant-reply previews.

AttributeDescriptionCaptured atPlatforms
gen_ai.operation.nameConstant invoke_agentturn closeall
gen_ai.provider.nameConstant nioturn closeall
gen_ai.conversation.idHost session IDturn closeall
gen_ai.agent.nameUser-configured agent_name; falls back to platform when unsetturn closeall
session.idMirror of gen_ai.conversation.id for OTel base-spec consumersturn closeall
gen_ai.usage.input_tokensInput tokens consumed across the turnStop · SubagentStop · SessionEndall
gen_ai.usage.output_tokensOutput tokens generated across the turnStop · SubagentStop · SessionEndall
gen_ai.usage.cache_creation.input_tokensCache-creation input tokensStop · SubagentStop · SessionEndall
gen_ai.usage.cache_read.input_tokensCache-read input tokensStop · SubagentStop · SessionEndall
nio.platformSource platform — claude-code / hermes / openclawturn closeall
nio.turn_numberPer-session counter, starts at 1turn closeall
nio.cwdWorking dir at turn startturn close (when set)all
nio.turn.user_promptFirst user message of the turn, redacted, ≤2 KBUserPromptSubmitall
nio.turn.assistant_replyFirst assistant reply of the turn, redacted, ≤2 KBllm_output (OpenClaw-native)OpenClaw only
nio.turn.cache_hit_ratecache_read / (input + cache_creation + cache_read), 0–1turn closeall

Token usage source differs by platform. Claude Code: Stop reads the transcript JSONL and sums message.usage from all assistant entries since turn start. Hermes: same code path as Claude Code if the transcript path is included in the post_llm_call payload; otherwise empty. OpenClaw: llm_output event payload carries usage directly; accumulated incrementally.

Span: execute_tool <name> — tool span

One per tool invocation. Span name is literally execute_tool ${toolName || 'unknown'}. Pre-event opens the span; post-event closes it (with retroactive start time on Claude Code/Hermes since the pre-side process is gone).

AttributeDescriptionCaptured atPlatforms
gen_ai.operation.nameConstant execute_toolPostToolUseall
gen_ai.tool.nameHost tool name (Bash, WebFetch, …)PreToolUse · PostToolUseall
gen_ai.tool.typeTool type, when knownPostToolUseall
gen_ai.tool.call.idHost tool-call id (tool_use_id on Claude Code, toolCallId on OpenClaw)PreToolUse · PostToolUseall
gen_ai.tool.call.argumentsTool input, redacted, ≤2 KBPreToolUseall
gen_ai.tool.call.resultTool output, redacted, ≤2 KBPostToolUseall
nio.tool.errorError message when the tool failedPostToolUseall
nio.tool.duration_msWall-clock tool execution time (ms) — absent on the deny / confirm-denied span (the tool didn't run; use nio.guard.eval_ms instead)PostToolUseOpenClaw only
nio.tool.run_idOpenClaw-internal run identifierPreToolUseOpenClaw only
nio.tool_summaryOne-line summary derived from tool inputPostToolUseall
nio.platformSource platform — claude-code / hermes / openclawPostToolUseall
nio.turn_numberParent turn's numberPostToolUseall
nio.cwdWorking dir at hook firePostToolUse (when set)all
nio.guard.decisionGuard verdict — allow / deny / confirm_allowed / confirm_deniedPreToolUseall
nio.guard.risk_levelGuard risk level — low / medium / high / critical / unknownPreToolUseall
nio.guard.risk_scoreGuard risk score, 0–1PreToolUseall
nio.guard.risk_tagsComma-joined rule IDs that firedPreToolUseall
nio.guard.phase_stoppedPhase that produced the verdict (0 = tool-gate, 16 = runtime pipeline)PreToolUseall
nio.guard.top_finding_rulerule_id of the highest-ranked finding (when any fired)PreToolUseall
nio.guard.eval_msWall-clock cost of the guard evaluation (ms)PreToolUseall

Span status: ERROR (with recordException(error)) when the tool failed or the guard denied / confirm-denied; OK otherwise.

Deny / confirm-denied spans. When the guard blocks a tool, PostToolUse never fires — so the span is emitted synchronously by the same process that ran the guard (the guard-hook.ts PreToolUse on Claude Code / Codex; the hook-cli.ts pre_tool_call branch on Hermes; the before_tool_call handler in the OpenClaw plugin). Span name is the same execute_tool <tool> as the allow path — the discrimination is on nio.guard.decision + ERROR status + the reason in the exception message. Wall-clock starts at the real evalStartMs so the span duration reflects the guard window, and nio.guard.eval_ms carries the same value as an explicit attribute for filtering.

Span: task:execute — task span

One per subagent dispatch. Opens at TaskCreated (Claude Code, Teammates / cloud-agent flows) or subagent_spawning (OpenClaw); closes at the matching completion event.

AttributeDescriptionCaptured atPlatforms
nio.task_idTask id from the dispatch eventTaskCreatedClaude Code + OpenClaw
nio.task_summaryDerived from task input (Claude Code: task_input.prompt; OpenClaw: empty)TaskCreatedClaude Code + OpenClaw
nio.platformSource platform — claude-code / openclawTaskCompletedClaude Code + OpenClaw
nio.session_idHost session idTaskCompletedClaude Code + OpenClaw
nio.turn_numberParent turn's numberTaskCompletedClaude Code + OpenClaw
nio.cwdWorking dir at task startTaskCompletedClaude Code + OpenClaw
Known gap · not yet GenAI-aligned

Span name is the literal task:execute (not execute_tool task); session id uses nio.session_id instead of gen_ai.conversation.id + session.id. The other two spans use GenAI semantic conventions; the task span is intentionally on the legacy schema until Claude Code and OpenClaw can migrate in lockstep.

Trace state lifecycle

Claude Code and Hermes spawn a fresh node process per hook event, so a PreToolUse in process A and the matching PostToolUse in process B can't share an OTEL Span object. Both platforms bridge state via an on-disk cache keyed by session id; pending spans land there at pre-event time and get materialised retroactively at post-event time with the original start timestamp. OpenClaw runs as a single daemon, so the equivalent state lives in an in-memory Map<sessionId, State> instead. All three platforms route through the same trace-collector helper functions — span names and attribute keys are identical regardless of where the state was kept.