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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -0
  3. data/README.md +129 -4
  4. data/app/controllers/pg_reports/dashboard_controller.rb +188 -25
  5. data/app/views/layouts/pg_reports/application.html.erb +282 -0
  6. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +184 -23
  7. data/app/views/pg_reports/dashboard/_show_styles.html.erb +373 -0
  8. data/app/views/pg_reports/dashboard/index.html.erb +419 -0
  9. data/config/locales/en.yml +45 -0
  10. data/config/locales/ru.yml +45 -0
  11. data/config/routes.rb +8 -0
  12. data/lib/pg_reports/configuration.rb +13 -0
  13. data/lib/pg_reports/dashboard/reports_registry.rb +24 -1
  14. data/lib/pg_reports/definitions/connections/connection_churn.yml +49 -0
  15. data/lib/pg_reports/definitions/connections/pool_saturation.yml +42 -0
  16. data/lib/pg_reports/definitions/connections/pool_usage.yml +43 -0
  17. data/lib/pg_reports/definitions/connections/pool_wait_times.yml +44 -0
  18. data/lib/pg_reports/definitions/queries/missing_index_queries.yml +3 -3
  19. data/lib/pg_reports/explain_analyzer.rb +338 -0
  20. data/lib/pg_reports/modules/schema_analysis.rb +4 -6
  21. data/lib/pg_reports/modules/system.rb +19 -2
  22. data/lib/pg_reports/query_monitor.rb +280 -0
  23. data/lib/pg_reports/sql/connections/connection_churn.sql +37 -0
  24. data/lib/pg_reports/sql/connections/pool_saturation.sql +90 -0
  25. data/lib/pg_reports/sql/connections/pool_usage.sql +31 -0
  26. data/lib/pg_reports/sql/connections/pool_wait_times.sql +19 -0
  27. data/lib/pg_reports/sql/queries/all_queries.sql +17 -15
  28. data/lib/pg_reports/sql/queries/expensive_queries.sql +9 -4
  29. data/lib/pg_reports/sql/queries/heavy_queries.sql +14 -12
  30. data/lib/pg_reports/sql/queries/low_cache_hit_queries.sql +16 -14
  31. data/lib/pg_reports/sql/queries/missing_index_queries.sql +18 -16
  32. data/lib/pg_reports/sql/queries/slow_queries.sql +14 -12
  33. data/lib/pg_reports/sql/system/databases_list.sql +8 -0
  34. data/lib/pg_reports/version.rb +1 -1
  35. data/lib/pg_reports.rb +2 -0
  36. metadata +56 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a57deebae9eda0df4aceb3cfeb1912e2559a7cd70c3c7f197fb86f06bb689b1
4
- data.tar.gz: 9369b5d6f54d8b0f2939f57d429f477aa0a8c2a862add70e2116232f364fe1e7
3
+ metadata.gz: c7f6e8be280967b6768f1646c2cdb0fa75b1d4da7d559151faa5f6f73dd4dfdb
4
+ data.tar.gz: 2121ea4314ec252406728fb6c5e98314b9703921b22d40a0b8483754dafd483c
5
5
  SHA512:
