pg_reports 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/app/controllers/pg_reports/dashboard_controller.rb +59 -4
  4. data/app/views/layouts/pg_reports/application.html.erb +1 -1
  5. data/app/views/pg_reports/dashboard/_show_modals.html.erb +8 -1
  6. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +56 -18
  7. data/app/views/pg_reports/dashboard/_show_styles.html.erb +122 -1
  8. data/app/views/pg_reports/dashboard/show.html.erb +89 -47
  9. data/config/locales/en.yml +13 -0
  10. data/config/locales/ru.yml +13 -0
  11. data/config/locales/uk.yml +13 -0
  12. data/lib/pg_reports/dashboard/reports_registry.rb +14 -0
  13. data/lib/pg_reports/definitions/connections/active_connections.yml +23 -0
  14. data/lib/pg_reports/definitions/connections/blocking_queries.yml +20 -0
  15. data/lib/pg_reports/definitions/connections/connection_stats.yml +18 -0
  16. data/lib/pg_reports/definitions/connections/idle_connections.yml +21 -0
  17. data/lib/pg_reports/definitions/connections/locks.yml +22 -0
  18. data/lib/pg_reports/definitions/connections/long_running_queries.yml +43 -0
  19. data/lib/pg_reports/definitions/indexes/bloated_indexes.yml +43 -0
  20. data/lib/pg_reports/definitions/indexes/duplicate_indexes.yml +19 -0
  21. data/lib/pg_reports/definitions/indexes/index_sizes.yml +29 -0
  22. data/lib/pg_reports/definitions/indexes/index_usage.yml +27 -0
  23. data/lib/pg_reports/definitions/indexes/invalid_indexes.yml +19 -0
  24. data/lib/pg_reports/definitions/indexes/missing_indexes.yml +27 -0
  25. data/lib/pg_reports/definitions/indexes/unused_indexes.yml +41 -0
  26. data/lib/pg_reports/definitions/queries/all_queries.yml +35 -0
  27. data/lib/pg_reports/definitions/queries/expensive_queries.yml +43 -0
  28. data/lib/pg_reports/definitions/queries/heavy_queries.yml +49 -0
  29. data/lib/pg_reports/definitions/queries/low_cache_hit_queries.yml +47 -0
  30. data/lib/pg_reports/definitions/queries/missing_index_queries.yml +31 -0
  31. data/lib/pg_reports/definitions/queries/slow_queries.yml +48 -0
  32. data/lib/pg_reports/definitions/system/activity_overview.yml +17 -0
  33. data/lib/pg_reports/definitions/system/cache_stats.yml +18 -0
  34. data/lib/pg_reports/definitions/system/database_sizes.yml +18 -0
  35. data/lib/pg_reports/definitions/system/extensions.yml +19 -0
  36. data/lib/pg_reports/definitions/system/settings.yml +20 -0
  37. data/lib/pg_reports/definitions/tables/bloated_tables.yml +43 -0
  38. data/lib/pg_reports/definitions/tables/cache_hit_ratios.yml +26 -0
  39. data/lib/pg_reports/definitions/tables/recently_modified.yml +27 -0
  40. data/lib/pg_reports/definitions/tables/row_counts.yml +29 -0
  41. data/lib/pg_reports/definitions/tables/seq_scans.yml +31 -0
  42. data/lib/pg_reports/definitions/tables/table_sizes.yml +31 -0
  43. data/lib/pg_reports/definitions/tables/vacuum_needed.yml +39 -0
  44. data/lib/pg_reports/filter.rb +58 -0
  45. data/lib/pg_reports/module_generator.rb +44 -0
  46. data/lib/pg_reports/modules/connections.rb +8 -73
  47. data/lib/pg_reports/modules/indexes.rb +9 -94
  48. data/lib/pg_reports/modules/queries.rb +9 -100
  49. data/lib/pg_reports/modules/schema_analysis.rb +156 -0
  50. data/lib/pg_reports/modules/system.rb +7 -59
  51. data/lib/pg_reports/modules/tables.rb +9 -96
  52. data/lib/pg_reports/report_definition.rb +161 -0
  53. data/lib/pg_reports/report_loader.rb +38 -0
  54. data/lib/pg_reports/sql/schema_analysis/unique_indexes.sql +35 -0
  55. data/lib/pg_reports/version.rb +1 -1
  56. data/lib/pg_reports.rb +24 -0
  57. metadata +38 -1
