Skip to content

Observability

cybersenses edited this page Nov 15, 2026 · 1 revision

Observability

Two kinds of trace, answering two different questions, plus a spend ledger that outranks both.

Session trace (JSONL) OTLP spans
Config [trace] [telemetry]
Question what happened in this session — replay and evidence where did the time and the tokens go — timeline
Storage ~/.codingdude/traces/<session>.jsonl your collector
Default on off

Session traces

Every session writes a JSONL trace. It is what the Trace panel replays and what an evidence check reads back. Every event carries a type — EDIT_APPLIED, CONTEXT_COMPACTED, DELEGATION_COMPLETED, LONG_RUN, AUTO_RESUMED, PHASE_START, and the run terminal events.

[trace]
deltas = "compact"   # or "full"

compact coalesces streaming text deltas into one line per flush window, which keeps long traces small; full keeps every delta separately. A long run otherwise writes one line per token.

Large payloads are capped head+tail in the trace with an explicit truncation marker — the file on disk (git) is the source of truth for a file edit, so the full content is recoverable without inflating the trace.

OTLP span export

[telemetry]
enabled = false                         # off by default
otlp_endpoint = "http://127.0.0.1:4318" # any local OTLP/HTTP collector
service_name = "codingdude"

One span per model request and per tool call, so a slow or failing run reads as a timeline: which agent, which request, how many tokens, how long.

  • Local only. send_to_logfire=False — no token, no account, nothing leaves the machine. Run prompts contain your code, so this is a constraint, not a default to relax.
  • Off by default, and never fatal. Every malformed value — the boolean as much as the endpoint — is logged once and turns tracing off. Tracing never stops a run or a worker: enabled = "false" (quoted, so not a TOML boolean) stays off, otlp_endpoint = "4318" falls back to the default listener, and an unreachable collector simply produces no traces.
  • Everything is named: main, fix-loop, claims-retry, claims-auditor, compaction, memory-extraction, evaluator, planning-reviewer, vision-inspector, and each delegation as delegate.<function>. Filter by agent name to see where the time actually went.
  • Global only. [telemetry] lives in ~/.codingdude/config.toml; a project .codingdude.toml cannot enable it, so one collector serves every project.

Any OTLP/HTTP collector works — Grafana Agent, Jaeger (:4318), an OpenTelemetry Collector, or a locally served logfire.

OpenLIT as the collector

OpenLIT is the documented self-hosted option: Apache-2.0, no fee, no account, fits the local-only constraint.

services:
  openlit:
    image: ghcr.io/openlit/openlit:latest
    container_name: openlit
    ports:
      - "3000:3000"   # UI
      - "4318:4318"   # OTLP/HTTP receiver — CodingDude's default endpoint
    environment:
      - INIT_DB_HOST=127.0.0.1
    volumes:
      - openlit-data:/app/data

volumes:
  openlit-data:
docker compose up -d

Then flip the switch — the endpoint already points at that receiver:

[telemetry]
enabled = true

Read traces at http://127.0.0.1:3000. A port already in use is the usual first problem; 4318 is shared with Jaeger and the OpenTelemetry Collector.

Enabling tracing needs your consent, every time. Nothing is exported until you set enabled = true yourself — no tool, agent or update will flip it for you.

Why the OpenLIT SDK is deliberately not used: openlit.init() instruments pydantic-ai through the same hooks logfire.instrument_pydantic_ai() already uses, so running both produces duplicate spans, and only one global TracerProvider can exist — the second set_tracer_provider is a silent no-op. OpenLIT is used strictly as an OTLP receiver: no new dependency, no second instrumentation.

Cost views are subordinate to the ledger. OpenLIT computes cost from its own pricing database plus gen_ai.usage.* attributes. The CodingDude usage ledger (db.session_usage) remains authoritative and is what the Budget Cap reads. Treat any dashboard cost number as an independent estimate; where it disagrees with the ledger, the ledger wins.

Not yet verified: that OpenLIT's dashboard renders spans produced by logfire's pydantic-ai instrumentation. They are standard OTLP spans, so it should — but it has not been run against a live collector here. Start the container and confirm before relying on it.

Cost and usage accounting

The ledger is per session and shared with delegations (they inherit the parent's accumulator), so the number you see is the number that was actually spent, not a sum of independent guesses. Per-function tagging is what makes

designer on the strong model, evaluator on the cheap one

a measurement rather than a preference.

Dashboards that are not adopted, and why

Not adopted Why
OpenLIT Evals our evaluator grades against a ticket contract, which a generic eval cannot
OpenLIT Guardrails tool policy is registry-locked in functions.py — that lock is the safety property
OpenLIT Vault a runtime availability dependency
OpenLIT Prompt Hub would move prompts out of the repo and break the one-shot cacheable-prefix design
GPU monitoring we do not self-host models

Clone this wiki locally