pg_reports 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +104 -0
- data/README.md +129 -4
- data/app/controllers/pg_reports/dashboard_controller.rb +188 -25
- data/app/views/layouts/pg_reports/application.html.erb +282 -0
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +184 -23
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +373 -0
- data/app/views/pg_reports/dashboard/index.html.erb +419 -0
- data/config/locales/en.yml +45 -0
- data/config/locales/ru.yml +45 -0
- data/config/routes.rb +8 -0
- data/lib/pg_reports/configuration.rb +13 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +24 -1
- data/lib/pg_reports/definitions/connections/connection_churn.yml +49 -0
- data/lib/pg_reports/definitions/connections/pool_saturation.yml +42 -0
- data/lib/pg_reports/definitions/connections/pool_usage.yml +43 -0
- data/lib/pg_reports/definitions/connections/pool_wait_times.yml +44 -0
- data/lib/pg_reports/definitions/queries/missing_index_queries.yml +3 -3
- data/lib/pg_reports/explain_analyzer.rb +338 -0
- data/lib/pg_reports/modules/schema_analysis.rb +4 -6
- data/lib/pg_reports/modules/system.rb +19 -2
- data/lib/pg_reports/query_monitor.rb +280 -0
- data/lib/pg_reports/sql/connections/connection_churn.sql +37 -0
- data/lib/pg_reports/sql/connections/pool_saturation.sql +90 -0
- data/lib/pg_reports/sql/connections/pool_usage.sql +31 -0
- data/lib/pg_reports/sql/connections/pool_wait_times.sql +19 -0
- data/lib/pg_reports/sql/queries/all_queries.sql +17 -15
- data/lib/pg_reports/sql/queries/expensive_queries.sql +9 -4
- data/lib/pg_reports/sql/queries/heavy_queries.sql +14 -12
- data/lib/pg_reports/sql/queries/low_cache_hit_queries.sql +16 -14
- data/lib/pg_reports/sql/queries/missing_index_queries.sql +18 -16
- data/lib/pg_reports/sql/queries/slow_queries.sql +14 -12
- data/lib/pg_reports/sql/system/databases_list.sql +8 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +2 -0
- metadata +56 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Connection Churn Analysis
|
|
2
|
+
# Analyzes connection creation/destruction patterns and short-lived connections
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: connection_churn
|
|
6
|
+
module: connections
|
|
7
|
+
description: "Connection churn and short-lived connection analysis"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: connections
|
|
11
|
+
file: connection_churn
|
|
12
|
+
|
|
13
|
+
title: "Connection Churn Analysis"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- database
|
|
17
|
+
- application
|
|
18
|
+
- total_connections
|
|
19
|
+
- avg_connection_age_seconds
|
|
20
|
+
- min_connection_age_seconds
|
|
21
|
+
- max_connection_age_seconds
|
|
22
|
+
- short_lived_connections
|
|
23
|
+
- churn_rate_pct
|
|
24
|
+
|
|
25
|
+
thresholds:
|
|
26
|
+
churn_rate_pct:
|
|
27
|
+
warning: 50
|
|
28
|
+
critical: 75
|
|
29
|
+
short_lived_connections:
|
|
30
|
+
warning: 10
|
|
31
|
+
critical: 25
|
|
32
|
+
|
|
33
|
+
problem_fields:
|
|
34
|
+
- churn_rate_pct
|
|
35
|
+
- short_lived_connections
|
|
36
|
+
|
|
37
|
+
problem_explanations:
|
|
38
|
+
churn_rate_pct: high_connection_churn
|
|
39
|
+
short_lived_connections: too_many_short_connections
|
|
40
|
+
|
|
41
|
+
documentation:
|
|
42
|
+
what: "Analyzes connection lifecycle patterns to identify excessive connection churn (frequent connect/disconnect cycles)."
|
|
43
|
+
why: "High connection churn wastes resources on connection setup/teardown and indicates potential connection pooling issues. Short-lived connections suggest the application isn't reusing connections efficiently."
|
|
44
|
+
nuances:
|
|
45
|
+
- "Connections under 10 seconds old are considered short-lived"
|
|
46
|
+
- "High churn rate (>50%) suggests missing or misconfigured connection pooling"
|
|
47
|
+
- "Many short-lived connections increase CPU overhead and authentication load"
|
|
48
|
+
- "Consider using PgBouncer or similar pooler to reduce churn"
|
|
49
|
+
- "Web applications should maintain a connection pool, not create connections per request"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Connection Pool Saturation Warnings
|
|
2
|
+
# Identifies connection pool saturation and potential exhaustion
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: pool_saturation
|
|
6
|
+
module: connections
|
|
7
|
+
description: "Connection pool saturation analysis and warnings"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: connections
|
|
11
|
+
file: pool_saturation
|
|
12
|
+
|
|
13
|
+
title: "Pool Saturation Warnings"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- metric
|
|
17
|
+
- current_value
|
|
18
|
+
- max_value
|
|
19
|
+
- utilization_pct
|
|
20
|
+
- status
|
|
21
|
+
- recommendation
|
|
22
|
+
|
|
23
|
+
thresholds:
|
|
24
|
+
utilization_pct:
|
|
25
|
+
warning: 70
|
|
26
|
+
critical: 85
|
|
27
|
+
|
|
28
|
+
problem_fields:
|
|
29
|
+
- utilization_pct
|
|
30
|
+
|
|
31
|
+
problem_explanations:
|
|
32
|
+
utilization_pct: pool_saturation
|
|
33
|
+
|
|
34
|
+
documentation:
|
|
35
|
+
what: "Overall connection pool health metrics with saturation warnings and recommendations."
|
|
36
|
+
why: "Pool saturation leads to connection exhaustion, causing 'too many connections' errors and application failures. Early detection prevents outages."
|
|
37
|
+
nuances:
|
|
38
|
+
- "Utilization consistently above 70% suggests need for pool tuning or scaling"
|
|
39
|
+
- "High idle in transaction connections waste resources and should be minimized"
|
|
40
|
+
- "Reserved connections (superuser_reserved_connections) reduce available pool"
|
|
41
|
+
- "Consider implementing connection pooling if not already in use"
|
|
42
|
+
- "Monitor trends over time - sudden spikes may indicate connection leaks"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Connection Pool Usage Statistics
|
|
2
|
+
# Shows current pool utilization, limits, and capacity warnings
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: pool_usage
|
|
6
|
+
module: connections
|
|
7
|
+
description: "Connection pool usage and capacity analysis"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: connections
|
|
11
|
+
file: pool_usage
|
|
12
|
+
|
|
13
|
+
title: "Connection Pool Usage"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- database
|
|
17
|
+
- total_connections
|
|
18
|
+
- active_connections
|
|
19
|
+
- idle_connections
|
|
20
|
+
- idle_in_transaction
|
|
21
|
+
- max_connections
|
|
22
|
+
- utilization_pct
|
|
23
|
+
- available_connections
|
|
24
|
+
|
|
25
|
+
thresholds:
|
|
26
|
+
utilization_pct:
|
|
27
|
+
warning: 70
|
|
28
|
+
critical: 85
|
|
29
|
+
|
|
30
|
+
problem_fields:
|
|
31
|
+
- utilization_pct
|
|
32
|
+
|
|
33
|
+
problem_explanations:
|
|
34
|
+
utilization_pct: high_pool_usage
|
|
35
|
+
|
|
36
|
+
documentation:
|
|
37
|
+
what: "Current connection pool utilization across all databases, showing active, idle, and available connections."
|
|
38
|
+
why: "High pool utilization can lead to connection exhaustion, causing application errors and degraded performance. Monitoring pool usage helps prevent connection starvation."
|
|
39
|
+
nuances:
|
|
40
|
+
- "Utilization above 70% indicates you're approaching pool limits"
|
|
41
|
+
- "Idle in transaction connections hold locks and can block other queries"
|
|
42
|
+
- "max_connections is a database-wide setting, not per-database"
|
|
43
|
+
- "Consider using connection pooling (PgBouncer/pgpool) for better resource management"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Connection Pool Wait Time Analysis
|
|
2
|
+
# Analyzes queries waiting for resources (locks, IO, etc.)
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: pool_wait_times
|
|
6
|
+
module: connections
|
|
7
|
+
description: "Analysis of queries waiting for resources"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: connections
|
|
11
|
+
file: pool_wait_times
|
|
12
|
+
|
|
13
|
+
title: "Pool Wait Time Analysis"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- pid
|
|
17
|
+
- database
|
|
18
|
+
- username
|
|
19
|
+
- wait_event_type
|
|
20
|
+
- wait_event
|
|
21
|
+
- state
|
|
22
|
+
- wait_duration_seconds
|
|
23
|
+
- query_start
|
|
24
|
+
- query
|
|
25
|
+
|
|
26
|
+
thresholds:
|
|
27
|
+
wait_duration_seconds:
|
|
28
|
+
warning: 10
|
|
29
|
+
critical: 60
|
|
30
|
+
|
|
31
|
+
problem_fields:
|
|
32
|
+
- wait_duration_seconds
|
|
33
|
+
|
|
34
|
+
problem_explanations:
|
|
35
|
+
wait_duration_seconds: long_wait_time
|
|
36
|
+
|
|
37
|
+
documentation:
|
|
38
|
+
what: "Queries currently waiting for resources like locks, I/O, or network operations."
|
|
39
|
+
why: "Long wait times indicate resource contention or bottlenecks. Understanding what queries are waiting for helps identify performance issues."
|
|
40
|
+
nuances:
|
|
41
|
+
- "ClientRead wait events indicate slow clients not consuming data fast enough"
|
|
42
|
+
- "Lock waits suggest contention between concurrent queries"
|
|
43
|
+
- "IO waits may indicate disk performance issues or need for more cache"
|
|
44
|
+
- "Wait times above 60 seconds are critical and should be investigated immediately"
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgReports
|
|
4
|
+
# Analyzes EXPLAIN ANALYZE output and extracts insights
|
|
5
|
+
class ExplainAnalyzer
|
|
6
|
+
# Node types and their characteristics
|
|
7
|
+
NODE_TYPES = {
|
|
8
|
+
"Seq Scan" => {color: "warning", description: "Full table scan - potentially slow for large tables"},
|
|
9
|
+
"Index Scan" => {color: "good", description: "Using an index efficiently"},
|
|
10
|
+
"Index Only Scan" => {color: "good", description: "Most efficient - reading only from index"},
|
|
11
|
+
"Bitmap Index Scan" => {color: "ok", description: "First step of bitmap scan"},
|
|
12
|
+
"Bitmap Heap Scan" => {color: "ok", description: "Using multiple indexes combined"},
|
|
13
|
+
"Nested Loop" => {color: "neutral", description: "Joining tables in a loop"},
|
|
14
|
+
"Hash Join" => {color: "good", description: "Efficient join using hash table"},
|
|
15
|
+
"Merge Join" => {color: "good", description: "Efficient join on sorted data"},
|
|
16
|
+
"Sort" => {color: "warning", description: "Sorting data in memory or disk"},
|
|
17
|
+
"HashAggregate" => {color: "ok", description: "Grouping using hash table"},
|
|
18
|
+
"GroupAggregate" => {color: "ok", description: "Grouping on sorted data"},
|
|
19
|
+
"Aggregate" => {color: "ok", description: "Computing aggregate functions"},
|
|
20
|
+
"Limit" => {color: "good", description: "Limiting result set"},
|
|
21
|
+
"Subquery Scan" => {color: "neutral", description: "Scanning a subquery result"},
|
|
22
|
+
"CTE Scan" => {color: "neutral", description: "Scanning a Common Table Expression"},
|
|
23
|
+
"Materialize" => {color: "warning", description: "Caching intermediate results"},
|
|
24
|
+
"Gather" => {color: "ok", description: "Parallel query coordination"},
|
|
25
|
+
"Gather Merge" => {color: "ok", description: "Parallel query with merge"}
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :raw_output, :lines, :problems, :summary
|
|
29
|
+
|
|
30
|
+
def initialize(explain_output)
|
|
31
|
+
@raw_output = explain_output
|
|
32
|
+
@lines = explain_output.split("\n")
|
|
33
|
+
@problems = []
|
|
34
|
+
@summary = {}
|
|
35
|
+
analyze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_h
|
|
39
|
+
{
|
|
40
|
+
raw_output: @raw_output,
|
|
41
|
+
annotated_lines: annotate_lines,
|
|
42
|
+
problems: @problems,
|
|
43
|
+
summary: @summary,
|
|
44
|
+
stats: extract_stats
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def analyze
|
|
51
|
+
detect_sequential_scans
|
|
52
|
+
detect_high_cost_operations
|
|
53
|
+
detect_sort_operations
|
|
54
|
+
detect_low_row_accuracy
|
|
55
|
+
detect_timing_issues
|
|
56
|
+
build_summary
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Annotate each line with metadata for rendering
|
|
60
|
+
def annotate_lines
|
|
61
|
+
@lines.map.with_index do |line, idx|
|
|
62
|
+
node_type = extract_node_type(line)
|
|
63
|
+
metrics = extract_metrics(line)
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
line_number: idx + 1,
|
|
67
|
+
text: line,
|
|
68
|
+
node_type: node_type,
|
|
69
|
+
node_info: NODE_TYPES[node_type],
|
|
70
|
+
metrics: metrics,
|
|
71
|
+
indent_level: line[/^ */].length / 2,
|
|
72
|
+
is_planning: line.include?("Planning"),
|
|
73
|
+
is_execution: line.include?("Execution"),
|
|
74
|
+
is_timing: line.match?(/Planning Time|Execution Time/)
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def extract_node_type(line)
|
|
80
|
+
NODE_TYPES.keys.find { |type| line.include?(type) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def extract_metrics(line)
|
|
84
|
+
metrics = {}
|
|
85
|
+
|
|
86
|
+
# Extract cost
|
|
87
|
+
if (match = line.match(/cost=([\d.]+)\.\.([\d.]+)/))
|
|
88
|
+
metrics[:startup_cost] = match[1].to_f
|
|
89
|
+
metrics[:total_cost] = match[2].to_f
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Extract rows
|
|
93
|
+
if (match = line.match(/rows=(\d+)/))
|
|
94
|
+
metrics[:rows_estimated] = match[1].to_i
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Extract actual rows
|
|
98
|
+
if (match = line.match(/rows=(\d+).*actual.*rows=(\d+)/))
|
|
99
|
+
metrics[:rows_estimated] = match[1].to_i
|
|
100
|
+
metrics[:rows_actual] = match[2].to_i
|
|
101
|
+
elsif (match = line.match(/actual.*rows=(\d+)/))
|
|
102
|
+
metrics[:rows_actual] = match[1].to_i
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Extract actual time
|
|
106
|
+
if (match = line.match(/actual time=([\d.]+)\.\.([\d.]+)/))
|
|
107
|
+
metrics[:actual_time_start] = match[1].to_f
|
|
108
|
+
metrics[:actual_time_end] = match[2].to_f
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Extract loops
|
|
112
|
+
if (match = line.match(/loops=(\d+)/))
|
|
113
|
+
metrics[:loops] = match[1].to_i
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Extract buffers
|
|
117
|
+
if (match = line.match(/Buffers: shared hit=(\d+)/))
|
|
118
|
+
metrics[:buffers_hit] = match[1].to_i
|
|
119
|
+
end
|
|
120
|
+
if (match = line.match(/read=(\d+)/))
|
|
121
|
+
metrics[:buffers_read] = match[1].to_i
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
metrics
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def extract_stats
|
|
128
|
+
stats = {}
|
|
129
|
+
|
|
130
|
+
@lines.each do |line|
|
|
131
|
+
if (match = line.match(/Planning Time: ([\d.]+) ms/))
|
|
132
|
+
stats[:planning_time] = match[1].to_f
|
|
133
|
+
elsif (match = line.match(/Execution Time: ([\d.]+) ms/))
|
|
134
|
+
stats[:execution_time] = match[1].to_f
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Extract top-level cost and rows from first line with cost
|
|
139
|
+
first_cost_line = @lines.find { |l| l.include?("cost=") }
|
|
140
|
+
if first_cost_line
|
|
141
|
+
if (match = first_cost_line.match(/cost=[\d.]+\.\.([\d.]+)/))
|
|
142
|
+
stats[:total_cost] = match[1].to_f
|
|
143
|
+
end
|
|
144
|
+
if (match = first_cost_line.match(/rows=(\d+)/))
|
|
145
|
+
stats[:rows_estimated] = match[1].to_i
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
stats
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def detect_sequential_scans
|
|
153
|
+
seq_scans = []
|
|
154
|
+
|
|
155
|
+
@lines.each_with_index do |line, idx|
|
|
156
|
+
next unless line.include?("Seq Scan")
|
|
157
|
+
|
|
158
|
+
table_name = extract_table_name(line)
|
|
159
|
+
metrics = extract_metrics(line)
|
|
160
|
+
|
|
161
|
+
# Consider it a problem if:
|
|
162
|
+
# 1. High cost (> 1000)
|
|
163
|
+
# 2. Many rows (> 1000)
|
|
164
|
+
# 3. Significant actual time (> 100ms per loop)
|
|
165
|
+
is_problem = false
|
|
166
|
+
reasons = []
|
|
167
|
+
|
|
168
|
+
if metrics[:total_cost] && metrics[:total_cost] > 1000
|
|
169
|
+
is_problem = true
|
|
170
|
+
reasons << "high cost (#{metrics[:total_cost].round(2)})"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if metrics[:rows_estimated] && metrics[:rows_estimated] > 1000
|
|
174
|
+
is_problem = true
|
|
175
|
+
reasons << "many rows (#{metrics[:rows_estimated]})"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
if metrics[:actual_time_end] && metrics[:actual_time_end] > 100
|
|
179
|
+
is_problem = true
|
|
180
|
+
reasons << "slow execution (#{metrics[:actual_time_end].round(2)}ms)"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if is_problem
|
|
184
|
+
@problems << {
|
|
185
|
+
type: :sequential_scan,
|
|
186
|
+
severity: :warning,
|
|
187
|
+
line_number: idx + 1,
|
|
188
|
+
table: table_name,
|
|
189
|
+
message: "Sequential scan on #{table_name || "table"}",
|
|
190
|
+
details: reasons.join(", "),
|
|
191
|
+
recommendation: "Consider adding an index on frequently filtered columns"
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
seq_scans << {table: table_name, line: idx + 1, is_problem: is_problem}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
seq_scans
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def detect_high_cost_operations
|
|
202
|
+
@lines.each_with_index do |line, idx|
|
|
203
|
+
metrics = extract_metrics(line)
|
|
204
|
+
next unless metrics[:total_cost]
|
|
205
|
+
|
|
206
|
+
# Flag operations with very high cost (> 10000)
|
|
207
|
+
if metrics[:total_cost] > 10000
|
|
208
|
+
node_type = extract_node_type(line)
|
|
209
|
+
@problems << {
|
|
210
|
+
type: :high_cost,
|
|
211
|
+
severity: :warning,
|
|
212
|
+
line_number: idx + 1,
|
|
213
|
+
node_type: node_type,
|
|
214
|
+
cost: metrics[:total_cost],
|
|
215
|
+
message: "Very high cost operation (#{metrics[:total_cost].round(2)})",
|
|
216
|
+
recommendation: "This operation is expensive - review if it can be optimized"
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def detect_sort_operations
|
|
223
|
+
@lines.each_with_index do |line, idx|
|
|
224
|
+
next unless line.include?("Sort")
|
|
225
|
+
|
|
226
|
+
# Check if sort spilled to disk
|
|
227
|
+
if line.match?(/external.*sort/i) || line.include?("Disk:")
|
|
228
|
+
@problems << {
|
|
229
|
+
type: :sort_spill,
|
|
230
|
+
severity: :critical,
|
|
231
|
+
line_number: idx + 1,
|
|
232
|
+
message: "Sort operation spilled to disk",
|
|
233
|
+
recommendation: "Increase work_mem or optimize query to reduce sort size"
|
|
234
|
+
}
|
|
235
|
+
elsif line.include?("Sort")
|
|
236
|
+
# Just a regular sort, note it but not necessarily a problem
|
|
237
|
+
metrics = extract_metrics(line)
|
|
238
|
+
if metrics[:actual_time_end] && metrics[:actual_time_end] > 1000
|
|
239
|
+
@problems << {
|
|
240
|
+
type: :slow_sort,
|
|
241
|
+
severity: :warning,
|
|
242
|
+
line_number: idx + 1,
|
|
243
|
+
message: "Slow sort operation (#{metrics[:actual_time_end].round(2)}ms)",
|
|
244
|
+
recommendation: "Consider reducing the dataset before sorting or using an index"
|
|
245
|
+
}
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def detect_low_row_accuracy
|
|
252
|
+
@lines.each_with_index do |line, idx|
|
|
253
|
+
metrics = extract_metrics(line)
|
|
254
|
+
next unless metrics[:rows_estimated] && metrics[:rows_actual]
|
|
255
|
+
|
|
256
|
+
estimated = metrics[:rows_estimated].to_f
|
|
257
|
+
actual = metrics[:rows_actual].to_f
|
|
258
|
+
|
|
259
|
+
# Skip if very small numbers
|
|
260
|
+
next if estimated < 10 && actual < 10
|
|
261
|
+
|
|
262
|
+
# Calculate ratio (avoid division by zero)
|
|
263
|
+
max_val = [estimated, actual].max
|
|
264
|
+
min_val = [estimated, actual].min
|
|
265
|
+
next if max_val == 0
|
|
266
|
+
|
|
267
|
+
ratio = max_val / min_val
|
|
268
|
+
|
|
269
|
+
# If estimation is off by more than 10x, it's a problem
|
|
270
|
+
if ratio > 10
|
|
271
|
+
@problems << {
|
|
272
|
+
type: :estimation_error,
|
|
273
|
+
severity: :warning,
|
|
274
|
+
line_number: idx + 1,
|
|
275
|
+
message: "Row estimation is significantly off (estimated: #{estimated.to_i}, actual: #{actual.to_i})",
|
|
276
|
+
recommendation: "Run ANALYZE on the involved tables to update statistics"
|
|
277
|
+
}
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def detect_timing_issues
|
|
283
|
+
stats = extract_stats
|
|
284
|
+
|
|
285
|
+
if stats[:execution_time] && stats[:execution_time] > 1000
|
|
286
|
+
@problems << {
|
|
287
|
+
type: :slow_query,
|
|
288
|
+
severity: :critical,
|
|
289
|
+
message: "Query execution is very slow (#{stats[:execution_time].round(2)}ms)",
|
|
290
|
+
recommendation: "Review the execution plan for optimization opportunities"
|
|
291
|
+
}
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if stats[:planning_time] && stats[:planning_time] > 100
|
|
295
|
+
@problems << {
|
|
296
|
+
type: :slow_planning,
|
|
297
|
+
severity: :info,
|
|
298
|
+
message: "Query planning is slow (#{stats[:planning_time].round(2)}ms)",
|
|
299
|
+
recommendation: "Consider simplifying the query or using prepared statements"
|
|
300
|
+
}
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def build_summary
|
|
305
|
+
@summary = {
|
|
306
|
+
total_problems: @problems.length,
|
|
307
|
+
critical_problems: @problems.count { |p| p[:severity] == :critical },
|
|
308
|
+
warnings: @problems.count { |p| p[:severity] == :warning },
|
|
309
|
+
info: @problems.count { |p| p[:severity] == :info }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# Add overall assessment
|
|
313
|
+
if @summary[:critical_problems] > 0
|
|
314
|
+
@summary[:status] = "critical"
|
|
315
|
+
@summary[:status_text] = "Critical issues detected"
|
|
316
|
+
@summary[:status_icon] = "🔴"
|
|
317
|
+
elsif @summary[:warnings] > 0
|
|
318
|
+
@summary[:status] = "warning"
|
|
319
|
+
@summary[:status_text] = "Potential issues detected"
|
|
320
|
+
@summary[:status_icon] = "🟡"
|
|
321
|
+
else
|
|
322
|
+
@summary[:status] = "good"
|
|
323
|
+
@summary[:status_text] = "No issues detected"
|
|
324
|
+
@summary[:status_icon] = "🟢"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Group problems by type for summary
|
|
328
|
+
problem_types = @problems.group_by { |p| p[:type] }
|
|
329
|
+
@summary[:problem_breakdown] = problem_types.transform_values(&:count)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def extract_table_name(line)
|
|
333
|
+
if (match = line.match(/on (\w+)/))
|
|
334
|
+
match[1]
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
@@ -89,12 +89,10 @@ module PgReports
|
|
|
89
89
|
].uniq
|
|
90
90
|
|
|
91
91
|
possible_names.each do |model_name|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# Model doesn't exist, try next one
|
|
97
|
-
end
|
|
92
|
+
model = model_name.constantize
|
|
93
|
+
return model if model.is_a?(Class) && model < ActiveRecord::Base
|
|
94
|
+
rescue NameError
|
|
95
|
+
# Model doesn't exist, try next one
|
|
98
96
|
end
|
|
99
97
|
|
|
100
98
|
nil
|
|
@@ -50,8 +50,7 @@ module PgReports
|
|
|
50
50
|
# @return [Hash] Metrics data
|
|
51
51
|
def live_metrics(long_query_threshold: 60)
|
|
52
52
|
data = executor.execute_from_file(:system, :live_metrics,
|
|
53
|
-
long_query_threshold: long_query_threshold
|
|
54
|
-
)
|
|
53
|
+
long_query_threshold: long_query_threshold)
|
|
55
54
|
|
|
56
55
|
row = data.first || {}
|
|
57
56
|
|
|
@@ -116,6 +115,24 @@ module PgReports
|
|
|
116
115
|
end
|
|
117
116
|
end
|
|
118
117
|
|
|
118
|
+
# Get list of all databases
|
|
119
|
+
# @return [Array<Hash>] List of databases with sizes
|
|
120
|
+
def databases_list
|
|
121
|
+
executor.execute_from_file(:system, :databases_list)
|
|
122
|
+
rescue
|
|
123
|
+
# Fallback to empty array if query fails
|
|
124
|
+
[]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get current database name
|
|
128
|
+
# @return [String] Current database name
|
|
129
|
+
def current_database
|
|
130
|
+
result = executor.execute("SELECT current_database() AS database")
|
|
131
|
+
result.first&.fetch("database", "unknown") || "unknown"
|
|
132
|
+
rescue
|
|
133
|
+
"unknown"
|
|
134
|
+
end
|
|
135
|
+
|
|
119
136
|
private
|
|
120
137
|
|
|
121
138
|
def executor
|