@@ -0,0 +1,18 @@
1
+ # Cache hit ratio for the entire database
2
+ # Simple report showing cache statistics
3
+
4
+ report:
5
+ name: cache_stats
6
+ module: system
7
+ description: "Database-wide cache hit statistics"
8
+
9
+ sql:
10
+ category: system
11
+ file: cache_stats
12
+
13
+ title: "Database Cache Statistics"
14
+
15
+ columns:
16
+ - database
17
+ - heap_hit_ratio
18
+ - index_hit_ratio
@@ -0,0 +1,18 @@
1
+ # Database sizes
2
+ # Simple report showing all database sizes
3
+
4
+ report:
5
+ name: database_sizes
6
+ module: system
7
+ description: "Sizes of all databases"
8
+
9
+ sql:
10
+ category: system
11
+ file: database_sizes
12
+
13
+ title: "Database Sizes"
14
+
15
+ columns:
16
+ - database
17
+ - size_mb
18
+ - size_pretty
@@ -0,0 +1,19 @@
1
+ # Extension information
2
+ # Simple report showing installed PostgreSQL extensions
3
+
4
+ report:
5
+ name: extensions
6
+ module: system
7
+ description: "Installed PostgreSQL extensions"
8
+
9
+ sql:
10
+ category: system
11
+ file: extensions
12
+
13
+ title: "Installed Extensions"
14
+
15
+ columns:
16
+ - name
17
+ - version
18
+ - schema
19
+ - description
@@ -0,0 +1,20 @@
1
+ # PostgreSQL settings
2
+ # Simple report showing important configuration settings
3
+
4
+ report:
5
+ name: settings
6
+ module: system
7
+ description: "Important PostgreSQL configuration settings"
8
+
9
+ sql:
10
+ category: system
11
+ file: settings
12
+
13
+ title: "PostgreSQL Settings"
14
+
15
+ columns:
16
+ - name
17
+ - setting
18
+ - unit
19
+ - category
20
+ - description
@@ -0,0 +1,43 @@
1
+ # Bloated tables - tables with high dead tuple ratio
2
+ # Report with threshold filtering and title interpolation
3
+
4
+ report:
5
+ name: bloated_tables
6
+ module: tables
7
+ description: "Tables with high dead tuple ratio"
8
+
9
+ sql:
10
+ category: tables
11
+ file: bloated_tables
12
+
13
+ title: "Bloated Tables (bloat >= ${threshold}%)"
14
+ title_vars:
15
+ threshold:
16
+ source: config
17
+ key: bloat_threshold_percent
18
+
19
+ columns:
20
+ - schema
21
+ - table_name
22
+ - live_rows
23
+ - dead_rows
24
+ - bloat_percent
25
+ - table_size_mb
26
+
27
+ parameters:
28
+ limit:
29
+ type: integer
30
+ default: 20
31
+ description: "Maximum number of results"
32
+
33
+ filters:
34
+ - field: bloat_percent
35
+ operator: gte
36
+ value:
37
+ source: config
38
+ key: bloat_threshold_percent
39
+ cast: float
40
+
41
+ problem_explanations:
42
+ dead_tuple_ratio: many_dead_tuples
43
+ n_dead_tup: many_dead_tuples
@@ -0,0 +1,26 @@
1
+ # Table cache hit ratios
2
+ # Simple report with limit parameter
3
+
4
+ report:
5
+ name: cache_hit_ratios
6
+ module: tables
7
+ description: "Table cache hit ratios showing buffer hits vs reads"
8
+
9
+ sql:
10
+ category: tables
11
+ file: cache_hit_ratios
12
+
13
+ title: "Table Cache Hit Ratios"
14
+
15
+ columns:
16
+ - schema
17
+ - table_name
18
+ - heap_blks_read
19
+ - heap_blks_hit
20
+ - cache_hit_ratio
21
+
22
+ parameters:
23
+ limit:
24
+ type: integer
25
+ default: 50
26
+ description: "Maximum number of results"
@@ -0,0 +1,27 @@
1
+ # Recently modified tables
2
+ # Simple report with limit parameter
3
+
4
+ report:
5
+ name: recently_modified
6
+ module: tables
7
+ description: "Tables with recent modifications (inserts, updates, deletes)"
8
+
9
+ sql:
10
+ category: tables
11
+ file: recently_modified
12
+
13
+ title: "Recently Modified Tables"
14
+
15
+ columns:
16
+ - schema
17
+ - table_name
18
+ - n_tup_ins
19
+ - n_tup_upd
20
+ - n_tup_del
21
+ - last_analyze
22
+
23
+ parameters:
24
+ limit:
25
+ type: integer
26
+ default: 20
27
+ description: "Maximum number of results"
@@ -0,0 +1,29 @@
1
+ # Table row counts
2
+ # Report with title interpolation using limit parameter
3
+
4
+ report:
5
+ name: row_counts
6
+ module: tables
7
+ description: "Table row counts sorted by count"
8
+
9
+ sql:
10
+ category: tables
11
+ file: row_counts
12
+
13
+ title: "Table Row Counts (top ${limit})"
14
+ title_vars:
15
+ limit:
16
+ source: param
17
+ key: limit
18
+
19
+ columns:
20
+ - schema
21
+ - table_name
22
+ - row_count
23
+ - table_size_mb
24
+
25
+ parameters:
26
+ limit:
27
+ type: integer
28
+ default: 50
29
+ description: "Maximum number of results"
@@ -0,0 +1,31 @@
1
+ # Sequential scan statistics
2
+ # Report with title interpolation using limit parameter
3
+
4
+ report:
5
+ name: seq_scans
6
+ module: tables
7
+ description: "Sequential scan statistics for tables"
8
+
9
+ sql:
10
+ category: tables
11
+ file: seq_scans
12
+
13
+ title: "Sequential Scans (top ${limit})"
14
+ title_vars:
15
+ limit:
16
+ source: param
17
+ key: limit
18
+
19
+ columns:
20
+ - schema
21
+ - table_name
22
+ - seq_scan
23
+ - seq_tup_read
24
+ - idx_scan
25
+ - rows_per_seq_scan
26
+
27
+ parameters:
28
+ limit:
29
+ type: integer
30
+ default: 20
31
+ description: "Maximum number of results"
@@ -0,0 +1,31 @@
1
+ # Table sizes including indexes
2
+ # Report with title interpolation using limit parameter
3
+
4
+ report:
5
+ name: table_sizes
6
+ module: tables
7
+ description: "Table sizes including indexes"
8
+
9
+ sql:
10
+ category: tables
11
+ file: table_sizes
12
+
13
+ title: "Table Sizes (top ${limit})"
14
+ title_vars:
15
+ limit:
16
+ source: param
17
+ key: limit
18
+
19
+ columns:
20
+ - schema
21
+ - table_name
22
+ - table_size_mb
23
+ - index_size_mb
24
+ - total_size_mb
25
+ - row_count
26
+
27
+ parameters:
28
+ limit:
29
+ type: integer
30
+ default: 50
31
+ description: "Maximum number of results"
@@ -0,0 +1,39 @@
1
+ # Tables needing vacuum - high dead rows count
2
+ # Report with threshold filtering and title interpolation
3
+
4
+ report:
5
+ name: vacuum_needed
6
+ module: tables
7
+ description: "Tables with high dead rows count needing vacuum"
8
+
9
+ sql:
10
+ category: tables
11
+ file: vacuum_needed
12
+
13
+ title: "Tables Needing Vacuum (dead rows >= ${threshold})"
14
+ title_vars:
15
+ threshold:
16
+ source: config
17
+ key: dead_rows_threshold
18
+
19
+ columns:
20
+ - schema
21
+ - table_name
22
+ - n_live_tup
23
+ - n_dead_tup
24
+ - last_vacuum
25
+ - last_autovacuum
26
+
27
+ parameters:
28
+ limit:
29
+ type: integer
30
+ default: 20
31
+ description: "Maximum number of results"
32
+
33
+ filters:
34
+ - field: n_dead_tup
35
+ operator: gte
36
+ value:
37
+ source: config
38
+ key: dead_rows_threshold
39
+ cast: integer
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgReports
4
+ # Applies filtering logic to report data based on YAML configuration
5
+ class Filter
6
+ OPERATORS = {
7
+ "eq" => ->(a, b) { a == b },
8
+ "ne" => ->(a, b) { a != b },
9
+ "lt" => ->(a, b) { a < b },
10
+ "lte" => ->(a, b) { a <= b },
11
+ "gt" => ->(a, b) { a > b },
12
+ "gte" => ->(a, b) { a >= b }
13
+ }.freeze
14
+
15
+ def initialize(config)
16
+ @field = config["field"]
17
+ @operator = config["operator"]
18
+ @value_config = config["value"]
19
+ @cast = config["cast"] || "string"
20
+ end
21
+
22
+ def apply(data, params)
23
+ threshold = resolve_value(params)
24
+ operator_fn = OPERATORS[@operator]
25
+
26
+ raise ArgumentError, "Unknown operator: #{@operator}" unless operator_fn
27
+
28
+ data.select do |row|
29
+ field_value = cast_value(row[@field], @cast)
30
+ operator_fn.call(field_value, threshold)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def resolve_value(params)
37
+ value = case @value_config["source"]
38
+ when "config"
39
+ PgReports.config.public_send(@value_config["key"])
40
+ when "param"
41
+ params[@value_config["key"].to_sym]
42
+ else
43
+ raise ArgumentError, "Unknown value source: #{@value_config["source"]}"
44
+ end
45
+
46
+ cast_value(value, @cast)
47
+ end
48
+
49
+ def cast_value(value, type)
50
+ case type
51
+ when "integer" then value.to_i
52
+ when "float" then value.to_f
53
+ when "string" then value.to_s
54
+ else value
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgReports
4
+ # Generates module methods dynamically from YAML report definitions
5
+ class ModuleGenerator
6
+ def self.generate!
7
+ ReportLoader.load_all.each do |module_name, reports|
8
+ module_class = get_module(module_name)
9
+ next unless module_class
10
+
11
+ reports.each do |report_name, definition|
12
+ define_report_method(module_class, report_name, definition)
13
+ end
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def self.get_module(module_name)
20
+ PgReports::Modules.const_get(module_name.capitalize)
21
+ rescue NameError
22
+ # Module doesn't exist, skip it
23
+ # We don't auto-create modules to avoid conflicts
24
+ nil
25
+ end
26
+
27
+ def self.define_report_method(module_class, report_name, definition)
28
+ params_config = definition.config["parameters"] || {}
29
+
30
+ # Extract default parameter values
31
+ defaults = params_config.transform_values { |v| v["default"] }
32
+
33
+ # Define the method on the module
34
+ # We capture the definition in a local variable to avoid closure issues
35
+ captured_definition = definition
36
+ captured_defaults = defaults
37
+
38
+ module_class.define_singleton_method(report_name) do |**params|
39
+ merged_params = captured_defaults.merge(params)
40
+ captured_definition.generate_report(**merged_params)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -3,82 +3,17 @@
3
3
  module PgReports
