# minitrail

> **Work in progress — use at your own risk.**

Lightweight, file-based [OpenTelemetry](https://opentelemetry.io/) tracing for AI agent frameworks — with an optional built-in web viewer.

minitrail captures every LLM call your agents make and writes one JSON file per trace to disk. No collector, no database, no infrastructure. Optionally generates live human-readable Markdown reports with token counts and cost breakdowns.

## Features

- **One-call setup** — `from minitrail import setup; provider = setup()` before your framework imports
- **File-per-trace JSON export** — each trace is a self-contained JSON file under `logs/json/`
- **Live Markdown export** — human-readable `.md` files with messages, token counts, cost tables, and extracted images
- **Built-in web viewer** — `minitrail serve` launches a FastAPI app with an interactive span waterfall
- **Auto-instrumentation** — automatically patches LangChain, LlamaIndex, CrewAI, and Haystack via [OpenInference](https://github.com/Arize-ai/openinference)
- **Cost tracking** — built-in pricing for Anthropic, OpenAI, Amazon Nova, Mistral, and Meta Llama models

## Installation


```bash
pip install "minitrail"               # Strands Agents
pip install "minitrail[langchain]"    # LangChain / LangGraph
pip install "minitrail[crewai]"       # CrewAI
pip install "minitrail[llama-index]"  # LlamaIndex
pip install "minitrail[all]"          # all supported frameworks
```

## Quick start

`setup()` must be called **before** importing your framework so the instrumentor can patch it.

```python
from minitrail import setup

provider = setup(
    service_name="my-agent",
    logs_dir="logs",
    markdown=True,          # also write human-readable Markdown
)

# --- import your framework AFTER setup() ---
from langchain.chat_models import init_chat_model
# ... your agent code ...

provider.force_flush()
provider.shutdown()
```

Traces are written to:

```
logs/
  json/                  # machine-readable JSON (always)
  human_readable/        # Markdown + images (when markdown=True)
    images/
```

## Viewing traces

When `markdown=True` is set, traces are written as Markdown files under `logs/human_readable/`. These can be read directly in any text editor, terminal, or Markdown viewer — no server required.

For an interactive experience with span waterfall, collapsible details, and per-model cost breakdowns, launch the built-in web viewer:

```bash
minitrail serve logs/
minitrail serve logs/ --port 9000
```

Or run as a module:

```bash
python -m minitrail serve logs/
```

## API reference

### `setup()`

```python
setup(
    service_name: str = "minitrail",
    logs_dir: str = "logs",
    markdown: bool = False,
    frameworks: list[str] | None = None,
    instrument: bool = True,
) -> TracerProvider
```

| Parameter | Description |
|---|---|
| `service_name` | Value for the `service.name` OTel resource attribute |
| `logs_dir` | Root directory for trace output |
| `markdown` | Also write human-readable `.md` files |
| `frameworks` | List of frameworks to instrument (e.g. `["langchain"]`). `None` = auto-detect all |
| `instrument` | Set to `False` to skip framework instrumentation |
| `pricing` | Optional path to a `minitrail_pricing.toml` file with custom model prices |

Returns the configured `TracerProvider`. Call `provider.force_flush()` and `provider.shutdown()` when done.

### Custom pricing

minitrail ships with built-in pricing for popular models (Anthropic, OpenAI, Amazon Nova, Mistral, Meta Llama). You can add new models or override existing prices by creating a `minitrail_pricing.toml` file. Prices are in **USD per 1 million tokens**:

```toml
[models]
"my-custom-model" = { input = 1.0, output = 2.0 }
"nova-pro"        = { input = 0.90, output = 3.50 }  # override default
```

The file is picked up in three ways (first match wins):

1. **`setup()` parameter** — `setup(pricing="path/to/minitrail_pricing.toml")`
2. **CLI flag** — `minitrail serve --pricing path/to/minitrail_pricing.toml`
3. **Auto-detect** — if a `minitrail_pricing.toml` exists in the current working directory, it is loaded automatically

User-defined prices are merged on top of the defaults — you only need to list the models you want to add or change.

> **Disclaimer:** Built-in model prices are approximate and may not reflect current provider pricing. Always check your provider's pricing page for authoritative rates. minitrail cost estimates are for informational purposes only.

### Exporters

For advanced use, the exporters can be used directly with any OpenTelemetry `TracerProvider`:

- `FilePerTraceExporter(directory)` — writes one JSON file per trace
- `MarkdownTraceExporter(directory)` — writes live Markdown reports

## Examples

Complete working examples are available in the [`examples/`](https://github.com/lperez31/minitrail/tree/main/examples) directory on GitHub. All examples use Amazon Bedrock (Nova Pro + Claude Opus) and require AWS credentials.

**Strands Agents:**
```bash
pip install minitrail strands-agents
python examples/example_strands.py
```

**LangGraph:**
```bash
pip install "minitrail[langchain]" langchain langchain-aws langgraph
python examples/example_langgraph.py
```

**LlamaIndex:**
```bash
pip install "minitrail[llama-index]" llama-index llama-index-llms-bedrock-converse
python examples/example_llama_index.py
```

**CrewAI:**
```bash
pip install minitrail "crewai[bedrock]" openinference-instrumentation-crewai
python examples/example_crewai.py
```

Then view the traces in `./logs/human_readable` or with the web UI:
```bash
minitrail serve ./logs
```

### Grouping calls into a single trace

Frameworks like LangGraph and CrewAI automatically group all LLM calls from a single run into one trace. However, **Strands Agents** and **LlamaIndex** emit independent spans per call, which results in multiple traces.

To group them, wrap your calls in a parent span:

```python
from opentelemetry import trace

tracer = trace.get_tracer("my-app")
with tracer.start_as_current_span("pipeline"):
    # all LLM calls inside this block will belong to the same trace
    result1 = llm.chat(...)
    result2 = llm.chat(...)
```

See [`example_strands.py`](https://github.com/lperez31/minitrail/blob/main/examples/example_strands.py) and [`example_llama_index.py`](https://github.com/lperez31/minitrail/blob/main/examples/example_llama_index.py) for complete examples.

## License

MIT
