Skip to content

babanin/pgmahout

Repository files navigation

pgMahout — Database Monitoring PoC

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.

Architecture

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)│
└─────────────┘                           └─────────────┘

Tech Stack

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

Why PostgreSQL 18 + VictoriaMetrics?

pgMahout uses two storage engines because time-series telemetry and relational metadata have fundamentally different access patterns, and no single database handles both well.

Why not VictoriaMetrics alone?

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 NOTHING semantics, 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.

Why not PostgreSQL alone?

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=5m in PromQL/MetricsQL). In PostgreSQL it requires date_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.

Why VictoriaMetrics over Prometheus, InfluxDB, or TimescaleDB?

  • 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=30d flag is simpler and performs better.

The scatter-gather pattern

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.

Prerequisites

  • Go 1.25+ (with CGO support for collector)
  • Docker & Docker Compose
  • pnpm, Node.js 20+

Quick Start

# 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

API Endpoints

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

Collected Metrics

All time-series measurements stored in VictoriaMetrics via Influx line protocol.

1. pg_stat_statements

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

2. pg_stat_activity_count

Tags: target, state, wait_event_type, wait_event, usename, command Fields: count

3. pg_stat_activity_age

Tags: target Fields: longest_query_seconds, longest_txn_seconds, idle_in_txn_count, longest_idle_in_txn_seconds

4. pg_stat_database

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

5. pg_stat_user_tables

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

6. pg_stat_user_indexes

Tags: target, database, schema, table, index Fields: idx_scan, idx_tup_read, idx_tup_fetch, idx_blks_read, idx_blks_hit

7. pg_stat_bgwriter

Tags: target Fields: checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint, buffers_clean, buffers_backend, buffers_backend_fsync, maxwritten_clean

8. pg_locks

Tags: target, mode Fields: granted, not_granted

9. pg_locks_summary

Tags: target Fields: total_granted, total_not_granted, longest_wait_seconds

10. pg_stat_replication

Tags: target, client_addr, application_name, state Fields: write_lag_ms, flush_lag_ms, replay_lag_ms, sent_lsn_offset

11. cloudsql_metrics

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

Metadata in PostgreSQL (not VM)

  • 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

Testing

# 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

Configuration

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

About

Tool for monitoring PostgreSQL instances

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages