Telemetry · Overview

Collector Signals — what Nio captures while an agent runs

Nio emits three OTEL signals — metrics, traces, and logs — covering every tool invocation, every guard decision, every conversation turn. Schemas are aligned across all three host platforms (Claude Code, Hermes, OpenClaw) so the same dashboards work everywhere. This page is the high-level overview; each signal has its own deep-dive page below.

Architecture

The three host platforms each have their own runtime model — Claude Code and Hermes spawn a node process per hook event, OpenClaw runs as a long-lived daemon — but they all converge on the same canonical hook event vocabulary, then on the same three collector modules that own the attribute schema. Schema consistency falls out of the architecture: every attribute key string is owned by exactly one module, no matter which platform produced the event.

   ┌─────────────┐     ┌────────────────┐     ┌───────────────┐
   │ Claude Code │     │     Hermes     │     │   OpenClaw    │
   │             │     │                │     │               │
   │ per-hook    │     │ per-hook spawn │     │ single daemon │
   │ spawn       │     │ (node hook-cli)│     │ process       │
   └──────┬──────┘     └────────┬───────┘     └───────┬───────┘
          │                     │                     │
          ▼                     ▼                     ▼
   ┌──────────────────────────────────────┐   ┌──────────────┐
   │   on-disk state cache                │   │ in-memory    │
   │   bridges span lifecycle across      │   │ Map<sessionId│
   │   short-lived hook processes         │   │  ,State>     │
   └──────────────────┬───────────────────┘   └──────┬───────┘
                      │                              │
                      └──────────────┬───────────────┘
                                     ▼
              ┌──────────────────────────────────────┐
              │   Canonical hook event vocabulary    │
              │   UserPromptSubmit · PreToolUse ·    │
              │   PostToolUse · TaskCreated ·        │
              │   TaskCompleted · Stop · Subagent    │
              │   Stop · SessionStart · SessionEnd   │
              └─────────────────┬────────────────────┘
                                ▼
              ┌──────────────────────────────────────┐
              │   Three collector modules unify      │
              │   the attribute schema:              │
              │                                      │
              │     trace-collector                  │
              │     metrics-collector                │
              │     logs-collector                   │
              │                                      │
              │   shared keys: gen_ai.* · nio.*      │
              │   shared values: span names,         │
              │   metric instruments                 │
              └────────┬─────────┬───────────┬───────┘
                       │         │           │
                       ▼         ▼           ▼
                    Metrics   Traces       Logs
                    (OTLP)    (OTLP)    (OTLP + local audit log)

Claude Code and Hermes have to bridge span lifecycle across short-lived hook processes — a PreToolUse in process A and the matching PostToolUse in process B share state via an on-disk cache. OpenClaw's daemon model holds the same state in memory. Both end up calling the same trace-collector helpers; the only difference is where the state lives between events.

Naming conventions

  • gen_ai.* — keys that follow the OTel GenAI semantic conventions. Used wherever there's a spec equivalent: tool name, conversation id, token usage, tool I/O.
  • nio.* — vendor extensions for concepts the GenAI spec doesn't cover: guard decisions, per-phase scoring, platform tag, redacted prompt / reply previews, per-task subagent metadata.
  • session.id — mirrored alongside gen_ai.conversation.id for OTel base-spec consumers that key off session id rather than conversation id. Same value.

Cross-signal: the same key name carries the same meaning across metrics, traces, and logs. gen_ai.tool.name on a metric label is the same string as the matching trace span attribute, which is the same string as the matching audit-log attribute. Joining signals in dashboards Just Works.

The three signals

Metrics OTLP · <endpoint>/v1/metrics

Four instruments — three counters and one histogram — covering tool invocations, turn boundaries, guard decisions, and the risk-score distribution. No local backup; metrics drop on the floor when the OTLP endpoint is empty.

Metrics →

Traces OTLP · <endpoint>/v1/traces

One trace per conversation turn, with child spans per tool call and per subagent dispatch. Span names follow GenAI conventions (invoke_agent UserPromptSubmit, execute_tool <name>) where applicable. No local backup.

Traces →

Logs OTLP · <endpoint>/v1/logs + local ~/.nio/audit.jsonl

An audit-log entry per guard decision, per skill scan, per session lifecycle event, and per dispatched hook event. Dual-written: OTEL Logs export and a local JSONL file (offline / air-gapped friendly).

Logs →

Configuration

All three signals are gated by the collector section in ~/.nio/config.yaml. Full field-by-field reference: Configuration · collector.

YAML cheatsheet (click to expand)
collector:
  endpoint: ""                      # OTLP base URL; empty = no OTLP export at all
  api_key: ""                       # Bearer token
  timeout: 5000                     # milliseconds
  protocol: http                    # http | grpc
  metrics:
    enabled: true                   # OTLP metrics export on/off
  traces:
    enabled: true                   # OTLP traces export on/off
  logs:
    enabled: true                   # OTLP logs export on/off
    local: true                     # local JSONL backup on/off
    path: "~/.nio/audit.jsonl"      # audit log + (sibling) traces-state-store.json
    max_size_mb: 100                # rotation threshold for the local file
  • When collector.endpoint is empty, the corresponding provider factory returns null and the platform code skips emit.
  • The audit-log local JSONL still works (controlled by collector.logs.local) even without an endpoint — handy for offline / air-gapped use.