4
4
  module Modules
5
5
  # Connection and lock analysis module
6
+ # Most report methods are generated from YAML definitions in lib/pg_reports/definitions/connections/
6
7
  module Connections
7
8
  extend self
8
9
 
9
- # Active connections
10
- # @return [Report] Report with active connections
11
- def active_connections
12
- data = executor.execute_from_file(:connections, :active_connections)
13
-
14
- Report.new(
15
- title: "Active Connections",
16
- data: data,
17
- columns: %w[pid database username application state query_start state_change query]
18
- )
19
- end
20
-
21
- # Connection statistics by state
22
- # @return [Report] Report with connection counts by state
23
- def connection_stats
24
- data = executor.execute_from_file(:connections, :connection_stats)
25
-
26
- Report.new(
27
- title: "Connection Statistics",
28
- data: data,
29
- columns: %w[database state count]
30
- )
31
- end
32
-
33
- # Long running queries
34
- # @return [Report] Report with long running queries
35
- def long_running_queries(min_duration_seconds: 60)
36
- data = executor.execute_from_file(:connections, :long_running_queries)
37
-
38
- filtered = data.select { |row| row["duration_seconds"].to_f >= min_duration_seconds }
39
-
40
- Report.new(
41
- title: "Long Running Queries (>= #{min_duration_seconds}s)",
42
- data: filtered,
43
- columns: %w[pid database username duration_seconds state query]
44
- )
45
- end
46
-
47
- # Blocking queries - queries that are blocking others
48
- # @return [Report] Report with blocking queries
49
- def blocking_queries
50
- data = executor.execute_from_file(:connections, :blocking_queries)
51
-
52
- Report.new(
53
- title: "Blocking Queries",
54
- data: data,
55
- columns: %w[blocked_pid blocking_pid blocked_query blocking_query blocked_duration]
56
- )
57
- end
58
-
59
- # Lock statistics
60
- # @return [Report] Report with lock statistics
61
- def locks
62
- data = executor.execute_from_file(:connections, :locks)
63
-
64
- Report.new(
65
- title: "Current Locks",
66
- data: data,
67
- columns: %w[pid database relation locktype mode granted waiting]
68
- )
69
- end
70
-
71
- # Idle connections
72
- # @return [Report] Report with idle connections
73
- def idle_connections
74
- data = executor.execute_from_file(:connections, :idle_connections)
75
-
76
- Report.new(
77
- title: "Idle Connections",
78
- data: data,
79
- columns: %w[pid database username application idle_duration state_change]
80
- )
81
- end
10
+ # The following methods are auto-generated from YAML:
11
+ # - active_connections
12
+ # - connection_stats
13
+ # - long_running_queries(min_duration_seconds: 60)
14
+ # - blocking_queries
15
+ # - locks
16
+ # - idle_connections
82
17
 
