Lightweight, high-performance monitoring platform for 20+ PostgreSQL instances. Uses a "Scatter-Gather" architecture: time-series metrics go to VictoriaMetrics, relational metadata (normalized SQL text) goes to a local PostgreSQL.
targets.yaml (20+ DSNs)
│
▼
┌─────────────┐ Influx Line Protocol ┌──────────────────┐
│ Collector │ ──────────────────────────▶ │ VictoriaMetrics │
│ (Go daemon) │ │ (port 8428) │
│ 10s ticker │ ── INSERT ON CONFLICT ──▶ └────────┬─────────┘
│ fan-out pool│ ┌────────┴─────────┐
└─────────────┘ │ PostgreSQL-meta │
│ (port 5433) │
┌──────────────────────────────────────┴──────────────────┘
│ scatter-gather (VM query + PG join)
▼
┌─────────────┐ JSON/REST ┌─────────────┐
│ Go API │ ◀─────────────────────── │ React SPA │
│ (chi, :8080)│ ──────────────────────▶ │ (Vite :5173)│
└─────────────┘ └─────────────┘
| Layer | Technology |
|---|---|
| Collector | Go 1.25, pgx/v5, pg_query_go/v6 (CGO) |
| API | Go 1.25, chi/v5, net/http |
| TSDB | VictoriaMetrics (single-node, Docker) |
| Metadata DB | PostgreSQL 18 (Docker, port 5433) |
| UI | React 18, Vite, TypeScript, Mantine v7, Tremor v3, TanStack Query v5 |
pgMahout uses two storage engines because time-series telemetry and relational metadata have fundamentally different access patterns, and no single database handles both well.
VictoriaMetrics excels at numeric time-series: it compresses counter/gauge data points 10-20x better than PostgreSQL, answers range queries over millions of samples in milliseconds, and handles high-throughput writes via the lightweight Influx line protocol. But it has no concept of relational data. pgMahout needs to store:
- Normalized SQL texts — full query bodies (often multi-KB), deduplicated and keyed by query hash. These are text
blobs that never change once written, need
INSERT ON CONFLICT DO NOTHINGsemantics, and are joined to time-series results at query time. - Target registry — monitored instance metadata (name, DSN, environment, squad) with auto-generated IDs, used for lookups, filtering, and foreign-key relationships.
- Settings snapshots — versioned pg_settings with SHA-256 deduplication, changelog diffs between versions, and
structured queries (
WHERE target_id = ? ORDER BY created_at DESC). - Collection history — audit trail of scrape outcomes per target with timestamps and error messages.
Storing any of this in VM labels or metric values would be an anti-pattern: labels have cardinality limits, values are numeric-only, and there is no way to do JOINs, text search, or transactional upserts.
PostgreSQL can technically store time-series (even with TimescaleDB), but for this workload it is the wrong trade-off:
- Write volume: 20+ targets × 5 sources × every 10s = thousands of data points per minute. Each point has 5-15 numeric fields. In PostgreSQL this means full-row INSERT with MVCC overhead, WAL amplification, and eventual VACUUM pressure. VictoriaMetrics appends compressed data points with near-zero overhead.
- Storage efficiency: VM achieves ~1.5 bytes/data-point on typical counter metrics via gorilla-style compression. PostgreSQL stores the same data at 50-100+ bytes/row (tuple header, alignment, indexes). Over 30 days of retention across 20 targets, this is the difference between hundreds of MB (VM) and tens of GB (PG).
- Query performance: "Give me QPS for the last 24h, downsampled to 5-minute buckets" is a native operation in VM
(
step=5min PromQL/MetricsQL). In PostgreSQL it requiresdate_trunc+GROUP BY+ sequential scan or BRIN index, which gets slower as data grows. VM's merge-tree storage is purpose-built for this. - Operational simplicity: VM is a single static binary, no extensions, no tuning, no vacuuming, no connection pooling. For a PoC monitoring tool, this matters.
- vs Prometheus: Prometheus is pull-based (requires exporters + scrape configs), stores data in a local TSDB with limited retention, and is designed for Kubernetes service discovery. pgMahout pushes metrics from a Go collector — VM's push-based Influx line protocol is a natural fit. VM also uses 5-7x less RAM and disk than Prometheus for the same data.
- vs InfluxDB: InfluxDB 3.x is moving to a commercial-only model. The open-source 1.x/2.x versions use significantly more resources than VM. VictoriaMetrics is fully open-source (Apache 2.0), API-compatible with both Prometheus and Influx, and consistently benchmarks lower on CPU, RAM, and disk for equivalent workloads.
- vs TimescaleDB: TimescaleDB is a PostgreSQL extension, which would let us use a single database — but it adds
operational complexity (chunk management, compression policies, continuous aggregates). For a PoC with 30-day
retention, VM's zero-config
--retentionPeriod=30dflag is simpler and performs better.
The API layer ties both stores together: handlers query VM for numeric time-series and PostgreSQL for metadata in parallel (WaitGroup + Mutex), then join the results in-memory before returning JSON. This keeps each storage engine doing what it does best while presenting a unified API to the frontend.
- Go 1.25+ (with CGO support for collector)
- Docker & Docker Compose
- pnpm, Node.js 20+
# 1. Boot infrastructure
make run
make migrate
# 2. Start collector (edit config/targets.yaml with real DSNs first)
CGO_ENABLED=1 go run ./cmd/collector
# 3. Start API
go run ./cmd/api
# 4. Start UI
cd ui && pnpm install && pnpm dev
# Open http://localhost:5173| Method | Path | Description |
|---|---|---|
| GET | /api/v1/fleet |
Fleet overview (all instances with health, QPS, sparklines) |
| GET | /api/v1/instances/{id}/metrics |
Time series metrics for charts |
| GET | /api/v1/instances/{id}/queries |
Top queries with normalized SQL |
| GET | /api/v1/instances/{id}/sessions |
Session counts by state |
All time-series measurements stored in VictoriaMetrics via Influx line protocol.
Tags: target, query_hash
Fields: calls, total_exec_ms, mean_exec_ms, rows, shared_blks_hit, shared_blks_read, min_exec_ms, max_exec_ms, stddev_exec_ms, temp_blks_read, temp_blks_written
Tags: target, state, wait_event_type, wait_event, usename, command
Fields: count
Tags: target
Fields: longest_query_seconds, longest_txn_seconds, idle_in_txn_count, longest_idle_in_txn_seconds
Tags: target, database
Fields: size, num_backends, xact_commit, xact_rollback, blks_read, blks_hit, xid_age, dead_tuples, autovacuum_count, deadlocks, conflicts, temp_files, temp_bytes, tup_returned, tup_fetched, tup_inserted, tup_updated, tup_deleted
Tags: target, database, schema, table
Fields: table_size, live_tuples, dead_tuples, vacuum_count, autovacuum_count, last_vacuum, last_autovacuum, last_autoanalyze, seq_scan, seq_tup_read, idx_scan, idx_tup_fetch
Tags: target, database, schema, table, index
Fields: idx_scan, idx_tup_read, idx_tup_fetch, idx_blks_read, idx_blks_hit
Tags: target
Fields: checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint, buffers_clean, buffers_backend, buffers_backend_fsync, maxwritten_clean
Tags: target, mode
Fields: granted, not_granted
Tags: target
Fields: total_granted, total_not_granted, longest_wait_seconds
Tags: target, client_addr, application_name, state
Fields: write_lag_ms, flush_lag_ms, replay_lag_ms, sent_lsn_offset
Tags: target
Fields: cpu_utilization, cpu_reserved_cores, memory_utilization, memory_quota, disk_utilization, disk_quota, disk_read_ops, disk_write_ops, connections, network_rx_bytes, network_tx_bytes, replica_lag
- pg_settings — versioned snapshots with SHA-256 dedup, changelog diffs (1h interval)
- Query texts — normalized SQL keyed by query hash
- Query callers — service/environment/version from Datadog DBM comments
- Target registry — instance metadata (name, DSN, environment, squad)
- Collection history — scrape outcomes per target
# Go unit tests (fast, no Docker)
make test
# Go integration tests (requires Docker for testcontainers-go)
make test-integration
# All Go tests
make test-all
# Frontend tests
make test-ui| Env Var | Default | Description |
|---|---|---|
TARGETS_FILE |
config/targets.yaml |
Path to targets YAML |
COLLECT_INTERVAL |
10s |
Collection cycle interval |
COLLECT_CONCURRENCY |
5 |
Max concurrent target scrapes |
META_DSN |
postgres://pgmahout:pgmahout@localhost:5433/pgmahout-db?sslmode=disable |
Metadata PostgreSQL DSN |
VM_URL |
http://localhost:8428 |
VictoriaMetrics base URL |
API_ADDR |
:8080 |
API server listen address |