Observability

Find out what happened when a user reports a problem.

Deploy & operate4 min read

needs Postgres

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

  1. Get the correlation id

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

    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.

  2. Look it up in the audit log

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

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

  3. 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:

ColumnWhat it holds
correlation_idThe id returned in the response header
function_name, function_kindWhat ran
actor_kind, actor_idWho ran it. actor_id is NULL for anonymous callers
status"ok", or the error code it failed with
duration_msHow long it took
git_shaThe commit of the code that served it
branch_idWhich content branch the call ran on
rate_keyWhat the rate limit counted: the caller's id, or their IP
created_atWhen

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:

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.

WhereHow
CLIgraft compilations
MCPlist_compilations
StudioDeveloper, then History
HTTPGET /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 and the revert routes in the Studio API.

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.

Check the server is healthy

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

{
  "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:

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

Run it with the operator login. A hardened 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