---
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** (writes land under the platform default location described below) so the dashboard rebuilds across process restarts without extra wiring. Pass `persist: false` to keep the prior ephemeral-RAM-only behaviour.

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](/typescript-sdk/configuration#persistent-dashboard-history) reference for the full precedence rules.

### In code

```typescript
import { Patter, Twilio } from "getpatter";

// Platform default location
const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15555550100", persist: true });

// Explicit path
const phone = new Patter({
  carrier: new Twilio(),
  phoneNumber: "+15555550100",
  persist: "/var/log/patter",
});
```

| `persist` value | Behaviour |
|-----------------|-----------|
| omitted / `undefined` (default) | **On**. Falls back to `PATTER_LOG_DIR` when set; otherwise writes under the platform default location. (Changed from opt-in to default-on in 0.6.2 for dashboard hydrate.) |
| `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 set explicitly the env var is ignored. When `persist` is left unset (the default) the logger uses `PATTER_LOG_DIR` if set, otherwise the platform default location. Pass `persist: false` to disable disk writes entirely.

## 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` are masked by default (last 4 digits). Change via:

```bash
# Mask last 4 digits (default): "***4567"
export PATTER_LOG_REDACT_PHONE=mask

# Store the full E.164 number (disables redaction)
export PATTER_LOG_REDACT_PHONE=full

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

`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(logRoot)` 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:

```typescript
import { MetricsStore } from "getpatter/dashboard/store";

const store = new MetricsStore();
const restored = store.hydrate("/var/log/patter");
console.log(`Restored ${restored} call(s) from disk`);
```

`hydrate()` is idempotent: `callId`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

```ts
import * as fs from 'node:fs';
import * as path from 'node:path';

const callDir = '/var/log/patter/calls/2026/04/23/CA9a2b...';

const metadata = JSON.parse(fs.readFileSync(path.join(callDir, 'metadata.json'), 'utf8'));
console.log(`${metadata.call_id}: ${metadata.duration_ms}ms, $${metadata.cost.total.toFixed(4)}`);

const transcript = fs.readFileSync(path.join(callDir, 'transcript.jsonl'), 'utf8');
for (const line of transcript.split('\n').filter(Boolean)) {
  const turn = JSON.parse(line);
  console.log(`[${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.
- When persistence is disabled (`persist: false`), `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 Python SDK writes the same schema, so a multi-runtime deployment produces a single coherent directory tree.
