query-clickhouse-via-metabase — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited query-clickhouse-via-metabase (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
PostHog's production ClickHouse clusters are reachable for ad-hoc analysis through internal Metabase instances. Both Metabases sit behind an AWS ALB with Cognito OAuth, so authentication is SSO-gated — Metabase API keys alone won't work.
This skill is for system.query_log analysis from inside the posthog repo. For pre-built canned queries (slow query summaries, materialization analysis), see the query-performance-analysis repo, which is the source of truth for those and uses the same Metabase API surface.
| Region | Metabase URL |
|---|---|
| US | https://metabase.prod-us.posthog.dev |
| EU | https://metabase.prod-eu.posthog.dev |
Database IDs are not stable — they change when Metabase's metadata DB is rebuilt or connections are re-added. Never hardcode an ID. Always discover the current list:
hogli metabase:databases --region us
hogli metabase:databases --region euRegional layout (names may vary; re-check with metabase:databases):
query_log and data reads).query_log analysis) and a data tier (production reads: events, persons, etc.). Pick the one whose name indicates the query tier.
the ingestion-layer and migrations databases.
Use hogli to get a valid cookie. It opens the system browser for SSO, captures cookies from the user's logged-in browser profile, and caches them at ~/.config/posthog/metabase/cookie-{region} (mode 0600).
# Log in once per region. --region is required (no default — you pick which one).
# Already-valid sessions are fast-pathed (no browser tab opens), so re-running
# is cheap.
hogli metabase:login --region us
hogli metabase:login --region euPrompt the user to run `hogli metabase:login` themselves — the harness blocks Keychain access from agent shells, so the user has to authenticate interactively.
metabase:queryhogli metabase:query reads the cached cookie internally and only emits results — the session value never appears in the agent's transcript. metabase:cookie exists for humans who want to hand-roll curl against Metabase.
hogli metabase:databases --region <region>.hogli metabase:query. Pipe SQL via stdin or --file.# 1. Find the ClickHouse database ID for your region
hogli metabase:databases --region us
# e.g. output row: 42 ClickHouse clickhouse
# 2. Run the query. The cookie is read internally; nothing leaks to stdout.
hogli metabase:query --region us --database-id 42 --save /tmp/out.tsv <<'SQL'
SELECT
JSONExtractInt(log_comment, 'team_id') AS team_id,
count() AS query_count,
formatReadableSize(sum(read_bytes)) AS total_bytes
FROM clusterAllReplicas(posthog, system, query_log)
WHERE event_time > now() - INTERVAL 1 DAY
AND is_initial_query
AND query_duration_ms > 30000
GROUP BY team_id
ORDER BY query_count DESC
LIMIT 20
SQLclusterAllReplicas(posthog, system, query_log) is the standard table reference — it fans out across the cluster.
For large result sets, use --save <path> so rows land in a file rather than streaming through the terminal/transcript. Default output is TSV; --format json gives you the raw /api/dataset response body.
If the DB ID is wrong, metabase:query exits non-zero with a pointer back to metabase:databases. Fail-fast is intentional — silently querying the wrong database is worse than failing.
query_duration_ms > 30000
OR exception_code IN (159, 160, 241)| Code | Meaning |
|---|---|
| 159 | TIMEOUT_EXCEEDED |
| 160 | TOO_SLOW |
| 241 | MEMORY_LIMIT_EXCEEDED |
SELECT
query_id,
JSONExtractInt(log_comment, 'team_id') AS team_id,
query_duration_ms,
formatReadableSize(memory_usage) AS memory,
formatReadableSize(read_bytes) AS read_bytes,
exception_code,
substring(query, 1, 200) AS query_preview
FROM clusterAllReplicas(posthog, system, query_log)
WHERE event_time > now() - INTERVAL 1 DAY
AND type = 'QueryFinish'
AND (query_duration_ms > 30000 OR exception_code IN (159, 160, 241))
AND JSONExtractString(log_comment, 'workload') NOT IN ('Workload.OFFLINE', 'OFFLINE')
AND JSONExtractString(log_comment, 'kind') NOT IN ('temporal')
AND JSONExtractString(log_comment, 'access_method') NOT IN ('personal_api_key')
AND is_initial_query
AND JSONExtractInt(log_comment, 'team_id') != 0
ORDER BY query_duration_ms DESC
LIMIT 100SELECT
JSONExtractInt(log_comment, 'team_id') AS team_id,
count() AS queries,
countIf(query_duration_ms > 30000) AS slow_queries,
formatReadableSize(sum(read_bytes)) AS total_read,
formatReadableSize(max(memory_usage)) AS peak_memory,
quantile(0.95)(query_duration_ms) AS p95_duration_ms
FROM clusterAllReplicas(posthog, system, query_log)
WHERE event_time > now() - INTERVAL 7 DAY
AND type = 'QueryFinish'
AND JSONExtractString(log_comment, 'workload') NOT IN ('Workload.OFFLINE', 'OFFLINE')
AND JSONExtractString(log_comment, 'kind') NOT IN ('temporal')
AND JSONExtractString(log_comment, 'access_method') NOT IN ('personal_api_key')
AND is_initial_query
AND JSONExtractInt(log_comment, 'team_id') != 0
GROUP BY team_id
ORDER BY total_read DESC
LIMIT 20query_idSaved card available in both regions — match the URL to where the query ran:
# US
https://metabase.prod-us.posthog.dev/question/795-look-up-query-by-query-id?query_id=<ID>&include_query_start=No&event_date=<YYYY-MM-DD>
# EU (same card ID may differ — find it in EU Metabase if 795 doesn't resolve)
https://metabase.prod-eu.posthog.dev/question/795-look-up-query-by-query-id?query_id=<ID>&include_query_start=No&event_date=<YYYY-MM-DD>The same can be reproduced programmatically with a WHERE query_id = '...' clause via /api/dataset against the right region's DB ID.
{
"data": {
"cols": [{"name": "team_id", "base_type": "type/Integer"}, ...],
"rows": [[55348, 142, "1.23 TiB"], ...]
},
"status": "completed",
"row_count": 20
}Quick TSV pipe:
... | python3 -c "
import json, sys
d = json.load(sys.stdin)
cols = [c['name'] for c in d['data']['cols']]
print('\t'.join(cols))
for row in d['data']['rows']:
print('\t'.join(str(v) for v in row))
"| Symptom | Cause | Fix |
|---|---|---|
HTTP 302 to /auth/... | Cookie expired or missing | Tell user to run hogli metabase:login --region <region> |
| HTTP 401 | Cookie rejected by ALB | Same as 302 |
"status": "failed" + error | ClickHouse error (syntax, table, etc.) | Read error; fix SQL |
| Hangs / timeout | Wide query_log scan | Narrow event_time range, add team_id filter, use cluster() |
query_log is large; default to 1h–24h, expand only when needed.QueryStart and ExceptionBeforeStart rows.WHERE by the worst offender to see individual queries.query_log themselves.JSONExtractString(log_comment, 'foo') returns '' if missing — always include an IS NOT NULL / != '' guard if filtering on it.hogli metabase:login --region <region> for every region you need; --region is required.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.