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.
| Attribute | Value |
|---|---|
service.name | nio-<platform> — one independent service per agent runtime: nio-claude-code, nio-codex, nio-hermes, nio-openclaw |
nio.platform | Raw 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.name | Operator-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.
| Attribute | Description | Captured at | Platforms |
|---|---|---|---|
gen_ai.operation.name | Constant invoke_agent | turn close | all |
gen_ai.provider.name | Constant nio | turn close | all |
gen_ai.conversation.id | Host session ID | turn close | all |
gen_ai.agent.name | User-configured agent_name; falls back to platform when unset | turn close | all |
session.id | Mirror of gen_ai.conversation.id for OTel base-spec consumers | turn close | all |
gen_ai.usage.input_tokens | Input tokens consumed across the turn | Stop · SubagentStop · SessionEnd | all |
gen_ai.usage.output_tokens | Output tokens generated across the turn | Stop · SubagentStop · SessionEnd | all |
gen_ai.usage.cache_creation.input_tokens | Cache-creation input tokens | Stop · SubagentStop · SessionEnd | all |
gen_ai.usage.cache_read.input_tokens | Cache-read input tokens | Stop · SubagentStop · SessionEnd | all |
nio.platform | Source platform — claude-code / hermes / openclaw | turn close | all |
nio.turn_number | Per-session counter, starts at 1 | turn close | all |
nio.cwd | Working dir at turn start | turn close (when set) | all |
nio.turn.user_prompt | First user message of the turn, redacted, ≤2 KB | UserPromptSubmit | all |
nio.turn.assistant_reply | First assistant reply of the turn, redacted, ≤2 KB | llm_output (OpenClaw-native) | OpenClaw only |
nio.turn.cache_hit_rate | cache_read / (input + cache_creation + cache_read), 0–1 | turn close | all |
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).
| Attribute | Description | Captured at | Platforms |
|---|---|---|---|
gen_ai.operation.name | Constant execute_tool | PostToolUse | all |
gen_ai.tool.name | Host tool name (Bash, WebFetch, …) | PreToolUse · PostToolUse | all |
gen_ai.tool.type | Tool type, when known | PostToolUse | all |
gen_ai.tool.call.id | Host tool-call id (tool_use_id on Claude Code, toolCallId on OpenClaw) | PreToolUse · PostToolUse | all |
gen_ai.tool.call.arguments | Tool input, redacted, ≤2 KB | PreToolUse | all |
gen_ai.tool.call.result | Tool output, redacted, ≤2 KB | PostToolUse | all |
nio.tool.error | Error message when the tool failed | PostToolUse | all |
nio.tool.duration_ms | Wall-clock tool execution time (ms) — absent on the deny / confirm-denied span (the tool didn't run; use nio.guard.eval_ms instead) | PostToolUse | OpenClaw only |
nio.tool.run_id | OpenClaw-internal run identifier | PreToolUse | OpenClaw only |
nio.tool_summary | One-line summary derived from tool input | PostToolUse | all |
nio.platform | Source platform — claude-code / hermes / openclaw | PostToolUse | all |
nio.turn_number | Parent turn's number | PostToolUse | all |
nio.cwd | Working dir at hook fire | PostToolUse (when set) | all |
nio.guard.decision | Guard verdict — allow / deny / confirm_allowed / confirm_denied | PreToolUse | all |
nio.guard.risk_level | Guard risk level — low / medium / high / critical / unknown | PreToolUse | all |
nio.guard.risk_score | Guard risk score, 0–1 | PreToolUse | all |
nio.guard.risk_tags | Comma-joined rule IDs that fired | PreToolUse | all |
nio.guard.phase_stopped | Phase that produced the verdict (0 = tool-gate, 1–6 = runtime pipeline) | PreToolUse | all |
nio.guard.top_finding_rule | rule_id of the highest-ranked finding (when any fired) | PreToolUse | all |
nio.guard.eval_ms | Wall-clock cost of the guard evaluation (ms) | PreToolUse | all |
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.
| Attribute | Description | Captured at | Platforms |
|---|---|---|---|
nio.task_id | Task id from the dispatch event | TaskCreated | Claude Code + OpenClaw |
nio.task_summary | Derived from task input (Claude Code: task_input.prompt; OpenClaw: empty) | TaskCreated | Claude Code + OpenClaw |
nio.platform | Source platform — claude-code / openclaw | TaskCompleted | Claude Code + OpenClaw |
nio.session_id | Host session id | TaskCompleted | Claude Code + OpenClaw |
nio.turn_number | Parent turn's number | TaskCompleted | Claude Code + OpenClaw |
nio.cwd | Working dir at task start | TaskCompleted | Claude Code + OpenClaw |
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.