pg_reports 0.4.0 → 0.5.1
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 +130 -0
- data/README.md +170 -4
- data/app/controllers/pg_reports/dashboard_controller.rb +348 -47
- data/app/views/layouts/pg_reports/application.html.erb +370 -79
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +1 -1
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +254 -29
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +373 -0
- data/app/views/pg_reports/dashboard/index.html.erb +485 -5
- 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 +21 -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 +26 -3
- data/lib/pg_reports/query_monitor.rb +293 -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 +55 -2
|
@@ -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
|
|
@@ -25,15 +26,40 @@ module PgReports
|
|
|
25
26
|
|
|
26
27
|
def live_metrics
|
|
27
28
|
threshold = params[:long_query_threshold]&.to_i || 60
|
|
28
|
-
data = Modules::System.live_metrics(long_query_threshold: threshold)
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
# Check if we have access to required statistics
|
|
31
|
+
begin
|
|
32
|
+
data = Modules::System.live_metrics(long_query_threshold: threshold)
|
|
33
|
+
|
|
34
|
+
# Validate that we got actual data
|
|
35
|
+
if data[:connections][:total].nil? && data[:transactions][:total].nil?
|
|
36
|
+
render json: {
|
|
37
|
+
success: false,
|
|
38
|
+
error: "Unable to fetch database statistics. Check database permissions.",
|
|
39
|
+
available: false
|
|
40
|
+
}, status: :service_unavailable
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
render json: {
|
|
45
|
+
success: true,
|
|
46
|
+
metrics: data,
|
|
47
|
+
timestamp: Time.current.to_i,
|
|
48
|
+
available: true
|
|
49
|
+
}
|
|
50
|
+
rescue PG::InsufficientPrivilege => e
|
|
51
|
+
render json: {
|
|
52
|
+
success: false,
|
|
53
|
+
error: "Insufficient database permissions to access statistics views",
|
|
54
|
+
available: false
|
|
55
|
+
}, status: :forbidden
|
|
56
|
+
rescue => e
|
|
57
|
+
render json: {
|
|
58
|
+
success: false,
|
|
59
|
+
error: e.message,
|
|
60
|
+
available: false
|
|
61
|
+
}, status: :unprocessable_entity
|
|
62
|
+
end
|
|
37
63
|
end
|
|
38
64
|
|
|
39
65
|
def show
|
|
@@ -72,11 +98,24 @@ module PgReports
|
|
|
72
98
|
problem_fields = Dashboard::ReportsRegistry.problem_fields(report_key)
|
|
73
99
|
problem_explanations = load_problem_explanations(category, report_key)
|
|
74
100
|
|
|
101
|
+
# Add query hashes for security
|
|
102
|
+
data_with_hashes = report.data.first(100).map do |row|
|
|
103
|
+
row_hash = row.dup
|
|
104
|
+
|
|
105
|
+
# If this row contains a query column, store it with a hash
|
|
106
|
+
if row_hash.key?("query") && row_hash["query"].present?
|
|
107
|
+
query_hash = store_query_with_hash(row_hash["query"])
|
|
108
|
+
row_hash["query_hash"] = query_hash
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
row_hash
|
|
112
|
+
end
|
|
113
|
+
|
|
75
114
|
render json: {
|
|
76
115
|
success: true,
|
|
77
116
|
title: report.title,
|
|
78
117
|
columns: report.columns,
|
|
79
|
-
data:
|
|
118
|
+
data: data_with_hashes,
|
|
80
119
|
total: report.size,
|
|
81
120
|
generated_at: report.generated_at.strftime("%Y-%m-%d %H:%M:%S"),
|
|
82
121
|
thresholds: thresholds,
|
|
@@ -134,18 +173,42 @@ module PgReports
|
|
|
134
173
|
end
|
|
135
174
|
|
|
136
175
|
def explain_analyze
|
|
137
|
-
|
|
176
|
+
query_hash = params[:query_hash]
|
|
138
177
|
query_params = params[:params] || {}
|
|
139
178
|
|
|
140
|
-
if
|
|
141
|
-
render json: {success: false, error: "Query is required"}, status: :unprocessable_entity
|
|
179
|
+
if query_hash.blank?
|
|
180
|
+
render json: {success: false, error: "Query hash is required"}, status: :unprocessable_entity
|
|
142
181
|
return
|
|
143
182
|
end
|
|
144
183
|
|
|
145
|
-
# Security:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
# Security: Check if raw query execution is allowed
|
|
185
|
+
unless PgReports.config.allow_raw_query_execution
|
|
186
|
+
render json: {
|
|
187
|
+
success: false,
|
|
188
|
+
error: "Query execution from dashboard is disabled. Enable it in configuration with 'config.allow_raw_query_execution = true'"
|
|
189
|
+
}, status: :forbidden
|
|
190
|
+
return
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Security: Retrieve and validate query by hash
|
|
194
|
+
begin
|
|
195
|
+
query = retrieve_query_by_hash(query_hash)
|
|
196
|
+
|
|
197
|
+
if query.nil?
|
|
198
|
+
render json: {success: false, error: "Query not found or expired. Please refresh the page."}, status: :not_found
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
rescue SecurityError => e
|
|
202
|
+
render json: {success: false, error: "Security violation: #{e.message}"}, status: :forbidden
|
|
203
|
+
return
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Check for trigger variables (NEW, OLD) which are only available in trigger context
|
|
207
|
+
if query.match?(/\b(NEW|OLD)\./i)
|
|
208
|
+
render json: {
|
|
209
|
+
success: false,
|
|
210
|
+
error: "Cannot EXPLAIN ANALYZE queries with trigger variables (NEW, OLD). These are only available within trigger functions."
|
|
211
|
+
}, status: :unprocessable_entity
|
|
149
212
|
return
|
|
150
213
|
end
|
|
151
214
|
|
|
@@ -164,39 +227,50 @@ module PgReports
|
|
|
164
227
|
result = ActiveRecord::Base.connection.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{final_query}")
|
|
165
228
|
explain_output = result.map { |r| r["QUERY PLAN"] }.join("\n")
|
|
166
229
|
|
|
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
|
|
230
|
+
# Analyze the EXPLAIN output
|
|
231
|
+
analyzer = ExplainAnalyzer.new(explain_output)
|
|
232
|
+
analysis = analyzer.to_h
|
|
181
233
|
|
|
182
|
-
render json: {
|
|
234
|
+
render json: {
|
|
235
|
+
success: true,
|
|
236
|
+
explain: explain_output,
|
|
237
|
+
stats: analysis[:stats],
|
|
238
|
+
annotated_lines: analysis[:annotated_lines],
|
|
239
|
+
problems: analysis[:problems],
|
|
240
|
+
summary: analysis[:summary]
|
|
241
|
+
}
|
|
183
242
|
rescue => e
|
|
184
243
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
185
244
|
end
|
|
186
245
|
|
|
187
246
|
def execute_query
|
|
188
|
-
|
|
247
|
+
query_hash = params[:query_hash]
|
|
189
248
|
query_params = params[:params] || {}
|
|
190
249
|
|
|
191
|
-
if
|
|
192
|
-
render json: {success: false, error: "Query is required"}, status: :unprocessable_entity
|
|
250
|
+
if query_hash.blank?
|
|
251
|
+
render json: {success: false, error: "Query hash is required"}, status: :unprocessable_entity
|
|
193
252
|
return
|
|
194
253
|
end
|
|
195
254
|
|
|
196
|
-
# Security:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
255
|
+
# Security: Check if raw query execution is allowed
|
|
256
|
+
unless PgReports.config.allow_raw_query_execution
|
|
257
|
+
render json: {
|
|
258
|
+
success: false,
|
|
259
|
+
error: "Query execution from dashboard is disabled. Enable it in configuration with 'config.allow_raw_query_execution = true'"
|
|
260
|
+
}, status: :forbidden
|
|
261
|
+
return
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Security: Retrieve and validate query by hash
|
|
265
|
+
begin
|
|
266
|
+
query = retrieve_query_by_hash(query_hash)
|
|
267
|
+
|
|
268
|
+
if query.nil?
|
|
269
|
+
render json: {success: false, error: "Query not found or expired. Please refresh the page."}, status: :not_found
|
|
270
|
+
return
|
|
271
|
+
end
|
|
272
|
+
rescue SecurityError => e
|
|
273
|
+
render json: {success: false, error: "Security violation: #{e.message}"}, status: :forbidden
|
|
200
274
|
return
|
|
201
275
|
end
|
|
202
276
|
|
|
@@ -287,6 +361,123 @@ module PgReports
|
|
|
287
361
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
288
362
|
end
|
|
289
363
|
|
|
364
|
+
def start_query_monitoring
|
|
365
|
+
monitor = PgReports::QueryMonitor.instance
|
|
366
|
+
|
|
367
|
+
result = monitor.start
|
|
368
|
+
|
|
369
|
+
if result[:success]
|
|
370
|
+
render json: result
|
|
371
|
+
else
|
|
372
|
+
render json: result, status: :unprocessable_entity
|
|
373
|
+
end
|
|
374
|
+
rescue => e
|
|
375
|
+
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def stop_query_monitoring
|
|
379
|
+
monitor = PgReports::QueryMonitor.instance
|
|
380
|
+
|
|
381
|
+
result = monitor.stop
|
|
382
|
+
|
|
383
|
+
if result[:success]
|
|
384
|
+
render json: result
|
|
385
|
+
else
|
|
386
|
+
render json: result, status: :unprocessable_entity
|
|
387
|
+
end
|
|
388
|
+
rescue => e
|
|
389
|
+
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def query_monitor_status
|
|
393
|
+
monitor = PgReports::QueryMonitor.instance
|
|
394
|
+
status = monitor.status
|
|
395
|
+
|
|
396
|
+
render json: {
|
|
397
|
+
success: true,
|
|
398
|
+
enabled: status[:enabled],
|
|
399
|
+
session_id: status[:session_id],
|
|
400
|
+
query_count: status[:query_count]
|
|
401
|
+
}
|
|
402
|
+
rescue => e
|
|
403
|
+
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def query_monitor_feed
|
|
407
|
+
monitor = PgReports::QueryMonitor.instance
|
|
408
|
+
|
|
409
|
+
unless monitor.enabled
|
|
410
|
+
render json: {success: false, message: "Monitoring not active"}
|
|
411
|
+
return
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
limit = params[:limit]&.to_i || 50
|
|
415
|
+
session_id = params[:session_id]
|
|
416
|
+
|
|
417
|
+
queries = monitor.queries(limit: limit, session_id: session_id)
|
|
418
|
+
|
|
419
|
+
render json: {
|
|
420
|
+
success: true,
|
|
421
|
+
queries: queries,
|
|
422
|
+
timestamp: Time.current.to_i
|
|
423
|
+
}
|
|
424
|
+
rescue => e
|
|
425
|
+
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def load_query_history
|
|
429
|
+
monitor = PgReports::QueryMonitor.instance
|
|
430
|
+
|
|
431
|
+
limit = params[:limit]&.to_i || 50
|
|
432
|
+
session_id = params[:session_id]
|
|
433
|
+
|
|
434
|
+
queries = monitor.load_from_log(limit: limit, session_id: session_id)
|
|
435
|
+
|
|
436
|
+
render json: {
|
|
437
|
+
success: true,
|
|
438
|
+
queries: queries,
|
|
439
|
+
timestamp: Time.current.to_i
|
|
440
|
+
}
|
|
441
|
+
rescue => e
|
|
442
|
+
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def download_query_monitor
|
|
446
|
+
monitor = PgReports::QueryMonitor.instance
|
|
447
|
+
|
|
448
|
+
# Allow download even when monitoring is stopped, as long as there are queries
|
|
449
|
+
queries = monitor.queries
|
|
450
|
+
if queries.empty?
|
|
451
|
+
render json: {success: false, error: "No queries to download"}, status: :unprocessable_entity
|
|
452
|
+
return
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
format_type = params[:format] || "txt"
|
|
456
|
+
filename = "query-monitor-#{Time.current.strftime("%Y%m%d-%H%M%S")}"
|
|
457
|
+
|
|
458
|
+
case format_type
|
|
459
|
+
when "csv"
|
|
460
|
+
csv_data = generate_query_monitor_csv(queries)
|
|
461
|
+
send_data csv_data,
|
|
462
|
+
filename: "#{filename}.csv",
|
|
463
|
+
type: "text/csv; charset=utf-8",
|
|
464
|
+
disposition: "attachment"
|
|
465
|
+
when "json"
|
|
466
|
+
send_data queries.to_json,
|
|
467
|
+
filename: "#{filename}.json",
|
|
468
|
+
type: "application/json; charset=utf-8",
|
|
469
|
+
disposition: "attachment"
|
|
470
|
+
else
|
|
471
|
+
text_data = generate_query_monitor_text(queries)
|
|
472
|
+
send_data text_data,
|
|
473
|
+
filename: "#{filename}.txt",
|
|
474
|
+
type: "text/plain; charset=utf-8",
|
|
475
|
+
disposition: "attachment"
|
|
476
|
+
end
|
|
477
|
+
rescue => e
|
|
478
|
+
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
479
|
+
end
|
|
480
|
+
|
|
290
481
|
private
|
|
291
482
|
|
|
292
483
|
def authenticate_dashboard!
|
|
@@ -326,7 +517,7 @@ module PgReports
|
|
|
326
517
|
|
|
327
518
|
# Also allow threshold overrides (calls_threshold, etc.)
|
|
328
519
|
params.each do |key, value|
|
|
329
|
-
if key.to_s.end_with?(
|
|
520
|
+
if key.to_s.end_with?("_threshold") && value.present?
|
|
330
521
|
result[key.to_sym] = value.to_i
|
|
331
522
|
end
|
|
332
523
|
end
|
|
@@ -356,7 +547,7 @@ module PgReports
|
|
|
356
547
|
result = query.dup
|
|
357
548
|
|
|
358
549
|
# Sort by param number descending to replace $10 before $1
|
|
359
|
-
params_hash.keys.map(&:to_i).sort.
|
|
550
|
+
params_hash.keys.map(&:to_i).sort.reverse_each do |num|
|
|
360
551
|
value = params_hash[num.to_s] || params_hash[num]
|
|
361
552
|
next if value.nil? || value.to_s.empty?
|
|
362
553
|
|
|
@@ -371,17 +562,15 @@ module PgReports
|
|
|
371
562
|
def quote_param_value(value)
|
|
372
563
|
str = value.to_s
|
|
373
564
|
|
|
374
|
-
# Check if it looks like
|
|
375
|
-
if str.
|
|
376
|
-
|
|
565
|
+
# Check if it looks like NULL
|
|
566
|
+
if str.downcase == "null"
|
|
567
|
+
"NULL"
|
|
377
568
|
# Check if it looks like a boolean
|
|
378
569
|
elsif str.downcase.in?(["true", "false"])
|
|
379
570
|
str.downcase
|
|
380
|
-
# Check if it looks like NULL
|
|
381
|
-
elsif str.downcase == "null"
|
|
382
|
-
"NULL"
|
|
383
571
|
else
|
|
384
|
-
# Quote as string
|
|
572
|
+
# Quote as string by default - PostgreSQL will handle type casting
|
|
573
|
+
# This ensures compatibility with both text and numeric columns
|
|
385
574
|
"'#{str.gsub("'", "''")}'"
|
|
386
575
|
end
|
|
387
576
|
end
|
|
@@ -397,5 +586,117 @@ module PgReports
|
|
|
397
586
|
"#{query} LIMIT #{limit}"
|
|
398
587
|
end
|
|
399
588
|
end
|
|
589
|
+
|
|
590
|
+
# Generate a secure hash for a query and store it in cache
|
|
591
|
+
def store_query_with_hash(query)
|
|
592
|
+
require "digest"
|
|
593
|
+
|
|
594
|
+
# Generate SHA256 hash of the query
|
|
595
|
+
query_hash = Digest::SHA256.hexdigest(query)
|
|
596
|
+
|
|
597
|
+
# Store in Rails cache with 1 hour expiration
|
|
598
|
+
begin
|
|
599
|
+
Rails.cache.write("pg_reports:query:#{query_hash}", query, expires_in: 1.hour)
|
|
600
|
+
rescue => e
|
|
601
|
+
# Log cache error but don't fail - we'll catch it on retrieval
|
|
602
|
+
Rails.logger.warn("PgReports: Failed to store query hash in cache: #{e.message}") if defined?(Rails.logger)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
query_hash
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# Retrieve and validate a query by its hash
|
|
609
|
+
def retrieve_query_by_hash(query_hash)
|
|
610
|
+
return nil if query_hash.blank?
|
|
611
|
+
|
|
612
|
+
# Retrieve from cache with error handling
|
|
613
|
+
query = nil
|
|
614
|
+
begin
|
|
615
|
+
query = Rails.cache.read("pg_reports:query:#{query_hash}")
|
|
616
|
+
rescue => e
|
|
617
|
+
# Cache backend is unavailable
|
|
618
|
+
Rails.logger.error("PgReports: Failed to read from cache: #{e.message}") if defined?(Rails.logger)
|
|
619
|
+
raise SecurityError, "Cache system unavailable. Query execution temporarily disabled for security. Please ensure Redis/cache backend is running."
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
if query.blank?
|
|
623
|
+
# Query not found - either expired or was never stored
|
|
624
|
+
return nil
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Strict validation: must be a SELECT query only
|
|
628
|
+
normalized = query.strip.gsub(/\s+/, " ").downcase
|
|
629
|
+
|
|
630
|
+
# Check for semicolons (prevents multiple statements)
|
|
631
|
+
if query.include?(";")
|
|
632
|
+
raise SecurityError, "Multiple statements are not allowed (semicolons detected)"
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Must start with SELECT (case insensitive)
|
|
636
|
+
unless normalized.start_with?("select")
|
|
637
|
+
raise SecurityError, "Only SELECT queries are allowed. Found: #{normalized.split.first&.upcase || 'unknown'}"
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# Check for dangerous keywords that might be in subqueries or CTEs
|
|
641
|
+
dangerous_keywords = %w[insert update delete drop alter create truncate grant revoke]
|
|
642
|
+
dangerous_keywords.each do |keyword|
|
|
643
|
+
if normalized.match?(/\b#{keyword}\b/)
|
|
644
|
+
raise SecurityError, "Dangerous keyword detected: #{keyword.upcase}"
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
query
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def generate_query_monitor_csv(queries)
|
|
652
|
+
require "csv"
|
|
653
|
+
|
|
654
|
+
CSV.generate do |csv|
|
|
655
|
+
# Header
|
|
656
|
+
csv << ["Timestamp", "Duration (ms)", "Query Name", "SQL", "Source File", "Source Line"]
|
|
657
|
+
|
|
658
|
+
# Data rows
|
|
659
|
+
queries.each do |query|
|
|
660
|
+
csv << [
|
|
661
|
+
query[:timestamp],
|
|
662
|
+
query[:duration_ms],
|
|
663
|
+
query[:name],
|
|
664
|
+
query[:sql],
|
|
665
|
+
query.dig(:source_location, :file),
|
|
666
|
+
query.dig(:source_location, :line)
|
|
667
|
+
]
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def generate_query_monitor_text(queries)
|
|
673
|
+
output = []
|
|
674
|
+
output << "=" * 80
|
|
675
|
+
output << "Query Monitor Export"
|
|
676
|
+
output << "Generated: #{Time.current.strftime("%Y-%m-%d %H:%M:%S")}"
|
|
677
|
+
output << "Total Queries: #{queries.size}"
|
|
678
|
+
output << "=" * 80
|
|
679
|
+
output << ""
|
|
680
|
+
|
|
681
|
+
queries.each_with_index do |query, index|
|
|
682
|
+
output << "Query ##{index + 1}"
|
|
683
|
+
output << "-" * 80
|
|
684
|
+
output << "Timestamp: #{query[:timestamp]}"
|
|
685
|
+
output << "Duration: #{query[:duration_ms]}ms"
|
|
686
|
+
output << "Name: #{query[:name]}"
|
|
687
|
+
|
|
688
|
+
if query[:source_location]
|
|
689
|
+
output << "Source: #{query[:source_location][:file]}:#{query[:source_location][:line]}"
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
output << ""
|
|
693
|
+
output << "SQL:"
|
|
694
|
+
output << query[:sql]
|
|
695
|
+
output << ""
|
|
696
|
+
output << ""
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
output.join("\n")
|
|
700
|
+
end
|
|
400
701
|
end
|
|
401
702
|
end
|