Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lbinspect

Offline, zero-network, zero-credential health scorecard for captured Databricks Lakebase / Postgres diagnostic dumps.

lbinspect reads a directory of diagnostic files that you (or a capture script) exported from a Lakebase/Postgres instance, analyzes seven operational domains — indexing, bloat, connection pools, autoscaling, query plans, cost, and zero-copy branching — and prints a single weighted health score (0–100) with per-domain sub-scores, ranked findings, and remediation SQL.

Everything runs locally against files on disk. lbinspect never connects to a database, calls a network service, or reads a credential. The only inputs are the dump directory and the flags you pass. It has zero third-party dependencies — pure Node.js (>= 20) built-ins.


Install

lbinspect is a plain ES-module Node project with no dependencies.

# from a checkout
node src/cli.js <dump-dir>

# or link it as a global `lbinspect` command
npm link
lbinspect <dump-dir>

package.json declares the binary:

"bin": { "lbinspect": "src/cli.js" }

Usage

lbinspect <dump-dir> [options]
Option Meaning
--format <fmt> Output format: terminal (default), json, markdown, html, sql, junit.
--fail-under <N> Exit non-zero when the health score is below N (0–100). The report still prints to stdout. For CI gates.
-h, --help Show help and exit.

Exit codes (CI semantics)

Code Meaning
0 Success — and, when --fail-under is given, the score met the threshold.
1 The health score is below --fail-under N. The report is still printed.
2 Bad arguments: missing/extra positional, unreadable dump dir, or invalid --format.

The report is always written to stdout; diagnostics and gate messages go to stderr. The terminal format honors NO_COLOR — set NO_COLOR=1 for plain, ANSI-free output (also nice for piping to a file).

Examples

# Print the terminal report (default)
lbinspect ./sample-dump

# Full-screen CLI monitor: box-framed panels, gauges, meters, severity histogram
lbinspect ./sample-dump --format dashboard

# Live monitor — redraw the dashboard every 2s, re-reading the dump each tick
lbinspect ./sample-dump --watch
lbinspect ./sample-dump --watch --interval 5

# Interactive explorer — arrow keys to browse findings, drill into detail
lbinspect ./sample-dump -i
#   ↑↓ / j k   move        PgUp/PgDn  page       Home/End  jump
#   /          filter      c          copy remediation SQL  q  quit
# (falls back to the static dashboard when output isn't a TTY)

# Machine-readable output for a dashboard or diff
lbinspect ./sample-dump --format json > report.json

# Self-contained, light-mode HTML dashboard (no external assets)
lbinspect ./sample-dump --format html > report.html

# Just the remediation SQL, ready to review and run
lbinspect ./sample-dump --format sql

# CI gate: fail the build if health drops below 80
lbinspect ./sample-dump --fail-under 80 --format junit

You can generate a realistic sample dump to try it against — the fixture generator writes the exact six-format layout below:

node src/fixtures/generate.js --dir ./sample-dump --seed 42
lbinspect ./sample-dump

Example output (terminal, NO_COLOR=1)

lbinspect report
Generated at 2026-07-22T14:14:10.548Z

Overall score: 42.2/100

Sub-scores
  Indexing     ████████████░░░░░░░░  62
  Bloat        ░░░░░░░░░░░░░░░░░░░░   0
  Connections  ███████░░░░░░░░░░░░░  34
  Autoscaling  █████████████████░░░  84
  Query Plans  ░░░░░░░░░░░░░░░░░░░░   0
  Cost         ████████████████░░░░  78
  Branching    ███████████████░░░░░  75

Rationale
  Overall health score 42.2/100.
Lowest domains: bloat 0/100 (5 finding(s)); queryPlans 0/100 (8 finding(s)); ...
Most urgent: [critical] Table bloat 66.5% on public.order_items ...
Cross-domain correlations:
• Autoscaler flapping coincides with connection-pool saturation: ...

Example output (dashboard, NO_COLOR=1)

The dashboard format (and --watch) renders a full-screen, box-framed monitor. Panels lay out in two columns on wide terminals and stack on narrow ones; with color on, gauges and bars are banded green/amber/red and findings are tinted by severity.

