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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7f6e8be280967b6768f1646c2cdb0fa75b1d4da7d559151faa5f6f73dd4dfdb
|
|
4
|
+
data.tar.gz: 2121ea4314ec252406728fb6c5e98314b9703921b22d40a0b8483754dafd483c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
+
[](https://badge.fury.io/rb/pg_reports)
|
|
3
4
|
[](https://www.ruby-lang.org/)
|
|
4
5
|
[](https://rubyonrails.org/)
|
|
5
6
|
[](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** -
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
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: {
|
|
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"
|
|
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?(
|
|
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.
|
|
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
|
|
375
|
-
if str.
|
|
376
|
-
|
|
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
|
|
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
|