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: 0e9a2129fba68b259fc72598215a7cc02ff8785c93a786603505d9b9987d5df1
4
- data.tar.gz: 7910113c5cc18115f59463067ab201f1942ec8b968570557af8aefda645098c7
3
+ metadata.gz: e98f6b7de8825ba0c851061a0d5e6f341986b9f710d594a9079a0a220c86f043
4
+ data.tar.gz: 713c08469b6c9d7aaea22c133e838cc73ac294acc64bba8fb21fa094c3680745
5
5
  SHA512:
6
- metadata.gz: 7e5573dd2cca17cc74cdfe104e6eb09249069d59d50b033effd15e9c46bcb63040da50d998d91c8a2c7e31b8dc4fd3f86c2692f5b60d83050db766942d9a5d46
7
- data.tar.gz: 025a5a37c86817e22265aff1227e332a1f5dd09cf0954a48321fe1ca5dc8c8f402cd13671c4b7b4c1b69e7ec739ef7f8ca47280b11894718d1bcaa946d7983b7
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 => e
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 || 'unknown'}"
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
- // Simple console log for now - can be enhanced with actual toast UI
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
- // TODO: Implement actual toast notification UI
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
- attr_reader :enabled, :session_id
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 @enabled
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
- @session_id = SecureRandom.uuid
37
+ new_session_id = SecureRandom.uuid
28
38
  @queries = []
29
- @enabled = true
30
39
 
31
- # Subscribe to sql.active_record events
32
- @subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |name, started, finished, unique_id, payload|
33
- handle_sql_event(name, started, finished, unique_id, payload)
34
- end
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: @session_id}
52
+ {success: true, message: "Query monitoring started", session_id: new_session_id}
40
53
  end
41
54
  rescue => e
42
- @enabled = false
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 @enabled
61
+ unless enabled
49
62
  return {success: false, message: "Monitoring not active"}
50
63
  end
51
64
 
52
- # Unsubscribe from notifications
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
- @enabled = false
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: 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: @enabled,
78
- session_id: @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 @enabled
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: @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: @session_id,
325
+ session_id: session_id,
261
326
  timestamp: Time.current.iso8601
262
327
  }
263
328
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgReports
4
- VERSION = "0.5.3"
4
+ VERSION = "0.5.4"
5
5
  end
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.5.3
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eldar Avatov