Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ src/
apps.rs Apps command (fetch, DB write)
consumers.rs Consumers command (paginated fetch, DB write)
endpoints.rs Endpoints command (fetch, DB write)
metrics.rs Metrics command (Arrow IPC or NDJSON streaming)
request_logs.rs Request logs command (Arrow IPC or NDJSON streaming)
request_details.rs Request details command (single request fetch, DB write)
sql.rs SQL command (query DuckDB, output NDJSON)
Expand Down Expand Up @@ -46,6 +47,7 @@ skills/
| `apps` | `GET /v1/apps` |
| `consumers` | `GET /v1/apps/{app_id}/consumers` |
| `endpoints` | `GET /v1/apps/{app_id}/endpoints` |
| `metrics` | `POST /v1/apps/{app_id}/metrics` |
| `request-logs` | `POST /v1/apps/{app_id}/request-logs` |
| `request-details` | `GET /v1/apps/{app_id}/request-logs/{request_uuid}` |
| `sql` | Local DuckDB |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ You can also set the API key via the `APITALLY_API_KEY` environment variable or
| `apps` | List all apps in your team |
| `consumers` | List consumers for an app |
| `endpoints` | List endpoints for an app |
| `metrics` | Fetch aggregated metrics for an app |
| `request-logs` | Fetch request log data for an app |
| `request-details` | Fetch full details for a specific request |
| `sql` | Run SQL queries against a local DuckDB database |
Expand Down
132 changes: 72 additions & 60 deletions skills/apitally-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
---
name: apitally-cli
description: >
Retrieve and investigate API request log data from Apitally. Fetches request logs,
consumers, and app metadata via the Apitally CLI, stores data in a local
DuckDB database, and runs SQL queries to investigate issues or answer questions.
Use when the user mentions Apitally, the Apitally CLI, API request logs, or API consumers.
Retrieve and investigate API metrics and request log data from Apitally. Fetches
aggregated metrics, request logs, consumers, and app metadata via the Apitally CLI,
stores data in a local DuckDB database, and runs SQL queries to investigate issues
or answer questions. Use when the user mentions Apitally, the Apitally CLI, API
metrics, API request logs, or API consumers.
---

# Apitally CLI

