This year, I decided to take a week off during autumn, which I don't think I have done since I attended university. I was in the building mood, and I am working on Postgres infrastructure at work at the moment, so I decided to spend some time reading up on Postgres observability. Of course, there are already a lot of great tools such as PGAdmin and Dbeaver readily available, but I've always felt that the best way to learn something is through project work, so I decided to try to implement my own observability platform from scratch in Rust. The project is still far from done, and is missing some frontend polish, but through using Postgres extensions such as 'pg_stat_statements', 'pg_stat_monitor' and 'pgstattuple', iteration was a fairly rapid process.
PGMon is an open-source monitoring platform for PostgreSQL built entirely in Rust. It runs as a single binary that embeds a Prometheus exporter, an Axum REST API, and a Vite/ React UI, giving DBA's full-stack visibility without external services. The system samples core system catalogs using a least-privilege role, correlates the resulting telemetry, and visualizes it in a dashboard.
PGMon dashboard showing some of the more important metrics like concurrent connections, transactions per second, queries per second, as well as latency and wraparound.
The runtime orchestrates five asynchronous loops: hot-path, workload, storage,
hourly, and aggregation, each with its own cadence and execution budget. Every iteration is
timed, logged, and exported as Prometheus histograms and success flags; failures increment
counters and update shared loop health. A readiness probe checks these metrics to ensure
fresh data on /healthz. This structure keeps the scheduler deterministic while
surfacing when a loop misses its target window.
The workload loop snapshots pg_stat_database,
pg_stat_statements, pg_stat_wal, and temp spill counters on each
pass. By diffing consecutive samples it derives TPS, QPS, mean latency, WAL throughput,
checkpoint ratios, temp rates, and more, storing the results in a ring buffer. The same
summary drives Prometheus gauges, populates the frontend history API, and feeds alert
logic.
When pg_stat_monitor is available, PGMon inspects histogram settings,
aggregates the resp_calls arrays, and interpolates percentile cutoffs to yield
p95/p99 latency. If the extension is missing or misconfigured, the collector downgrades
gracefully so the rest of the telemetry remains intact.
Workload analytics view displaying top queries with performance metrics including calls, total time, mean latency, and cache hit ratios.
A shared state engine, protected by asynchronous read–write locks, maintains the latest snapshots, workload history, and alert lifecycle. High-resolution series live in bounded buffers; a dedicated aggregation loop runs after midnight UTC to roll daily summaries, and on Mondays to roll ISO-week summaries. The persistence module checkpoints this state to disk using atomic write-and-rename, allowing the UI and API to restart with historical context intact.
Alert lifecycles are tracked via set diffs: when the hot-path loop updates the overview, PGMon computes which alerts started or cleared and records structured events with timestamps. The frontend and API can therefore show not just the current status but the sequence of alert transitions.
The hourly loop handles the heavier lifts:
pg_get_expr bounds,
normalizes them into UTC timestamps, infers the dominant cadence by examining boundary
deltas, then flags future scheduling gaps and suggests the next partition window.autovacuum_freeze_max_age, exports wraparound metrics, and pushes alerts when
thresholds are crossed.pgstattuple and
pgstattuple_approx modes to measure free space and dead tuples, calculating
reclaimable bytes on the fly.The storage loop executes a CTE-driven query that aggregates relation sizes, cache hit ratios, and dead tuple estimates in one pass. It derives bloat bytes from tuple density, surfaces heap/index/TOAST footprints, and identifies heavy unused indexes based on scan counts and size thresholds. Results are exported to Prometheus and cached in state for the API/Ops UI.
A rule-driven engine turns the collected telemetry into prioritized actions. It correlates storage stats, autovacuum history, bloat samples, and stale statistics to propose VACUUM ANALYZE, VACUUM FULL, ANALYZE, or autovacuum tuning steps. Each recommendation includes severity, SQL to execute, rationale written for humans, estimated runtime, and whether an exclusive lock is required, so operators get actionable next steps instead of raw metrics.
Recommendations interface showing maintenance suggestions with severity levels, SQL commands, and detailed rationale for VACUUM ANALYZE and autovacuum tuning operations.
Axum routes expose JSON APIs, the Prometheus scrape endpoint, and the bundled
frontend from the same binary. CI enforces cargo fmt -- --check, cargo
clippy --all-targets --all-features -D warnings, complete test suites, and
frontend builds to keep the system shippable. Docker packaging makes local evaluation
straightforward, while production deployments can run the single binary in an air-gapped
environment by pointing it at an existing Postgres role.
PGMon demonstrates a complete Rust observability platform for Postgres: deterministic polling, statistically sound workload analytics, durable historical state, deep domain collectors, and a recommendation layer that bridges telemetry to database operations.
dbfriend is a Python command-line tool designed to simplify the loading and synchronization of spatial data into PostGIS databases. It focuses on data integrity and safety, ensuring that your database operations are reliable and efficient. By handling complex tasks intelligently, dbfriend helps GIS professionals and database administrators streamline their workflows.
All database operations are executed within transactions, ensuring data integrity and automatic rollback on failure.
dbfriend automatically creates backups before modifying any existing tables, keeping up to three historical versions per table for easy restoration and added data safety.
Load data from various spatial file formats, including GeoJSON, Shapefile, GeoPackage, KML, and GML, providing flexibility in handling different data sources.
Prevent duplicates and ensure data consistency by comparing geometries using hashes to detect new, updated, and identical features efficiently.
Update existing geometries based on attribute changes, so your database always reflects the most current data.
Automatically detects and renames geometry columns to a standard format, simplifying data processing and integration.
Verifies CRS compatibility and automatically reprojects data as needed, ensuring spatial data aligns correctly within your database.
Automatically creates spatial indexes on imported data, improving query performance and data retrieval speeds.
dbfriend in action: processing spatial files and managing PostGIS database operations.
Sosilogikk is a Python module intended to streamline the use of Python libraries like Shapely or Fiona for GIS analyses, on the Norwegian vector data format SOSI (Samordnet Opplegg for Stedfestet Informasjon). Sosilogikk allows the user to seamlessly load a .SOS file into a GeoPandas GeoDataFrame through only a few lines of code.
Example structure of a vector object in a SOSI-file. The dot- and coordinates-format makes it difficult to use with Python libraries.
Sosilogikk applied to a large SOSI-file, resulting in an excel-like table.
Using the .to_file method, you can easily export the GeoDataFrame to any OGR-supported vector format, allowing software like ArcGIS or QGIS to be used.
Drainage lines in Flatgeobuf format visualized in QGIS.
Modern cloud-native GIS applications often need to efficiently store and transmit large volumes of geographic data between services. While GeoJSON is the standard format for geographic data exchange, its text-based nature makes it suboptimal for cloud storage and transmission. This solution combines MongoDB's BSON format with delta encoding to create a highly efficient geographic data pipeline.
BSON (Binary JSON) is MongoDB's binary format, specifically designed for cloud-scale data operations. Unlike traditional JSON, BSON provides native support for different numeric types and binary data, making it ideal for geographic coordinate storage. This becomes particularly important in microservice architectures where data needs to be efficiently serialized, transmitted, and stored across different cloud services. In cloud environments where MongoDB Atlas is increasingly common, this native format compatibility translates to significant performance benefits and reduced processing costs.
Delta encoding is a compression technique that stores the differences (deltas) between consecutive values rather than the values themselves. For geographic coordinates, this is particularly effective because consecutive points in a geometry are typically close to each other, resulting in small delta values that require fewer bits to store.
Visualization of delta encoding: Starting with a sequence of numbers (top row), we compute the differences between consecutive values (second row). Negative differences are then shifted to positive values (third row) for efficient binary representation (bottom row). This process significantly reduces storage requirements while maintaining perfect reversibility. (Adapted from Xia et al., The VLDB Journal, 2024)
The implementation in BSON_encoder.py follows these key steps:
Here's a simplified example showing the transformation:
Original coordinates: [(100.123456, 50.123456), (100.123476, 50.123476)]
After scaling by 1e6: [(100123456, 50123456), (100123476, 50123476)]
Delta encoded: [(100123456, 50123456), (20, 20)] # Second point stored as difference
In modern cloud architectures, geographic data flows between various services - from storage to processing to web APIs. This combined approach of delta encoding and BSON serialization dramatically reduces the bandwidth required for these operations. Testing with real-world infrastructure data:
Comparison of data sizes: Original GeoJSON format vs BSON-encoded format with delta compression. The combined approach reduces the file size by almost 90% while maintaining full coordinate precision.
While the size reduction is impressive, the real value lies in the format's cloud-native nature. The compressed data remains fully compatible with MongoDB's geospatial queries and indexes, allowing for efficient spatial operations directly on the compressed data. The compression is completely reversible, and the flattened GeoJSON structure results in smaller file sizes even after decompression.
Working in GIS production environments has highlighted an interesting challenge: the gap between what can be automated and what typically is automated. While tools like ArcGIS and QGIS excel at interactive analysis, many workflows would benefit from programmatic automation - yet often remain manual processes.
GIS workflows frequently involve repetitive tasks that are perfect candidates for automation:
The challenge isn't identifying what to automate - it's making automation accessible to GIS professionals who may not have programming experience. This is where Docker has proven particularly valuable.
Docker's containerization approach solves several fundamental challenges in GIS automation:
In practice, implementing Docker in a GIS environment involves creating a layer of abstraction between the technical complexity and the end user. The implementation typically involves wrapping Docker commands in a user-friendly interface - the user doesn't need to understand the underlying system, they simply interact with familiar buttons and inputs while Docker handles the complex environment management behind the scenes. To accomplish this, we can create a launcher script in the form of a batch file that presents the user with inputs through a simple graphical user interface. As I work in an environment where Python comes pre-installed, I chose to use a Python script for this task.
Example of a Docker-based GIS tool launcher in Python that abstracts away the complexity of container management and environment setup.
Using Docker in production has revealed several interesting insights:
The integration of Docker in GIS workflows opens interesting possibilities for the future of spatial data processing. As cloud infrastructure becomes more prevalent in GIS, containerized workflows could become the standard way of handling automated spatial analysis. The key will be maintaining the balance between powerful automation capabilities and user-friendly interfaces.
Python is a powerful language for rapid development, especially in the GIS domain, thanks to libraries like GeoPandas and Shapely. However, when processing large datasets or performing complex calculations, Python's speed can become a limitation. This is where Rust comes in - offering the speed we need while letting us keep Python's ease of use.
Bindings are essentially a way to connect two different programming languages, allowing them to work together. In this case, we use Rust bindings to integrate Rust's high-performance capabilities into Python workflows. This means we can write the most performance-critical parts of our GIS analysis in Rust, while still using Python for the overall workflow.
Many traditional GIS tools are written in C++, and for good reason - C++ offers excellent performance and has been the go-to language for computationally intensive tasks for decades. However, Rust brings some unique advantages to the table. While matching C++'s performance, Rust's compiler enforces memory safety and thread safety at compile time, preventing many common programming errors before they can become runtime bugs. This is particularly valuable when working with large spatial datasets where data integrity is crucial.
Additionally, Rust's modern tooling and package management system makes it easier to create and maintain bindings compared to C++. The language's focus on safe concurrency also makes it particularly well-suited for parallel processing of spatial data, an increasingly important consideration as datasets continue to grow in size and complexity.
To demonstrate the performance difference between Python and Rust, I performed a simple GIS task: creating buffers around 1 million point geometries, and checking how many of the buffers overlapped with each other. The results were striking - the Rust implementation completed in just 2 seconds, while the Python version took 84 seconds to finish the same task.
Results from the performance comparison.
The key features that make this implementation fast:
How we expose the Rust function to Python:
By incorporating Rust into a Python-based workflow, we can leverage the strengths of both languages. Python remains the glue that holds the workflow together, providing ease of use and flexibility, while Rust handles the heavy lifting where performance is critical. This combination allows us to build robust GIS applications that are both user-friendly and highly efficient.