Skip to content

Commit

Permalink
feat: add support for diagnose report (#2)
Browse files Browse the repository at this point in the history
* feat: add initial diagnostic support

Introduced a new `Diagnose` struct with a `run` method to execute various diagnostic checks. Currently, not all diagnostic checks are fully implemented.

* refactor: extract extension check to a separate function

* feat: add index cache hit rate check

* refactor: shorten column name in table header

* refactor: simplify diagnose module logic

* refactor: update table rendering and add color to rows

* feat(diagnose): add SSL connection check

* refactor: remove strum dependency and simplify Check enum

Removed the strum dependency from the project and adjusted the Check enum and related code to use standard Rust features.

* feat: add unused indexes check with size parsing

* feat: add null indexes check to diagnostics

* feat(diagnose): add bloat detection check

* feat: Add duplicate index detection to diagnose module

* feat: Add detection of outlier queries by execution ratio

* refactor(diagnose): simplify Diagnose struct

* chore: add diagnose report to test suite

* refactor: remove redundant CheckResult constructor

Replaced the CheckResult::new() constructor with struct literal initialization.

* refactor: simplify cache and SSL check logic

Refactor to use concise if-let-else syntax.

* refactor: restructure diagnose module and add recommendation

Moved diagnose logic into a dedicated submodule with separate files for the run logic and recommendations. Implemented a new recommendation system for diagnosis checks and added structured report rendering.

* feat: add new recommendations for various database checks

* docs: add diagnose report section and example to README

* feat(bin): add a new cli option for diagnose command

* style: fix fmt formatting issues
  • Loading branch information
yadazula authored Oct 27, 2024
1 parent ade8020 commit d2fd3c1
Show file tree
Hide file tree
Showing 10 changed files with 594 additions and 9 deletions.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ license = "MIT"
name = "pg-extras"
readme = "README.md"
repository = "https://github.com/pawurb/pg-extras-rs"
version = "0.6.1"
version = "0.6.2"

exclude = ["docker-compose.yml.sample", "live_tests.sh"]

Expand All @@ -24,6 +24,8 @@ sqlx = { version = "0.8", features = [
] }

tokio = { version = "1.40", features = ["full"] }
unicode-width = "0.2.0"
textwrap = { version = "0.16.1", features = ["terminal_size"] }

[profile.release]
lto = true
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ cache_hit(Some("other_schema".to_string)).await?;

You can customize the default `public` schema by setting `ENV['PG_EXTRAS_SCHEMA']` value.

## Diagnose Report

The simplest way to start using pg-extras is to execute a `diagnose` method. It runs a set of checks and prints out a report highlighting areas that may require additional investigation:

```rust
use pg_extras::{diagnose, diagnose::report::render_diagnose_report};

render_diagnose_report(diagnose().await?);

```

![Diagnose report](pg-extras-diagnose-report.png)

Keep reading to learn about methods that `diagnose` uses under the hood.

## Available methods

Expand Down
22 changes: 14 additions & 8 deletions bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use clap::{Parser, Subcommand};
use pg_extras::diagnose::report::render_diagnose_report;
use pg_extras::{
all_locks, bloat, blocking, buffercache_stats, buffercache_usage, cache_hit, calls,
connections, db_settings, duplicate_indexes, extensions, index_cache_hit, index_scans,
index_size, index_usage, indexes, locks, long_running_queries, mandelbrot, null_indexes,
outliers, records_rank, render_table, seq_scans, ssl_used, table_cache_hit, table_index_scans,
table_indexes_size, table_size, tables, total_index_size, total_table_size, unused_indexes,
vacuum_stats, AllLocks, Bloat, Blocking, BuffercacheStats, BuffercacheUsage, CacheHit, Calls,
Connections, DbSettings, DuplicateIndexes, Extensions, IndexCacheHit, IndexScans, IndexSize,
IndexUsage, Indexes, Locks, LongRunningQueries, Mandelbrot, NullIndexes, Outliers,
PgExtrasError, Query, RecordsRank, SeqScans, SslUsed, TableCacheHit, TableIndexScans,
connections, db_settings, diagnose, duplicate_indexes, extensions, index_cache_hit,
index_scans, index_size, index_usage, indexes, locks, long_running_queries, mandelbrot,
null_indexes, outliers, records_rank, render_table, seq_scans, ssl_used, table_cache_hit,
table_index_scans, table_indexes_size, table_size, tables, total_index_size, total_table_size,
unused_indexes, vacuum_stats, AllLocks, Bloat, Blocking, BuffercacheStats, BuffercacheUsage,
CacheHit, Calls, Connections, DbSettings, DuplicateIndexes, Extensions, IndexCacheHit,
IndexScans, IndexSize, IndexUsage, Indexes, Locks, LongRunningQueries, Mandelbrot, NullIndexes,
Outliers, PgExtrasError, Query, RecordsRank, SeqScans, SslUsed, TableCacheHit, TableIndexScans,
TableIndexesSize, TableSize, Tables, TotalIndexSize, TotalTableSize, UnusedIndexes,
VacuumStats,
};
Expand Down Expand Up @@ -46,6 +47,8 @@ pub enum PgSubcommand {
Connections(EmptyArgs),
#[command(about = extract_desc(&DbSettings::description()))]
DbSettings(EmptyArgs),
#[command(about = "Diagnose common database problems")]
Diagnose(EmptyArgs),
#[command(about = extract_desc(&DuplicateIndexes::description()))]
DuplicateIndexes(EmptyArgs),
#[command(about = extract_desc(&Extensions::description()))]
Expand Down Expand Up @@ -142,6 +145,9 @@ async fn execute() -> Result<(), PgExtrasError> {
PG::DbSettings(_args) => {
render_table(db_settings().await?);
}
PG::Diagnose(_args) => {
render_diagnose_report(diagnose().await?);
}
PG::DuplicateIndexes(_args) => {
render_table(duplicate_indexes().await?);
}
Expand Down
Binary file added pg-extras-diagnose-report.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/diagnose/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod recommendation;
pub mod report;
pub mod run;
pub mod size_parser;
90 changes: 90 additions & 0 deletions src/diagnose/recommendation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use crate::diagnose::run::Check;
use lazy_static::lazy_static;
use std::collections::HashMap;

lazy_static! {
pub static ref Recommendations: HashMap<Check, (&'static str, Vec<&'static str>)> = {
let mut m = HashMap::new();
m.insert(
Check::TableCacheHit,
(
"Cache hit rate is too low",
vec![
"Review database settings: Consider comparing the database settings with ones recommended by PGTune and tweak values to improve performance.",
"Validate database specs: A low buffer cache hit ratio can be a sign that the Postgres instance is too small for the workload.",
],
),
);
m.insert(
Check::IndexCacheHit,
(
"Cache hit rate is too low",
vec![
"Review database settings: Consider comparing the database settings with ones recommended by PGTune and tweak values to improve performance.",
"Validate database specs: A low buffer cache hit ratio can be a sign that the Postgres instance is too small for the workload.",
],
),
);
m.insert(
Check::UnusedIndexes,
(
"Remove unused indexes",
vec![
"Consider eliminating indexes that are unused, which can impact the performance.",
"If the index is large, remember to use the CONCURRENTLY option when dropping it, to avoid exclusively blocking the whole related table."
],
),
);
m.insert(
Check::NullIndexes,
(
"Optimize \"NULL\" indexes",
vec![
"NULL values unnecessarily bloat the index size and slow down insert operations on a related table.",
"Consider replacing the index with a partial one that excludes NULL values.",
],
),
);
m.insert(
Check::DuplicateIndexes,
(
"Remove duplicate indexes",
vec![
"Consider removing the duplicate indexes to improve performance.",
"If the index is large, remember to use the CONCURRENTLY option when dropping it, to avoid exclusively blocking the whole related table."
],
),

);
m.insert(
Check::SslUsed,
(
"SSL is not used",
vec![
"Connecting to the database via an unencrypted connection is a critical security risk.",
"Consider enabling SSL to encrypt the connection between the client and the server.",
],
),
);
m.insert(
Check::Bloat,
(
"Get rid of unnecessary bloat",
vec![
"Review AUTOVACUUM settings: If it is misconfigured, it might result in your table consisting of mostly dead rows that are blocking the disk space and slowing down queries.",
],
),
);
m.insert(
Check::Outliers,
(
"Add missing indexes",
vec![
"Spot the queries that are consuming a lot of your database resources and are potentially missing an index.",
"Perform EXPLAIN ANALYZE and check if the query planner does Seq Scan on one of the tables.",
],
),
);
m
};
}
48 changes: 48 additions & 0 deletions src/diagnose/report.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use crate::diagnose::recommendation;
use crate::diagnose::run::CheckResult;
use prettytable::{Cell, Row as TableRow, Table};

pub fn render_diagnose_report(items: Vec<CheckResult>) {
let term_width = textwrap::termwidth();
let recommendation_width = term_width / 3; // need to adjust this to make the recommendation text wrap nicely

let mut table = Table::new();

let mut header_cell = Cell::new("Diagnose Report").style_spec("bH3");
header_cell.align(prettytable::format::Alignment::CENTER);
table.set_titles(TableRow::new(vec![header_cell]));

table.add_row(row!["Check", "Message", "Recommendation"]);

for item in items {
let style = if item.ok { "Fg" } else { "Fr" };

let status_and_name = format!("[{}] - {}", if item.ok { "√" } else { "x" }, item.check);

// get the recommendation for the check
let recommendation = if item.ok {
"None".to_string()
} else {
let (header, details) = recommendation::Recommendations.get(&item.check).unwrap();
// build the recommendation text by concatenating the header and details with bullet points
format!(
"{}:\n{}",
header,
details
.iter()
.map(|detail| format!("• {}", detail))
.collect::<Vec<String>>()
.join("\n")
)
};

table.add_row(TableRow::new(vec![
Cell::new(status_and_name.as_str()).style_spec(style),
Cell::new(item.message.as_str()).style_spec(style),
Cell::new(textwrap::fill(&recommendation, recommendation_width).as_str())
.style_spec(style),
]));
}

table.printstd();
}
Loading

0 comments on commit d2fd3c1

Please sign in to comment.