OTel Instrumentation for council.py
templates/council.py dispatches questions to 2+ LLM voices, then a Chairman model synthesizes. Currently no observability. This page documents how to add OTel-compatible tracing with zero SDK dependencies.
What to Instrument
Three span types, nested parent → child: 1. Session span (top-level) Wraps the entire council run. Attributes:question (truncated to ~200 chars), voice_count, chairman_enabled.
2. Voice spans (children of session)
One per model dispatched. Created during asyncio.gather().
Attributes: gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens (if available), duration.
3. Chairman span (child of session)
Synthesis step. Runs after voices complete.
Attributes: voice_count, synthesis_length (chars).
Minimal Implementation — No SDK Required
File output: JSONL appended to~/.claude/logs/council-traces.jsonl. One JSON object per span.
GenAI Semantic Convention Attributes
Per LLM Observability (OTel GenAI conventions, still incubating as of 2026-05):
Token counts require parsing CLI stdout — optional. Omit if the subprocess output format is unstable.
Integration Point in council.py
In therun() async function:
Why File Output Over SDK
Installingopentelemetry-sdk + an exporter adds pip dependencies to a personal CLI tool. JSONL files are:
- Zero dependencies — pure stdlib
- Grep/jq-able without a collector running
- Forward-compatible: pipe to OTel Collector later via
otel-file-exporterorfilelogreceiver
Reading the Traces
Related Pages
- Council Pattern — council.py architecture; when to use; cost model
- OpenTelemetry — OTel data model, span structure, GenAI semantic conventions
- LLM Observability — GenAI OTel conventions; span types for LLM calls
- Pi Agent (pi-mono) — Pi as dispatch layer for council voices