pg_reports 0.5.3 → 0.6.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 +61 -0
- data/README.md +12 -4
- data/app/controllers/pg_reports/dashboard_controller.rb +6 -2
- data/app/views/layouts/pg_reports/application.html.erb +70 -61
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +53 -1
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +31 -11
- data/app/views/pg_reports/dashboard/index.html.erb +80 -9
- data/app/views/pg_reports/dashboard/show.html.erb +6 -2
- data/config/locales/en.yml +109 -0
- data/config/locales/ru.yml +81 -0
- data/config/locales/uk.yml +126 -0
- data/lib/pg_reports/compatibility.rb +63 -0
- data/lib/pg_reports/configuration.rb +2 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +36 -0
- data/lib/pg_reports/definitions/indexes/fk_without_indexes.yml +30 -0
- data/lib/pg_reports/definitions/indexes/index_correlation.yml +31 -0
- data/lib/pg_reports/definitions/indexes/inefficient_indexes.yml +45 -0
- data/lib/pg_reports/definitions/queries/temp_file_queries.yml +39 -0
- data/lib/pg_reports/definitions/system/wraparound_risk.yml +31 -0
- data/lib/pg_reports/definitions/tables/tables_without_pk.yml +28 -0
- data/lib/pg_reports/engine.rb +6 -0
- data/lib/pg_reports/modules/indexes.rb +3 -0
- data/lib/pg_reports/modules/queries.rb +1 -0
- data/lib/pg_reports/modules/system.rb +27 -0
- data/lib/pg_reports/modules/tables.rb +1 -0
- data/lib/pg_reports/query_monitor.rb +139 -42
- data/lib/pg_reports/sql/indexes/fk_without_indexes.sql +23 -0
- data/lib/pg_reports/sql/indexes/index_correlation.sql +27 -0
- data/lib/pg_reports/sql/indexes/inefficient_indexes.sql +22 -0
- data/lib/pg_reports/sql/queries/temp_file_queries.sql +16 -0
- data/lib/pg_reports/sql/system/checkpoint_stats.sql +20 -0
- data/lib/pg_reports/sql/system/checkpoint_stats_legacy.sql +19 -0
- data/lib/pg_reports/sql/system/wraparound_risk.sql +21 -0
- data/lib/pg_reports/sql/tables/tables_without_pk.sql +20 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +5 -0
- metadata +16 -1
|
@@ -8,65 +8,99 @@ module PgReports
|
|
|
8
8
|
class QueryMonitor
|
|
9
9
|
include Singleton
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
CACHE_KEY_ENABLED = "pg_reports:query_monitor:enabled"
|
|
12
|
+
CACHE_KEY_SESSION_ID = "pg_reports:query_monitor:session_id"
|
|
13
|
+
CACHE_TTL = 24.hours
|
|
12
14
|
|
|
13
15
|
def initialize
|
|
14
|
-
@enabled = false
|
|
15
16
|
@subscriber = nil
|
|
16
17
|
@mutex = Mutex.new
|
|
17
|
-
@session_id = nil
|
|
18
18
|
@queries = []
|
|
19
|
+
@handling_event = false
|
|
20
|
+
|
|
21
|
+
# Local state — used by the event handler to avoid cache reads
|
|
22
|
+
# (which generate SQL events and cause infinite recursion with DB-backed caches)
|
|
23
|
+
@enabled = false
|
|
24
|
+
@session_id = nil
|
|
25
|
+
|
|
26
|
+
sync_from_cache
|
|
27
|
+
ensure_subscription_if_enabled
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def enabled
|
|
31
|
+
@enabled
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def session_id
|
|
35
|
+
@session_id
|
|
19
36
|
end
|
|
20
37
|
|
|
21
38
|
def start
|
|
22
39
|
@mutex.synchronize do
|
|
23
|
-
if
|
|
40
|
+
if enabled
|
|
41
|
+
Rails.logger.info("PgReports: Monitoring already active, session_id=#{session_id}") if defined?(Rails)
|
|
24
42
|
return {success: false, message: "Monitoring already active"}
|
|
25
43
|
end
|
|
26
44
|
|
|
27
|
-
|
|
45
|
+
new_session_id = SecureRandom.uuid
|
|
28
46
|
@queries = []
|
|
47
|
+
|
|
48
|
+
# Update local state first (used by event handler — no cache round-trip)
|
|
29
49
|
@enabled = true
|
|
50
|
+
@session_id = new_session_id
|
|
30
51
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
# Store state in cache so other processes can see it
|
|
53
|
+
cache_write(CACHE_KEY_ENABLED, true)
|
|
54
|
+
cache_write(CACHE_KEY_SESSION_ID, new_session_id)
|
|
55
|
+
|
|
56
|
+
Rails.logger.info("PgReports: Monitoring started, session_id=#{new_session_id}") if defined?(Rails)
|
|
57
|
+
|
|
58
|
+
# Subscribe to sql.active_record events in THIS process
|
|
59
|
+
ensure_subscription
|
|
35
60
|
|
|
36
61
|
# Write session start marker to file
|
|
37
62
|
write_session_marker("session_start")
|
|
38
63
|
|
|
39
|
-
{success: true, message: "Query monitoring started", session_id:
|
|
64
|
+
{success: true, message: "Query monitoring started", session_id: new_session_id}
|
|
40
65
|
end
|
|
41
66
|
rescue => e
|
|
42
67
|
@enabled = false
|
|
68
|
+
@session_id = nil
|
|
69
|
+
cache_write(CACHE_KEY_ENABLED, false)
|
|
43
70
|
{success: false, error: e.message}
|
|
44
71
|
end
|
|
45
72
|
|
|
46
73
|
def stop
|
|
47
74
|
@mutex.synchronize do
|
|
48
|
-
unless
|
|
75
|
+
unless enabled
|
|
49
76
|
return {success: false, message: "Monitoring not active"}
|
|
50
77
|
end
|
|
51
78
|
|
|
52
|
-
|
|
79
|
+
current_session_id = @session_id
|
|
80
|
+
|
|
81
|
+
# Clear local state immediately — stops event handler from processing
|
|
82
|
+
@enabled = false
|
|
83
|
+
@session_id = nil
|
|
84
|
+
|
|
85
|
+
# Unsubscribe from notifications in THIS process
|
|
53
86
|
if @subscriber
|
|
54
87
|
ActiveSupport::Notifications.unsubscribe(@subscriber)
|
|
55
88
|
@subscriber = nil
|
|
56
89
|
end
|
|
57
90
|
|
|
58
91
|
# Write session end marker to file
|
|
59
|
-
write_session_marker("session_end")
|
|
92
|
+
write_session_marker("session_end", current_session_id)
|
|
60
93
|
|
|
61
94
|
# Flush queries to file
|
|
62
95
|
flush_to_file
|
|
63
96
|
|
|
64
|
-
|
|
97
|
+
# Clear state from cache so other processes see it
|
|
98
|
+
cache_delete(CACHE_KEY_ENABLED)
|
|
99
|
+
cache_delete(CACHE_KEY_SESSION_ID)
|
|
100
|
+
|
|
65
101
|
@queries = []
|
|
66
|
-
session_id = @session_id
|
|
67
|
-
@session_id = nil
|
|
68
102
|
|
|
69
|
-
{success: true, message: "Query monitoring stopped", session_id:
|
|
103
|
+
{success: true, message: "Query monitoring stopped", session_id: current_session_id}
|
|
70
104
|
end
|
|
71
105
|
rescue => e
|
|
72
106
|
{success: false, error: e.message}
|
|
@@ -74,8 +108,8 @@ module PgReports
|
|
|
74
108
|
|
|
75
109
|
def status
|
|
76
110
|
{
|
|
77
|
-
enabled:
|
|
78
|
-
session_id:
|
|
111
|
+
enabled: enabled,
|
|
112
|
+
session_id: session_id,
|
|
79
113
|
query_count: @queries.size
|
|
80
114
|
}
|
|
81
115
|
end
|
|
@@ -130,31 +164,90 @@ module PgReports
|
|
|
130
164
|
|
|
131
165
|
private
|
|
132
166
|
|
|
133
|
-
|
|
167
|
+
# Cache helpers - work with or without Rails.cache
|
|
168
|
+
def cache_read(key)
|
|
169
|
+
return nil unless cache_available?
|
|
170
|
+
Rails.cache.read(key)
|
|
171
|
+
rescue => e
|
|
172
|
+
Rails.logger.warn("PgReports: Cache read failed: #{e.message}") if defined?(Rails.logger)
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def cache_write(key, value)
|
|
177
|
+
return false unless cache_available?
|
|
178
|
+
Rails.cache.write(key, value, expires_in: CACHE_TTL)
|
|
179
|
+
rescue => e
|
|
180
|
+
Rails.logger.warn("PgReports: Cache write failed: #{e.message}") if defined?(Rails.logger)
|
|
181
|
+
false
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def cache_delete(key)
|
|
185
|
+
return false unless cache_available?
|
|
186
|
+
Rails.cache.delete(key)
|
|
187
|
+
rescue => e
|
|
188
|
+
Rails.logger.warn("PgReports: Cache delete failed: #{e.message}") if defined?(Rails.logger)
|
|
189
|
+
false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def cache_available?
|
|
193
|
+
defined?(Rails) && defined?(Rails.cache)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Sync local state from shared cache (called on initialize for multi-process support)
|
|
197
|
+
def sync_from_cache
|
|
198
|
+
@enabled = enabled
|
|
199
|
+
@session_id = session_id
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Ensure this process is subscribed to notifications if monitoring is enabled
|
|
203
|
+
def ensure_subscription_if_enabled
|
|
134
204
|
return unless @enabled
|
|
205
|
+
ensure_subscription
|
|
206
|
+
end
|
|
135
207
|
|
|
136
|
-
|
|
137
|
-
return if
|
|
208
|
+
def ensure_subscription
|
|
209
|
+
return if @subscriber # Already subscribed
|
|
138
210
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# Extract source location
|
|
144
|
-
source_location = extract_source_location
|
|
145
|
-
|
|
146
|
-
# Build query entry
|
|
147
|
-
query_entry = {
|
|
148
|
-
type: "query",
|
|
149
|
-
session_id: @session_id,
|
|
150
|
-
sql: sql,
|
|
151
|
-
duration_ms: duration_ms,
|
|
152
|
-
name: query_name,
|
|
153
|
-
source_location: source_location,
|
|
154
|
-
timestamp: Time.current.iso8601
|
|
155
|
-
}
|
|
211
|
+
@subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |name, started, finished, unique_id, payload|
|
|
212
|
+
handle_sql_event(name, started, finished, unique_id, payload)
|
|
213
|
+
end
|
|
156
214
|
|
|
157
|
-
|
|
215
|
+
Rails.logger.debug("PgReports: Subscribed to sql.active_record in process #{Process.pid}") if defined?(Rails.logger)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def handle_sql_event(name, started, finished, unique_id, payload)
|
|
219
|
+
# Use local @enabled instead of enabled (which hits cache and may generate SQL,
|
|
220
|
+
# causing infinite recursion with database-backed cache stores like SolidCache)
|
|
221
|
+
return unless @enabled
|
|
222
|
+
return if @handling_event
|
|
223
|
+
|
|
224
|
+
@handling_event = true
|
|
225
|
+
begin
|
|
226
|
+
# Skip if should be filtered
|
|
227
|
+
return if should_skip?(payload)
|
|
228
|
+
|
|
229
|
+
duration_ms = ((finished - started) * 1000).round(2)
|
|
230
|
+
sql = payload[:sql]
|
|
231
|
+
query_name = payload[:name]
|
|
232
|
+
|
|
233
|
+
# Extract source location
|
|
234
|
+
source_location = extract_source_location
|
|
235
|
+
|
|
236
|
+
# Build query entry
|
|
237
|
+
query_entry = {
|
|
238
|
+
type: "query",
|
|
239
|
+
session_id: @session_id,
|
|
240
|
+
sql: sql,
|
|
241
|
+
duration_ms: duration_ms,
|
|
242
|
+
name: query_name,
|
|
243
|
+
source_location: source_location,
|
|
244
|
+
timestamp: Time.current.iso8601
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
add_to_buffer(query_entry)
|
|
248
|
+
ensure
|
|
249
|
+
@handling_event = false
|
|
250
|
+
end
|
|
158
251
|
end
|
|
159
252
|
|
|
160
253
|
def should_skip?(payload)
|
|
@@ -197,6 +290,10 @@ module PgReports
|
|
|
197
290
|
# Exclude test paths
|
|
198
291
|
next if path.include?("/spec/")
|
|
199
292
|
|
|
293
|
+
# IMPORTANT: Exclude query_monitor.rb itself to prevent false positives
|
|
294
|
+
# when gem is installed from RubyGems
|
|
295
|
+
next if path.include?("/query_monitor.rb")
|
|
296
|
+
|
|
200
297
|
# Filter queries from pg_reports internal modules only:
|
|
201
298
|
# - Installed gem: /gems/pg_reports-X.Y.Z/lib/
|
|
202
299
|
# - Local gem: /pg_reports/lib/pg_reports/modules/
|
|
@@ -252,12 +349,12 @@ module PgReports
|
|
|
252
349
|
end
|
|
253
350
|
end
|
|
254
351
|
|
|
255
|
-
def write_session_marker(marker_type)
|
|
352
|
+
def write_session_marker(marker_type, sid = @session_id)
|
|
256
353
|
return unless log_file_enabled?
|
|
257
354
|
|
|
258
355
|
marker = {
|
|
259
356
|
type: marker_type,
|
|
260
|
-
session_id:
|
|
357
|
+
session_id: sid,
|
|
261
358
|
timestamp: Time.current.iso8601
|
|
262
359
|
}
|
|
263
360
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Foreign keys without indexes on the referencing (child) table
|
|
2
|
+
-- Missing indexes cause sequential scans on DELETE/UPDATE of parent rows
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
c.conname AS constraint_name,
|
|
6
|
+
c.conrelid::regclass::text AS child_table,
|
|
7
|
+
a.attname AS child_column,
|
|
8
|
+
c.confrelid::regclass::text AS parent_table,
|
|
9
|
+
pa.attname AS parent_column,
|
|
10
|
+
pg_size_pretty(pg_relation_size(c.conrelid)) AS child_table_size,
|
|
11
|
+
ROUND(pg_relation_size(c.conrelid) / 1024.0 / 1024.0, 2) AS child_table_size_mb
|
|
12
|
+
FROM pg_constraint c
|
|
13
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
|
14
|
+
JOIN pg_attribute pa ON pa.attrelid = c.confrelid AND pa.attnum = ANY(c.confkey)
|
|
15
|
+
WHERE c.contype = 'f'
|
|
16
|
+
AND NOT EXISTS (
|
|
17
|
+
SELECT 1
|
|
18
|
+
FROM pg_index i
|
|
19
|
+
WHERE i.indrelid = c.conrelid
|
|
20
|
+
AND a.attnum = ANY(i.indkey)
|
|
21
|
+
AND i.indkey[0] = a.attnum
|
|
22
|
+
)
|
|
23
|
+
ORDER BY pg_relation_size(c.conrelid) DESC;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Index correlation: how well physical row order matches index order
|
|
2
|
+
-- Low correlation on frequently range-scanned columns means excessive random I/O
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
s.schemaname AS schema,
|
|
6
|
+
s.tablename AS table_name,
|
|
7
|
+
s.attname AS column_name,
|
|
8
|
+
i.indexrelname AS index_name,
|
|
9
|
+
ROUND(s.correlation::numeric, 4) AS correlation,
|
|
10
|
+
ABS(s.correlation) AS abs_correlation,
|
|
11
|
+
s.n_distinct,
|
|
12
|
+
pg_size_pretty(pg_relation_size(c.oid)) AS table_size,
|
|
13
|
+
ROUND(pg_relation_size(c.oid) / 1024.0 / 1024.0, 2) AS table_size_mb,
|
|
14
|
+
si.idx_scan
|
|
15
|
+
FROM pg_stats s
|
|
16
|
+
JOIN pg_class c ON c.relname = s.tablename
|
|
17
|
+
AND c.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = s.schemaname)
|
|
18
|
+
JOIN pg_index idx ON idx.indrelid = c.oid
|
|
19
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = s.attname
|
|
20
|
+
AND a.attnum = idx.indkey[0]
|
|
21
|
+
JOIN pg_stat_user_indexes i ON i.indexrelid = idx.indexrelid
|
|
22
|
+
JOIN pg_stat_user_indexes si ON si.indexrelid = idx.indexrelid
|
|
23
|
+
WHERE s.schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
24
|
+
AND ABS(s.correlation) < 0.5
|
|
25
|
+
AND pg_relation_size(c.oid) > 10 * 1024 * 1024
|
|
26
|
+
AND si.idx_scan > 100
|
|
27
|
+
ORDER BY ABS(s.correlation) ASC, pg_relation_size(c.oid) DESC;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Inefficient index scans: indexes that are used but read far more entries than they fetch
|
|
2
|
+
-- A high idx_tup_read / idx_tup_fetch ratio indicates the index column order
|
|
3
|
+
-- does not match query predicates, forcing PostgreSQL to scan large index ranges
|
|
4
|
+
-- Reference: https://www.datadoghq.com/blog/detect-inefficient-index-scans-with-dbm/
|
|
5
|
+
|
|
6
|
+
SELECT
|
|
7
|
+
schemaname AS schema,
|
|
8
|
+
relname AS table_name,
|
|
9
|
+
indexrelname AS index_name,
|
|
10
|
+
idx_scan,
|
|
11
|
+
idx_tup_read,
|
|
12
|
+
idx_tup_fetch,
|
|
13
|
+
ROUND((idx_tup_read::numeric / NULLIF(idx_tup_fetch, 0)), 1) AS read_to_fetch_ratio,
|
|
14
|
+
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
|
|
15
|
+
ROUND(pg_relation_size(indexrelid) / 1024.0 / 1024.0, 2) AS index_size_mb,
|
|
16
|
+
pg_get_indexdef(indexrelid) AS index_definition
|
|
17
|
+
FROM pg_stat_user_indexes
|
|
18
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
19
|
+
AND idx_scan > 0
|
|
20
|
+
AND idx_tup_fetch > 0
|
|
21
|
+
AND (idx_tup_read::numeric / NULLIF(idx_tup_fetch, 0)) > 10
|
|
22
|
+
ORDER BY (idx_tup_read::numeric / NULLIF(idx_tup_fetch, 0)) DESC;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Queries that spill to disk via temporary files
|
|
2
|
+
-- High temp file usage indicates insufficient work_mem for these queries
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
queryid,
|
|
6
|
+
LEFT(query, :max_query_length) AS query,
|
|
7
|
+
calls,
|
|
8
|
+
ROUND(temp_blks_written::numeric * 8 / 1024, 2) AS temp_mb_written,
|
|
9
|
+
ROUND(temp_blks_read::numeric * 8 / 1024, 2) AS temp_mb_read,
|
|
10
|
+
ROUND((total_exec_time / 1000)::numeric, 2) AS total_time_sec,
|
|
11
|
+
ROUND((mean_exec_time)::numeric, 2) AS mean_time_ms,
|
|
12
|
+
rows
|
|
13
|
+
FROM pg_stat_statements
|
|
14
|
+
WHERE temp_blks_written > 0
|
|
15
|
+
AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
16
|
+
ORDER BY temp_blks_written DESC;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Checkpoint and background writer statistics (PostgreSQL 17+)
|
|
2
|
+
-- Uses pg_stat_checkpointer (introduced in PG 17) + pg_stat_bgwriter for bgwriter-only stats
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
pg_stat_get_checkpointer_num_timed() AS checkpoints_timed,
|
|
6
|
+
pg_stat_get_checkpointer_num_requested() AS checkpoints_requested,
|
|
7
|
+
ROUND(pg_stat_get_checkpointer_write_time()::numeric / 1000, 2) AS checkpoint_write_time_sec,
|
|
8
|
+
ROUND(pg_stat_get_checkpointer_sync_time()::numeric / 1000, 2) AS checkpoint_sync_time_sec,
|
|
9
|
+
pg_stat_get_checkpointer_buffers_written() AS buffers_checkpoint,
|
|
10
|
+
pg_stat_get_bgwriter_buf_written_clean() AS buffers_clean,
|
|
11
|
+
pg_stat_get_bgwriter_maxwritten_clean() AS bgwriter_stops,
|
|
12
|
+
pg_stat_get_buf_alloc() AS buffers_alloc,
|
|
13
|
+
CASE
|
|
14
|
+
WHEN (pg_stat_get_checkpointer_num_timed() + pg_stat_get_checkpointer_num_requested()) > 0
|
|
15
|
+
THEN ROUND(
|
|
16
|
+
pg_stat_get_checkpointer_num_requested()::numeric /
|
|
17
|
+
(pg_stat_get_checkpointer_num_timed() + pg_stat_get_checkpointer_num_requested()) * 100, 1)
|
|
18
|
+
ELSE 0
|
|
19
|
+
END AS requested_pct,
|
|
20
|
+
pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Checkpoint and background writer statistics (PostgreSQL < 17)
|
|
2
|
+
-- All stats from pg_stat_bgwriter (before checkpoint columns were moved out)
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
checkpoints_timed,
|
|
6
|
+
checkpoints_req AS checkpoints_requested,
|
|
7
|
+
ROUND(checkpoint_write_time::numeric / 1000, 2) AS checkpoint_write_time_sec,
|
|
8
|
+
ROUND(checkpoint_sync_time::numeric / 1000, 2) AS checkpoint_sync_time_sec,
|
|
9
|
+
buffers_checkpoint,
|
|
10
|
+
buffers_clean,
|
|
11
|
+
maxwritten_clean AS bgwriter_stops,
|
|
12
|
+
buffers_alloc,
|
|
13
|
+
CASE
|
|
14
|
+
WHEN (checkpoints_timed + checkpoints_req) > 0
|
|
15
|
+
THEN ROUND(checkpoints_req::numeric / (checkpoints_timed + checkpoints_req) * 100, 1)
|
|
16
|
+
ELSE 0
|
|
17
|
+
END AS requested_pct,
|
|
18
|
+
stats_reset
|
|
19
|
+
FROM pg_stat_bgwriter;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Transaction ID wraparound risk
|
|
2
|
+
-- When age(datfrozenxid) approaches 2 billion, PostgreSQL will shut down
|
|
3
|
+
-- to prevent data corruption. Monitor this to trigger preventive VACUUM FREEZE.
|
|
4
|
+
|
|
5
|
+
SELECT
|
|
6
|
+
d.datname AS database_name,
|
|
7
|
+
age(d.datfrozenxid) AS xid_age,
|
|
8
|
+
ROUND(age(d.datfrozenxid)::numeric / 2147483647 * 100, 2) AS pct_towards_wraparound,
|
|
9
|
+
2147483647 - age(d.datfrozenxid) AS remaining_xids,
|
|
10
|
+
current_setting('autovacuum_freeze_max_age')::bigint AS freeze_max_age,
|
|
11
|
+
CASE
|
|
12
|
+
WHEN age(d.datfrozenxid) > current_setting('autovacuum_freeze_max_age')::bigint
|
|
13
|
+
THEN 'CRITICAL - exceeds freeze_max_age'
|
|
14
|
+
WHEN age(d.datfrozenxid) > current_setting('autovacuum_freeze_max_age')::bigint * 0.75
|
|
15
|
+
THEN 'WARNING - approaching freeze_max_age'
|
|
16
|
+
ELSE 'OK'
|
|
17
|
+
END AS status,
|
|
18
|
+
pg_size_pretty(pg_database_size(d.datname)) AS database_size
|
|
19
|
+
FROM pg_database d
|
|
20
|
+
WHERE d.datallowconn = true
|
|
21
|
+
ORDER BY age(d.datfrozenxid) DESC;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Tables without primary keys
|
|
2
|
+
-- Missing PKs break logical replication and make row identification unreliable
|
|
3
|
+
|
|
4
|
+
SELECT
|
|
5
|
+
n.nspname AS schema,
|
|
6
|
+
c.relname AS table_name,
|
|
7
|
+
c.reltuples::bigint AS estimated_rows,
|
|
8
|
+
pg_size_pretty(pg_relation_size(c.oid)) AS table_size,
|
|
9
|
+
ROUND(pg_relation_size(c.oid) / 1024.0 / 1024.0, 2) AS table_size_mb
|
|
10
|
+
FROM pg_class c
|
|
11
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
12
|
+
WHERE c.relkind = 'r'
|
|
13
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
14
|
+
AND NOT EXISTS (
|
|
15
|
+
SELECT 1
|
|
16
|
+
FROM pg_index i
|
|
17
|
+
WHERE i.indrelid = c.oid
|
|
18
|
+
AND i.indisprimary
|
|
19
|
+
)
|
|
20
|
+
ORDER BY pg_relation_size(c.oid) DESC;
|
data/lib/pg_reports/version.rb
CHANGED
data/lib/pg_reports.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "active_record"
|
|
|
6
6
|
|
|
7
7
|
require_relative "pg_reports/version"
|
|
8
8
|
require_relative "pg_reports/error"
|
|
9
|
+
require_relative "pg_reports/compatibility"
|
|
9
10
|
require_relative "pg_reports/configuration"
|
|
10
11
|
require_relative "pg_reports/sql_loader"
|
|
11
12
|
require_relative "pg_reports/executor"
|
|
@@ -138,3 +139,7 @@ end
|
|
|
138
139
|
|
|
139
140
|
# Generate YAML-based methods on load
|
|
140
141
|
PgReports::ModuleGenerator.generate!
|
|
142
|
+
|
|
143
|
+
# Check Ruby and Rails versions immediately (no DB needed)
|
|
144
|
+
PgReports::Compatibility.check_ruby!
|
|
145
|
+
PgReports::Compatibility.check_rails!
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pg_reports
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eldar Avatov
|
|
@@ -161,6 +161,7 @@ files:
|
|
|
161
161
|
- config/routes.rb
|
|
162
162
|
- lib/pg_reports.rb
|
|
163
163
|
- lib/pg_reports/annotation_parser.rb
|
|
164
|
+
- lib/pg_reports/compatibility.rb
|
|
164
165
|
- lib/pg_reports/configuration.rb
|
|
165
166
|
- lib/pg_reports/dashboard/reports_registry.rb
|
|
166
167
|
- lib/pg_reports/definitions/connections/active_connections.yml
|
|
@@ -175,8 +176,11 @@ files:
|
|
|
175
176
|
- lib/pg_reports/definitions/connections/pool_wait_times.yml
|
|
176
177
|
- lib/pg_reports/definitions/indexes/bloated_indexes.yml
|
|
177
178
|
- lib/pg_reports/definitions/indexes/duplicate_indexes.yml
|
|
179
|
+
- lib/pg_reports/definitions/indexes/fk_without_indexes.yml
|
|
180
|
+
- lib/pg_reports/definitions/indexes/index_correlation.yml
|
|
178
181
|
- lib/pg_reports/definitions/indexes/index_sizes.yml
|
|
179
182
|
- lib/pg_reports/definitions/indexes/index_usage.yml
|
|
183
|
+
- lib/pg_reports/definitions/indexes/inefficient_indexes.yml
|
|
180
184
|
- lib/pg_reports/definitions/indexes/invalid_indexes.yml
|
|
181
185
|
- lib/pg_reports/definitions/indexes/missing_indexes.yml
|
|
182
186
|
- lib/pg_reports/definitions/indexes/unused_indexes.yml
|
|
@@ -186,17 +190,20 @@ files:
|
|
|
186
190
|
- lib/pg_reports/definitions/queries/low_cache_hit_queries.yml
|
|
187
191
|
- lib/pg_reports/definitions/queries/missing_index_queries.yml
|
|
188
192
|
- lib/pg_reports/definitions/queries/slow_queries.yml
|
|
193
|
+
- lib/pg_reports/definitions/queries/temp_file_queries.yml
|
|
189
194
|
- lib/pg_reports/definitions/system/activity_overview.yml
|
|
190
195
|
- lib/pg_reports/definitions/system/cache_stats.yml
|
|
191
196
|
- lib/pg_reports/definitions/system/database_sizes.yml
|
|
192
197
|
- lib/pg_reports/definitions/system/extensions.yml
|
|
193
198
|
- lib/pg_reports/definitions/system/settings.yml
|
|
199
|
+
- lib/pg_reports/definitions/system/wraparound_risk.yml
|
|
194
200
|
- lib/pg_reports/definitions/tables/bloated_tables.yml
|
|
195
201
|
- lib/pg_reports/definitions/tables/cache_hit_ratios.yml
|
|
196
202
|
- lib/pg_reports/definitions/tables/recently_modified.yml
|
|
197
203
|
- lib/pg_reports/definitions/tables/row_counts.yml
|
|
198
204
|
- lib/pg_reports/definitions/tables/seq_scans.yml
|
|
199
205
|
- lib/pg_reports/definitions/tables/table_sizes.yml
|
|
206
|
+
- lib/pg_reports/definitions/tables/tables_without_pk.yml
|
|
200
207
|
- lib/pg_reports/definitions/tables/vacuum_needed.yml
|
|
201
208
|
- lib/pg_reports/engine.rb
|
|
202
209
|
- lib/pg_reports/error.rb
|
|
@@ -226,8 +233,11 @@ files:
|
|
|
226
233
|
- lib/pg_reports/sql/connections/pool_wait_times.sql
|
|
227
234
|
- lib/pg_reports/sql/indexes/bloated_indexes.sql
|
|
228
235
|
- lib/pg_reports/sql/indexes/duplicate_indexes.sql
|
|
236
|
+
- lib/pg_reports/sql/indexes/fk_without_indexes.sql
|
|
237
|
+
- lib/pg_reports/sql/indexes/index_correlation.sql
|
|
229
238
|
- lib/pg_reports/sql/indexes/index_sizes.sql
|
|
230
239
|
- lib/pg_reports/sql/indexes/index_usage.sql
|
|
240
|
+
- lib/pg_reports/sql/indexes/inefficient_indexes.sql
|
|
231
241
|
- lib/pg_reports/sql/indexes/invalid_indexes.sql
|
|
232
242
|
- lib/pg_reports/sql/indexes/missing_indexes.sql
|
|
233
243
|
- lib/pg_reports/sql/indexes/unused_indexes.sql
|
|
@@ -237,20 +247,25 @@ files:
|
|
|
237
247
|
- lib/pg_reports/sql/queries/low_cache_hit_queries.sql
|
|
238
248
|
- lib/pg_reports/sql/queries/missing_index_queries.sql
|
|
239
249
|
- lib/pg_reports/sql/queries/slow_queries.sql
|
|
250
|
+
- lib/pg_reports/sql/queries/temp_file_queries.sql
|
|
240
251
|
- lib/pg_reports/sql/schema_analysis/unique_indexes.sql
|
|
241
252
|
- lib/pg_reports/sql/system/activity_overview.sql
|
|
242
253
|
- lib/pg_reports/sql/system/cache_stats.sql
|
|
254
|
+
- lib/pg_reports/sql/system/checkpoint_stats.sql
|
|
255
|
+
- lib/pg_reports/sql/system/checkpoint_stats_legacy.sql
|
|
243
256
|
- lib/pg_reports/sql/system/database_sizes.sql
|
|
244
257
|
- lib/pg_reports/sql/system/databases_list.sql
|
|
245
258
|
- lib/pg_reports/sql/system/extensions.sql
|
|
246
259
|
- lib/pg_reports/sql/system/live_metrics.sql
|
|
247
260
|
- lib/pg_reports/sql/system/settings.sql
|
|
261
|
+
- lib/pg_reports/sql/system/wraparound_risk.sql
|
|
248
262
|
- lib/pg_reports/sql/tables/bloated_tables.sql
|
|
249
263
|
- lib/pg_reports/sql/tables/cache_hit_ratios.sql
|
|
250
264
|
- lib/pg_reports/sql/tables/recently_modified.sql
|
|
251
265
|
- lib/pg_reports/sql/tables/row_counts.sql
|
|
252
266
|
- lib/pg_reports/sql/tables/seq_scans.sql
|
|
253
267
|
- lib/pg_reports/sql/tables/table_sizes.sql
|
|
268
|
+
- lib/pg_reports/sql/tables/tables_without_pk.sql
|
|
254
269
|
- lib/pg_reports/sql/tables/vacuum_needed.sql
|
|
255
270
|
- lib/pg_reports/sql_loader.rb
|
|
256
271
|
- lib/pg_reports/telegram_sender.rb
|