╭ Overview ──────────────────────────────────────────────────────────────────╮
│lbinspect  ·  Lakebase / Postgres health monitor                              │
│instance: lbinspect-fixture-instance   pg: 16.4 (Lakebase)                    │
╰──────────────────────────────────────────────────────────────────────────────╯
╭ Health ─────────────────────────╮ ╭ Domains ──────────────────────────────╮
│                        40.2/100 │ │Indexing    ███████████████░░░░░░░  62 │
│ █████████████░░░░░░░░░░░░░░░░░░ │ │Bloat       ░░░░░░░░░░░░░░░░░░░░░░   0 │
│                         AT RISK │ │Connections ████████░░░░░░░░░░░░░░  26 │
╰─────────────────────────────────╯ │Autoscaling █████████████████████  84 │
╭ Severity ───────────────────────╮ │ …                                     │
│critical ████░░░░░░░░░░░░░░░   3 │ ╰───────────────────────────────────────╯
│high     ████████░░░░░░░░░░░   7 │ ╭ Cost / reclaimable ───────────────────╮
│ …                               │ │• ~76.3 GiB reclaimable across 65 idx  │
╰─────────────────────────────────╯ ╰───────────────────────────────────────╯
╭ Findings (64) ───────────────────────────────────────────────────────────────╮
│[CRITICAL] public.order_items — Table bloat 64.7% on public.order_items       │
│ … +51 more finding(s)                                                        │
╰──────────────────────────────────────────────────────────────────────────────╯
SCORE 40.2/100   CRITICAL:3  HIGH:7  MEDIUM:18  LOW:10  INFO:26

The six capture formats (on-disk dump layout)

A dump directory holds these files. JSON/JSONL files use lbinspect's own capture format and carry the exact camelCase field names; TSV files mirror native Postgres pg_stat_* column names (snake_case), because that is what a real \copy … TO capture produces. Missing or empty files degrade gracefully (that domain simply contributes no data), so a partial dump still scores.

<dump-dir>/
  meta.json           SnapshotMeta            JSON object   (camelCase)
  stat_tables.tsv     StatTable[] (per table) TSV + header  (snake_case cols)
  stat_indexes.tsv    IndexStat[] (per index) TSV + header  (snake_case cols)
  storage.tsv         StorageStat[]           TSV + header  (snake_case cols)
  pool.json           Pool[]                  JSON array    (camelCase)
  autoscale.jsonl     AutoscaleEvent[]        JSONL, 1/line (camelCase)
  branches.json       BranchTree[] (flat)     JSON array    (camelCase)
  plans/
    plan-01.json      Plan                    JSON object   (camelCase + raw)
    plan-02.json      … one file per plan, zero-padded 2-digit index, sorted.

Six formats: TSV (stat_tables, stat_indexes, storage), JSON array (pool, branches), JSONL (autoscale), per-file JSON (plans/), plus meta.json.

1. stat_tables.tsvStatTable (one row per table)

Tab-separated, first line header, nulls as \N. Columns (TSV → core field): schema→schema, table→table, relid→relid, seq_scan→seqScan, seq_tup_read→seqTupRead, idx_scan→idxScan, idx_tup_fetch→idxTupFetch, n_live_tup→nLiveTup, n_dead_tup→nDeadTup, n_tup_ins/upd/del→nTupIns/Upd/Del, last_autovacuum→lastAutovacuum, last_analyze→lastAnalyze. Indexes are not inline — they live in stat_indexes.tsv and are joined by (schema, table).

2. stat_indexes.tsvIndexStat (one row per index)

schema, table (join keys), index_name→name, idx_scan→idxScan, idx_tup_read→idxTupRead, idx_tup_fetch→idxTupFetch, size_bytes→sizeBytes, is_unique→isUnique (t/f), is_primary→isPrimary (t/f).

3. storage.tsvStorageStat (one row per relation, per branch)

schema, table, size_bytes→sizeBytes, table_bytes→tableBytes, index_bytes→indexBytes, bloat_bytes→bloatBytes, bloat_pct→bloatPct (0–100), branch_ref→branchRef (a branchId, or \Nnull). Root-branch rows use schema=public; child-branch rows use schema=<branchId> so every relation key is globally unique.

4. pool.jsonPool[]

JSON array of camelCase pool objects:

[
  { "name": "lakebase-primary-rw", "maxConnections": 100, "activeConnections": 100,
    "idleConnections": 0, "waitingClients": 13, "totalConnections": 100 }
]

5. autoscale.jsonlAutoscaleEvent[]

One compact JSON object per line, chronological, camelCase:

{"timestamp":"2026-06-01T11:05:00.000Z","epochMs":1780311900000,"fromCapacity":2,"toCapacity":4,"direction":"up","reason":"cpu_high"}

6. branches.jsonBranchTree[] (flat list)

JSON array of camelCase branch nodes; exactly one root (parent: null, depth: 0). children, depth, and storageRefs (relation keys present in storage.tsv) are pre-derived and internally consistent:

