Skip to main content

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 the run() async function:

Why File Output Over SDK

Installing opentelemetry-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-exporter or filelogreceiver
Trade-off: no live tail, no distributed trace correlation across unrelated runs. Acceptable for a local council CLI.

Reading the Traces