> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cycls.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Logs, cost and SQL

> Find a user's failed request by its reference id, aggregate model spend, and run SQL across logs and billing.

This page is for operators debugging a report from a user, and for developers
tracking spend. Every command here needs `CYCLS_API_KEY` set.

## Find one failed request

When an agent run fails with an unhandled exception, the user sees a short
reference and the full detail goes to structured logs.

```
Something went wrong. Reference: abc12345
```

```bash theme={null}
cycls logs my-agent --query 'jsonPayload.error_id="abc12345"'
```

The structured error record contains:

| Field      | Value                                |
| ---------- | ------------------------------------ |
| `source`   | `"agent"`                            |
| `level`    | `"error"`                            |
| `error_id` | the short hex id shown to the user   |
| `message`  | the exception text, untruncated      |
| `stack`    | the full traceback                   |
| `user_id`  | the tenant user, null when anonymous |
| `chat_id`  | the chat, null when anonymous        |

Errors the loop handles itself, such as a rate limit retry, a compaction failure
or a tool timeout, appear to the user as callouts and are not logged as errors.
They are handled behavior, not failures.

## Tail and filter

```bash theme={null}
cycls logs my-agent                 # the most recent batch, then exit
cycls logs my-agent -f              # tail, polling every 2 seconds
cycls logs my-agent -s 30m          # a narrower window: 30m, 24h, 7d
cycls logs my-agent -q 'jsonPayload.level="error"'
cycls logs my-agent -q 'jsonPayload.user_id="user_2yY1..."' -f
```

`-q` passes a structured filter to the log backend and is reapplied on every
poll, so it composes with `-f`.

## Aggregate model spend

Each model turn emits a `level=usage` record. `cycls cost` aggregates it.

```bash theme={null}
cycls cost my-agent
# my-agent  $0.027126  (4 turns, 24h)

cycls cost my-agent --since 7d
cycls cost my-agent --month 2026-09
cycls cost my-agent --by user        # or chat, or model
```

| Flag                         | Meaning                                                                             |
| ---------------------------- | ----------------------------------------------------------------------------------- |
| `-s, --since`                | `30m`, `24h`, `7d`. Default `24h`                                                   |
| `-m, --month [YYYY-MM]`      | a calendar month. No value means the current one. Cannot be combined with `--since` |
| `-b, --by user\|chat\|model` | group the rows                                                                      |

Costs are zero unless the agent sets token prices:

```python theme={null}
llm = cycls.LLM().price(input=3, output=15, cache_read=0.30, cache_write=6)
```

Per-chat totals also persist in the chat index and are returned by `GET /chats`,
so a sidebar can show spend without rereading turns.

## SQL over logs and billing

```bash theme={null}
cycls sql 'SELECT ...'       # inline
cycls sql -f query.sql       # from a file
cat query.sql | cycls sql    # from stdin
```

Output is an aligned table on a terminal and JSON when piped. Use
`--format table|json|csv` to force one. An empty result prints `(0 rows)` to
stderr and exits 0.

Two tables are available, scoped automatically to the deployments your API key
owns.

### `logs`

| Column                              | Type   | Notes                                                             |
| ----------------------------------- | ------ | ----------------------------------------------------------------- |
| `timestamp`, `severity`, `log_name` |        |                                                                   |
| `resource.labels`                   | JSON   | `JSON_VALUE(resource.labels, '$.service_name')` is the deployment |
| `json_payload`                      | JSON   | structured SDK records, keyed by `level`                          |
| `text_payload`                      | STRING | plain stdout and stderr                                           |

`json_payload` records by `level`:

| `level`     | Fields                                                             |
| ----------- | ------------------------------------------------------------------ |
| `error`     | `error_id`, `message`, `stack`, `user_id`, `chat_id`               |
| `usage`     | `model`, `input`, `output`, `cached`, `cache_create`, `cost`, `ms` |
| `tool_call` | `tool`, `ms`, `ok`, `output_bytes`, `connector`, `error`           |
| `approval`  | `tool`, `risk`, `how`                                              |

### `billing`

| Column                                   | Type            | Notes                   |
| ---------------------------------------- | --------------- | ----------------------- |
| `usage_start_time`, `usage_end_time`     | TIMESTAMP       |                         |
| `service.description`, `sku.description` | STRING          | compute service and SKU |
| `cost`, `currency`                       | FLOAT64, STRING | usually USD             |
| `resource.name`                          | STRING          | the deployment name     |

### Example queries

```sql theme={null}
-- Model spend per user, last 30 days
SELECT JSON_VALUE(json_payload, '$.user_id')                    AS user_id,
       SUM(CAST(JSON_VALUE(json_payload, '$.cost') AS FLOAT64)) AS spend
FROM logs
WHERE JSON_VALUE(json_payload, '$.level') = 'usage'
  AND timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
GROUP BY user_id
ORDER BY spend DESC
```

```sql theme={null}
-- Infrastructure cost per deployment, last 30 days
SELECT resource.name AS deployment, ROUND(SUM(cost), 4) AS usd
FROM billing
WHERE usage_start_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
GROUP BY deployment
ORDER BY usd DESC
```

```sql theme={null}
-- Tool latency at p95, last 7 days
SELECT JSON_VALUE(json_payload, '$.tool') AS tool,
       APPROX_QUANTILES(CAST(JSON_VALUE(json_payload, '$.ms') AS INT64), 100)[OFFSET(95)] AS p95_ms,
       COUNT(*) AS calls
FROM logs
WHERE JSON_VALUE(json_payload, '$.level') = 'tool_call'
  AND timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY tool
ORDER BY p95_ms DESC
```

Known behavior: the first `cycls sql` call after a deployment is created can take
5 to 10 seconds while the backend sets up. A per-query scanned-bytes cap applies,
so add `LIMIT` or narrower predicates on wide scans. SQL engine errors are
surfaced verbatim.

## Emit your own records

```python theme={null}
cycls.log("cap_hit", user=context.user, chat_id=context.chat_id,
          kind="user_free_monthly", count=entry["count"])
```

The first argument is the level. `user` and `chat_id` attach attribution, and any
other keyword becomes a field in `json_payload`, queryable with `-q` and `sql`.

## Next

<Card title="CLI reference" icon="terminal" href="/ship/cli">
  Every command and flag.
</Card>
