---
title: "Call logging"
description: "Opt-in per-call filesystem logs: metadata, transcripts, and operational events."
icon: "folder-open"
---

Patter can persist every call to a directory tree on disk so you can replay transcripts, audit tool calls, and track latency/cost trends without running a hosted dashboard.

Persistence is **on by default since 0.6.2** — `Patter(...)` writes under the platform default location unless you pass `persist=False` (force-off) or override the path. The default was flipped from off→on because the dashboard's hydrate path needs on-disk records to survive process restarts.

The same on-disk layout also backs the local dashboard's call history: when persistence is enabled, `phone.serve()` rebuilds the in-memory dashboard from disk on startup so call history survives process restarts without an external database.

## Enable

You can turn persistence on either in code (recommended for application defaults) or via an environment variable (recommended for ops overrides). Both reach the same on-disk layout — see the [Configuration](/python-sdk/configuration#persistent-dashboard-history) reference for the full precedence rules.

### In code

```python
from getpatter import Patter, Twilio

# Platform default location
phone = Patter(carrier=Twilio(), phone_number="+15555550100", persist=True)

# Explicit path
phone = Patter(carrier=Twilio(), phone_number="+15555550100", persist="/var/log/patter")
```

| `persist` value | Behaviour |
|-----------------|-----------|
| omitted / `None` (default) | Reads `PATTER_LOG_DIR` if set; otherwise falls back to the platform default location. On by default since 0.6.2. |
| `False` | Force-off, even if `PATTER_LOG_DIR` is set. |
| `True` | Platform default location (see below). |
| `"<path>"` | Use the supplied path (`~` expanded). |

### Via env var

Set `PATTER_LOG_DIR` before starting the server:

```bash
# Explicit path
export PATTER_LOG_DIR=/var/log/patter

# Or use the platform-idiomatic default directory
export PATTER_LOG_DIR=auto
```

Platform defaults for `auto` (and for `persist=True`):

- macOS: `~/Library/Application Support/patter`
- Linux: `$XDG_DATA_HOME/patter` (falls back to `~/.local/share/patter`)
- Windows: `%LOCALAPPDATA%\patter`

When `persist` is unset and the env var is unset, Patter falls back to the platform default path (persistence is on by default since 0.6.2). When `persist` is set explicitly, the env var is ignored. Pass `persist=False` to force the logger to be a no-op.

## Layout

```
<PATTER_LOG_DIR>/calls/YYYY/MM/DD/<call_id>/
├── metadata.json     # envelope; written at call start, updated at call end
├── transcript.jsonl  # one JSON object per turn (role/text/ts/latency/cost)
└── events.jsonl      # operational events (tool_call, barge_in, error, ...)
```

`metadata.json` is written atomically (tmp file + rename) so a reader never sees a half-written file. JSONL files are append-only.

## Metadata schema

```json
{
  "schema_version": "1.0",
  "call_id": "CA9a2b...",
  "trace_id": null,
  "started_at": "2026-04-23T18:02:12.413Z",
  "ended_at":   "2026-04-23T18:03:47.892Z",
  "duration_ms": 95479,
  "status": "completed",
  "caller": "***4567",
  "callee": "***7890",
  "telephony_provider": "twilio",
  "provider_mode": "openai_realtime",
  "agent": { "provider": "openai_realtime", "voice": "nova" },
  "turns": 8,
  "cost":    { "total": 0.1234, "stt": 0.01, "tts": 0.02, "llm": 0.08 },
  "latency": { "p50_ms": 412, "p95_ms": 870, "p99_ms": 1240 },
  "error": null
}
```

## Phone redaction

Caller / callee numbers in `metadata.json` default to **full** (raw E.164) since 0.6.2 so the dashboard's reveal toggle has something to reconstruct from. The on-disk path under `~/Library/Application Support/patter/` (macOS) / XDG data dir (Linux) / `%LOCALAPPDATA%\patter` (Windows) is user-private. Override via:

```bash
# Store the full E.164 number (default since 0.6.2)
export PATTER_LOG_REDACT_PHONE=full

# Mask last 4 digits: "***4567"
export PATTER_LOG_REDACT_PHONE=mask

# Replace with a sha256 prefix for correlation without storing the number
export PATTER_LOG_REDACT_PHONE=hash_only
```

Set `PATTER_LOG_REDACT_PHONE=mask` for setups that ship logs off-host.

`transcript.jsonl` is **not** redacted — it can contain customer PII spoken during the call. Gate access to the log root and/or wire up your own redaction pipeline before exporting.

## Retention

Old day directories are cleaned up automatically. The sweep runs on ~2% of calls (sampled; no daemon) so a long-running server doesn't accumulate indefinitely.

```bash
# Default: 30 days
export PATTER_LOG_RETENTION_DAYS=30

# Disable cleanup (keep forever)
export PATTER_LOG_RETENTION_DAYS=0
```

<Warning>
Retention defaults to 30 days. Set `PATTER_LOG_RETENTION_DAYS=0` if you need to keep call history indefinitely (the default is intentionally conservative to avoid runaway disk use). Phone redaction defaults to last-4 masking; review the [Phone redaction](#phone-redaction) section before storing customer numbers.
</Warning>

## Dashboard hydration

When persistence is enabled, `phone.serve()` calls `MetricsStore.hydrate(log_root)` once at startup so the local dashboard repopulates its 500-call ring buffer from the on-disk envelopes before the first new call lands. There's nothing to wire up — it just happens.

If you instantiate the store yourself (custom dashboard host, separate process), call `hydrate()` directly:

```python
from getpatter.dashboard.store import MetricsStore

store = MetricsStore()
restored = store.hydrate("/var/log/patter")
print(f"Restored {restored} call(s) from disk")
```

`hydrate()` is idempotent: `call_id`s already in the store are skipped, and unparseable records are logged at debug level rather than aborting. The on-disk JSON/JSONL files are the source of truth — the in-memory store is a cache on top.

## Reading a call

```python
import json
from pathlib import Path

call_dir = Path("/var/log/patter/calls/2026/04/23/CA9a2b...")

metadata = json.loads((call_dir / "metadata.json").read_text())
print(f"{metadata['call_id']}: {metadata['duration_ms']}ms, ${metadata['cost']['total']:.4f}")

with (call_dir / "transcript.jsonl").open() as fh:
    for line in fh:
        turn = json.loads(line)
        print(f"[{turn['ts']}] {turn['role']}: {turn['text']}")
```

## Safety guarantees

- File-write errors never raise into the call path — a full disk or a permissions hiccup logs a warning and the call continues uninterrupted.
- Writes happen on a background thread (`asyncio.to_thread`) so they never block the audio pipeline.
- When persistence is disabled (`persist=False`, or `persist` unset and `PATTER_LOG_DIR` unset), `CallLogger.enabled` is `False` and every method returns immediately.

## Interop

Patter session reports include per-turn STT/LLM/TTS spans, tool invocations, latency metrics, and barge-in events. `transcript.jsonl` rows slot into OpenTelemetry `gen_ai.*` turn spans. The TypeScript SDK writes the same schema, so a multi-runtime deployment produces a single coherent directory tree.