{
  "branchId": "br_001", "name": "feature/deep-1", "parent": "br_000",
  "createdAt": "2026-03-11T12:00:00.000Z", "children": ["br_002"],
  "sizeBytes": 662700032, "uniqueBytes": 7340032, "depth": 1,
  "storageRefs": ["br_001.payments", "br_001.order_items"]
}

meta.jsonSnapshotMeta

{
  "capturedAt": "2026-06-01T12:00:00.000Z",
  "pgVersion": "16.4 (Lakebase)",
  "instanceName": "lbinspect-fixture-instance"
}

dumpDir is not written to disk — lbinspect stamps it at load time, since it knows where it read from.

plans/plan-NN.jsonPlan (one file per plan)

Each file is a single camelCase Plan with flattened top-of-tree fields plus a raw array holding the real EXPLAIN (FORMAT JSON) output. statTableRef is the relation key of the scanned StatTable, or null.

{
  "id": "q_events_scan",
  "query": "SELECT * FROM public.events WHERE payload->>'type' = $1 ORDER BY ts DESC",
  "nodeType": "Seq Scan", "relationName": "events",
  "planRows": 723, "actualRows": 23859,
  "totalCost": 1614392, "actualTotalTimeMs": 4647, "executionTimeMs": 4647.538,
  "statTableRef": "public.events",
  "raw": [ { "Plan": { "Node Type": "Seq Scan", "Relation Name": "events", "...": "..." } } ]
}

How the health score is computed

The score is a weighted mean of seven per-domain sub-scores, each derived by subtracting severity penalties from a perfect 100.

1. Findings → domains. Seven pure analyzers each read the assembled snapshot and emit Findings at a severity (info / low / medium / high / critical). Every finding is bucketed into exactly one scoring domain:

Domain Analyzer What it flags
indexing index-advisor unused / duplicate / missing indexes
bloat bloat table bloat %, high dead-tuple ratios
connections connection-pool pool saturation, waiting clients, suspected leaks
autoscaling autoscaling flapping, scale-to-zero churn, under-provisioning
queryPlans query-plan seq-scan pressure, row-estimate skew, expensive/slow plans
cost cost wasted storage/index spend, expensive compute, amplification
branching zero-copy-branch branch sprawl, deep nesting, stale leaves, storage amplification

2. Per-domain sub-score. Start at 100, subtract the penalty for each finding in that domain, then clamp to [0, 100] and round to 1 decimal place. Penalties per finding:

Severity info low medium high critical
Penalty 0 3 8 18 40

So a domain with one critical and one medium finding scores 100 − 40 − 8 = 52; enough severe findings drive a domain to 0.

3. Weighted aggregation. The overall score is the weighted mean of the sub-scores, over only the domains that actually have input data, normalized by the sum of those domains' weights. A domain with no relevant data is excluded entirely — an autoscale-only dump is not handed six free 100s. If no domain has data at all, the score is 100. Domain weights:

Domain indexing bloat connections autoscaling queryPlans cost branching
Weight 1.0 1.2 1.3 1.0 1.4 1.1 0.6

Query plans and connections weigh heaviest (they hit live latency); branching weighs least (hygiene, not an outage).

4. Cross-domain correlations. The rationale surfaces correlated problems that share a root cause and should be fixed together, e.g.:

  • Bloat + seq-scan on the same table — wasted pages inflate every seq scan.
  • Autoscale flapping + pool saturation — scale events reset warm connections while the pool is already full.
  • Scale-to-zero churn + waiting clients — cold starts add reconnection latency to queued requests.
  • Missing/unused index + seq-scan-heavy tables — indexing debt visible in the plans.
  • Branch sprawl + storage-amplification spend — branching hygiene compounding future storage cost.

These are reported for context; they do not double-count against the score.


Architecture

Pure, offline, dependency-free ES modules. One shared core module (src/core/index.js) defines every shape, threshold, and factory; everything else imports it directly (no barrels, no registries):

dump dir ─▶ src/snapshot.js  (assembleSnapshot: runs the six loaders)
         ─▶ src/scorecard.js (scoreSnapshot: runs the seven analyzers → Report)
         ─▶ src/exporters/*  (one pure Report → string per --format)
         ─▶ stdout

The CLI (src/cli.js) owns all wiring and flag parsing (via node:util parseArgs); each stage is a pure function tested in isolation, plus one end-to-end test (test/e2e.test.js) that drives the built CLI as a child process over a generated dump.

Run the full suite offline:

node --test

About

Offline, zero-network health scorecard for captured Databricks Lakebase / Postgres diagnostic dumps — interactive TUI dashboard + findings explorer, six export formats, CI gate. Zero dependencies.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages