# Observability

> Trace a user's problem to the exact call, see what each compile changed, and check that your deployment is healthy.

*Find out what happened when a user reports a problem.*

> Needs the Postgres storage engine.

A user says the contact form failed. This page walks you from that report to
the exact call, who made it, which version of your code handled it, and how it
ended. Then it covers spotting patterns across many calls, seeing what each
compile changed, and health checks.

Everything here is stored in ordinary Postgres tables, so you can read it with
SQL. The parts an agent needs are also available over MCP and the CLI.

## Trace one failed call

**Get the correlation id**

Every function response carries an `x-graft-correlation-id` header, whether the
call worked or failed:

```sh
curl -i -X POST https://cms.example.com/api/fn/submitContact \
  -H 'content-type: application/json' \
  -d '{"email":"a@b.co"}'
# x-graft-correlation-id: 4f1c…
```

Show it in your app's error message, or log it, so a user's report can include
it.

**Look it up in the audit log**

```sql
select * from audit_log where correlation_id = '4f1c…';
```

The lookup is indexed, so it is fast even on a large log.

**Read the row**

The row tells you who called, what ran, how it ended, and which commit of your
code served it. The next section lists every column.

## What the audit log records

One row for every function call, whatever the outcome:

| Column | What it holds |
| --- | --- |
| `correlation_id` | The id returned in the response header |
| `function_name`, `function_kind` | What ran |
| `actor_kind`, `actor_id` | Who ran it. `actor_id` is `NULL` for anonymous callers |
| `status` | `"ok"`, or the error code it failed with |
| `duration_ms` | How long it took |
| `git_sha` | The commit of the code that served it |
| `branch_id` | Which content branch the call ran on |
| `rate_key` | What the rate limit counted: the caller's id, or their IP |
| `created_at` | When |

The inputs are not recorded, so the log never holds a copy of your users'
data. If you need the exact input of a call that waited for approval, it is on
that call's row in the `approvals` table.

A row is written even when the call is refused, for example for bad input or
a missing permission. Graft writes the row as soon as it knows who is calling,
then fills in the result.

`git_sha` comes from `VERCEL_GIT_COMMIT_SHA` or `GITHUB_SHA` when your platform
sets them, and `graft serve` reads it from the local checkout. It answers
"which deploy did this?" without searching your logs.

## Spot patterns across calls

Because refused calls are logged too, `status` shows how your functions fail.
For example, the errors from the last day:

```sql
select status, count(*)
from audit_log
where created_at > now() - interval '1 day'
group by status
order by count(*) desc;
```

## See what each compile changed

The `compilations` table records every compile: the branch, the Git commit it
read, the number of documents, and how many it added, changed, and removed.

| Where | How |
| --- | --- |
| CLI | `graft compilations` |
| MCP | `list_compilations` |
| Studio | Developer, then History |
| HTTP | `GET /api/studio/v1/compilations` |

Each row records the Git commit it compiled, so you can restore your content to
exactly how it was then. See [Developer views](/docs/studio#developer-views) and
the revert routes in the [Studio API](/docs/studio-api#routes).

## Review past approvals

The `approvals` table is a record in its own right. Every destructive call that
waited for approval has a row with who asked, the exact input, the decision,
and who decided. Which database login made the decision is recorded by the
database itself, so it cannot be faked by the app. See
[Hardening](/docs/hardening).

## Check the server is healthy

`graft serve` answers `GET /healthz`. It makes a real round trip to the
database before answering:

```json
{
  "ok": true,
  "branch": "main",
  "collections": 3,
  "functions": 2,
  "gitSha": "d7aa173…"
}
```

If the database does not answer, it returns `503` with
`{ "ok": false, "error": "…" }`. Point your platform's health check at it. The
`gitSha` field also confirms which build is live.

## Keep the log a manageable size

Graft never deletes audit rows. On a busy site the table grows, and rate limits
only read recent rows. Delete old ones on your own schedule, keeping whatever
your compliance rules require:

```sql
delete from audit_log where created_at < now() - interval '90 days';
```

Run it with the operator login. A [hardened](/docs/hardening) app login cannot
delete audit rows, on purpose.

## What Graft leaves to your platform

- **Logs.** Graft writes to standard output. Collect it the way your host does.
- **Metrics.** There is no metrics endpoint. The audit log is the source, so
  aggregate it with SQL or export it.
- **Tracing.** Graft does not emit traces. Pass the correlation id into your own
  spans if you use tracing.

If writing an audit row fails, Graft logs the failure with the correlation id
and still completes the call. Failing a user's request because the record could
not be written would be worse than missing one record.

## Next steps

- [Audit & approvals](/docs/audit-and-approvals) explains what the log is for.
- [Rate limits](/docs/rate-limits) covers the other job these rows do.
- [Deploy](/docs/deploy) shows where `/healthz` lives.