6
- metadata.gz: 0e6248b26056ffe059105c2399f7a9c8e607e3f34030d4af6f14b324deb76435163b8b2116ff5d7502350410f0a6a20368ac3757b9247d30328da3330a941a86
7
- data.tar.gz: 5d67f8d3c5663421b839301c3c4463502b7cbaeb57d3351d808ff00ebb355141b6c7cf676df091f7a2bb52238809cb02b8e9123d16b7a597bbc636e0cf888540
6
+ metadata.gz: a3a5d23ca386675cc409a7f8536fcd2600d0a04f1576edd15ae0a939aa78e01226e25e776bbb0ed2c66dccf4b24121329c4263389f7b17a6ccc4cf7897063433
7
+ data.tar.gz: 411a3c78f3b45f3fa6c1a7a5bfee766cf0dd59bcab0bacf5e9b338971350b15d169413b1a6a99b540ecb850890bd2dafd60560e7d06e9990a023955310eba965
data/CHANGELOG.md CHANGED
@@ -5,6 +5,110 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.5.0] - 2026-02-07
11
+
12
+ ### Added
13
+
14
+ - **EXPLAIN ANALYZE Advanced Analyzer** - intelligent query plan analysis with problem detection:
15
+ - New `ExplainAnalyzer` service class for parsing and analyzing EXPLAIN output
16
+ - Color-coded node types (🟢 efficient, 🔵 normal, 🟡 potential issues)
17
+ - Automatic problem detection:
18
+ - Sequential scans on large tables (cost > 1000, rows > 1000)
19
+ - High-cost operations (> 10,000)
20
+ - Sort operations spilling to disk
21
+ - Slow sorts (> 1 second)
22
+ - Inaccurate row estimates (> 10x deviation)
23
+ - Slow execution/planning times
24
+ - Summary card with overall status (🟢 No issues / 🟡 Warnings / 🔴 Critical)
25
+ - Problem list with detailed explanations and recommendations
26
+ - Line-by-line plan annotations with problem indicators
27
+ - Metric highlighting (cost, rows, time, loops)
28
+ - Copy to clipboard functionality
29
+ - **Connection Pool Analytics** - comprehensive pool monitoring and diagnostics:
30
+ - `pool_usage` report - real-time utilization metrics per database:
31
+ - Active, idle, idle-in-transaction connection breakdown
32
+ - Utilization percentage with thresholds (70% warning, 85% critical)
33
+ - Available connection capacity calculation
34
+ - `pool_wait_times` report - resource wait analysis:
35
+ - Queries waiting for locks, I/O, or network operations
36
+ - Wait event type classification (ClientRead, Lock, IO)
37
+ - Duration tracking with severity thresholds (10s warning, 60s critical)
38
+ - `pool_saturation` report - health warnings with recommendations:
39
+ - Overall pool metrics with status indicators (🟢🟡🔴)
40
+ - Automatic severity assessment per metric
41
+ - Context-aware recommendations embedded in SQL
42
+ - Tracks total, active, idle, idle-in-transaction, and waiting connections
43
+ - `connection_churn` report - lifecycle and churn analysis:
44
+ - Connection age distribution per application
45
+ - Short-lived connection detection (< 10 seconds)
46
+ - Churn rate percentage calculation (50% warning, 75% critical)
47
+ - Identifies missing or misconfigured connection pooling
48
+ - Complete i18n translations (English and Russian) for all new reports
49
+ - Documentation for each report with usage patterns and nuances
50
+ - **SQL Query Monitoring** - real-time query capture and analysis:
51
+ - New `QueryMonitor` singleton service for capturing all SQL queries via ActiveSupport::Notifications
52
+ - Dashboard panel with start/stop controls and live query feed
53
+ - Query capture features:
54
+ - SQL text, execution duration (color-coded: green < 10ms, yellow < 100ms, red > 100ms)
55
+ - Source location (file:line) with click-to-open in IDE
56
+ - Timestamp and query name
57
+ - Session-based tracking with unique UUIDs
58
+ - Smart filtering:
59
+ - Automatically excludes SCHEMA, CACHE, EXPLAIN queries
60
+ - Filters DDL statements (CREATE, ALTER, DROP)
61
+ - Excludes pg_reports' internal queries
62
+ - Configurable backtrace filtering for source location extraction
63
+ - UI features:
64
+ - Collapsible/expandable queries (truncated to 100 chars by default)
65
+ - Real-time updates via 2-second polling
66
+ - Reverse chronological order (newest first)
67
+ - Query counter with session badge
68
+ - Persistence:
69
+ - JSON Lines (JSONL) log format in `log/pg_reports.log`
70
+ - Session markers (session_start/session_end)
71
+ - In-memory circular buffer (configurable, default 100 queries)
72
+ - Automatic log loading on dashboard open
73
+ - Results persist after stopping monitoring
74
+ - Export capabilities:
75
+ - Download in TXT, CSV, or JSON formats
76
+ - Works even after monitoring stopped
77
+ - Includes all query metadata (timestamp, duration, source, SQL)
78
+ - Uses hidden iframe for downloads (doesn't interrupt monitoring)
79
+ - Configuration options:
80
+ - `query_monitor_log_file` - custom log file path
81
+ - `query_monitor_max_queries` - buffer size (default: 100)
82
+ - `query_monitor_backtrace_filter` - Proc for filtering backtrace lines
83
+ - New routes and controller actions:
84
+ - `POST /query_monitor/start` - start monitoring
85
+ - `POST /query_monitor/stop` - stop monitoring
86
+ - `GET /query_monitor/status` - check monitoring status
87
+ - `GET /query_monitor/feed` - get live query feed
88
+ - `GET /query_monitor/history` - load queries from log file
89
+ - `GET /query_monitor/download` - export queries
90
+ - Comprehensive test coverage (39 unit tests + 5 integration tests)
91
+
92
+ ### Changed
93
+
94
+ - **Unified status indicators** - consistent 🟢🟡🔴 emoji usage across all reports:
95
+ - Replaced ✅ checkmark with 🟢 green circle for "good" status
96
+ - Replaced ⚠️ warning sign with 🟡 yellow circle for "warning" status
97
+ - Retained 🔴 red circle for "critical" status
98
+ - Applied to EXPLAIN analyzer summary and connection pool reports
99
+ - **Simplified database filtering** - all reports now use only current database from project settings:
100
+ - Removed database selector UI component from dashboard
101
+ - All SQL queries now filter by `current_database()` function automatically
102
+ - Current database name displayed in Live Monitoring header
103
+ - Removed `database` parameter from all query reports
104
+ - Removed `databases_list` endpoint and related routes
105
+ - Cleaner, more focused dashboard experience
106
+ - **Optimized gem dependencies** - replaced full Rails framework dependency with specific components:
107
+ - Now using `activesupport`, `activerecord`, `actionpack`, `railties` instead of `rails`
108
+ - Removed unnecessary components: actioncable, actionmailer, actiontext, activejob, activestorage
109
+ - Reduced total dependencies from 97 to 80 gems (-17.5%)
110
+ - Faster installation and smaller footprint
111
+
8
112
  ## [0.4.0] - 2026-01-29
9
113
 
10
114
  ### Added
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # PgReports
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/pg_reports.svg)](https://badge.fury.io/rb/pg_reports)
3
4
  [![Ruby](https://img.shields.io/badge/Ruby-2.7%2B-red.svg)](https://www.ruby-lang.org/)
4
5
  [![Rails](https://img.shields.io/badge/Rails-5.0%2B-red.svg)](https://rubyonrails.org/)
5
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -20,7 +21,9 @@ A comprehensive PostgreSQL monitoring and analysis library for Rails application
20
21
  - 📥 **Export** - Download reports in TXT, CSV, or JSON format
21
22
  - 🔗 **IDE Integration** - Open source locations in VS Code, Cursor, RubyMine, or IntelliJ (with WSL support)
22
23
  - 📌 **Comparison Mode** - Save records to compare before/after optimization
23
- - 📊 **EXPLAIN ANALYZE** - Run query plans directly from the dashboard
24
+ - 📊 **EXPLAIN ANALYZE** - Advanced query plan analyzer with problem detection and recommendations
25
+ - 🔍 **SQL Query Monitoring** - Real-time monitoring of all executed SQL queries with source location tracking
26
+ - 🔌 **Connection Pool Analytics** - Monitor pool usage, wait times, saturation warnings, and connection churn
24
27
  - 🗑️ **Migration Generator** - Generate Rails migrations to drop unused indexes
25
28
 
26
29
  ## Installation
@@ -194,6 +197,10 @@ config.active_record.query_log_tags = [:controller, :action]
194
197
  | `blocking_queries` | Queries blocking others |
195
198
  | `locks` | Current database locks |
196
199
  | `idle_connections` | Idle connections |
200
+ | `pool_usage` | 🆕 Connection pool utilization analysis |
201
+ | `pool_wait_times` | 🆕 Resource wait time analysis |
202
+ | `pool_saturation` | 🆕 Pool health warnings with recommendations |
203
+ | `connection_churn` | 🆕 Connection lifecycle and churn rate analysis |
197
204
  | `kill_connection(pid)` | Terminate a backend process |
198
205
  | `cancel_query(pid)` | Cancel a running query |
199
206
 
@@ -267,6 +274,7 @@ The dashboard provides:
267
274
 
268
275
  - 📊 Overview of all report categories with descriptions
269
276
  - ⚡ One-click report execution
277
+ - 🔍 Filter parameters - adjust thresholds and limits on the fly
270
278
  - 🔍 Expandable rows for full query text
271
279
  - 📋 Copy query to clipboard
272
280
  - 📥 Download in multiple formats (TXT, CSV, JSON)
@@ -291,6 +299,16 @@ Click on source locations in reports to open the file directly in your IDE. Supp
291
299
 
292
300
  Use the ⚙️ button to set your default IDE and skip the selection menu.
293
301
 
302
+ ### Filter Parameters
303
+
304
+ Each report page includes a collapsible "Параметры фильтрации" (Filter Parameters) section where you can:
305
+
306
+ 1. **Adjust thresholds** - Override default thresholds (e.g., slow query threshold, unused index scans)
307
+ 2. **Change limits** - Set the maximum number of results to display
308
+ 3. **Real-time refresh** - Reports automatically refresh when you change parameters
309
+
310
+ Parameters show their current configured values and allow you to experiment with different thresholds without changing your configuration file.
311
+
294
312
  ### Save Records for Comparison
295
313
 
296
314
  When optimizing queries, you can save records to compare before/after results:
@@ -304,13 +322,85 @@ Records are stored in browser localStorage per report type.
304
322
 
305
323
  ### EXPLAIN ANALYZE
306
324
 
307
- For query reports, you can run EXPLAIN ANALYZE directly:
325
+ The advanced query analyzer provides intelligent problem detection and recommendations:
308
326
 
309
327
  1. Expand a row with a query
310
328
  2. Click "📊 EXPLAIN ANALYZE"
311
- 3. View the execution plan with timing statistics
329
+ 3. View the color-coded execution plan with:
330
+ - **🟢🟡🔴 Status indicator** - Overall query health assessment
331
+ - **📈 Key metrics** - Planning/Execution time, Cost, Rows
332
+ - **⚠️ Detected problems** - Sequential scans, high costs, slow sorts, estimation errors
333
+ - **💡 Recommendations** - Actionable advice for each issue
334
+ - **🎨 Colored plan** - Node types color-coded by performance impact:
335
+ - 🟢 Green: Efficient operations (Index Scan, Hash Join)
336
+ - 🔵 Blue: Normal operations (Bitmap Scan, HashAggregate)
337
+ - 🟡 Yellow: Potential issues (Seq Scan, Sort, Materialize)
338
+ - **Line-by-line annotations** - Problems highlighted on specific plan lines
339
+
340
+ **Problem Detection:**
341
+ - Sequential scans on large tables (> 1000 rows)
342
+ - High-cost operations (> 10,000 cost units)
343
+ - Sorts spilling to disk
344
+ - Slow sort operations (> 1s)
345
+ - Inaccurate row estimates (> 10x off)
346
+ - Slow execution/planning times
347
+
348
+ > Note: Queries with parameter placeholders ($1, $2) from pg_stat_statements require parameter input before analysis.
349
+
350
+ ### SQL Query Monitoring
351
+
352
+ Monitor all SQL queries executed in your Rails application in real-time:
353
+
354
+ 1. Visit the dashboard at `/pg_reports`
355
+ 2. Click **"▶ Start Monitoring"** button in the SQL Query Monitor panel
356
+ 3. Execute operations in your application (web requests, console commands, background jobs)
357
+ 4. View captured queries in the dashboard with:
358
+ - **SQL text** - Formatted with syntax highlighting
359
+ - **Execution duration** - Color-coded: 🟢 green (< 10ms), 🟡 yellow (< 100ms), 🔴 red (> 100ms)
360
+ - **Source location** - File:line with click-to-open in IDE
361
+ - **Timestamp** - When the query was executed
362
+ 5. Click **"⏹ Stop Monitoring"** when done
363
+
364
+ **Features:**
365
+ - Uses Rails' built-in **ActiveSupport::Notifications** (`sql.active_record` events)
366
+ - Global monitoring session (shared by all dashboard users)
367
+ - Automatically filters internal queries (SCHEMA, CACHE, pg_reports' own queries)
368
+ - Keeps last N queries in memory (configurable, default 100)
369
+ - 2-second auto-refresh while monitoring is active
370
+ - Session-based tracking with unique IDs
371
+ - Logged to `log/pg_reports.log` in JSON Lines format
372
+
373
+ **Configuration:**
374
+
375
+ ```ruby
376
+ PgReports.configure do |config|
377
+ # Query monitoring
378
+ config.query_monitor_log_file = Rails.root.join("log", "custom_monitor.log")
379
+ config.query_monitor_max_queries = 200 # Keep last 200 queries (default: 100)
380
+
381
+ # Custom backtrace filtering to show only application code
382
+ config.query_monitor_backtrace_filter = ->(location) {
383
+ !location.path.match?(%r{/(gems|ruby|railties)/})
384
+ }
385
+ end
386
+ ```
387
+
388
+ **Log Format:**
312
389
 
313
- > Note: Queries with parameter placeholders ($1, $2) from pg_stat_statements cannot be analyzed directly. Copy the query and replace parameters with actual values.
390
+ The log file uses JSON Lines format (one JSON object per line):
391
+
392
+ ```json
393
+ {"type":"session_start","session_id":"550e8400-e29b-41d4-a716-446655440000","timestamp":"2024-02-07T10:30:00Z"}
394
+ {"type":"query","session_id":"550e8400-e29b-41d4-a716-446655440000","sql":"SELECT * FROM users WHERE id = 1","duration_ms":2.34,"name":"User Load","source_location":{"file":"app/controllers/users_controller.rb","line":15,"method":"show"},"timestamp":"2024-02-07T10:30:01Z"}
395
+ {"type":"session_end","session_id":"550e8400-e29b-41d4-a716-446655440000","timestamp":"2024-02-07T10:35:00Z"}
396
+ ```
397
+
398
+ **Use Cases:**
399
+ - 🐛 Debug N+1 query problems during development
400
+ - 🐌 Identify slow queries in real-time
401
+ - 🔍 Track down source of unexpected queries
402
+ - 📊 Monitor query patterns during feature development
403
+ - 📚 Teaching tool for understanding ActiveRecord behavior
314
404
 
315
405
  ### Migration Generator
316
406
 
@@ -321,6 +411,41 @@ For unused or invalid indexes, generate Rails migrations:
321
411
  3. Copy the code or create the file directly
322
412
  4. The file opens automatically in your configured IDE
323
413
 
414
+ ### Connection Pool Analytics
415
+
416
+ Monitor your connection pool health with specialized reports:
417
+
418
+ **Pool Usage** - Real-time utilization metrics:
419
+ - Total, active, idle connections per database
420
+ - Pool utilization percentage with 🟢🟡🔴 indicators
421
+ - Idle in transaction connections (resource waste)
422
+ - Available connection capacity
423
+
424
+ **Wait Times** - Identify resource bottlenecks:
425
+ - Queries waiting for locks, I/O, or network
426
+ - Wait event types (ClientRead, Lock, IO)
427
+ - Wait duration with severity thresholds
428
+ - Helps diagnose contention issues
429
+
430
+ **Pool Saturation** - Health warnings with actionable recommendations:
431
+ - Overall pool metrics with status indicators
432
+ - Automatic severity assessment (Normal/Elevated/Warning/Critical)
433
+ - Context-aware recommendations for each metric
434
+ - Detects approaching exhaustion, high idle transactions
435
+
436
+ **Connection Churn** - Lifecycle analysis:
437
+ - Connection age distribution by application
438
+ - Short-lived connection detection (< 10 seconds)
439
+ - Churn rate percentage calculation
440
+ - Identifies missing/misconfigured connection pooling
441
+
442
+ ```ruby
443
+ # Console usage
444
+ PgReports.pool_usage.display
445
+ PgReports.pool_saturation.display
446
+ PgReports.connection_churn.display
447
+ ```
448
+
324
449
  ### Authentication
325
450
 
326
451
  ```ruby
@@ -9,6 +9,7 @@ module PgReports
9
9
 
10
10
  def index
11
11
  @pg_stat_status = PgReports.pg_stat_statements_status
12
+ @current_database = PgReports.system.current_database
12
13
  end
13
14
 
14
15
  def enable_pg_stat_statements
@@ -164,22 +165,18 @@ module PgReports
164
165
  result = ActiveRecord::Base.connection.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{final_query}")
165
166
  explain_output = result.map { |r| r["QUERY PLAN"] }.join("\n")
166
167
 
167
- # Extract stats from the output
168
- stats = {}
169
- if (match = explain_output.match(/Planning Time: ([\d.]+) ms/))
170
- stats[:planning_time] = match[1].to_f
171
- end
172
- if (match = explain_output.match(/Execution Time: ([\d.]+) ms/))
173
- stats[:execution_time] = match[1].to_f
174
- end
175
- if (match = explain_output.match(/cost=[\d.]+\.\.([\d.]+)/))
176
- stats[:total_cost] = match[1].to_f
177
- end
178
- if (match = explain_output.match(/rows=(\d+)/))
179
- stats[:rows] = match[1].to_i
180
- end
168
+ # Analyze the EXPLAIN output
169
+ analyzer = ExplainAnalyzer.new(explain_output)
170
+ analysis = analyzer.to_h
181
171
 
182
- render json: {success: true, explain: explain_output, stats: stats}
172
+ render json: {
173
+ success: true,
174
+ explain: explain_output,
175
+ stats: analysis[:stats],
176
+ annotated_lines: analysis[:annotated_lines],
177
+ problems: analysis[:problems],
178
+ summary: analysis[:summary]
179
+ }
183
180
  rescue => e
184
181
  render json: {success: false, error: e.message}, status: :unprocessable_entity
185
182
  end
@@ -195,7 +192,7 @@ module PgReports
195
192
 
196
193
  # Security: Only allow SELECT and SHOW queries
197
194
  normalized = query.strip.gsub(/\s+/, " ").downcase
198
- unless normalized.start_with?("select") || normalized.start_with?("show")
195
+ unless normalized.start_with?("select", "show")
199
196
  render json: {success: false, error: "Only SELECT and SHOW queries are allowed"}, status: :unprocessable_entity
200
197
  return
201
198
  end
@@ -287,6 +284,123 @@ module PgReports
287
284
  render json: {success: false, error: e.message}, status: :unprocessable_entity
288
285
  end
289
286
 
287
+ def start_query_monitoring
288
+ monitor = QueryMonitor.instance
289
+
290
+ result = monitor.start
291
+
292
+ if result[:success]
293
+ render json: result
294
+ else
295
+ render json: result, status: :unprocessable_entity
296
+ end
297
+ rescue => e
298
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
299
+ end
300
+
301
+ def stop_query_monitoring
302
+ monitor = QueryMonitor.instance
303
+
304
+ result = monitor.stop
305
+
306
+ if result[:success]
307
+ render json: result
308
+ else
309
+ render json: result, status: :unprocessable_entity
310
+ end
311
+ rescue => e
312
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
313
+ end
314
+
315
+ def query_monitor_status
316
+ monitor = QueryMonitor.instance
317
+ status = monitor.status
318
+
319
+ render json: {
320
+ success: true,
321
+ enabled: status[:enabled],
322
+ session_id: status[:session_id],
323
+ query_count: status[:query_count]
324
+ }
325
+ rescue => e
326
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
327
+ end
328
+
329
+ def query_monitor_feed
330
+ monitor = QueryMonitor.instance
331
+
332
+ unless monitor.enabled
333
+ render json: {success: false, message: "Monitoring not active"}
334
+ return
335
+ end
336
+
337
+ limit = params[:limit]&.to_i || 50
338
+ session_id = params[:session_id]
339
+
340
+ queries = monitor.queries(limit: limit, session_id: session_id)
341
+
342
+ render json: {
343
+ success: true,
344
+ queries: queries,
345
+ timestamp: Time.current.to_i
346
+ }
347
+ rescue => e
348
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
349
+ end
350
+
351
+ def load_query_history
352
+ monitor = QueryMonitor.instance
353
+
354
+ limit = params[:limit]&.to_i || 100
355
+ session_id = params[:session_id]
356
+
357
+ queries = monitor.load_from_log(limit: limit, session_id: session_id)
358
+
359
+ render json: {
360
+ success: true,
361
+ queries: queries,
362
+ timestamp: Time.current.to_i
363
+ }
364
+ rescue => e
365
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
366
+ end
367
+
368
+ def download_query_monitor
369
+ monitor = QueryMonitor.instance
370
+
371
+ # Allow download even when monitoring is stopped, as long as there are queries
372
+ queries = monitor.queries
373
+ if queries.empty?
374
+ render json: {success: false, error: "No queries to download"}, status: :unprocessable_entity
375
+ return
376
+ end
377
+
378
+ format_type = params[:format] || "txt"
379
+ filename = "query-monitor-#{Time.current.strftime("%Y%m%d-%H%M%S")}"
380
+
381
+ case format_type
382
+ when "csv"
383
+ csv_data = generate_query_monitor_csv(queries)
384
+ send_data csv_data,
385
+ filename: "#{filename}.csv",
386
+ type: "text/csv; charset=utf-8",
387
+ disposition: "attachment"
388
+ when "json"
389
+ send_data queries.to_json,
390
+ filename: "#{filename}.json",
391
+ type: "application/json; charset=utf-8",
392
+ disposition: "attachment"
393
+ else
394
+ text_data = generate_query_monitor_text(queries)
395
+ send_data text_data,
396
+ filename: "#{filename}.txt",
397
+ type: "text/plain; charset=utf-8",
398
+ disposition: "attachment"
399
+ end
400
+ rescue => e
401
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
402
+ end
403
+
290
404
  private
291
405
 
292
406
  def authenticate_dashboard!
@@ -326,7 +440,7 @@ module PgReports
326
440
 
327
441
  # Also allow threshold overrides (calls_threshold, etc.)
328
442
  params.each do |key, value|
329
- if key.to_s.end_with?('_threshold') && value.present?
443
+ if key.to_s.end_with?("_threshold") && value.present?
330
444
  result[key.to_sym] = value.to_i
331
445
  end
332
446
  end
@@ -356,7 +470,7 @@ module PgReports
356
470
  result = query.dup
357
471
 
358
472
  # Sort by param number descending to replace $10 before $1
359
- params_hash.keys.map(&:to_i).sort.reverse.each do |num|
473
+ params_hash.keys.map(&:to_i).sort.reverse_each do |num|
360
474
  value = params_hash[num.to_s] || params_hash[num]
361
475
  next if value.nil? || value.to_s.empty?
362
476
 
@@ -371,17 +485,15 @@ module PgReports
371
485
  def quote_param_value(value)
372
486
  str = value.to_s
373
487
 
374
- # Check if it looks like a number
375
- if str.match?(/\A-?\d+(\.\d+)?\z/)
376
- str
488
+ # Check if it looks like NULL
489
+ if str.downcase == "null"
490
+ "NULL"
377
491
  # Check if it looks like a boolean
378
492
  elsif str.downcase.in?(["true", "false"])
379
493
  str.downcase
380
- # Check if it looks like NULL
381
- elsif str.downcase == "null"
382
- "NULL"
383
494
  else
384
- # Quote as string, escape single quotes
495
+ # Quote as string by default - PostgreSQL will handle type casting
496
+ # This ensures compatibility with both text and numeric columns
385
497
  "'#{str.gsub("'", "''")}'"
386
498
  end
387
499
  end
@@ -397,5 +509,56 @@ module PgReports
397
509
  "#{query} LIMIT #{limit}"
398
510
  end
399
511
  end
512
+
513
+ def generate_query_monitor_csv(queries)
514
+ require "csv"
515
+
516
+ CSV.generate do |csv|
517
+ # Header
518
+ csv << ["Timestamp", "Duration (ms)", "Query Name", "SQL", "Source File", "Source Line"]
519
+
520
+ # Data rows
521
+ queries.each do |query|
522
+ csv << [
523
+ query[:timestamp],
524
+ query[:duration_ms],
525
+ query[:name],
526
+ query[:sql],
527
+ query.dig(:source_location, :file),
528
+ query.dig(:source_location, :line)
529
+ ]
530
+ end
531
+ end
532
+ end
533
+
534
+ def generate_query_monitor_text(queries)
535
+ output = []
536
+ output << "=" * 80
537
+ output << "Query Monitor Export"
538
+ output << "Generated: #{Time.current.strftime("%Y-%m-%d %H:%M:%S")}"
539
+ output << "Total Queries: #{queries.size}"
540
+ output << "=" * 80
541
+ output << ""
542
+
543
+ queries.each_with_index do |query, index|
544
+ output << "Query ##{index + 1}"
545
+ output << "-" * 80
546
+ output << "Timestamp: #{query[:timestamp]}"
547
+ output << "Duration: #{query[:duration_ms]}ms"
548
+ output << "Name: #{query[:name]}"
549
+
550
+ if query[:source_location]
551
+ output << "Source: #{query[:source_location][:file]}:#{query[:source_location][:line]}"
552
+ end
553
+
554
+ output << ""
555
+ output << "SQL:"
556
+ output << query[:sql]
557
+ output << ""
558
+ output << ""
559
+ end
560
+
561
+ output.join("\n")
562
+ end
400
563
  end
401
564
  end