pg_reports 0.5.3 → 0.5.4
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e98f6b7de8825ba0c851061a0d5e6f341986b9f710d594a9079a0a220c86f043
|
|
4
|
+
data.tar.gz: 713c08469b6c9d7aaea22c133e838cc73ac294acc64bba8fb21fa094c3680745
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e0380c79d58828741eb17529e23272bb3e6fcafcfa7193e2a378e436a55b72b9e5be73e06ec3709836620e50ae704f7213cda83146fec489919695f364d7375
|
|
7
|
+
data.tar.gz: 7c5fdd5d289836e04a7b907d88e191e7a270d7bfffbc056230cc1469c4a83ff434e5fbf55dc0c49b6f84bdc2fc949e7b6858627e2e9387f0d9d7c22db423e77e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.4] - 2026-02-11
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Live Query Monitor critical fix for multi-process servers** - monitoring now works correctly with Puma, Unicorn, and other multi-process web servers:
|
|
15
|
+
- Migrated from Singleton instance variables to Rails.cache for cross-process state sharing
|
|
16
|
+
- Fixed "Monitoring not active" errors when requests hit different worker processes
|
|
17
|
+
- Each process now subscribes to SQL notifications when monitoring is enabled
|
|
18
|
+
- State (enabled/session_id) stored in Rails.cache with 24-hour TTL
|
|
19
|
+
- Added cache helper methods with graceful error handling
|
|
20
|
+
- Monitoring state now persists across all processes in multi-worker environments
|
|
21
|
+
- Exclude `query_monitor.rb` itself from `query_from_pg_reports?` check to prevent false positives
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Enhanced error handling for Query Monitor**:
|
|
26
|
+
- Toast notification system with visual feedback (success/error/warning types)
|
|
27
|
+
- Server errors now displayed to users with clear messages
|
|
28
|
+
- Automatic monitoring stop and UI reset when errors occur
|
|
29
|
+
- Smooth animations with auto-dismiss after 4 seconds
|
|
30
|
+
|
|
10
31
|
## [0.5.3] - 2026-02-11
|
|
11
32
|
|
|
12
33
|
### Fixed
|
|
@@ -47,7 +47,7 @@ module PgReports
|
|
|
47
47
|
timestamp: Time.current.to_i,
|
|
48
48
|
available: true
|
|
49
49
|
}
|
|
50
|
-
rescue PG::InsufficientPrivilege
|
|
50
|
+
rescue PG::InsufficientPrivilege
|
|
51
51
|
render json: {
|
|
52
52
|
success: false,
|
|
53
53
|
error: "Insufficient database permissions to access statistics views",
|
|
@@ -363,8 +363,10 @@ module PgReports
|
|
|
363
363
|
|
|
364
364
|
def start_query_monitoring
|
|
365
365
|
monitor = PgReports::QueryMonitor.instance
|
|
366
|
+
Rails.logger.info("PgReports: start_query_monitoring called. Instance: #{monitor.object_id}")
|
|
366
367
|
|
|
367
368
|
result = monitor.start
|
|
369
|
+
Rails.logger.info("PgReports: start result: #{result.inspect}")
|
|
368
370
|
|
|
369
371
|
if result[:success]
|
|
370
372
|
render json: result
|
|
@@ -372,6 +374,7 @@ module PgReports
|
|
|
372
374
|
render json: result, status: :unprocessable_entity
|
|
373
375
|
end
|
|
374
376
|
rescue => e
|
|
377
|
+
Rails.logger.error("PgReports: start_query_monitoring error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
|
|
375
378
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
376
379
|
end
|
|
377
380
|
|
|
@@ -407,6 +410,7 @@ module PgReports
|
|
|
407
410
|
monitor = PgReports::QueryMonitor.instance
|
|
408
411
|
|
|
409
412
|
unless monitor.enabled
|
|
413
|
+
Rails.logger.warn("PgReports: query_monitor_feed called but monitoring not active. Instance: #{monitor.object_id}, enabled: #{monitor.enabled}, session_id: #{monitor.session_id}")
|
|
410
414
|
render json: {success: false, message: "Monitoring not active"}
|
|
411
415
|
return
|
|
412
416
|
end
|
|
@@ -634,7 +638,7 @@ module PgReports
|
|
|
634
638
|
|
|
635
639
|
# Must start with SELECT (case insensitive)
|
|
636
640
|
unless normalized.start_with?("select")
|
|
637
|
-
raise SecurityError, "Only SELECT queries are allowed. Found: #{normalized.split.first&.upcase ||
|
|
641
|
+
raise SecurityError, "Only SELECT queries are allowed. Found: #{normalized.split.first&.upcase || "unknown"}"
|
|
638
642
|
end
|
|
639
643
|
|
|
640
644
|
# Check for dangerous keywords that might be in subqueries or CTEs
|
|
@@ -789,6 +789,44 @@ pg_stat_statements.track = all</pre>
|
|
|
789
789
|
.metric-status.ok { background: var(--accent-green); }
|
|
790
790
|
.metric-status.warning { background: var(--accent-amber); }
|
|
791
791
|
.metric-status.critical { background: var(--accent-rose); }
|
|
792
|
+
|
|
793
|
+
/* Toast Notifications */
|
|
794
|
+
.toast {
|
|
795
|
+
position: fixed;
|
|
796
|
+
bottom: 2rem;
|
|
797
|
+
right: 2rem;
|
|
798
|
+
padding: 1rem 1.5rem;
|
|
799
|
+
background: var(--bg-card);
|
|
800
|
+
border: 1px solid var(--border-color);
|
|
801
|
+
border-radius: 12px;
|
|
802
|
+
color: var(--text-primary);
|
|
803
|
+
font-size: 0.875rem;
|
|
804
|
+
font-weight: 500;
|
|
805
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
806
|
+
z-index: 9999;
|
|
807
|
+
opacity: 0;
|
|
808
|
+
transform: translateY(20px);
|
|
809
|
+
transition: all 0.3s ease;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.toast.show {
|
|
813
|
+
opacity: 1;
|
|
814
|
+
transform: translateY(0);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.toast.toast-success {
|
|
818
|
+
border-left: 3px solid var(--accent-green);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.toast.toast-error {
|
|
822
|
+
border-left: 3px solid var(--accent-rose);
|
|
823
|
+
background: rgba(244, 63, 94, 0.1);
|
|
824
|
+
border-color: var(--accent-rose);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.toast.toast-warning {
|
|
828
|
+
border-left: 3px solid var(--accent-amber);
|
|
829
|
+
}
|
|
792
830
|
</style>
|
|
793
831
|
|
|
794
832
|
<script>
|
|
@@ -1568,9 +1606,20 @@ pg_stat_statements.track = all</pre>
|
|
|
1568
1606
|
|
|
1569
1607
|
if (data.success && data.queries) {
|
|
1570
1608
|
renderQueryFeed(data.queries);
|
|
1609
|
+
} else if (!data.success) {
|
|
1610
|
+
// Server returned an error (e.g., "Monitoring not active")
|
|
1611
|
+
const errorMsg = data.message || data.error || 'Query monitoring error';
|
|
1612
|
+
showToast(errorMsg, 'error');
|
|
1613
|
+
|
|
1614
|
+
// Stop polling and update UI since monitoring is not active
|
|
1615
|
+
queryMonitorEnabled = false;
|
|
1616
|
+
currentSessionId = null;
|
|
1617
|
+
stopQueryMonitorPolling();
|
|
1618
|
+
updateQueryMonitorUI(false);
|
|
1571
1619
|
}
|
|
1572
1620
|
} catch (error) {
|
|
1573
1621
|
console.error('Failed to fetch query feed:', error);
|
|
1622
|
+
showToast('Network error: ' + error.message, 'error');
|
|
1574
1623
|
}
|
|
1575
1624
|
}
|
|
1576
1625
|
|
|
@@ -1664,13 +1713,29 @@ pg_stat_statements.track = all</pre>
|
|
|
1664
1713
|
}
|
|
1665
1714
|
|
|
1666
1715
|
function showToast(message, type = 'success') {
|
|
1667
|
-
//
|
|
1716
|
+
// Log to console as well
|
|
1668
1717
|
if (type === 'error') {
|
|
1669
1718
|
console.error(message);
|
|
1670
1719
|
} else {
|
|
1671
1720
|
console.log(message);
|
|
1672
1721
|
}
|
|
1673
|
-
|
|
1722
|
+
|
|
1723
|
+
// Create toast element
|
|
1724
|
+
const toast = document.createElement('div');
|
|
1725
|
+
toast.className = `toast toast-${type}`;
|
|
1726
|
+
toast.textContent = message;
|
|
1727
|
+
|
|
1728
|
+
// Add to page
|
|
1729
|
+
document.body.appendChild(toast);
|
|
1730
|
+
|
|
1731
|
+
// Trigger animation
|
|
1732
|
+
setTimeout(() => toast.classList.add('show'), 10);
|
|
1733
|
+
|
|
1734
|
+
// Remove after 4 seconds
|
|
1735
|
+
setTimeout(() => {
|
|
1736
|
+
toast.classList.remove('show');
|
|
1737
|
+
setTimeout(() => toast.remove(), 300);
|
|
1738
|
+
}, 4000);
|
|
1674
1739
|
}
|
|
1675
1740
|
|
|
1676
1741
|
// Initialize on page load
|
|
@@ -8,48 +8,63 @@ 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
|
+
ensure_subscription_if_enabled
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def enabled
|
|
23
|
+
cache_read(CACHE_KEY_ENABLED) || false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def session_id
|
|
27
|
+
cache_read(CACHE_KEY_SESSION_ID)
|
|
19
28
|
end
|
|
20
29
|
|
|
21
30
|
def start
|
|
22
31
|
@mutex.synchronize do
|
|
23
|
-
if
|
|
32
|
+
if enabled
|
|
33
|
+
Rails.logger.info("PgReports: Monitoring already active, session_id=#{session_id}") if defined?(Rails)
|
|
24
34
|
return {success: false, message: "Monitoring already active"}
|
|
25
35
|
end
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
new_session_id = SecureRandom.uuid
|
|
28
38
|
@queries = []
|
|
29
|
-
@enabled = true
|
|
30
39
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
# Store state in cache so all processes can see it
|
|
41
|
+
cache_write(CACHE_KEY_ENABLED, true)
|
|
42
|
+
cache_write(CACHE_KEY_SESSION_ID, new_session_id)
|
|
43
|
+
|
|
44
|
+
Rails.logger.info("PgReports: Monitoring started, session_id=#{new_session_id}") if defined?(Rails)
|
|
45
|
+
|
|
46
|
+
# Subscribe to sql.active_record events in THIS process
|
|
47
|
+
ensure_subscription
|
|
35
48
|
|
|
36
49
|
# Write session start marker to file
|
|
37
50
|
write_session_marker("session_start")
|
|
38
51
|
|
|
39
|
-
{success: true, message: "Query monitoring started", session_id:
|
|
52
|
+
{success: true, message: "Query monitoring started", session_id: new_session_id}
|
|
40
53
|
end
|
|
41
54
|
rescue => e
|
|
42
|
-
|
|
55
|
+
cache_write(CACHE_KEY_ENABLED, false)
|
|
43
56
|
{success: false, error: e.message}
|
|
44
57
|
end
|
|
45
58
|
|
|
46
59
|
def stop
|
|
47
60
|
@mutex.synchronize do
|
|
48
|
-
unless
|
|
61
|
+
unless enabled
|
|
49
62
|
return {success: false, message: "Monitoring not active"}
|
|
50
63
|
end
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
current_session_id = session_id
|
|
66
|
+
|
|
67
|
+
# Unsubscribe from notifications in THIS process
|
|
53
68
|
if @subscriber
|
|
54
69
|
ActiveSupport::Notifications.unsubscribe(@subscriber)
|
|
55
70
|
@subscriber = nil
|
|
@@ -61,12 +76,13 @@ module PgReports
|
|
|
61
76
|
# Flush queries to file
|
|
62
77
|
flush_to_file
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
# Clear state from cache
|
|
80
|
+
cache_delete(CACHE_KEY_ENABLED)
|
|
81
|
+
cache_delete(CACHE_KEY_SESSION_ID)
|
|
82
|
+
|
|
65
83
|
@queries = []
|
|
66
|
-
session_id = @session_id
|
|
67
|
-
@session_id = nil
|
|
68
84
|
|
|
69
|
-
{success: true, message: "Query monitoring stopped", session_id:
|
|
85
|
+
{success: true, message: "Query monitoring stopped", session_id: current_session_id}
|
|
70
86
|
end
|
|
71
87
|
rescue => e
|
|
72
88
|
{success: false, error: e.message}
|
|
@@ -74,8 +90,8 @@ module PgReports
|
|
|
74
90
|
|
|
75
91
|
def status
|
|
76
92
|
{
|
|
77
|
-
enabled:
|
|
78
|
-
session_id:
|
|
93
|
+
enabled: enabled,
|
|
94
|
+
session_id: session_id,
|
|
79
95
|
query_count: @queries.size
|
|
80
96
|
}
|
|
81
97
|
end
|
|
@@ -130,8 +146,53 @@ module PgReports
|
|
|
130
146
|
|
|
131
147
|
private
|
|
132
148
|
|
|
149
|
+
# Cache helpers - work with or without Rails.cache
|
|
150
|
+
def cache_read(key)
|
|
151
|
+
return nil unless cache_available?
|
|
152
|
+
Rails.cache.read(key)
|
|
153
|
+
rescue => e
|
|
154
|
+
Rails.logger.warn("PgReports: Cache read failed: #{e.message}") if defined?(Rails.logger)
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def cache_write(key, value)
|
|
159
|
+
return false unless cache_available?
|
|
160
|
+
Rails.cache.write(key, value, expires_in: CACHE_TTL)
|
|
161
|
+
rescue => e
|
|
162
|
+
Rails.logger.warn("PgReports: Cache write failed: #{e.message}") if defined?(Rails.logger)
|
|
163
|
+
false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def cache_delete(key)
|
|
167
|
+
return false unless cache_available?
|
|
168
|
+
Rails.cache.delete(key)
|
|
169
|
+
rescue => e
|
|
170
|
+
Rails.logger.warn("PgReports: Cache delete failed: #{e.message}") if defined?(Rails.logger)
|
|
171
|
+
false
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def cache_available?
|
|
175
|
+
defined?(Rails) && defined?(Rails.cache)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Ensure this process is subscribed to notifications if monitoring is enabled
|
|
179
|
+
def ensure_subscription_if_enabled
|
|
180
|
+
return unless enabled
|
|
181
|
+
ensure_subscription
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def ensure_subscription
|
|
185
|
+
return if @subscriber # Already subscribed
|
|
186
|
+
|
|
187
|
+
@subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |name, started, finished, unique_id, payload|
|
|
188
|
+
handle_sql_event(name, started, finished, unique_id, payload)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
Rails.logger.debug("PgReports: Subscribed to sql.active_record in process #{Process.pid}") if defined?(Rails.logger)
|
|
192
|
+
end
|
|
193
|
+
|
|
133
194
|
def handle_sql_event(name, started, finished, unique_id, payload)
|
|
134
|
-
return unless
|
|
195
|
+
return unless enabled
|
|
135
196
|
|
|
136
197
|
# Skip if should be filtered
|
|
137
198
|
return if should_skip?(payload)
|
|
@@ -146,7 +207,7 @@ module PgReports
|
|
|
146
207
|
# Build query entry
|
|
147
208
|
query_entry = {
|
|
148
209
|
type: "query",
|
|
149
|
-
session_id:
|
|
210
|
+
session_id: session_id,
|
|
150
211
|
sql: sql,
|
|
151
212
|
duration_ms: duration_ms,
|
|
152
213
|
name: query_name,
|
|
@@ -197,6 +258,10 @@ module PgReports
|
|
|
197
258
|
# Exclude test paths
|
|
198
259
|
next if path.include?("/spec/")
|
|
199
260
|
|
|
261
|
+
# IMPORTANT: Exclude query_monitor.rb itself to prevent false positives
|
|
262
|
+
# when gem is installed from RubyGems
|
|
263
|
+
next if path.include?("/query_monitor.rb")
|
|
264
|
+
|
|
200
265
|
# Filter queries from pg_reports internal modules only:
|
|
201
266
|
# - Installed gem: /gems/pg_reports-X.Y.Z/lib/
|
|
202
267
|
# - Local gem: /pg_reports/lib/pg_reports/modules/
|
|
@@ -257,7 +322,7 @@ module PgReports
|
|
|
257
322
|
|
|
258
323
|
marker = {
|
|
259
324
|
type: marker_type,
|
|
260
|
-
session_id:
|
|
325
|
+
session_id: session_id,
|
|
261
326
|
timestamp: Time.current.iso8601
|
|
262
327
|
}
|
|
263
328
|
|
data/lib/pg_reports/version.rb
CHANGED