pg_reports 0.8.2 → 0.9.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 +38 -0
- data/app/controllers/pg_reports/dashboard_controller.rb +57 -6
- data/app/views/layouts/pg_reports/application.html.erb +616 -201
- data/app/views/pg_reports/dashboard/_database_selector.html.erb +2 -1
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +10 -7
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +100 -51
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +392 -476
- data/app/views/pg_reports/dashboard/index.html.erb +190 -388
- data/app/views/pg_reports/dashboard/show.html.erb +13 -11
- data/config/brakeman.ignore +24 -1
- data/config/locales/en.yml +53 -48
- data/config/locales/ru.yml +53 -48
- data/config/locales/uk.yml +53 -48
- data/lib/pg_reports/connection/error_translator.rb +43 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +10 -0
- data/lib/pg_reports/explain_analyzer.rb +0 -3
- data/lib/pg_reports/query_monitor.rb +15 -3
- data/lib/pg_reports/version.rb +1 -1
- metadata +1 -1
data/config/locales/uk.yml
CHANGED
|
@@ -556,7 +556,7 @@ uk:
|
|
|
556
556
|
page_title: "PgReports — Дашборд"
|
|
557
557
|
navigation:
|
|
558
558
|
dashboard: "Дашборд"
|
|
559
|
-
back: "
|
|
559
|
+
back: "Назад"
|
|
560
560
|
database_selector:
|
|
561
561
|
label: "База даних"
|
|
562
562
|
target_selector:
|
|
@@ -564,40 +564,41 @@ uk:
|
|
|
564
564
|
actions:
|
|
565
565
|
cancel: "Скасувати"
|
|
566
566
|
retry: "Повторити"
|
|
567
|
-
copy: "
|
|
568
|
-
copy_query: "
|
|
569
|
-
copy_code: "
|
|
567
|
+
copy: "Копіювати"
|
|
568
|
+
copy_query: "Копіювати запит"
|
|
569
|
+
copy_code: "Копіювати код"
|
|
570
570
|
copy_to_clipboard_title: "Скопіювати в буфер обміну"
|
|
571
|
-
copied_feedback: "
|
|
571
|
+
copied_feedback: "Скопійовано!"
|
|
572
572
|
clear_all: "Очистити все"
|
|
573
|
-
run_report: "
|
|
574
|
-
export: "
|
|
575
|
-
download_text: "
|
|
576
|
-
download_csv: "
|
|
577
|
-
download_json: "
|
|
578
|
-
download: "
|
|
573
|
+
run_report: "Запустити звіт"
|
|
574
|
+
export: "Експорт"
|
|
575
|
+
download_text: "Текст (.txt)"
|
|
576
|
+
download_csv: "CSV (.csv)"
|
|
577
|
+
download_json: "JSON (.json)"
|
|
578
|
+
download: "Завантажити"
|
|
579
579
|
copy_ai_prompt: "Копіювати промпт"
|
|
580
|
-
send_telegram: "
|
|
580
|
+
send_telegram: "Telegram"
|
|
581
581
|
sending: "Надсилання..."
|
|
582
|
-
reset_statistics: "
|
|
582
|
+
reset_statistics: "Скинути статистику"
|
|
583
583
|
resetting: "Скидання..."
|
|
584
584
|
confirm_reset: "Так, скинути"
|
|
585
|
-
create_extension: "
|
|
585
|
+
create_extension: "Створити розширення"
|
|
586
586
|
creating: "Створення..."
|
|
587
587
|
ide_settings_button_title: "Налаштування IDE"
|
|
588
|
-
explain_analyze: "
|
|
589
|
-
execute_query: "
|
|
588
|
+
explain_analyze: "EXPLAIN ANALYZE"
|
|
589
|
+
execute_query: "Виконати запит"
|
|
590
590
|
run_query: "SQL-консоль"
|
|
591
|
-
create_migration_file: "
|
|
592
|
-
start_monitoring: "
|
|
593
|
-
stop_monitoring: "
|
|
591
|
+
create_migration_file: "Створити файл і відкрити в IDE"
|
|
592
|
+
start_monitoring: "Запустити моніторинг"
|
|
593
|
+
stop_monitoring: "Зупинити моніторинг"
|
|
594
594
|
starting: "Запуск..."
|
|
595
595
|
stopping: "Зупинка..."
|
|
596
|
-
load_history: "
|
|
596
|
+
load_history: "Завантажити історію"
|
|
597
|
+
load_history_unavailable_title: "Збереженої історії немає — задайте config.query_monitor_log_file, щоб запити зберігалися між перезапусками"
|
|
597
598
|
loading: "Завантаження..."
|
|
598
599
|
running: "Виконання..."
|
|
599
|
-
save_for_comparison: "
|
|
600
|
-
saved_marker: "
|
|
600
|
+
save_for_comparison: "Зберегти для порівняння"
|
|
601
|
+
saved_marker: "Збережено"
|
|
601
602
|
status:
|
|
602
603
|
pg_stat_ready: "Активно"
|
|
603
604
|
not_preloaded: "Потрібне передзавантаження"
|
|
@@ -615,19 +616,20 @@ uk:
|
|
|
615
616
|
create_extension_title: "Створення розширення pg_stat_statements"
|
|
616
617
|
create_extension_intro: "Розширення pg_stat_statements ще не створено в цій базі даних. Натисніть, щоб створити його:"
|
|
617
618
|
create_extension_note: "Якщо бібліотеку не передзавантажено, її також потрібно додати до shared_preload_libraries та перезапустити PostgreSQL."
|
|
618
|
-
reset_stats_title: "
|
|
619
|
+
reset_stats_title: "Скидання статистики"
|
|
619
620
|
reset_stats_confirm: "Ви впевнені, що хочете скинути статистику pg_stat_statements?"
|
|
620
621
|
reset_stats_warning: "Ця дія очистить усю зібрану статистику запитів і не може бути скасована."
|
|
621
|
-
ide_settings_title: "
|
|
622
|
-
problem_detected_title: "
|
|
623
|
-
query_analyzer_title: "
|
|
622
|
+
ide_settings_title: "Налаштування IDE"
|
|
623
|
+
problem_detected_title: "Виявлено проблему"
|
|
624
|
+
query_analyzer_title: "Аналізатор запиту"
|
|
624
625
|
query_label: "Запит:"
|
|
625
626
|
parameters_label: "Параметри:"
|
|
626
|
-
migration_title: "
|
|
627
|
+
migration_title: "Міграція видалення індексу"
|
|
627
628
|
migration_subtitle: "Згенерована міграція для видалення індексу:"
|
|
628
629
|
migration_warning: "Створення міграції згенерує файл міграції у вашому проєкті. Запуск цієї міграції видалить індекс із БД, що може значно вплинути на продуктивність застосунку."
|
|
629
630
|
migration_warning_dev_only: "Цю операцію слід виконувати лише в локальному dev-середовищі."
|
|
630
|
-
query_execution_disabled_title: "
|
|
631
|
+
query_execution_disabled_title: "Виконання запитів вимкнено"
|
|
632
|
+
migration_disabled_title: "Створення міграцій вимкнено"
|
|
631
633
|
query_execution_disabled_intro: "Щоб увімкнути цю функцію, додайте до конфігурації:"
|
|
632
634
|
query_execution_disabled_env_note: "...або замість цього встановіть змінну середовища:"
|
|
633
635
|
run_query_title: "SQL-консоль"
|
|
@@ -670,25 +672,27 @@ uk:
|
|
|
670
672
|
waiting_for_locks: "чекають блокувань"
|
|
671
673
|
percent_used_suffix: "% використано"
|
|
672
674
|
categories:
|
|
673
|
-
requires_pg_stat: "
|
|
675
|
+
requires_pg_stat: "Потрібен pg_stat_statements"
|
|
676
|
+
pg_stat_missing_reason: "Потрібен pg_stat_statements, але розширення не встановлено в базі «%{database}». Створіть його там або перемкніться на базу, де воно є."
|
|
677
|
+
pg_stat_not_preloaded_reason: "Потрібен pg_stat_statements. Розширення є в базі «%{database}», але відсутнє в shared_preload_libraries — даних не буде, доки PostgreSQL не перезапустять із ним."
|
|
674
678
|
reports_count_suffix: "звітів"
|
|
675
|
-
primary_only_reason: "
|
|
676
|
-
standalone_no_host_app_reason: "
|
|
679
|
+
primary_only_reason: "Ця категорія працює лише на первинній базі — вона перевіряє моделі хост-застосунку. Щоб користуватися, перемкніться на базу за замовчуванням."
|
|
680
|
+
standalone_no_host_app_reason: "Недоступно в автономному режимі — ця категорія перевіряє моделі хост-застосунку, а при самостійному запуску дашборда хост-застосунку немає."
|
|
677
681
|
documentation:
|
|
678
|
-
toggle_title: "
|
|
679
|
-
what_section: "
|
|
680
|
-
why_section: "
|
|
681
|
-
nuances_section: "
|
|
682
|
-
thresholds_section: "
|
|
683
|
-
threshold_warning_label: "
|
|
684
|
-
threshold_critical_label: "
|
|
682
|
+
toggle_title: "Що показує цей звіт?"
|
|
683
|
+
what_section: "Що"
|
|
684
|
+
why_section: "Чому це важливо"
|
|
685
|
+
nuances_section: "Нюанси"
|
|
686
|
+
thresholds_section: "Пороги"
|
|
687
|
+
threshold_warning_label: "Warning:"
|
|
688
|
+
threshold_critical_label: "Critical:"
|
|
685
689
|
threshold_inverted_note: "(менше — гірше)"
|
|
686
690
|
filters:
|
|
687
|
-
title: "
|
|
691
|
+
title: "Параметри фільтрації"
|
|
688
692
|
current_value: "зараз"
|
|
689
693
|
saved:
|
|
690
|
-
title: "
|
|
691
|
-
saved_at_prefix: "
|
|
694
|
+
title: "Збережено для порівняння"
|
|
695
|
+
saved_at_prefix: "Збережено:"
|
|
692
696
|
click_to_expand: "Натисніть, щоб розгорнути"
|
|
693
697
|
confirm_clear_all: "Видалити всі збережені записи для цього звіту?"
|
|
694
698
|
remove_title: "Видалити"
|
|
@@ -702,9 +706,9 @@ uk:
|
|
|
702
706
|
execution_time_label: "Час виконання:"
|
|
703
707
|
null_placeholder: "<null>"
|
|
704
708
|
sections:
|
|
705
|
-
recommendation: "
|
|
706
|
-
detected_issues: "
|
|
707
|
-
execution_plan: "
|
|
709
|
+
recommendation: "Рекомендація"
|
|
710
|
+
detected_issues: "Виявлені проблеми"
|
|
711
|
+
execution_plan: "План виконання"
|
|
708
712
|
line_label: "Рядок"
|
|
709
713
|
current_label: "Поточне:"
|
|
710
714
|
threshold_label: "Поріг:"
|
|
@@ -712,8 +716,8 @@ uk:
|
|
|
712
716
|
warning_eq: "warning"
|
|
713
717
|
critical_eq: "critical"
|
|
714
718
|
levels:
|
|
715
|
-
critical: "
|
|
716
|
-
warning: "
|
|
719
|
+
critical: "Критично"
|
|
720
|
+
warning: "Увага"
|
|
717
721
|
errors:
|
|
718
722
|
error_prefix: "Помилка:"
|
|
719
723
|
unable_fetch_metrics: "Не вдалося отримати статистику БД."
|
|
@@ -741,8 +745,9 @@ uk:
|
|
|
741
745
|
run_query_failed: "Не вдалося виконати запит"
|
|
742
746
|
query_required: "Введіть запит"
|
|
743
747
|
create_migration_failed: "Не вдалося створити міграцію"
|
|
744
|
-
explain_disabled_toast: "
|
|
745
|
-
execute_disabled_toast: "
|
|
748
|
+
explain_disabled_toast: "EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
|
|
749
|
+
execute_disabled_toast: "Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
|
|
750
|
+
migration_disabled_toast: "Створення міграцій вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
|
|
746
751
|
query_monitoring_error: "Помилка моніторингу запитів"
|
|
747
752
|
query_hash_required: "Потрібен хеш запиту"
|
|
748
753
|
query_execution_disabled: "Виконання запитів із дашборду вимкнено. Увімкніть у конфігурації: 'config.allow_raw_query_execution = true'"
|
|
@@ -9,6 +9,16 @@ module PgReports
|
|
|
9
9
|
# PgReports::Connection::ErrorTranslator.translate(error)
|
|
10
10
|
# # => { title: "...", detail: "...", hint: "GRANT ...", code: "42501" }
|
|
11
11
|
module ErrorTranslator
|
|
12
|
+
# Relations the reports read that are supplied by an extension rather than
|
|
13
|
+
# by core PostgreSQL. Used to turn "relation X does not exist" into the
|
|
14
|
+
# actionable "extension Y isn't installed here".
|
|
15
|
+
EXTENSION_RELATIONS = {
|
|
16
|
+
"pg_stat_statements" => "pg_stat_statements",
|
|
17
|
+
"pg_stat_statements_info" => "pg_stat_statements",
|
|
18
|
+
"pgstattuple" => "pgstattuple",
|
|
19
|
+
"pg_buffercache" => "pg_buffercache"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
12
22
|
module_function
|
|
13
23
|
|
|
14
24
|
# Returns a Hash with :title, :detail, :hint, :code, :raw_message.
|
|
@@ -23,6 +33,8 @@ module PgReports
|
|
|
23
33
|
when "28000", "28P01" then auth_failed(message)
|
|
24
34
|
when "08001", "08006", "08000", "08003", "08004" then connection_refused(message)
|
|
25
35
|
when "53300" then too_many_connections(message)
|
|
36
|
+
when "42P01" then undefined_table(message)
|
|
37
|
+
when "42883" then undefined_function(message)
|
|
26
38
|
else generic(error)
|
|
27
39
|
end
|
|
28
40
|
|
|
@@ -92,6 +104,37 @@ module PgReports
|
|
|
92
104
|
}
|
|
93
105
|
end
|
|
94
106
|
|
|
107
|
+
# A report asked for a relation this database doesn't have. Almost always an
|
|
108
|
+
# extension that exists on one database in the cluster but not the one
|
|
109
|
+
# currently selected — extensions are per-database, the dashboard is not.
|
|
110
|
+
def undefined_table(message)
|
|
111
|
+
target = extract_object(message, /relation "([^"]+)" does not exist/)
|
|
112
|
+
|
|
113
|
+
if target && EXTENSION_RELATIONS.key?(target)
|
|
114
|
+
extension = EXTENSION_RELATIONS.fetch(target)
|
|
115
|
+
return {
|
|
116
|
+
title: "#{extension} not available",
|
|
117
|
+
detail: "This report reads \"#{target}\", which does not exist on the selected database.",
|
|
118
|
+
hint: "CREATE EXTENSION IF NOT EXISTS #{extension};"
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
title: "Relation not found",
|
|
124
|
+
detail: target ? "The selected database has no relation named \"#{target}\"." : "The report referenced a relation that does not exist on the selected database.",
|
|
125
|
+
hint: nil
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def undefined_function(message)
|
|
130
|
+
target = extract_object(message, /function ([\w.]+)\(/)
|
|
131
|
+
{
|
|
132
|
+
title: "Function not found",
|
|
133
|
+
detail: target ? "The selected database has no function named \"#{target}\"." : "The report called a function that does not exist on the selected database.",
|
|
134
|
+
hint: "It is usually provided by an extension that is not installed on this database."
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
95
138
|
def generic(error)
|
|
96
139
|
{
|
|
97
140
|
title: error.class.name.split("::").last,
|
|
@@ -237,6 +237,10 @@ module PgReports
|
|
|
237
237
|
name: "Queries",
|
|
238
238
|
icon: "⚡",
|
|
239
239
|
color: "#6366f1",
|
|
240
|
+
# Every report here reads the pg_stat_statements view. The extension is
|
|
241
|
+
# created per-database, so availability has to be re-checked against
|
|
242
|
+
# whichever database the dashboard is pointed at — not once at boot.
|
|
243
|
+
requires: :pg_stat_statements,
|
|
240
244
|
reports: {
|
|
241
245
|
slow_queries: {name: "Slow Queries", description: "Queries with high mean execution time"},
|
|
242
246
|
heavy_queries: {name: "Heavy Queries", description: "Most frequently called queries"},
|
|
@@ -412,6 +416,12 @@ module PgReports
|
|
|
412
416
|
def self.target_constraint(category)
|
|
413
417
|
REPORTS.dig(category.to_sym, :target_constraint)
|
|
414
418
|
end
|
|
419
|
+
|
|
420
|
+
# A database-level dependency the whole category needs, or nil. Currently
|
|
421
|
+
# only :pg_stat_statements — the caller resolves what "available" means.
|
|
422
|
+
def self.requires(category)
|
|
423
|
+
REPORTS.dig(category.to_sym, :requires)
|
|
424
|
+
end
|
|
415
425
|
end
|
|
416
426
|
end
|
|
417
427
|
end
|
|
@@ -313,15 +313,12 @@ module PgReports
|
|
|
313
313
|
if @summary[:critical_problems] > 0
|
|
314
314
|
@summary[:status] = "critical"
|
|
315
315
|
@summary[:status_text] = "Critical issues detected"
|
|
316
|
-
@summary[:status_icon] = "🔴"
|
|
317
316
|
elsif @summary[:warnings] > 0
|
|
318
317
|
@summary[:status] = "warning"
|
|
319
318
|
@summary[:status_text] = "Potential issues detected"
|
|
320
|
-
@summary[:status_icon] = "🟡"
|
|
321
319
|
else
|
|
322
320
|
@summary[:status] = "good"
|
|
323
321
|
@summary[:status_text] = "No issues detected"
|
|
324
|
-
@summary[:status_icon] = "🟢"
|
|
325
322
|
end
|
|
326
323
|
|
|
327
324
|
# Group problems by type for summary
|
|
@@ -106,10 +106,18 @@ module PgReports
|
|
|
106
106
|
{
|
|
107
107
|
enabled: enabled,
|
|
108
108
|
session_id: session_id,
|
|
109
|
-
query_count: @queries.size
|
|
109
|
+
query_count: @queries.size,
|
|
110
|
+
history_available: history_available?
|
|
110
111
|
}
|
|
111
112
|
end
|
|
112
113
|
|
|
114
|
+
# Whether #load_from_log has anything to return. History is persisted only
|
|
115
|
+
# when a log file is configured, and the file exists only once something has
|
|
116
|
+
# been written to it — without both, "Load history" can only ever no-op.
|
|
117
|
+
def history_available?
|
|
118
|
+
log_file_enabled? && File.exist?(log_file_path)
|
|
119
|
+
end
|
|
120
|
+
|
|
113
121
|
def queries(limit: nil, session_id: nil)
|
|
114
122
|
result = @queries.dup
|
|
115
123
|
|
|
@@ -348,6 +356,10 @@ module PgReports
|
|
|
348
356
|
end
|
|
349
357
|
end
|
|
350
358
|
|
|
359
|
+
# JSON.generate, not #to_json: ActiveSupport overrides #to_json and (up to
|
|
360
|
+
# Rails 7.0) passes json's removed `quirks_mode` option, so on json >= 3 it
|
|
361
|
+
# raises ArgumentError. The rescue below would swallow that and leave the
|
|
362
|
+
# log silently empty. json's own generator takes no such option.
|
|
351
363
|
def write_session_marker(marker_type, sid = @session_id)
|
|
352
364
|
return unless log_file_enabled?
|
|
353
365
|
|
|
@@ -358,7 +370,7 @@ module PgReports
|
|
|
358
370
|
}
|
|
359
371
|
|
|
360
372
|
File.open(log_file_path, "a") do |f|
|
|
361
|
-
f.puts marker
|
|
373
|
+
f.puts JSON.generate(marker)
|
|
362
374
|
end
|
|
363
375
|
rescue => e
|
|
364
376
|
# Silently fail - don't break monitoring if file write fails
|
|
@@ -371,7 +383,7 @@ module PgReports
|
|
|
371
383
|
|
|
372
384
|
File.open(log_file_path, "a") do |f|
|
|
373
385
|
@queries.each do |query|
|
|
374
|
-
f.puts query
|
|
386
|
+
f.puts JSON.generate(query)
|
|
375
387
|
end
|
|
376
388
|
end
|
|
377
389
|
rescue => e
|
data/lib/pg_reports/version.rb
CHANGED