Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## v0.6.0

**WAL Monitoring**

### New Commands

- **`pgcrate dba wal`**: Monitor WAL generation, archiving, and disk consumption
- WAL configuration: `wal_level`, segment size, current LSN
- WAL directory size and segment count (via `pg_ls_waldir()`; requires `pg_monitor` or superuser)
- Archiving status: mode, command, last archived/failed WAL, failed count, pending file count
- Generation rate estimated from `pg_stat_wal` (PostgreSQL 14+)
- Issue detection with severity: oversized WAL directory, archive failures, archive lag, `wal_level=minimal`, archiving disabled
- Exit codes: 0 healthy, 1 warning, 2 critical
- Full JSON support with `pgcrate.diagnostics.wal` schema

### Improvements

- **`pgcrate capabilities`**: Reports the `diagnostics.wal` capability
- Checks `pg_stat_archiver` SELECT and `pg_ls_waldir()` access
- Degrades gracefully when `pg_ls_waldir()` is unavailable (directory size and pending file counts omitted)

---

## v0.5.0

**Query Analysis and Index Remediation**
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pgcrate"
version = "0.4.0"
version = "0.6.0"
edition = "2021"
description = "PostgreSQL companion for teams not using Rails or Django. Migrations, introspection, diffing, and more."
license = "MIT"
Expand Down
81 changes: 81 additions & 0 deletions schemas/wal.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "pgcrate.diagnostics.wal",
"title": "pgcrate wal output",
"description": "WAL generation, archiving, and disk consumption analysis",
"allOf": [
{ "$ref": "envelope.schema.json" }
],
"properties": {
"schema_id": { "const": "pgcrate.diagnostics.wal" },
"data": { "$ref": "#/$defs/walData" }
},
"$defs": {
"walData": {
"type": "object",
"additionalProperties": false,
"required": ["wal_level", "current_wal_lsn", "wal_segment_size_bytes", "wal_directory", "archiving", "generation_rate", "issues", "overall_status"],
"properties": {
"wal_level": { "type": "string" },
"current_wal_lsn": { "type": "string" },
"wal_segment_size_bytes": { "type": "integer" },
"wal_directory": { "$ref": "#/$defs/walDirectory" },
"archiving": { "$ref": "#/$defs/archiveStatus" },
"generation_rate": { "$ref": "#/$defs/generationRate" },
"issues": {
"type": "array",
"items": { "$ref": "#/$defs/walIssue" }
},
"overall_status": { "$ref": "#/$defs/walStatus" }
}
},
"walStatus": {
"type": "string",
"enum": ["healthy", "warning", "critical"]
},
"walDirectory": {
"type": "object",
"additionalProperties": false,
"required": [],
"properties": {
"size_bytes": { "type": ["integer", "null"] },
"segment_count": { "type": ["integer", "null"] }
}
},
"archiveStatus": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "archive_mode", "failed_count"],
"properties": {
"enabled": { "type": "boolean" },
"archive_mode": { "type": "string" },
"archive_command": { "type": ["string", "null"] },
"last_archived_wal": { "type": ["string", "null"] },
"last_archived_time": { "type": ["string", "null"] },
"failed_count": { "type": "integer" },
"last_failed_wal": { "type": ["string", "null"] },
"last_failed_time": { "type": ["string", "null"] },
"pending_count": { "type": ["integer", "null"] }
}
},
"generationRate": {
"type": "object",
"additionalProperties": false,
"required": ["measurement_note"],
"properties": {
"bytes_per_second": { "type": ["number", "null"] },
"measurement_note": { "type": "string" }
}
},
"walIssue": {
"type": "object",
"additionalProperties": false,
"required": ["code", "message", "severity"],
"properties": {
"code": { "type": "string" },
"message": { "type": "string" },
"severity": { "$ref": "#/$defs/walStatus" }
}
}
}
}
59 changes: 59 additions & 0 deletions src/commands/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ pub async fn run_capabilities(client: &Client, read_only: bool) -> Result<Capabi
let has_pg_terminate = check_function_privilege(client, "pg_terminate_backend(int)").await;
let has_pg_stat_statements = check_extension_and_privilege(client, "pg_stat_statements").await;
let has_pg_stat_replication = check_privilege(client, "pg_stat_replication", "SELECT").await;
let has_pg_stat_archiver = check_privilege(client, "pg_stat_archiver", "SELECT").await;
let has_pg_ls_waldir = check_function_access(client, "pg_ls_waldir()").await;

let capabilities = vec![
// diagnostics.triage - always available (uses minimal queries)
Expand Down Expand Up @@ -111,6 +113,8 @@ pub async fn run_capabilities(client: &Client, read_only: bool) -> Result<Capabi
check_bloat_capability(has_pg_stat_user_tables),
// diagnostics.replication - needs pg_stat_replication
check_replication_capability(has_pg_stat_replication),
// diagnostics.wal - needs pg_stat_archiver, optionally pg_ls_waldir
check_wal_capability(has_pg_stat_archiver, has_pg_ls_waldir),
// diagnostics.context - always available
CapabilityInfo {
id: "diagnostics.context",
Expand Down Expand Up @@ -191,6 +195,17 @@ async fn check_extension_and_privilege(client: &Client, extension: &str) -> bool
.unwrap_or(false)
}

async fn check_function_access(client: &Client, function: &str) -> bool {
// Try to actually call the function - some functions require specific roles
// (e.g., pg_ls_waldir requires pg_monitor or superuser). LIMIT 0 still
// triggers the permission check at execution without materializing rows;
// query() (not query_one) is required since the result is zero rows.
client
.query(&format!("SELECT 1 FROM {} LIMIT 0", function), &[])
.await
.is_ok()
}

fn check_locks_capability(
has_pg_stat_activity: bool,
has_pg_cancel: bool,
Expand Down Expand Up @@ -381,6 +396,50 @@ fn check_replication_capability(has_pg_stat_replication: bool) -> CapabilityInfo
}
}

fn check_wal_capability(has_pg_stat_archiver: bool, has_pg_ls_waldir: bool) -> CapabilityInfo {
let requirements = vec![
Requirement {
what: "pg_stat_archiver SELECT".to_string(),
met: has_pg_stat_archiver,
},
Requirement {
what: "pg_ls_waldir() access (pg_monitor role)".to_string(),
met: has_pg_ls_waldir,
},
];

let mut reasons = vec![];
let mut limitations = vec![];

let status = if !has_pg_stat_archiver {
reasons.push(ReasonInfo::new(
ReasonCode::MissingPrivilege,
"Cannot read pg_stat_archiver",
));
CapabilityStatus::Unavailable
} else if !has_pg_ls_waldir {
reasons.push(ReasonInfo::new(
ReasonCode::MissingPrivilege,
"Cannot access pg_ls_waldir (requires pg_monitor or superuser)",
));
limitations.push("WAL directory size and segment count unavailable".to_string());
limitations.push("Archive pending file count unavailable".to_string());
CapabilityStatus::Degraded
} else {
CapabilityStatus::Available
};

CapabilityInfo {
id: "diagnostics.wal",
name: "WAL",
description: "WAL generation, archiving, and disk consumption",
status,
reasons,
requirements,
limitations,
}
}

fn check_bloat_capability(has_pg_stat_user_tables: bool) -> CapabilityInfo {
let requirements = vec![Requirement {
what: "pg_stat_user_tables SELECT".to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub mod stats_age;
pub mod storage;
pub mod triage;
pub mod vacuum;
pub mod wal;
pub mod xid;

// Re-export snapshot commands from new module
Expand Down
Loading
Loading