bqtop 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bqtop-0.3.1/.github/workflows/ci.yml +18 -0
- bqtop-0.3.1/.github/workflows/release.yml +69 -0
- bqtop-0.3.1/.gitignore +10 -0
- bqtop-0.3.1/CHANGELOG.md +32 -0
- bqtop-0.3.1/LICENSE +21 -0
- bqtop-0.3.1/PKG-INFO +189 -0
- bqtop-0.3.1/README.md +171 -0
- bqtop-0.3.1/docs/screenshot.svg +300 -0
- bqtop-0.3.1/examples/config.example.toml +63 -0
- bqtop-0.3.1/pyproject.toml +43 -0
- bqtop-0.3.1/scripts/build_binary.sh +25 -0
- bqtop-0.3.1/scripts/formula.py +69 -0
- bqtop-0.3.1/scripts/screenshot.py +27 -0
- bqtop-0.3.1/src/bqtop/__init__.py +3 -0
- bqtop-0.3.1/src/bqtop/__main__.py +5 -0
- bqtop-0.3.1/src/bqtop/app.py +395 -0
- bqtop-0.3.1/src/bqtop/cli.py +145 -0
- bqtop-0.3.1/src/bqtop/config.example.toml +63 -0
- bqtop-0.3.1/src/bqtop/config.py +144 -0
- bqtop-0.3.1/src/bqtop/fmt.py +108 -0
- bqtop-0.3.1/src/bqtop/model.py +138 -0
- bqtop-0.3.1/src/bqtop/pricing.py +61 -0
- bqtop-0.3.1/src/bqtop/render.py +246 -0
- bqtop-0.3.1/src/bqtop/sources/__init__.py +19 -0
- bqtop-0.3.1/src/bqtop/sources/audit_log.py +73 -0
- bqtop-0.3.1/src/bqtop/sources/base.py +179 -0
- bqtop-0.3.1/src/bqtop/sources/demo.py +120 -0
- bqtop-0.3.1/src/bqtop/sources/information_schema.py +79 -0
- bqtop-0.3.1/src/bqtop/store.py +162 -0
- bqtop-0.3.1/src/bqtop/wizard.py +171 -0
- bqtop-0.3.1/tests/test_config.py +81 -0
- bqtop-0.3.1/tests/test_fmt.py +33 -0
- bqtop-0.3.1/tests/test_store.py +152 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
python: ["3.12", "3.13"]
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v5
|
|
15
|
+
- run: uv python install ${{ matrix.python }}
|
|
16
|
+
- run: uv sync --python ${{ matrix.python }} --extra dev
|
|
17
|
+
- run: uv run ruff check src tests
|
|
18
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["v*"]
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: write
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
binaries:
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
include:
|
|
15
|
+
- { os: macos-latest, name: macos-arm64 }
|
|
16
|
+
- { os: macos-15-intel, name: macos-x86_64 }
|
|
17
|
+
- { os: ubuntu-latest, name: linux-x86_64 }
|
|
18
|
+
- { os: ubuntu-24.04-arm, name: linux-arm64 }
|
|
19
|
+
runs-on: ${{ matrix.os }}
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: astral-sh/setup-uv@v5
|
|
23
|
+
- run: uv python install 3.13
|
|
24
|
+
- run: uv sync --python 3.13
|
|
25
|
+
- run: scripts/build_binary.sh
|
|
26
|
+
- uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: ${{ matrix.name }}
|
|
29
|
+
path: |
|
|
30
|
+
dist/*.tar.gz
|
|
31
|
+
dist/*.sha256
|
|
32
|
+
|
|
33
|
+
release:
|
|
34
|
+
needs: binaries
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
- uses: actions/download-artifact@v4
|
|
39
|
+
with: { path: assets }
|
|
40
|
+
- name: version
|
|
41
|
+
id: v
|
|
42
|
+
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
|
43
|
+
- name: formula
|
|
44
|
+
run: python3 scripts/formula.py "${{ steps.v.outputs.version }}" assets > assets/bqtop.rb
|
|
45
|
+
- name: github release
|
|
46
|
+
env: { GH_TOKEN: "${{ github.token }}" }
|
|
47
|
+
run: |
|
|
48
|
+
gh release create "$GITHUB_REF_NAME" --title "bqtop ${{ steps.v.outputs.version }}" --generate-notes \
|
|
49
|
+
assets/*/*.tar.gz assets/*/*.sha256 assets/bqtop.rb
|
|
50
|
+
- name: update homebrew tap
|
|
51
|
+
if: env.TAP_TOKEN != ''
|
|
52
|
+
env: { TAP_TOKEN: "${{ secrets.TAP_GITHUB_TOKEN }}" }
|
|
53
|
+
run: |
|
|
54
|
+
git clone "https://x-access-token:${TAP_TOKEN}@github.com/dadadima/homebrew-tap.git" tap
|
|
55
|
+
mkdir -p tap/Formula && cp assets/bqtop.rb tap/Formula/bqtop.rb
|
|
56
|
+
cd tap && git config user.name "bqtop-release" && git config user.email "bqtop-release@users.noreply.github.com"
|
|
57
|
+
git add Formula/bqtop.rb && git commit -m "bqtop ${{ steps.v.outputs.version }}" && git push
|
|
58
|
+
|
|
59
|
+
pypi:
|
|
60
|
+
needs: binaries
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
environment: pypi
|
|
63
|
+
permissions: { id-token: write, contents: read }
|
|
64
|
+
continue-on-error: true # until the PyPI trusted publisher is configured
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v4
|
|
67
|
+
- uses: astral-sh/setup-uv@v5
|
|
68
|
+
- run: uv build
|
|
69
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
bqtop-0.3.1/.gitignore
ADDED
bqtop-0.3.1/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.1 (2026-09-30)
|
|
4
|
+
|
|
5
|
+
- `[source].credentials_file`: run bqtop as a service account instead of Application Default
|
|
6
|
+
Credentials, for setups where the user login expires.
|
|
7
|
+
- `--check` reports the identity actually used and the coverage: jobs and projects seen in the last 24h,
|
|
8
|
+
with a hint when `JOBS_BY_FOLDER` only sees the billing project itself.
|
|
9
|
+
|
|
10
|
+
## 0.3.0 (2026-09-30)
|
|
11
|
+
|
|
12
|
+
- `bqtop --init` is an interactive setup: detects the gcloud project and timezone, asks source, scope,
|
|
13
|
+
pricing and refresh, writes the config and runs `--check`. `--init -y` writes the example silently.
|
|
14
|
+
- dbt models panel: `d` swaps hot tables for cost per dbt model, parsed from dbt's query comment
|
|
15
|
+
(`node_id`). Also in `--once` output and `--json` (`by_model`).
|
|
16
|
+
- Single-file binaries for macOS (arm64, x86_64) and Linux (arm64, x86_64) on every `v*` tag, via
|
|
17
|
+
PyInstaller, attached to the GitHub release together with a Homebrew formula.
|
|
18
|
+
- Homebrew: `brew install dadadima/tap/bqtop`.
|
|
19
|
+
- Release workflow also publishes to PyPI once a trusted publisher is configured.
|
|
20
|
+
|
|
21
|
+
## 0.2.0 (2026-09-30)
|
|
22
|
+
|
|
23
|
+
- Local job store with incremental refresh; sort, filter and window changes never hit BigQuery.
|
|
24
|
+
- Pricing model: `auto` / `on_demand` / `slots`, per-project overrides, reservation-aware.
|
|
25
|
+
- `[budgets]` in USD/day for principals and projects, next to `[quotas]`.
|
|
26
|
+
- Cost sparkline, `/` filter, enter to drill down, job detail with full query, `?` help, `p` pause.
|
|
27
|
+
- `--demo`, `--check` with permission hints, `--watch`, `--json`, `-f`, multi-region.
|
|
28
|
+
- Tests, ruff, CI, screenshot from demo data.
|
|
29
|
+
|
|
30
|
+
## 0.1.0 (2026-09-30)
|
|
31
|
+
|
|
32
|
+
- First cut: INFORMATION_SCHEMA and audit-log sources, four panels, `--once`.
|
bqtop-0.3.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Davide Di Matteo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bqtop-0.3.1/PKG-INFO
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bqtop
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: htop for BigQuery: live jobs, principals, projects, hot tables and cost in your terminal
|
|
5
|
+
Project-URL: Homepage, https://github.com/dadadima/bqtop
|
|
6
|
+
Author: Davide Di Matteo
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: bigquery,cost,gcp,monitoring,tui
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Requires-Dist: google-cloud-bigquery>=3.25
|
|
12
|
+
Requires-Dist: rich>=13
|
|
13
|
+
Requires-Dist: textual>=0.80
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# bqtop
|
|
20
|
+
|
|
21
|
+
htop for BigQuery. A terminal dashboard that shows who is running what on your BigQuery projects
|
|
22
|
+
right now, what it costs, which tables are hot, and where today's spend stands against quotas and
|
|
23
|
+
budgets.
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
It reads the metadata BigQuery already keeps, backfills once, then refreshes incrementally like `htop`.
|
|
28
|
+
No agents, no sinks to build, no dashboards to click through.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
brew install dadadima/tap/bqtop # macOS / Linux binary, no Python needed
|
|
32
|
+
# or: uv tool install git+https://github.com/dadadima/bqtop (pipx works too)
|
|
33
|
+
# or: grab a binary from https://github.com/dadadima/bqtop/releases
|
|
34
|
+
|
|
35
|
+
bqtop --demo # look around on synthetic data, no GCP needed
|
|
36
|
+
bqtop --init # interactive setup: detects your project, writes the config
|
|
37
|
+
bqtop # the TUI
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`bqtop --init` asks five questions (source, project, scope, pricing, timezone), writes
|
|
41
|
+
`~/.config/bqtop/config.toml` and runs `bqtop --check`, which tells you who you are, what the source
|
|
42
|
+
scan costs, and the exact role to grant if something is missing.
|
|
43
|
+
|
|
44
|
+
## What you see
|
|
45
|
+
|
|
46
|
+
| panel | what |
|
|
47
|
+
| -------------- | --------------------------------------------------------------------------------------------------------- |
|
|
48
|
+
| summary | jobs, running, errors, cache-hit ratio, bytes billed and cost in the window, cost today, slot-hours |
|
|
49
|
+
| timeline | cost per time bucket across the window, so a spike is visible before the bill is |
|
|
50
|
+
| principals | who: jobs, running, errors, billed, cost, cost today, slot-hours, daily budget % |
|
|
51
|
+
| projects | where: same, plus today's `QueryUsagePerDay` quota % or budget % |
|
|
52
|
+
| hot tables | which tables the money goes to (a job touching three tables counts against all three: an upper bound) |
|
|
53
|
+
| dbt models | `d` swaps in cost per dbt model, parsed from dbt's query comment (`node_id`) |
|
|
54
|
+
| jobs | live stream, running first, then newest: state, principal, project, type, duration, billed, cost, query |
|
|
55
|
+
|
|
56
|
+
Press `enter` on a principal, project or table to drill down (everything filters to it), on a job to see
|
|
57
|
+
its details and full query text.
|
|
58
|
+
|
|
59
|
+
## Keys
|
|
60
|
+
|
|
61
|
+
| key | action |
|
|
62
|
+
| ------- | ------------------------------------------------------------------- |
|
|
63
|
+
| `q` | quit |
|
|
64
|
+
| `r` | refresh now |
|
|
65
|
+
| `w` | wider window: 1h → 6h → 24h → 72h → 168h |
|
|
66
|
+
| `s` | cycle sort: cost, bytes, jobs, errors, slots |
|
|
67
|
+
| `j` | hide/show the tables panel (widens the job stream) |
|
|
68
|
+
| `d` | tables panel: hot tables ↔ dbt models |
|
|
69
|
+
| `/` | filter on principal, project, table, query text or error message |
|
|
70
|
+
| `esc` | clear the filter / close a dialog |
|
|
71
|
+
| `p` | pause auto-refresh |
|
|
72
|
+
| `enter` | drill down on a row; job details on a job |
|
|
73
|
+
| `?` | help |
|
|
74
|
+
|
|
75
|
+
Sorting, filtering and drill-down never touch BigQuery: they re-aggregate the local store. Widening the
|
|
76
|
+
window backfills the missing range once.
|
|
77
|
+
|
|
78
|
+
## Sources
|
|
79
|
+
|
|
80
|
+
Pick one with `[source].kind`. Both are standard GCP surfaces.
|
|
81
|
+
|
|
82
|
+
| kind | reads | running jobs | permissions |
|
|
83
|
+
| -------------------- | ---------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `information_schema` | `INFORMATION_SCHEMA.JOBS_BY_{PROJECT,FOLDER,ORGANIZATION,USER}` | yes | `bigquery.jobs.listAll` at that level, e.g. `roles/bigquery.resourceViewer`; nothing extra for `user` |
|
|
85
|
+
| `audit_log` | a routed `cloudaudit_googleapis_com_data_access` table | no | `dataViewer` on the sink table, `jobUser` on the billing project |
|
|
86
|
+
|
|
87
|
+
`information_schema` is the default: real time, includes `RUNNING` jobs, nothing to set up.
|
|
88
|
+
`scope = "folder"` gives one view over every project under the folder that directly contains
|
|
89
|
+
`billing_project`, sub-folders included. To watch a whole tree, run bqtop from a project that sits right
|
|
90
|
+
under the top folder; `bqtop --check` prints how many projects the source actually sees.
|
|
91
|
+
`audit_log` is for setups that already route BigQuery audit logs to a table (a folder- or org-level
|
|
92
|
+
sink), which also works when you cannot get `jobs.listAll` everywhere.
|
|
93
|
+
|
|
94
|
+
Authentication is whatever `google-cloud-bigquery` finds: Application Default Credentials
|
|
95
|
+
(`gcloud auth application-default login`), a service-account key via `GOOGLE_APPLICATION_CREDENTIALS`,
|
|
96
|
+
or the metadata server. Set `[source].credentials_file` to a service-account key to pin the identity
|
|
97
|
+
instead, useful when your user login expires daily. `bqtop --check` tells you who you are and what is missing.
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
`~/.config/bqtop/config.toml`, or `./bqtop.toml` in the current directory. Full example with comments in
|
|
102
|
+
[`examples/config.example.toml`](examples/config.example.toml).
|
|
103
|
+
|
|
104
|
+
```toml
|
|
105
|
+
[source]
|
|
106
|
+
kind = "information_schema"
|
|
107
|
+
billing_project = "my-admin-project" # runs and pays for bqtop's own queries
|
|
108
|
+
regions = ["us"]
|
|
109
|
+
scope = "folder" # project | folder | organization | user
|
|
110
|
+
|
|
111
|
+
[pricing]
|
|
112
|
+
mode = "auto" # auto | on_demand | slots
|
|
113
|
+
on_demand_usd_per_tib = 6.25
|
|
114
|
+
slot_usd_per_hour = 0.06 # Editions pay-as-you-go: standard 0.04, enterprise 0.06, plus 0.10
|
|
115
|
+
|
|
116
|
+
[pricing.projects]
|
|
117
|
+
"my-dbt-project" = { mode = "slots", slot_usd_per_hour = 0.04 }
|
|
118
|
+
|
|
119
|
+
[quotas] # daily QueryUsagePerDay caps, shown as % used today
|
|
120
|
+
"my-agents-project" = "5 TiB"
|
|
121
|
+
|
|
122
|
+
[budgets] # daily spend budgets, USD, by principal or project
|
|
123
|
+
"svc-airflow@my-ingest.iam.gserviceaccount.com" = 50
|
|
124
|
+
"my-agents-project" = "$10"
|
|
125
|
+
|
|
126
|
+
[ui]
|
|
127
|
+
refresh_seconds = 60
|
|
128
|
+
window_hours = 24
|
|
129
|
+
max_window_hours = 168
|
|
130
|
+
timezone = "Europe/Brussels"
|
|
131
|
+
top_n = 15
|
|
132
|
+
stream_rows = 40
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Pricing
|
|
136
|
+
|
|
137
|
+
BigQuery bills analysis either on demand (bytes) or through slots (Editions). `mode = "auto"` prices
|
|
138
|
+
each job by how it actually ran: in a reservation, slot-hours × `slot_usd_per_hour`; otherwise bytes
|
|
139
|
+
billed × `on_demand_usd_per_tib`. Override per project when a project is on a different edition. The
|
|
140
|
+
result is an estimate, not the invoice: storage, streaming inserts, Storage Read API and egress are not
|
|
141
|
+
in job metadata.
|
|
142
|
+
|
|
143
|
+
### Quotas and budgets
|
|
144
|
+
|
|
145
|
+
`[quotas]` holds `QueryUsagePerDay` caps in bytes per project. `[budgets]` holds dollars per day for a
|
|
146
|
+
principal or a project. Both compare against **today**: bytes billed or cost since local midnight in
|
|
147
|
+
`[ui].timezone`, independent of the window you are looking at. Cells turn yellow at 60 % and red at 90 %.
|
|
148
|
+
|
|
149
|
+
## Scripting
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
bqtop --once # Rich tables, one snapshot
|
|
153
|
+
bqtop --once -w 6 --sort errors # 6 hours, worst offenders first
|
|
154
|
+
bqtop --once -f svc-airflow # only jobs matching a text
|
|
155
|
+
bqtop --once --json | jq '.by_principal[0]'
|
|
156
|
+
bqtop --watch 30 # plain-text refresh every 30 s, for tmux panes
|
|
157
|
+
bqtop --check # exits non-zero with hints if something is missing
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## What it costs to run
|
|
161
|
+
|
|
162
|
+
bqtop bills its own queries to `billing_project`. On `information_schema` each refresh is one query with
|
|
163
|
+
a 10 MiB minimum, so a 60 s refresh is under a dollar a month. On `audit_log` the sink table is
|
|
164
|
+
day-partitioned, so each incremental refresh rescans the current day's partition; the summary line shows
|
|
165
|
+
what the last refresh billed and the session total. `bqtop --check` prints what a 24 h backfill scans.
|
|
166
|
+
|
|
167
|
+
## Not covered
|
|
168
|
+
|
|
169
|
+
Storage Read API sessions, network egress, storage and streaming costs do not appear in job metadata.
|
|
170
|
+
If you stream tables out with Spark, DuckDB or Arrow clients, that spend is invisible here; Cloud Billing
|
|
171
|
+
export is the place for it. A billing-export source is the natural next step.
|
|
172
|
+
|
|
173
|
+
## Releases
|
|
174
|
+
|
|
175
|
+
Every `v*` tag builds single-file binaries (PyInstaller) for macOS arm64 / x86_64 and Linux arm64 /
|
|
176
|
+
x86_64, attaches them to a GitHub release with their sha256 and a generated Homebrew formula, and
|
|
177
|
+
updates [`dadadima/homebrew-tap`](https://github.com/dadadima/homebrew-tap). `scripts/build_binary.sh`
|
|
178
|
+
does the same locally.
|
|
179
|
+
|
|
180
|
+
## Development
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
uv sync --extra dev
|
|
184
|
+
uv run pytest
|
|
185
|
+
uv run ruff check src tests && uv run ruff format src tests
|
|
186
|
+
uv run python scripts/screenshot.py # regenerate docs/screenshot.svg from the demo source
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
MIT.
|
bqtop-0.3.1/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# bqtop
|
|
2
|
+
|
|
3
|
+
htop for BigQuery. A terminal dashboard that shows who is running what on your BigQuery projects
|
|
4
|
+
right now, what it costs, which tables are hot, and where today's spend stands against quotas and
|
|
5
|
+
budgets.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
It reads the metadata BigQuery already keeps, backfills once, then refreshes incrementally like `htop`.
|
|
10
|
+
No agents, no sinks to build, no dashboards to click through.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
brew install dadadima/tap/bqtop # macOS / Linux binary, no Python needed
|
|
14
|
+
# or: uv tool install git+https://github.com/dadadima/bqtop (pipx works too)
|
|
15
|
+
# or: grab a binary from https://github.com/dadadima/bqtop/releases
|
|
16
|
+
|
|
17
|
+
bqtop --demo # look around on synthetic data, no GCP needed
|
|
18
|
+
bqtop --init # interactive setup: detects your project, writes the config
|
|
19
|
+
bqtop # the TUI
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`bqtop --init` asks five questions (source, project, scope, pricing, timezone), writes
|
|
23
|
+
`~/.config/bqtop/config.toml` and runs `bqtop --check`, which tells you who you are, what the source
|
|
24
|
+
scan costs, and the exact role to grant if something is missing.
|
|
25
|
+
|
|
26
|
+
## What you see
|
|
27
|
+
|
|
28
|
+
| panel | what |
|
|
29
|
+
| -------------- | --------------------------------------------------------------------------------------------------------- |
|
|
30
|
+
| summary | jobs, running, errors, cache-hit ratio, bytes billed and cost in the window, cost today, slot-hours |
|
|
31
|
+
| timeline | cost per time bucket across the window, so a spike is visible before the bill is |
|
|
32
|
+
| principals | who: jobs, running, errors, billed, cost, cost today, slot-hours, daily budget % |
|
|
33
|
+
| projects | where: same, plus today's `QueryUsagePerDay` quota % or budget % |
|
|
34
|
+
| hot tables | which tables the money goes to (a job touching three tables counts against all three: an upper bound) |
|
|
35
|
+
| dbt models | `d` swaps in cost per dbt model, parsed from dbt's query comment (`node_id`) |
|
|
36
|
+
| jobs | live stream, running first, then newest: state, principal, project, type, duration, billed, cost, query |
|
|
37
|
+
|
|
38
|
+
Press `enter` on a principal, project or table to drill down (everything filters to it), on a job to see
|
|
39
|
+
its details and full query text.
|
|
40
|
+
|
|
41
|
+
## Keys
|
|
42
|
+
|
|
43
|
+
| key | action |
|
|
44
|
+
| ------- | ------------------------------------------------------------------- |
|
|
45
|
+
| `q` | quit |
|
|
46
|
+
| `r` | refresh now |
|
|
47
|
+
| `w` | wider window: 1h → 6h → 24h → 72h → 168h |
|
|
48
|
+
| `s` | cycle sort: cost, bytes, jobs, errors, slots |
|
|
49
|
+
| `j` | hide/show the tables panel (widens the job stream) |
|
|
50
|
+
| `d` | tables panel: hot tables ↔ dbt models |
|
|
51
|
+
| `/` | filter on principal, project, table, query text or error message |
|
|
52
|
+
| `esc` | clear the filter / close a dialog |
|
|
53
|
+
| `p` | pause auto-refresh |
|
|
54
|
+
| `enter` | drill down on a row; job details on a job |
|
|
55
|
+
| `?` | help |
|
|
56
|
+
|
|
57
|
+
Sorting, filtering and drill-down never touch BigQuery: they re-aggregate the local store. Widening the
|
|
58
|
+
window backfills the missing range once.
|
|
59
|
+
|
|
60
|
+
## Sources
|
|
61
|
+
|
|
62
|
+
Pick one with `[source].kind`. Both are standard GCP surfaces.
|
|
63
|
+
|
|
64
|
+
| kind | reads | running jobs | permissions |
|
|
65
|
+
| -------------------- | ---------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `information_schema` | `INFORMATION_SCHEMA.JOBS_BY_{PROJECT,FOLDER,ORGANIZATION,USER}` | yes | `bigquery.jobs.listAll` at that level, e.g. `roles/bigquery.resourceViewer`; nothing extra for `user` |
|
|
67
|
+
| `audit_log` | a routed `cloudaudit_googleapis_com_data_access` table | no | `dataViewer` on the sink table, `jobUser` on the billing project |
|
|
68
|
+
|
|
69
|
+
`information_schema` is the default: real time, includes `RUNNING` jobs, nothing to set up.
|
|
70
|
+
`scope = "folder"` gives one view over every project under the folder that directly contains
|
|
71
|
+
`billing_project`, sub-folders included. To watch a whole tree, run bqtop from a project that sits right
|
|
72
|
+
under the top folder; `bqtop --check` prints how many projects the source actually sees.
|
|
73
|
+
`audit_log` is for setups that already route BigQuery audit logs to a table (a folder- or org-level
|
|
74
|
+
sink), which also works when you cannot get `jobs.listAll` everywhere.
|
|
75
|
+
|
|
76
|
+
Authentication is whatever `google-cloud-bigquery` finds: Application Default Credentials
|
|
77
|
+
(`gcloud auth application-default login`), a service-account key via `GOOGLE_APPLICATION_CREDENTIALS`,
|
|
78
|
+
or the metadata server. Set `[source].credentials_file` to a service-account key to pin the identity
|
|
79
|
+
instead, useful when your user login expires daily. `bqtop --check` tells you who you are and what is missing.
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
`~/.config/bqtop/config.toml`, or `./bqtop.toml` in the current directory. Full example with comments in
|
|
84
|
+
[`examples/config.example.toml`](examples/config.example.toml).
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
[source]
|
|
88
|
+
kind = "information_schema"
|
|
89
|
+
billing_project = "my-admin-project" # runs and pays for bqtop's own queries
|
|
90
|
+
regions = ["us"]
|
|
91
|
+
scope = "folder" # project | folder | organization | user
|
|
92
|
+
|
|
93
|
+
[pricing]
|
|
94
|
+
mode = "auto" # auto | on_demand | slots
|
|
95
|
+
on_demand_usd_per_tib = 6.25
|
|
96
|
+
slot_usd_per_hour = 0.06 # Editions pay-as-you-go: standard 0.04, enterprise 0.06, plus 0.10
|
|
97
|
+
|
|
98
|
+
[pricing.projects]
|
|
99
|
+
"my-dbt-project" = { mode = "slots", slot_usd_per_hour = 0.04 }
|
|
100
|
+
|
|
101
|
+
[quotas] # daily QueryUsagePerDay caps, shown as % used today
|
|
102
|
+
"my-agents-project" = "5 TiB"
|
|
103
|
+
|
|
104
|
+
[budgets] # daily spend budgets, USD, by principal or project
|
|
105
|
+
"svc-airflow@my-ingest.iam.gserviceaccount.com" = 50
|
|
106
|
+
"my-agents-project" = "$10"
|
|
107
|
+
|
|
108
|
+
[ui]
|
|
109
|
+
refresh_seconds = 60
|
|
110
|
+
window_hours = 24
|
|
111
|
+
max_window_hours = 168
|
|
112
|
+
timezone = "Europe/Brussels"
|
|
113
|
+
top_n = 15
|
|
114
|
+
stream_rows = 40
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Pricing
|
|
118
|
+
|
|
119
|
+
BigQuery bills analysis either on demand (bytes) or through slots (Editions). `mode = "auto"` prices
|
|
120
|
+
each job by how it actually ran: in a reservation, slot-hours × `slot_usd_per_hour`; otherwise bytes
|
|
121
|
+
billed × `on_demand_usd_per_tib`. Override per project when a project is on a different edition. The
|
|
122
|
+
result is an estimate, not the invoice: storage, streaming inserts, Storage Read API and egress are not
|
|
123
|
+
in job metadata.
|
|
124
|
+
|
|
125
|
+
### Quotas and budgets
|
|
126
|
+
|
|
127
|
+
`[quotas]` holds `QueryUsagePerDay` caps in bytes per project. `[budgets]` holds dollars per day for a
|
|
128
|
+
principal or a project. Both compare against **today**: bytes billed or cost since local midnight in
|
|
129
|
+
`[ui].timezone`, independent of the window you are looking at. Cells turn yellow at 60 % and red at 90 %.
|
|
130
|
+
|
|
131
|
+
## Scripting
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
bqtop --once # Rich tables, one snapshot
|
|
135
|
+
bqtop --once -w 6 --sort errors # 6 hours, worst offenders first
|
|
136
|
+
bqtop --once -f svc-airflow # only jobs matching a text
|
|
137
|
+
bqtop --once --json | jq '.by_principal[0]'
|
|
138
|
+
bqtop --watch 30 # plain-text refresh every 30 s, for tmux panes
|
|
139
|
+
bqtop --check # exits non-zero with hints if something is missing
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## What it costs to run
|
|
143
|
+
|
|
144
|
+
bqtop bills its own queries to `billing_project`. On `information_schema` each refresh is one query with
|
|
145
|
+
a 10 MiB minimum, so a 60 s refresh is under a dollar a month. On `audit_log` the sink table is
|
|
146
|
+
day-partitioned, so each incremental refresh rescans the current day's partition; the summary line shows
|
|
147
|
+
what the last refresh billed and the session total. `bqtop --check` prints what a 24 h backfill scans.
|
|
148
|
+
|
|
149
|
+
## Not covered
|
|
150
|
+
|
|
151
|
+
Storage Read API sessions, network egress, storage and streaming costs do not appear in job metadata.
|
|
152
|
+
If you stream tables out with Spark, DuckDB or Arrow clients, that spend is invisible here; Cloud Billing
|
|
153
|
+
export is the place for it. A billing-export source is the natural next step.
|
|
154
|
+
|
|
155
|
+
## Releases
|
|
156
|
+
|
|
157
|
+
Every `v*` tag builds single-file binaries (PyInstaller) for macOS arm64 / x86_64 and Linux arm64 /
|
|
158
|
+
x86_64, attaches them to a GitHub release with their sha256 and a generated Homebrew formula, and
|
|
159
|
+
updates [`dadadima/homebrew-tap`](https://github.com/dadadima/homebrew-tap). `scripts/build_binary.sh`
|
|
160
|
+
does the same locally.
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
uv sync --extra dev
|
|
166
|
+
uv run pytest
|
|
167
|
+
uv run ruff check src tests && uv run ruff format src tests
|
|
168
|
+
uv run python scripts/screenshot.py # regenerate docs/screenshot.svg from the demo source
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
MIT.
|