83
18
  # Kill a specific backend process
84
19
  # @param pid [Integer] Process ID to terminate
@@ -3,103 +3,18 @@
3
3
  module PgReports
4
4
  module Modules
5
5
  # Index analysis module
6
+ # All report methods are generated from YAML definitions in lib/pg_reports/definitions/indexes/
6
7
  module Indexes
7
8
  extend self
8
9
 
9
- # Unused indexes - indexes that are rarely or never scanned
10
- # @return [Report] Report with unused indexes
11
- def unused_indexes(limit: 50)
12
- data = executor.execute_from_file(:indexes, :unused_indexes)
13
- threshold = PgReports.config.unused_index_threshold_scans
14
-
15
- filtered = data.select { |row| row["idx_scan"].to_i <= threshold }
16
- .first(limit)
17
-
18
- Report.new(
19
- title: "Unused Indexes (scans <= #{threshold})",
20
- data: filtered,
21
- columns: %w[schema table_name index_name idx_scan index_size_mb]
22
- )
23
- end
24
-
25
- # Duplicate indexes - indexes that may be redundant
26
- # @return [Report] Report with duplicate indexes
27
- def duplicate_indexes
28
- data = executor.execute_from_file(:indexes, :duplicate_indexes)
29
-
30
- Report.new(
31
- title: "Duplicate Indexes",
32
- data: data,
33
- columns: %w[table_name index_name duplicate_of index_size_mb]
34
- )
35
- end
36
-
37
- # Invalid indexes - indexes that are not valid (e.g., failed to build)
38
- # @return [Report] Report with invalid indexes
39
- def invalid_indexes
40
- data = executor.execute_from_file(:indexes, :invalid_indexes)
41
-
42
- Report.new(
43
- title: "Invalid Indexes",
44
- data: data,
45
- columns: %w[schema table_name index_name index_definition]
46
- )
47
- end
48
-
49
- # Missing indexes - tables with high sequential scans
50
- # @return [Report] Report suggesting missing indexes
51
- def missing_indexes(limit: 20)
52
- data = executor.execute_from_file(:indexes, :missing_indexes)
53
- .first(limit)
54
-
55
- Report.new(
56
- title: "Tables Potentially Missing Indexes",
57
- data: data,
58
- columns: %w[schema table_name seq_scan seq_tup_read idx_scan table_size_mb]
59
- )
60
- end
61
-
62
- # Index usage statistics
63
- # @return [Report] Report with index usage statistics
64
- def index_usage(limit: 50)
65
- data = executor.execute_from_file(:indexes, :index_usage)
66
- .first(limit)
67
-
68
- Report.new(
69
- title: "Index Usage Statistics",
70
- data: data,
71
- columns: %w[schema table_name index_name idx_scan idx_tup_read index_size_mb]
72
- )
73
- end
74
-
75
- # Bloated indexes - indexes with high bloat
76
- # @return [Report] Report with bloated indexes
77
- def bloated_indexes(limit: 20)
78
- data = executor.execute_from_file(:indexes, :bloated_indexes)
79
- threshold = PgReports.config.bloat_threshold_percent
80
-
81
- filtered = data.select { |row| row["bloat_percent"].to_f >= threshold }
82
- .first(limit)
83
-
84
- Report.new(
85
- title: "Bloated Indexes (bloat >= #{threshold}%)",
86
- data: filtered,
87
- columns: %w[schema table_name index_name index_size_mb bloat_size_mb bloat_percent]
88
- )
89
- end
90
-
91
- # Index sizes
92
- # @return [Report] Report with index sizes
93
- def index_sizes(limit: 50)
94
- data = executor.execute_from_file(:indexes, :index_sizes)
95
- .first(limit)
96
-
97
- Report.new(
98
- title: "Index Sizes (top #{limit})",
99
- data: data,
100
- columns: %w[schema table_name index_name index_size_mb]
101
- )
102
- end
10
+ # The following methods are auto-generated from YAML:
11
+ # - unused_indexes(limit: 50)
12
+ # - duplicate_indexes
13
+ # - invalid_indexes
14
+ # - missing_indexes(limit: 20)
15
+ # - index_usage(limit: 50)
16
+ # - bloated_indexes(limit: 20)
17
+ # - index_sizes(limit: 50)
103
18
 
104
19
  private
105
20