The Apitally CLI retrieves API request log data from [Apitally](https://apitally.io) and optionally stores it in a local DuckDB database for investigation with SQL. Each record is an individual API request with method, URL, status code, response time, consumer, headers, payloads, exceptions, and more. Request log retention is **15 days**.
The Apitally CLI retrieves API metrics and request log data from [Apitally](https://apitally.io) and optionally stores it in a local DuckDB database for investigation with SQL. Two main data sources:

- **Metrics** — pre-aggregated data (request counts, error rates, response time percentiles, throughput). Retention: **30 days** at 1-minute intervals, **13 months** at 30-minute intervals.
- **Request logs** — individual API requests with method, URL, status code, response time, consumer, headers, payloads, exceptions, traces, and more. Retention: **15 days**.

Run commands with `npx` (no install needed):

Expand Down Expand Up @@ -38,6 +42,7 @@ All commands are run via `npx @apitally/cli <command>`. For full details, see [r
- `apps [--db [<path>]]` -- list apps (get app IDs)
- `consumers <app-id> [--requests-since <dt>] [--db [<path>]]` -- list consumers for an app (get consumer IDs)
- `endpoints <app-id> [--method <methods>] [--path <pattern>] [--db [<path>]]` -- list endpoints for an app
- `metrics <app-id> --since <dt> [--until <dt>] --metrics <json> [--interval <interval>] [--group-by <json>] [--filters <json>] [--timezone <tz>] [--db [<path>]]` -- fetch aggregated metrics
- `request-logs <app-id> --since <dt> [--until <dt>] [--fields <json>] [--filters <json>] [--limit <n>] [--db [<path>]]` -- fetch request logs (max 1,000,000 rows at once)
- `request-details <app-id> <request-uuid> [--db [<path>]]` -- fetch full details for a single request (including headers, payloads, exception info, application logs, and spans)
- `sql "<query>" [--db <path>]` -- run SQL against local DuckDB
Expand All @@ -51,97 +56,104 @@ All commands are run via `npx @apitally/cli <command>`. For full details, see [r

3. **Determine the time range** — check if the user specified a time range (e.g. "last 24 hours", "since Monday", a specific date). If not, default to the last 7 days. Use this time range consistently for `--requests-since` / `--since` / `--until` flags and SQL `WHERE` conditions throughout the investigation.

4. **Fetch endpoints if needed** — skip this step unless you need to discover available endpoints to filter request logs. Fetch endpoints using the `endpoints` command:
4. **Fetch supporting data if needed** — skip unless you need endpoint discovery or consumer identification.
- **Endpoints**: use `endpoints` to discover available method/path combinations for filtering. Use `--method` and/or `--path` to filter (e.g. `--path '*users*'`).

```
npx @apitally/cli endpoints <app-id> [--method <methods>] [--path <pattern>]
```
```
npx @apitally/cli endpoints <app-id> [--method <methods>] [--path <pattern>]
```

Use `--method` and/or `--path` to filter (e.g. `--path '*users*'`). Read the NDJSON output to identify relevant endpoints, then use their method/path to filter request logs in step 6.
- **Consumers**: use `consumers` to map identifiers (emails, usernames, groups) to `consumer_id` values and vice versa, if the question involves consumers.

5. **Fetch consumers if needed** — skip this step if the investigation doesn't involve consumers. Otherwise, fetch consumers into DuckDB using the `consumers` command:
```
npx @apitally/cli consumers <app-id> [--requests-since "<since>"] --db
```

```
npx @apitally/cli consumers <app-id> [--requests-since "<since>"] --db
```
```
npx @apitally/cli sql "SELECT consumer_id, identifier, name, \"group\" FROM consumers WHERE app_id = <app-id> AND identifier ILIKE '%@example.com'"
```

If the user is asking about specific consumers (e.g. by email, name, or group), query to find their `consumer_id` and use it as a filter when fetching request logs in step 6:
5. **Fetch data** — choose based on the question. Always read the [command reference](references/commands.md) for available options.
- **Metrics** — for questions that can be answered with aggregated metrics: traffic volume, error rates, response time trends, throughput, endpoint comparisons. Use `--group-by` and `--interval` to break down by environment, endpoint, consumer, status code, or time period.

```
npx @apitally/cli sql "SELECT consumer_id, identifier, name, \"group\" FROM consumers WHERE app_id = <app-id> AND identifier ILIKE '%@example.com'"
```
```
npx @apitally/cli metrics <app-id> --since "<since>" \
--metrics '["requests","error_rate","response_time_p50","response_time_p95"]' \
--group-by '["method","path"]' --interval day --db
```

6. **Fetch request logs** into DuckDB using the `request-logs` command with time range, fields, and filters tailored to the investigation. Always read the [command reference](references/commands.md) for available fields and filters.

```
npx @apitally/cli request-logs <app-id> --since "<since>" \
--fields '<json-array-of-field-names>' \
--filters '<json-array-of-filter-objects>' \
--db
```
- **Request logs** — for questions that require individual request data: specific errors, exceptions, headers, payloads, traces, etc. Narrow down fields and use filters to avoid fetching unnecessarily large volumes of data. Refetching replaces existing records in DuckDB (no duplicates).

If filtering by endpoint, add method/path filters: `[{"field":"method","op":"eq","value":"GET"},{"field":"path","op":"eq","value":"/v1/users/{user_id}"}]`
```
npx @apitally/cli request-logs <app-id> --since "<since>" \
--fields '<json-array-of-field-names>' \
--filters '<json-array-of-filter-objects>' \
--db
```

If filtering by consumers, add a consumer filter: `[{"field":"consumer_id","op":"in","value":[1,2,3]}]`
Filter by endpoint: `--filters '[{"field":"method","op":"eq","value":"GET"},{"field":"path","op":"eq","value":"/v1/users/{user_id}"}]'`
Filter by consumer: `--filters '[{"field":"consumer_id","op":"in","value":[1,2,3]}]'`

Narrow down fields and use filters as much as possible to avoid fetching unnecessarily large volumes of data. Refetching data later (e.g. with more fields) replaces existing records in DuckDB and does not create duplicates.
- **Both** — for broad investigations, start with metrics for an overview, then fetch request logs to drill into specifics.

7. **Query DuckDB** using the `sql` command — **CRITICAL: The DuckDB database is persistent and retains data from previous fetches, including other sessions. You MUST filter your SQL queries to match the scope of your current investigation.** Always include `WHERE` conditions on `app_id`, `timestamp`, and any other relevant fields. Without these filters, results will include unrelated data and will be **wrong**.
6. **Query DuckDB** using the `sql` command — **CRITICAL: The DuckDB database is persistent and retains data from previous fetches, including other sessions. You MUST filter your SQL queries to match the scope of your current investigation.** Always include `WHERE` conditions on `app_id`, `period_start`/`timestamp`, and any other relevant fields. Without these filters, results will include unrelated data and will be **wrong**.

```
npx @apitally/cli sql "SELECT method, path, status_code, COUNT(*) as n FROM request_logs WHERE app_id = <app-id> AND timestamp >= '<since>' AND status_code >= 400 GROUP BY ALL ORDER BY n DESC"
```

Read the [DuckDB schema reference](references/duckdb_tables.md) for available tables, columns and relationships.

8. **Iterate if needed** — refine filters, fetch additional fields (headers, bodies, exceptions), or widen the time range as needed.
7. **Iterate if needed** — refine filters, fetch additional fields (headers, bodies, exceptions), or widen the time range as needed.

## Investigation Patterns

### Inspect a specific request
### Error investigation

Use `request-details` to fetch full details (headers, body, exception, application logs, spans) for a single request:
Fetch request counts grouped by endpoint and status code to find the most frequent errors:

```
npx @apitally/cli request-details <app-id> <request-uuid>
npx @apitally/cli metrics <app-id> --since "<since>" \
--metrics '["requests"]' \
--group-by '["method","path","status_code"]' \
--filters '[{"field":"status_code","op":"gte","value":400}]' --db
```

### Trace a consumer's activity

```sql
SELECT r.timestamp, r.method, r.path, r.status_code, r.response_time_ms,
c.identifier, c.name as consumer_name
FROM request_logs r
JOIN consumers c ON r.app_id = c.app_id AND r.consumer_id = c.consumer_id
WHERE r.app_id = <app-id>
AND r.timestamp >= '<since>'
AND r.timestamp < '<until>'
AND c.identifier = 'user@example.com'
ORDER BY r.timestamp DESC
SELECT method, path, status_code, sum(requests) as requests_sum
FROM metrics
WHERE app_id = <app-id>
AND period_start >= '<since>'
GROUP BY method, path, status_code
ORDER BY requests_sum DESC
```

### Exception investigation

Fetch with exception fields first:
Then fetch request logs for a specific error to investigate further:

```
npx @apitally/cli request-logs <app-id> --since "<since>" \
--fields '["timestamp","request_uuid","method","path","status_code","exception_type","exception_message","exception_stacktrace"]' \
--filters '[{"field":"status_code","op":"eq","value":500}]' \
--db
--fields '["timestamp","request_uuid","url","status_code","response_body_json","exception_type","exception_message"]' \
--filters '[{"field":"method","op":"eq","value":"<method>"},{"field":"path","op":"eq","value":"<path>"},{"field":"status_code","op":"eq","value":<status_code>}]' \
--limit 5
```

Then group by exception type:
Use `request-details` to fetch full details (headers, body, exception, application logs, spans) for a specific request:

```
npx @apitally/cli request-details <app-id> <request-uuid>
```

### Trace a consumer's activity

```sql
SELECT exception_type, exception_message, COUNT(*) as count,
MIN(timestamp) as first_seen, MAX(timestamp) as last_seen
FROM request_logs
WHERE app_id = <app-id>
AND timestamp >= '<since>'
AND exception_type IS NOT NULL
GROUP BY exception_type, exception_message
ORDER BY count DESC
SELECT r.timestamp, r.method, r.url, r.status_code, r.response_time_ms
FROM request_logs r
JOIN consumers c ON r.app_id = c.app_id AND r.consumer_id = c.consumer_id
WHERE r.app_id = <app-id>
AND r.timestamp >= '<since>'
AND r.timestamp < '<until>'
AND c.identifier = 'user@example.com'
ORDER BY r.timestamp ASC
```

### Query headers
Expand Down
66 changes: 64 additions & 2 deletions skills/apitally-cli/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,68 @@ Example NDJSON output (without `--db`):
{"id":2,"method":"GET","path":"/v1/users/{user_id}"}
```

## `metrics`

```
npx @apitally/cli metrics <app-id> --since <datetime> --metrics <json> \
[--until <datetime>] [--interval <interval>] [--group-by <json>] \
[--filters <json>] [--timezone <tz>] [--db [<path>]]
```
Comment thread
itssimon marked this conversation as resolved.

Fetch aggregated metrics for an app. Outputs NDJSON to stdout by default.

- `--since`: Start of time range, inclusive (ISO 8601, required)
- `--until`: End of time range, exclusive (ISO 8601, defaults to now)
- `--metrics`: JSON array of metric names to include (required)
- `--interval`: Time interval for grouping (`month`, `day`, `hour`, `minute`). When omitted, returns a single row per group for the entire time range
- `--group-by`: JSON array of field names to group by, in addition to time period
- `--filters`: JSON array of filter objects (see below)
- `--timezone`: Timezone for intervals and to interpret since/until if not tz-aware (defaults to UTC)
- `--db`: Write to `metrics` table in DuckDB instead of outputting NDJSON to stdout

**Deduplication in DuckDB:** Deletes all existing rows for the same `app_id` within the fetched time range before inserting new data.

Comment thread
itssimon marked this conversation as resolved.
### Available metrics

| Metric | Type | Description |
| --------------------- | ------- | -------------------------------------- |
| `requests` | integer | Total request count |
| `requests_per_minute` | float | Requests per minute |
| `bytes_received` | integer | Total bytes received |
| `bytes_sent` | integer | Total bytes sent |
| `client_errors` | integer | 4xx errors (excluding expected errors) |
| `server_errors` | integer | 5xx errors (excluding expected errors) |
| `error_rate` | float | Ratio of errors to total requests |
| `response_time_p50` | integer | 50th percentile response time (ms) |
| `response_time_p75` | integer | 75th percentile response time (ms) |
| `response_time_p95` | integer | 95th percentile response time (ms) |

### Group-by fields

`env`, `consumer_id`, `method`, `path`, `status_code`

### Filters

Pass `--filters` as a JSON array of filter objects. Supported fields and operators:

- **string fields** (`env`, `method`, `path`): `eq`, `neq`, `in`, `not_in`, `like`, `not_like`, `contains`, `not_contains`
- **numeric fields** (`consumer_id`, `status_code`): `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `is_null`, `is_not_null`

Filter examples:

```json
[{"field":"method","op":"eq","value":"GET"}]
[{"field":"status_code","op":"gte","value":400}]
[{"field":"path","op":"like","value":"/v1/users/%"}]
```

Example NDJSON output (without `--db`):

```json
{"period_start":"2026-01-01T00:00:00Z","period_end":"2026-01-01T01:00:00Z","env":"prod","requests":1234,"error_rate":0.02}
{"period_start":"2026-01-01T01:00:00Z","period_end":"2026-01-01T02:00:00Z","env":"prod","requests":987,"error_rate":0.01}
```

## `request-logs`

```
Expand Down Expand Up @@ -206,9 +268,9 @@ Run a SQL query against a local DuckDB database. The query can be passed as an a

- `--db`: Path to DuckDB database

Available tables: `apps`, `app_envs`, `consumers`, `endpoints`, `request_logs`, `application_logs`, `spans`. See [duckdb_tables.md](duckdb_tables.md) for schemas.
Available tables: `apps`, `app_envs`, `consumers`, `endpoints`, `metrics`, `request_logs`, `application_logs`, `spans`. See [duckdb_tables.md](duckdb_tables.md) for schemas.

**Important:** The database may contain data from previous sessions. Always filter queries by `app_id`, `timestamp`, and other relevant fields to avoid including unrelated data.
**Important:** The database may contain data from previous sessions. Always filter queries by `app_id`, time (`timestamp` for `request_logs`, `period_start`/`period_end` for `metrics`), and other relevant fields to avoid including unrelated data.

DuckDB uses a [PostgreSQL-compatible SQL dialect](https://duckdb.org/docs/stable/sql/dialect/overview).

Expand Down
32 changes: 31 additions & 1 deletion skills/apitally-cli/references/duckdb_tables.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# DuckDB Table Schemas

Tables are created automatically when using the `--db` flag with `apps`, `consumers`, `endpoints`, `request-logs`, or `request-details` commands. DuckDB uses a [PostgreSQL-compatible SQL dialect](https://duckdb.org/docs/stable/sql/dialect/overview).
Tables are created automatically when using the `--db` flag with `apps`, `consumers`, `endpoints`, `metrics`, `request-logs`, or `request-details` commands. DuckDB uses a [PostgreSQL-compatible SQL dialect](https://duckdb.org/docs/stable/sql/dialect/overview).

## apps

Expand Down Expand Up @@ -56,6 +56,33 @@ CREATE TABLE endpoints (
);
```

## metrics

```sql
CREATE TABLE metrics (
app_id INTEGER NOT NULL,
period_start TIMESTAMPTZ NOT NULL,
period_end TIMESTAMPTZ NOT NULL,
env VARCHAR,
consumer_id BIGINT,
method VARCHAR,
path VARCHAR,
status_code INTEGER,
requests BIGINT,
requests_per_minute DOUBLE,
bytes_received BIGINT,
bytes_sent BIGINT,
client_errors BIGINT,
server_errors BIGINT,
error_rate DOUBLE,
response_time_p50 INTEGER, -- milliseconds
response_time_p75 INTEGER, -- milliseconds
response_time_p95 INTEGER -- milliseconds
);
```

Columns are only populated if included in `--metrics` or `--group-by` during fetch. No unique constraint; deduplication is handled by deleting existing rows for the time range being inserted.

## request_logs

```sql
Expand Down Expand Up @@ -129,9 +156,12 @@ Populated by the `request-details` command when using `--db`.
## Relationships

- `request_logs.consumer_id` references `consumers.consumer_id` (join on both `app_id` and `consumer_id`)
- `metrics.consumer_id` references `consumers.consumer_id` (join on both `app_id` and `consumer_id`, only when metrics are grouped by consumer_id)
- `endpoints.app_id` references `apps.app_id`
- `metrics.app_id` references `apps.app_id`
- `request_logs.app_id` references `apps.app_id`
- `app_envs.app_id` references `apps.app_id`
- `request_logs.env` matches `app_envs.name` (string, not a foreign key to `app_env_id`)
- `metrics.env` matches `app_envs.name` (string, only when metrics are grouped by env)
- `application_logs.request_uuid` references `request_logs.request_uuid` (join on both `app_id` and `request_uuid`)
- `spans.request_uuid` references `request_logs.request_uuid` (join on both `app_id` and `request_uuid`)
Loading
Loading