pg_reports 0.8.2 → 0.9.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 +48 -0
- data/app/controllers/pg_reports/dashboard_controller.rb +104 -6
- data/app/views/layouts/pg_reports/application.html.erb +712 -214
- data/app/views/pg_reports/dashboard/_database_selector.html.erb +2 -1
- data/app/views/pg_reports/dashboard/_settings_modal.html.erb +55 -0
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +11 -49
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +101 -90
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +392 -476
- data/app/views/pg_reports/dashboard/index.html.erb +217 -493
- data/app/views/pg_reports/dashboard/show.html.erb +13 -11
- data/config/brakeman.ignore +24 -1
- data/config/locales/en.yml +58 -50
- data/config/locales/ru.yml +58 -50
- data/config/locales/uk.yml +58 -50
- data/config/routes.rb +1 -0
- 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 +2 -1
data/config/locales/uk.yml
CHANGED
|
@@ -554,9 +554,12 @@ uk:
|
|
|
554
554
|
title: "PgReports"
|
|
555
555
|
subtitle: "Дашборд аналізу PostgreSQL"
|
|
556
556
|
page_title: "PgReports — Дашборд"
|
|
557
|
+
language:
|
|
558
|
+
label: "Мова"
|
|
559
|
+
name: "Українська"
|
|
557
560
|
navigation:
|
|
558
561
|
dashboard: "Дашборд"
|
|
559
|
-
back: "
|
|
562
|
+
back: "Назад"
|
|
560
563
|
database_selector:
|
|
561
564
|
label: "База даних"
|
|
562
565
|
target_selector:
|
|
@@ -564,40 +567,41 @@ uk:
|
|
|
564
567
|
actions:
|
|
565
568
|
cancel: "Скасувати"
|
|
566
569
|
retry: "Повторити"
|
|
567
|
-
copy: "
|
|
568
|
-
copy_query: "
|
|
569
|
-
copy_code: "
|
|
570
|
+
copy: "Копіювати"
|
|
571
|
+
copy_query: "Копіювати запит"
|
|
572
|
+
copy_code: "Копіювати код"
|
|
570
573
|
copy_to_clipboard_title: "Скопіювати в буфер обміну"
|
|
571
|
-
copied_feedback: "
|
|
574
|
+
copied_feedback: "Скопійовано!"
|
|
572
575
|
clear_all: "Очистити все"
|
|
573
|
-
run_report: "
|
|
574
|
-
export: "
|
|
575
|
-
download_text: "
|
|
576
|
-
download_csv: "
|
|
577
|
-
download_json: "
|
|
578
|
-
download: "
|
|
576
|
+
run_report: "Запустити звіт"
|
|
577
|
+
export: "Експорт"
|
|
578
|
+
download_text: "Текст (.txt)"
|
|
579
|
+
download_csv: "CSV (.csv)"
|
|
580
|
+
download_json: "JSON (.json)"
|
|
581
|
+
download: "Завантажити"
|
|
579
582
|
copy_ai_prompt: "Копіювати промпт"
|
|
580
|
-
send_telegram: "
|
|
583
|
+
send_telegram: "Telegram"
|
|
581
584
|
sending: "Надсилання..."
|
|
582
|
-
reset_statistics: "
|
|
585
|
+
reset_statistics: "Скинути статистику"
|
|
583
586
|
resetting: "Скидання..."
|
|
584
587
|
confirm_reset: "Так, скинути"
|
|
585
|
-
create_extension: "
|
|
588
|
+
create_extension: "Створити розширення"
|
|
586
589
|
creating: "Створення..."
|
|
587
|
-
|
|
588
|
-
explain_analyze: "
|
|
589
|
-
execute_query: "
|
|
590
|
+
settings_button_title: "Налаштування"
|
|
591
|
+
explain_analyze: "EXPLAIN ANALYZE"
|
|
592
|
+
execute_query: "Виконати запит"
|
|
590
593
|
run_query: "SQL-консоль"
|
|
591
|
-
create_migration_file: "
|
|
592
|
-
start_monitoring: "
|
|
593
|
-
stop_monitoring: "
|
|
594
|
+
create_migration_file: "Створити файл і відкрити в IDE"
|
|
595
|
+
start_monitoring: "Запустити моніторинг"
|
|
596
|
+
stop_monitoring: "Зупинити моніторинг"
|
|
594
597
|
starting: "Запуск..."
|
|
595
598
|
stopping: "Зупинка..."
|
|
596
|
-
load_history: "
|
|
599
|
+
load_history: "Завантажити історію"
|
|
600
|
+
load_history_unavailable_title: "Збереженої історії немає — задайте config.query_monitor_log_file, щоб запити зберігалися між перезапусками"
|
|
597
601
|
loading: "Завантаження..."
|
|
598
602
|
running: "Виконання..."
|
|
599
|
-
save_for_comparison: "
|
|
600
|
-
saved_marker: "
|
|
603
|
+
save_for_comparison: "Зберегти для порівняння"
|
|
604
|
+
saved_marker: "Збережено"
|
|
601
605
|
status:
|
|
602
606
|
pg_stat_ready: "Активно"
|
|
603
607
|
not_preloaded: "Потрібне передзавантаження"
|
|
@@ -615,25 +619,26 @@ uk:
|
|
|
615
619
|
create_extension_title: "Створення розширення pg_stat_statements"
|
|
616
620
|
create_extension_intro: "Розширення pg_stat_statements ще не створено в цій базі даних. Натисніть, щоб створити його:"
|
|
617
621
|
create_extension_note: "Якщо бібліотеку не передзавантажено, її також потрібно додати до shared_preload_libraries та перезапустити PostgreSQL."
|
|
618
|
-
reset_stats_title: "
|
|
622
|
+
reset_stats_title: "Скидання статистики"
|
|
619
623
|
reset_stats_confirm: "Ви впевнені, що хочете скинути статистику pg_stat_statements?"
|
|
620
624
|
reset_stats_warning: "Ця дія очистить усю зібрану статистику запитів і не може бути скасована."
|
|
621
|
-
|
|
622
|
-
problem_detected_title: "
|
|
623
|
-
query_analyzer_title: "
|
|
625
|
+
settings_title: "Налаштування"
|
|
626
|
+
problem_detected_title: "Виявлено проблему"
|
|
627
|
+
query_analyzer_title: "Аналізатор запиту"
|
|
624
628
|
query_label: "Запит:"
|
|
625
629
|
parameters_label: "Параметри:"
|
|
626
|
-
migration_title: "
|
|
630
|
+
migration_title: "Міграція видалення індексу"
|
|
627
631
|
migration_subtitle: "Згенерована міграція для видалення індексу:"
|
|
628
632
|
migration_warning: "Створення міграції згенерує файл міграції у вашому проєкті. Запуск цієї міграції видалить індекс із БД, що може значно вплинути на продуктивність застосунку."
|
|
629
633
|
migration_warning_dev_only: "Цю операцію слід виконувати лише в локальному dev-середовищі."
|
|
630
|
-
query_execution_disabled_title: "
|
|
634
|
+
query_execution_disabled_title: "Виконання запитів вимкнено"
|
|
635
|
+
migration_disabled_title: "Створення міграцій вимкнено"
|
|
631
636
|
query_execution_disabled_intro: "Щоб увімкнути цю функцію, додайте до конфігурації:"
|
|
632
637
|
query_execution_disabled_env_note: "...або замість цього встановіть змінну середовища:"
|
|
633
638
|
run_query_title: "SQL-консоль"
|
|
634
639
|
run_query_placeholder: "SELECT * FROM users LIMIT 10;"
|
|
635
640
|
settings:
|
|
636
|
-
default_ide_label: "IDE за замовчуванням для посилань на
|
|
641
|
+
default_ide_label: "IDE за замовчуванням для посилань на джерела"
|
|
637
642
|
ide_show_menu: "Показувати меню (за замовчуванням)"
|
|
638
643
|
ide_vscode_wsl: "VS Code (WSL)"
|
|
639
644
|
ide_vscode: "VS Code"
|
|
@@ -670,25 +675,27 @@ uk:
|
|
|
670
675
|
waiting_for_locks: "чекають блокувань"
|
|
671
676
|
percent_used_suffix: "% використано"
|
|
672
677
|
categories:
|
|
673
|
-
requires_pg_stat: "
|
|
678
|
+
requires_pg_stat: "Потрібен pg_stat_statements"
|
|
679
|
+
pg_stat_missing_reason: "Потрібен pg_stat_statements, але розширення не встановлено в базі «%{database}». Створіть його там або перемкніться на базу, де воно є."
|
|
680
|
+
pg_stat_not_preloaded_reason: "Потрібен pg_stat_statements. Розширення є в базі «%{database}», але відсутнє в shared_preload_libraries — даних не буде, доки PostgreSQL не перезапустять із ним."
|
|
674
681
|
reports_count_suffix: "звітів"
|
|
675
|
-
primary_only_reason: "
|
|
676
|
-
standalone_no_host_app_reason: "
|
|
682
|
+
primary_only_reason: "Ця категорія працює лише на первинній базі — вона перевіряє моделі хост-застосунку. Щоб користуватися, перемкніться на базу за замовчуванням."
|
|
683
|
+
standalone_no_host_app_reason: "Недоступно в автономному режимі — ця категорія перевіряє моделі хост-застосунку, а при самостійному запуску дашборда хост-застосунку немає."
|
|
677
684
|
documentation:
|
|
678
|
-
toggle_title: "
|
|
679
|
-
what_section: "
|
|
680
|
-
why_section: "
|
|
681
|
-
nuances_section: "
|
|
682
|
-
thresholds_section: "
|
|
683
|
-
threshold_warning_label: "
|
|
684
|
-
threshold_critical_label: "
|
|
685
|
+
toggle_title: "Що показує цей звіт?"
|
|
686
|
+
what_section: "Що"
|
|
687
|
+
why_section: "Чому це важливо"
|
|
688
|
+
nuances_section: "Нюанси"
|
|
689
|
+
thresholds_section: "Пороги"
|
|
690
|
+
threshold_warning_label: "Warning:"
|
|
691
|
+
threshold_critical_label: "Critical:"
|
|
685
692
|
threshold_inverted_note: "(менше — гірше)"
|
|
686
693
|
filters:
|
|
687
|
-
title: "
|
|
694
|
+
title: "Параметри фільтрації"
|
|
688
695
|
current_value: "зараз"
|
|
689
696
|
saved:
|
|
690
|
-
title: "
|
|
691
|
-
saved_at_prefix: "
|
|
697
|
+
title: "Збережено для порівняння"
|
|
698
|
+
saved_at_prefix: "Збережено:"
|
|
692
699
|
click_to_expand: "Натисніть, щоб розгорнути"
|
|
693
700
|
confirm_clear_all: "Видалити всі збережені записи для цього звіту?"
|
|
694
701
|
remove_title: "Видалити"
|
|
@@ -702,9 +709,9 @@ uk:
|
|
|
702
709
|
execution_time_label: "Час виконання:"
|
|
703
710
|
null_placeholder: "<null>"
|
|
704
711
|
sections:
|
|
705
|
-
recommendation: "
|
|
706
|
-
detected_issues: "
|
|
707
|
-
execution_plan: "
|
|
712
|
+
recommendation: "Рекомендація"
|
|
713
|
+
detected_issues: "Виявлені проблеми"
|
|
714
|
+
execution_plan: "План виконання"
|
|
708
715
|
line_label: "Рядок"
|
|
709
716
|
current_label: "Поточне:"
|
|
710
717
|
threshold_label: "Поріг:"
|
|
@@ -712,8 +719,8 @@ uk:
|
|
|
712
719
|
warning_eq: "warning"
|
|
713
720
|
critical_eq: "critical"
|
|
714
721
|
levels:
|
|
715
|
-
critical: "
|
|
716
|
-
warning: "
|
|
722
|
+
critical: "Критично"
|
|
723
|
+
warning: "Увага"
|
|
717
724
|
errors:
|
|
718
725
|
error_prefix: "Помилка:"
|
|
719
726
|
unable_fetch_metrics: "Не вдалося отримати статистику БД."
|
|
@@ -741,8 +748,9 @@ uk:
|
|
|
741
748
|
run_query_failed: "Не вдалося виконати запит"
|
|
742
749
|
query_required: "Введіть запит"
|
|
743
750
|
create_migration_failed: "Не вдалося створити міграцію"
|
|
744
|
-
explain_disabled_toast: "
|
|
745
|
-
execute_disabled_toast: "
|
|
751
|
+
explain_disabled_toast: "EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
|
|
752
|
+
execute_disabled_toast: "Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
|
|
753
|
+
migration_disabled_toast: "Створення міграцій вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
|
|
746
754
|
query_monitoring_error: "Помилка моніторингу запитів"
|
|
747
755
|
query_hash_required: "Потрібен хеш запиту"
|
|
748
756
|
query_execution_disabled: "Виконання запитів із дашборду вимкнено. Увімкніть у конфігурації: 'config.allow_raw_query_execution = true'"
|
data/config/routes.rb
CHANGED
|
@@ -9,6 +9,7 @@ PgReports::Engine.routes.draw do
|
|
|
9
9
|
|
|
10
10
|
post "switch_database", to: "dashboard#switch_database", as: :switch_database
|
|
11
11
|
post "switch_target", to: "dashboard#switch_target", as: :switch_target
|
|
12
|
+
post "switch_locale", to: "dashboard#switch_locale", as: :switch_locale
|
|
12
13
|
|
|
13
14
|
post "enable_pg_stat_statements", to: "dashboard#enable_pg_stat_statements", as: :enable_pg_stat_statements
|
|
14
15
|
post "reset_statistics", to: "dashboard#reset_statistics", as: :reset_statistics
|
|
@@ -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
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pg_reports
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eldar Avatov
|
|
@@ -153,6 +153,7 @@ files:
|
|
|
153
153
|
- app/views/layouts/pg_reports/application.html.erb
|
|
154
154
|
- app/views/pg_reports/dashboard/_database_selector.html.erb
|
|
155
155
|
- app/views/pg_reports/dashboard/_fake_source_data.html.erb
|
|
156
|
+
- app/views/pg_reports/dashboard/_settings_modal.html.erb
|
|
156
157
|
- app/views/pg_reports/dashboard/_show_modals.html.erb
|
|
157
158
|
- app/views/pg_reports/dashboard/_show_scripts.html.erb
|
|
158
159
|
- app/views/pg_reports/dashboard/_show_styles.html.erb
|