# Observability

KOAN provides three observability tools: OpenTelemetry tracing, structured logging, and cost tracking.

## Install

```bash
pip install koan[telemetry]
```

## OpenTelemetry Tracing

`KoanTracer` wraps OTel spans around LLM calls, tool calls, and agent runs:

```python
from koan.telemetry import KoanTracer

tracer = KoanTracer()

with tracer.agent_span("researcher", run_id="r-1", thread_id="t-1") as span:
    # Nested LLM call span
    with tracer.llm_span("gpt-4o", provider="openai") as llm_span:
        response = await client.complete(config, messages)
        tracer.record_llm_usage(
            llm_span,
            input_tokens=response.usage.input_tokens,
            output_tokens=response.usage.output_tokens,
        )

    # Nested tool call span
    with tracer.tool_span("web_search", arguments='{"query": "AI"}') as tool_span:
        result = await registry.execute("web_search", {"query": "AI"})
        if result.is_error:
            tracer.record_error(tool_span, Exception(result.error))
```

### Span Hierarchy

```
agent.researcher (run_id, thread_id)
  ├── llm.openai.gpt-4o (tokens.input, tokens.output)
  ├── tool.web_search (tool.name, tool.arguments)
  └── llm.openai.gpt-4o (tokens.input, tokens.output)
```

### Span Attributes

| Attribute | Description |
|-----------|-------------|
| `koan.agent.name` | Agent name |
| `koan.run_id` | Run identifier |
| `koan.thread_id` | Thread identifier |
| `koan.llm.model` | Model name |
| `koan.llm.provider` | Provider (openai, anthropic) |
| `koan.tokens.input` | Input token count |
| `koan.tokens.output` | Output token count |
| `koan.tokens.total` | Total token count |
| `koan.tool.name` | Tool name |
| `koan.tool.arguments` | Tool arguments (JSON string) |
| `koan.error` | Whether an error occurred |
| `koan.error.message` | Error message |

### Export to Jaeger/OTLP

Configure the OTel SDK to export spans:

```python
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)

# KoanTracer will automatically use the configured provider
tracer = KoanTracer()
```

## Structured Logging

`setup_logging()` configures structlog with correlation IDs:

```python
from koan.telemetry import setup_logging, bind_context, clear_context
import structlog

# Configure once at startup
setup_logging(
    level="DEBUG",
    json_output=False,    # Human-readable (set True for production)
    run_id="r-123",       # Bound to all log entries
    thread_id="t-456",
)

log = structlog.get_logger()
log.info("agent.started", agent="researcher")
# Output: 2026-03-07T20:30:00Z [info] agent.started agent=researcher run_id=r-123 thread_id=t-456
```

### Dynamic Context

Bind additional context for subsequent log entries:

```python
bind_context(user_id="alice", session="s-789")
log.info("processing")
# Includes user_id and session in output

clear_context()  # Reset all bound context
```

### JSON Output (Production)

```python
setup_logging(level="INFO", json_output=True)
log.info("request.processed", latency_ms=42, status="ok")
# Output: {"event": "request.processed", "level": "info", "timestamp": "...", "latency_ms": 42, "status": "ok"}
```

## Cost Tracking

`CostTracker` accumulates token usage across LLM calls and estimates cost:

```python
from koan.telemetry import CostTracker

tracker = CostTracker()

# Record usage after each LLM call
tracker.record("gpt-4o-mini", input_tokens=500, output_tokens=200)
tracker.record("gpt-4o-mini", input_tokens=300, output_tokens=100)
tracker.record("gpt-4o", input_tokens=1000, output_tokens=500)

# Get summary
summary = tracker.summary()
print(f"Total cost: ${summary.total_cost_usd:.4f}")
print(f"Total tokens: {summary.total_tokens}")
print(f"Calls: {summary.calls}")
print(f"By model: {summary.by_model}")
# {'gpt-4o-mini': 0.000255, 'gpt-4o': 0.0075}

# Access individual records
for record in tracker.records:
    print(f"{record.model}: {record.input_tokens}in/{record.output_tokens}out = ${record.cost_usd:.6f}")

# Reset
tracker.reset()
```

### Supported Models

Cost estimates are available for:

| Model | Input (per 1M) | Output (per 1M) |
|-------|----------------|-----------------|
| gpt-4o | $2.50 | $10.00 |
| gpt-4o-mini | $0.15 | $0.60 |
| gpt-4-turbo | $10.00 | $30.00 |
| o1 | $15.00 | $60.00 |
| o3-mini | $1.10 | $4.40 |
| claude-opus-4-6 | $15.00 | $75.00 |
| claude-sonnet-4-6 | $3.00 | $15.00 |
| claude-haiku-4-5 | $0.80 | $4.00 |

Unknown models return `$0.00` cost (usage is still tracked).
