pg_reports 0.9.0 → 1.0.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +12 -8
  4. data/app/controllers/pg_reports/dashboard_controller.rb +277 -221
  5. data/app/controllers/pg_reports/metrics_controller.rb +13 -4
  6. data/app/views/layouts/pg_reports/application.html.erb +222 -93
  7. data/app/views/pg_reports/dashboard/_settings_modal.html.erb +55 -0
  8. data/app/views/pg_reports/dashboard/_show_modals.html.erb +1 -42
  9. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +75 -105
  10. data/app/views/pg_reports/dashboard/index.html.erb +346 -238
  11. data/app/views/pg_reports/dashboard/show.html.erb +1 -1
  12. data/bin/pg_reports +2 -2
  13. data/config/brakeman.ignore +10 -56
  14. data/config/locales/en.yml +45 -11
  15. data/config/locales/ru.yml +44 -11
  16. data/config/locales/uk.yml +44 -11
  17. data/config/routes.rb +2 -0
  18. data/lib/pg_reports/access_policy.rb +39 -0
  19. data/lib/pg_reports/compatibility.rb +10 -2
  20. data/lib/pg_reports/configuration.rb +33 -2
  21. data/lib/pg_reports/connection/error_translator.rb +21 -0
  22. data/lib/pg_reports/connection/target.rb +43 -10
  23. data/lib/pg_reports/csv_cell.rb +20 -0
  24. data/lib/pg_reports/dashboard/reports_registry.rb +37 -30
  25. data/lib/pg_reports/definitions/connections/connection_stats.yml +4 -0
  26. data/lib/pg_reports/definitions/connections/idle_connections.yml +4 -0
  27. data/lib/pg_reports/definitions/connections/locks.yml +1 -0
  28. data/lib/pg_reports/definitions/connections/long_running_queries.yml +0 -1
  29. data/lib/pg_reports/definitions/indexes/bloated_indexes.yml +1 -2
  30. data/lib/pg_reports/definitions/indexes/index_correlation.yml +2 -1
  31. data/lib/pg_reports/definitions/indexes/missing_indexes.yml +1 -0
  32. data/lib/pg_reports/definitions/queries/frequent_queries.yml +50 -0
  33. data/lib/pg_reports/definitions/system/cache_stats.yml +2 -1
  34. data/lib/pg_reports/definitions/tables/bloated_tables.yml +1 -2
  35. data/lib/pg_reports/definitions/tables/recently_modified.yml +3 -3
  36. data/lib/pg_reports/definitions/tables/unused_tables.yml +1 -1
  37. data/lib/pg_reports/drop_index_migration.rb +56 -0
  38. data/lib/pg_reports/engine.rb +4 -3
  39. data/lib/pg_reports/executor.rb +46 -9
  40. data/lib/pg_reports/modules/queries.rb +1 -0
  41. data/lib/pg_reports/query_monitor.rb +478 -192
  42. data/lib/pg_reports/rate_limiter.rb +63 -0
  43. data/lib/pg_reports/report.rb +1 -1
  44. data/lib/pg_reports/report_definition.rb +11 -1
  45. data/lib/pg_reports/sql/connections/connection_stats.sql +2 -1
  46. data/lib/pg_reports/sql/connections/idle_connections.sql +1 -0
  47. data/lib/pg_reports/sql/connections/locks.sql +1 -0
  48. data/lib/pg_reports/sql/indexes/bloated_indexes.sql +16 -8
  49. data/lib/pg_reports/sql/queries/activity_sample.sql +22 -0
  50. data/lib/pg_reports/sql/queries/frequent_queries.sql +42 -0
  51. data/lib/pg_reports/sql_redactor.rb +32 -0
  52. data/lib/pg_reports/standalone.rb +83 -20
  53. data/lib/pg_reports/version.rb +1 -1
  54. data/lib/pg_reports.rb +7 -2
  55. metadata +18 -9
@@ -48,7 +48,7 @@
48
48
  <%= t("pg_reports.ui.actions.send_telegram") %>
49
49
  </button>
50
50
  <% end %>
51
- <button type="button" class="btn-icon" onclick="showIdeSettingsModal()" title="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>" aria-label="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>">
51
+ <button type="button" class="btn-icon" onclick="showSettingsModal()" title="<%= t("pg_reports.ui.actions.settings_button_title") %>" aria-label="<%= t("pg_reports.ui.actions.settings_button_title") %>">
52
52
  <svg class="icon icon-lg"><use href="#i-settings"></use></svg>
53
53
  </button>
54
54
  </div>
data/bin/pg_reports CHANGED
@@ -46,7 +46,7 @@ banner = <<~BANNER
46
46
 
47
47
  Configuration is layered, lowest to highest precedence:
48
48
  PG_REPORTS_* env vars < config file < the flags below.
49
- A config file (--config, else ./pg_reports.rb or config/pg_reports.rb) is
49
+ A config file (--config PATH, never loaded unless you name it) is
50
50
  plain Ruby calling `PgReports.configure` — the way to set every option that
51
51
  has no flag (thresholds, Telegram, Grafana, dashboard_auth, …).
52
52
 
@@ -60,7 +60,7 @@ parser = OptionParser.new do |o|
60
60
  o.on("-m", "--mount PATH", "Path to mount the dashboard at (default: /)") { |v| options[:mount] = v }
61
61
  o.on("-d", "--database-url URL", "PostgreSQL connection URL") { |v| options[:database_url] = v }
62
62
  o.on("-s", "--server NAME", "Web server to use (e.g. puma, webrick)") { |v| options[:server] = v }
63
- o.on("-c", "--config PATH", "Ruby config file to load (default: ./pg_reports.rb or config/pg_reports.rb)") { |v| options[:config_file] = v }
63
+ o.on("-c", "--config PATH", "Ruby config file to load") { |v| options[:config_file] = v }
64
64
  o.on("--[no-]allow-raw-query-execution", "Allow running raw SQL / EXPLAIN ANALYZE from the dashboard") { |v| overrides[:allow_raw_query_execution] = v }
65
65
  o.on("--[no-]allow-migration-creation", "Allow the dashboard to write migration files to db/migrate/") { |v| overrides[:allow_migration_creation] = v }
66
66
  o.on("--[no-]external-fonts", "Load Google Fonts in the dashboard (off by default)") { |v| overrides[:load_external_fonts] = v }
@@ -1,51 +1,5 @@
1
1
  {
2
2
  "ignored_warnings": [
3
- {
4
- "warning_type": "SQL Injection",
5
- "warning_code": 0,
6
- "fingerprint": "5209397462e689c349f77e534f897c12a8550b9a013bd535153a929c12b944e2",
7
- "check_name": "SQL",
8
- "message": "Possible SQL injection",
9
- "file": "app/controllers/pg_reports/dashboard_controller.rb",
10
- "line": 357,
11
- "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
12
- "code": "ActiveRecord::Base.connection.execute(\"SELECT COUNT(*) FROM (#{substitute_params(retrieve_query_by_hash(params[:query_hash]), (params[:params] or {}))}) AS count_query\")",
13
- "render_path": null,
14
- "location": {
15
- "type": "method",
16
- "class": "PgReports::DashboardController",
17
- "method": "execute_query"
18
- },
19
- "user_input": "params[:params]",
20
- "confidence": "High",
21
- "cwe_id": [
22
- 89
23
- ],
24
- "note": "Reviewed & accepted: user-query execution feature (execute_query count). Gated by config.allow_raw_query_execution; the query is fetched by hash from an internal registry (retrieve_query_by_hash), parameters are validated/substituted, and SecurityError checks reject dangerous input. Not arbitrary attacker-controlled SQL."
25
- },
26
- {
27
- "warning_type": "SQL Injection",
28
- "warning_code": 0,
29
- "fingerprint": "52294539b2a9666f9a79f7ad52cdd1dc531a3b71e487e5134174029cd5138571",
30
- "check_name": "SQL",
31
- "message": "Possible SQL injection",
32
- "file": "app/controllers/pg_reports/dashboard_controller.rb",
33
- "line": 440,
34
- "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
35
- "code": "ActiveRecord::Base.connection.execute(\"SELECT COUNT(*) FROM (#{params[:query].to_s}) AS count_query\")",
36
- "render_path": null,
37
- "location": {
38
- "type": "method",
39
- "class": "PgReports::DashboardController",
40
- "method": "run_query"
41
- },
42
- "user_input": "params[:query]",
43
- "confidence": "High",
44
- "cwe_id": [
45
- 89
46
- ],
47
- "note": "Reviewed & accepted: SQL Console (run_query count). Gated by config.allow_raw_query_execution; the client-typed query passes enforce_select_only! (SELECT-only, no semicolons, keyword denylist) and runs under a bounded statement_timeout. Intentional DBA tooling; same risk profile as execute_query/explain_analyze."
48
- },
49
3
  {
50
4
  "warning_type": "Cross-Site Request Forgery",
51
5
  "warning_code": 7,
@@ -71,27 +25,27 @@
71
25
  {
72
26
  "warning_type": "SQL Injection",
73
27
  "warning_code": 0,
74
- "fingerprint": "f133378f2542f3c09b527aebcc0939a1d227c46b0ab124be613e1e5aac829c79",
28
+ "fingerprint": "d79e7adc175add96cdc7054b79ce7861dca0ce3dfd0bd8ec025f953b62806ae3",
75
29
  "check_name": "SQL",
76
30
  "message": "Possible SQL injection",
77
- "file": "app/controllers/pg_reports/dashboard_controller.rb",
78
- "line": 278,
31
+ "file": "lib/pg_reports/executor.rb",
32
+ "line": 42,
79
33
  "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
80
- "code": "ActiveRecord::Base.connection.execute(\"EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{substitute_params(retrieve_query_by_hash(params[:query_hash]), (params[:params] or {}))}\")",
34
+ "code": "connection.exec_query(\"#{\"/* pg_reports */\"} #{interpolate_params(sql, params)}\", \"PgReports\")",
81
35
  "render_path": null,
82
36
  "location": {
83
37
  "type": "method",
84
- "class": "PgReports::DashboardController",
85
- "method": "explain_analyze"
38
+ "class": "PgReports::Executor",
39
+ "method": "execute"
86
40
  },
87
- "user_input": "params[:params]",
88
- "confidence": "High",
41
+ "user_input": "interpolate_params(sql, params)",
42
+ "confidence": "Medium",
89
43
  "cwe_id": [
90
44
  89
91
45
  ],
92
- "note": "Reviewed & accepted: EXPLAIN ANALYZE feature. Same safeguards as execute_query - query resolved by hash from internal registry, params validated, SecurityError guards. Intentional DBA tooling."
46
+ "note": "Reviewed & accepted: `sql` comes from SQL files shipped with the gem or string literals in its modules, never from a request. interpolate_params makes one pass and every value goes through quote_value, so a value cannot become SQL. Dashboard raw queries use exec_params in DashboardController instead."
93
47
  }
94
48
  ],
95
- "updated": "2026-07-10 19:15:00 +0300",
49
+ "updated": "2026-11-07 12:00:00 +0300",
96
50
  "brakeman_version": "8.0.5"
97
51
  }
@@ -24,6 +24,18 @@ en:
24
24
  - "Prepared statements can reduce parsing overhead for frequent queries."
25
25
  ai_prompt: "Reduce the call frequency of these heavy PostgreSQL queries. For each query: identify the caller in application code, suggest caching strategies (Redis/Memcached), detect N+1 patterns and suggest eager loading fixes. Show code changes needed."
26
26
 
27
+ frequent_queries:
28
+ title: "Most Frequent Queries"
29
+ what: "Queries executed at the highest rate, in calls per second, sorted from most to least frequent down to the threshold."
30
+ how: "Divides calls from pg_stat_statements by the time the query has been tracked: since the entry was created (stats_since, PostgreSQL 17+) or since the last statistics reset (pg_stat_statements_info.stats_reset, PostgreSQL 14–16). Unlike Heavy Queries, which ranks by the cumulative call count, a query's place here does not depend on how long it has been accumulating calls."
31
+ nuances:
32
+ - "This is an average over the whole tracked period (see tracked_since), not the current load: a recent spike is diluted by a long quiet period, and the report cannot show when calls actually happened."
33
+ - "On PostgreSQL 14–16 every query shares the same window, so a query deployed long after the last reset is understated. Reset the statistics and let them accumulate for a representative period to compare fairly."
34
+ - "An entry evicted from pg_stat_statements (see pg_stat_statements.max) loses its counters, so the rate starts over when it is seen again."
35
+ - "Very frequent, fast queries are candidates for application-level caching; many similar queries point to N+1 and a need for eager loading."
36
+ - "Requires PostgreSQL 14+ and pg_stat_statements 1.9+. After a major upgrade run ALTER EXTENSION pg_stat_statements UPDATE."
37
+ ai_prompt: "Reduce the execution rate of these high-frequency PostgreSQL queries. For each query: identify the caller in application code, determine whether the rate is justified, suggest caching (Redis/Memcached), batching, or eager loading to fix N+1 patterns. Show code changes needed."
38
+
27
39
  expensive_queries:
28
40
  title: "Expensive Queries"
29
41
  what: "Queries consuming the most total time (total_time = mean_time × calls)."
@@ -589,6 +601,9 @@ en:
589
601
  title: "PgReports"
590
602
  subtitle: "PostgreSQL Analysis Dashboard"
591
603
  page_title: "PgReports Dashboard"
604
+ language:
605
+ label: "Language"
606
+ name: "English"
592
607
  navigation:
593
608
  dashboard: "Dashboard"
594
609
  back: "Back"
@@ -619,7 +634,7 @@ en:
619
634
  confirm_reset: "Yes, Reset"
620
635
  create_extension: "Create Extension"
621
636
  creating: "Creating..."
622
- ide_settings_button_title: "IDE Settings"
637
+ settings_button_title: "Settings"
623
638
  explain_analyze: "EXPLAIN ANALYZE"
624
639
  execute_query: "Execute Query"
625
640
  run_query: "SQL Console"
@@ -654,7 +669,7 @@ en:
654
669
  reset_stats_title: "Reset Statistics"
655
670
  reset_stats_confirm: "Are you sure you want to reset pg_stat_statements statistics?"
656
671
  reset_stats_warning: "This action will clear all collected query statistics and cannot be undone."
657
- ide_settings_title: "IDE Settings"
672
+ settings_title: "Settings"
658
673
  problem_detected_title: "Problem Detected"
659
674
  query_analyzer_title: "Query Analyzer"
660
675
  query_label: "Query:"
@@ -670,7 +685,7 @@ en:
670
685
  run_query_title: "SQL Console"
671
686
  run_query_placeholder: "SELECT * FROM users LIMIT 10;"
672
687
  settings:
673
- default_ide_label: "Default IDE for source links:"
688
+ default_ide_label: "Default IDE for source links"
674
689
  ide_show_menu: "Show menu (default)"
675
690
  ide_vscode_wsl: "VS Code (WSL)"
676
691
  ide_vscode: "VS Code"
@@ -683,14 +698,27 @@ en:
683
698
  update_interval: "Updates every 5s"
684
699
  toggle_title: "Toggle live monitoring"
685
700
  query_monitor_title: "SQL Query Monitor"
686
- session_label: "Session:"
701
+ auto_stop_label: "Stops in"
687
702
  queries_label: "Queries:"
688
703
  feed_empty: "Click \"Start Monitoring\" to begin capturing SQL queries"
689
704
  feed_no_queries: "No queries captured yet..."
690
705
  unknown_source: "Unknown source"
691
- expand_collapse_title: "Expand/Collapse"
692
- scope_note_short: "scope: host application"
693
- scope_note_long: "Query Monitor subscribes to ActiveSupport::Notifications in the host application's process. It captures every SQL the host app runs, on whichever database the host app is connected to. The dashboard's database selector does not change this scope."
706
+ col_time: "Time"
707
+ col_duration: "ms"
708
+ col_source: "Source"
709
+ col_sql: "SQL"
710
+ session_ended: "Monitoring ended"
711
+ scope_process_warning: "Only this server process is covered: Rails.cache is NullStore or MemoryStore, so other processes cannot see the session. Requests served by another worker will show monitoring as stopped. Use a shared cache (Redis, Memcached) or set config.query_monitor_cache_store."
712
+ scope_note_short_host: "scope: host application"
713
+ scope_note_long_host: "Query Monitor captures the SQL run by every process of the host application that shares its cache: web workers and background jobs alike, on whichever database each is connected to. The dashboard's database selector does not change this scope. A session stops by itself after config.query_monitor_max_duration."
714
+ scope_note_short_sampled: "sampled from pg_stat_activity"
715
+ scope_note_long_sampled: "Samples pg_stat_activity on the selected database every 0.5s. It sees every client of that database, but a backend only keeps its latest statement: fast queries replaced between two samples are missed, so this is a sample rather than a complete log. A session stops by itself after config.query_monitor_max_duration."
716
+ source_switch_label: "Capture method"
717
+ source_notifications: "App"
718
+ source_activity: "Database"
719
+ source_notifications_title: "Every query of this application, with its source. Queries from other clients are not seen."
720
+ source_activity_title: "Samples what all clients of the selected database run. Fast queries between samples are missed."
721
+ hidden_sessions_warning: "%{count} sessions are hidden: this role cannot read other roles' queries. Grant access with: GRANT pg_read_all_stats TO your_role;"
694
722
  metrics:
695
723
  connections_label: "Connections"
696
724
  tps_label: "TPS"
@@ -782,20 +810,23 @@ en:
782
810
  create_migration_failed: "Failed to create migration"
783
811
  explain_disabled_toast: "EXPLAIN ANALYZE is disabled. Enable in configuration: config.allow_raw_query_execution = true"
784
812
  execute_disabled_toast: "Query execution is disabled. Enable in configuration: config.allow_raw_query_execution = true"
785
- migration_disabled_toast: "Migration creation is disabled. Enable in configuration: config.allow_raw_query_execution = true"
813
+ migration_disabled_toast: "Migration creation is disabled. Enable in configuration: config.allow_migration_creation = true"
786
814
  query_monitoring_error: "Query monitoring error"
787
815
  query_hash_required: "Query hash is required"
788
816
  query_execution_disabled: "Query execution from dashboard is disabled. Enable it in configuration with 'config.allow_raw_query_execution = true'"
789
817
  query_not_found_expired: "Query not found or expired. Please refresh the page."
790
- query_monitor_unavailable_standalone: "SQL Query Monitor isn't available in standalone mode — there is no host application process to observe."
818
+ query_monitor_disabled: "SQL Query Monitor is turned off by config.query_monitor_enabled."
791
819
  query_timed_out: "Query timed out after %{ms}ms (statement_timeout). Try narrowing the query or increase config.raw_query_statement_timeout_ms."
820
+ report_timed_out: "The report was stopped after %{ms}ms (statement_timeout). Increase config.report_statement_timeout_ms if this database needs longer."
821
+ report_lock_timed_out: "The report needs a lock another session holds, so it was stopped instead of waiting. See Blocking Queries and Locks."
792
822
  rate_limit_exceeded: "Too many requests. Please wait a bit before trying again."
793
823
  security_violation_prefix: "Security violation:"
794
824
  trigger_variables_not_allowed: "Cannot EXPLAIN ANALYZE queries with trigger variables (NEW, OLD). These are only available within trigger functions."
795
825
  missing_parameter_values: "Please provide values for all parameter placeholders ($1, $2, etc.)"
796
826
  migration_dev_only: "Migration creation is only allowed in development environment"
797
- filename_code_required: "File name and code are required"
798
- invalid_filename_format: "Invalid migration file name format"
827
+ index_not_found: "Index not found, or its name exists in several schemas"
828
+ migration_exists: "A migration for this index already exists in db/migrate"
829
+ migration_primary_only: "Migrations can only be generated from the application's primary database"
799
830
  migrations_dir_not_found: "Migrations directory not found"
800
831
  success:
801
832
  statistics_reset: "Statistics have been reset successfully"
@@ -828,6 +859,9 @@ en:
828
859
  heavy_queries:
829
860
  name: "Heavy Queries"
830
861
  description: "Most frequently called queries"
862
+ frequent_queries:
863
+ name: "Most Frequent Queries"
864
+ description: "Queries executed at the highest rate (calls per second)"
831
865
  expensive_queries:
832
866
  name: "Expensive Queries"
833
867
  description: "Queries consuming most total time"
@@ -22,6 +22,17 @@ ru:
22
22
  - "Рассмотрите batch-операции вместо множества одиночных запросов."
23
23
  - "Prepared statements могут снизить накладные расходы на парсинг для частых запросов."
24
24
 
25
+ frequent_queries:
26
+ title: "Most Frequent Queries"
27
+ what: "Запросы, выполняющиеся с наибольшей частотой (вызовов в секунду), отсортированные по убыванию вплоть до порога."
28
+ how: "Поле calls из pg_stat_statements делится на время, в течение которого запрос отслеживается: с момента создания записи (stats_since, PostgreSQL 17+) или с последнего сброса статистики (pg_stat_statements_info.stats_reset, PostgreSQL 14–16). В отличие от Heavy Queries, где сортировка идёт по накопленному числу вызовов, позиция запроса здесь не зависит от того, как долго он накапливал вызовы."
29
+ nuances:
30
+ - "Это среднее значение за весь отслеживаемый период (см. tracked_since), а не текущая нагрузка: недавний всплеск размывается долгим затишьем, а когда именно были вызовы, отчёт показать не может."
31
+ - "На PostgreSQL 14–16 окно у всех запросов общее, поэтому для запроса, появившегося намного позже последнего сброса, частота занижена. Для честного сравнения сбросьте статистику и дайте ей накопиться за показательный период."
32
+ - "Запись, вытесненная из pg_stat_statements (см. pg_stat_statements.max), теряет счётчики, и при следующем появлении частота считается заново."
33
+ - "Очень частые быстрые запросы — кандидаты на кэширование на уровне приложения, а много похожих запросов указывает на N+1 и необходимость eager loading."
34
+ - "Нужны PostgreSQL 14+ и pg_stat_statements 1.9+. После мажорного обновления выполните ALTER EXTENSION pg_stat_statements UPDATE."
35
+
25
36
  expensive_queries:
26
37
  title: "Expensive Queries"
27
38
  what: "Запросы, потребляющие наибольшее суммарное время (total_time = mean_time × calls)."
@@ -554,6 +565,9 @@ ru:
554
565
  title: "PgReports"
555
566
  subtitle: "Дашборд анализа PostgreSQL"
556
567
  page_title: "PgReports — Дашборд"
568
+ language:
569
+ label: "Язык"
570
+ name: "Русский"
557
571
  navigation:
558
572
  dashboard: "Дашборд"
559
573
  back: "Назад"
@@ -584,7 +598,7 @@ ru:
584
598
  confirm_reset: "Да, сбросить"
585
599
  create_extension: "Создать расширение"
586
600
  creating: "Создание..."
587
- ide_settings_button_title: "Настройки IDE"
601
+ settings_button_title: "Настройки"
588
602
  explain_analyze: "EXPLAIN ANALYZE"
589
603
  execute_query: "Выполнить запрос"
590
604
  run_query: "SQL-консоль"
@@ -619,7 +633,7 @@ ru:
619
633
  reset_stats_title: "Сброс статистики"
620
634
  reset_stats_confirm: "Вы уверены, что хотите сбросить статистику pg_stat_statements?"
621
635
  reset_stats_warning: "Это действие очистит всю собранную статистику запросов и не может быть отменено."
622
- ide_settings_title: "Настройки IDE"
636
+ settings_title: "Настройки"
623
637
  problem_detected_title: "Обнаружена проблема"
624
638
  query_analyzer_title: "Анализатор запроса"
625
639
  query_label: "Запрос:"
@@ -635,7 +649,7 @@ ru:
635
649
  run_query_title: "SQL-консоль"
636
650
  run_query_placeholder: "SELECT * FROM users LIMIT 10;"
637
651
  settings:
638
- default_ide_label: "IDE по умолчанию для ссылок на исходники:"
652
+ default_ide_label: "IDE по умолчанию для ссылок на исходники"
639
653
  ide_show_menu: "Показывать меню (по умолчанию)"
640
654
  ide_vscode_wsl: "VS Code (WSL)"
641
655
  ide_vscode: "VS Code"
@@ -648,14 +662,27 @@ ru:
648
662
  update_interval: "Обновление каждые 5с"
649
663
  toggle_title: "Переключить live-мониторинг"
650
664
  query_monitor_title: "Монитор SQL-запросов"
651
- session_label: "Сессия:"
665
+ auto_stop_label: "Остановится через"
652
666
  queries_label: "Запросов:"
653
667
  feed_empty: "Нажмите «Запустить мониторинг», чтобы начать захват SQL-запросов"
654
668
  feed_no_queries: "Запросы пока не захвачены..."
655
669
  unknown_source: "Источник неизвестен"
656
- expand_collapse_title: "Развернуть / свернуть"
657
- scope_note_short: "область: хост-приложение"
658
- scope_note_long: "Монитор SQL-запросов подписан на ActiveSupport::Notifications в процессе хост-приложения и захватывает каждый SQL, который выполняет приложение, в той базе, к которой приложение подключено. Селектор базы данных в дашборде на эту область не влияет."
670
+ col_time: "Время"
671
+ col_duration: "мс"
672
+ col_source: "Источник"
673
+ col_sql: "SQL"
674
+ session_ended: "Мониторинг завершён"
675
+ scope_process_warning: "Отслеживается только этот процесс сервера: Rails.cache — это NullStore или MemoryStore, поэтому другие процессы не видят сессию. На запросах, попавших в другой воркер, мониторинг будет выглядеть остановленным. Подключите общий кэш (Redis, Memcached) или задайте config.query_monitor_cache_store."
676
+ scope_note_short_host: "область: хост-приложение"
677
+ scope_note_long_host: "Монитор записывает SQL всех процессов приложения, использующих общий кэш: и веб-воркеров, и фоновых задач — в той базе, к которой подключён каждый из них. Переключатель базы в дашборде на это не влияет. Сессия останавливается сама через config.query_monitor_max_duration."
678
+ scope_note_short_sampled: "выборка из pg_stat_activity"
679
+ scope_note_long_sampled: "Каждые 0,5 с читается pg_stat_activity выбранной базы. Видны все её клиенты, но backend хранит только последний запрос: быстрые запросы, сменившиеся между двумя чтениями, не попадут в ленту. Это выборка, а не полный журнал. Сессия останавливается сама через config.query_monitor_max_duration."
680
+ source_switch_label: "Способ записи"
681
+ source_notifications: "App"
682
+ source_activity: "Database"
683
+ source_notifications_title: "Все запросы этого приложения с источником. Запросы других клиентов не видны."
684
+ source_activity_title: "Выборка запросов всех клиентов выбранной базы. Быстрые запросы между чтениями теряются."
685
+ hidden_sessions_warning: "Скрыто сессий: %{count} — этой роли недоступны запросы других ролей. Выдайте доступ: GRANT pg_read_all_stats TO your_role;"
659
686
  metrics:
660
687
  connections_label: "Соединения"
661
688
  tps_label: "TPS"
@@ -747,20 +774,23 @@ ru:
747
774
  create_migration_failed: "Не удалось создать миграцию"
748
775
  explain_disabled_toast: "EXPLAIN ANALYZE отключен. Включите в конфигурации: config.allow_raw_query_execution = true"
749
776
  execute_disabled_toast: "Выполнение запросов отключено. Включите в конфигурации: config.allow_raw_query_execution = true"
750
- migration_disabled_toast: "Создание миграций отключено. Включите в конфигурации: config.allow_raw_query_execution = true"
777
+ migration_disabled_toast: "Создание миграций отключено. Включите в конфигурации: config.allow_migration_creation = true"
751
778
  query_monitoring_error: "Ошибка мониторинга запросов"
752
779
  query_hash_required: "Требуется хэш запроса"
753
780
  query_execution_disabled: "Выполнение запросов из дашборда отключено. Включите в конфигурации: 'config.allow_raw_query_execution = true'"
754
781
  query_not_found_expired: "Запрос не найден или истёк срок действия. Обновите страницу."
755
- query_monitor_unavailable_standalone: "SQL Query Monitor недоступен в автономном режиме — нет процесса основного приложения для наблюдения."
782
+ query_monitor_disabled: "SQL-монитор отключён настройкой config.query_monitor_enabled."
756
783
  query_timed_out: "Превышено время ожидания запроса (%{ms} мс, statement_timeout). Сузьте запрос или увеличьте config.raw_query_statement_timeout_ms."
784
+ report_timed_out: "Отчёт остановлен через %{ms} мс (statement_timeout). Если этой базе нужно больше времени, увеличьте config.report_statement_timeout_ms."
785
+ report_lock_timed_out: "Отчёту нужна блокировка, которую держит другая сессия, поэтому он остановлен, а не ждёт. Смотрите Blocking Queries и Locks."
757
786
  rate_limit_exceeded: "Слишком много запросов. Подождите немного и попробуйте снова."
758
787
  security_violation_prefix: "Нарушение безопасности:"
759
788
  trigger_variables_not_allowed: "Нельзя выполнить EXPLAIN ANALYZE для запросов с триггерными переменными (NEW, OLD). Они доступны только в контексте триггерных функций."
760
789
  missing_parameter_values: "Укажите значения для всех плейсхолдеров параметров ($1, $2 и т.д.)"
761
790
  migration_dev_only: "Создание миграций разрешено только в development-окружении"
762
- filename_code_required: "Имя файла и код обязательны"
763
- invalid_filename_format: "Неверный формат имени файла миграции"
791
+ index_not_found: "Индекс не найден, либо такое имя есть в нескольких схемах"
792
+ migration_exists: "Миграция для этого индекса уже есть в db/migrate"
793
+ migration_primary_only: "Миграции можно создавать только из основной базы приложения"
764
794
  migrations_dir_not_found: "Каталог миграций не найден"
765
795
  success:
766
796
  statistics_reset: "Статистика успешно сброшена"
@@ -793,6 +823,9 @@ ru:
793
823
  heavy_queries:
794
824
  name: "Частые запросы"
795
825
  description: "Самые часто вызываемые запросы"
826
+ frequent_queries:
827
+ name: "Самые частые запросы"
828
+ description: "Запросы с наибольшей частотой выполнения (вызовов в секунду)"
796
829
  expensive_queries:
797
830
  name: "Дорогие запросы"
798
831
  description: "Запросы с наибольшим суммарным временем"
@@ -22,6 +22,17 @@ uk:
22
22
  - "Розгляньте batch-операції замість множини одиночних запитів."
23
23
  - "Prepared statements можуть знизити накладні витрати на парсинг для частих запитів."
24
24
 
25
+ frequent_queries:
26
+ title: "Most Frequent Queries"
27
+ what: "Запити, що виконуються з найбільшою частотою (викликів за секунду), відсортовані за спаданням аж до порога."
28
+ how: "Поле calls з pg_stat_statements ділиться на час, протягом якого запит відстежується: з моменту створення запису (stats_since, PostgreSQL 17+) або з останнього скидання статистики (pg_stat_statements_info.stats_reset, PostgreSQL 14–16). На відміну від Heavy Queries, де сортування йде за накопиченою кількістю викликів, позиція запиту тут не залежить від того, як довго він накопичував виклики."
29
+ nuances:
30
+ - "Це середнє значення за весь відстежуваний період (див. tracked_since), а не поточне навантаження: нещодавній сплеск розмивається довгим затишшям, а коли саме були виклики, звіт показати не може."
31
+ - "На PostgreSQL 14–16 вікно в усіх запитів спільне, тому для запиту, що з'явився набагато пізніше за останнє скидання, частота занижена. Для чесного порівняння скиньте статистику й дайте їй накопичитися за показовий період."
32
+ - "Запис, витіснений з pg_stat_statements (див. pg_stat_statements.max), втрачає лічильники, і при наступній появі частота рахується заново."
33
+ - "Дуже часті швидкі запити — кандидати на кешування на рівні застосунку, а багато схожих запитів вказує на N+1 і потребу в eager loading."
34
+ - "Потрібні PostgreSQL 14+ і pg_stat_statements 1.9+. Після мажорного оновлення виконайте ALTER EXTENSION pg_stat_statements UPDATE."
35
+
25
36
  expensive_queries:
26
37
  title: "Expensive Queries"
27
38
  what: "Запити, що споживають найбільше сумарного часу (total_time = mean_time × calls)."
@@ -554,6 +565,9 @@ uk:
554
565
  title: "PgReports"
555
566
  subtitle: "Дашборд аналізу PostgreSQL"
556
567
  page_title: "PgReports — Дашборд"
568
+ language:
569
+ label: "Мова"
570
+ name: "Українська"
557
571
  navigation:
558
572
  dashboard: "Дашборд"
559
573
  back: "Назад"
@@ -584,7 +598,7 @@ uk:
584
598
  confirm_reset: "Так, скинути"
585
599
  create_extension: "Створити розширення"
586
600
  creating: "Створення..."
587
- ide_settings_button_title: "Налаштування IDE"
601
+ settings_button_title: "Налаштування"
588
602
  explain_analyze: "EXPLAIN ANALYZE"
589
603
  execute_query: "Виконати запит"
590
604
  run_query: "SQL-консоль"
@@ -619,7 +633,7 @@ uk:
619
633
  reset_stats_title: "Скидання статистики"
620
634
  reset_stats_confirm: "Ви впевнені, що хочете скинути статистику pg_stat_statements?"
621
635
  reset_stats_warning: "Ця дія очистить усю зібрану статистику запитів і не може бути скасована."
622
- ide_settings_title: "Налаштування IDE"
636
+ settings_title: "Налаштування"
623
637
  problem_detected_title: "Виявлено проблему"
624
638
  query_analyzer_title: "Аналізатор запиту"
625
639
  query_label: "Запит:"
@@ -635,7 +649,7 @@ uk:
635
649
  run_query_title: "SQL-консоль"
636
650
  run_query_placeholder: "SELECT * FROM users LIMIT 10;"
637
651
  settings:
638
- default_ide_label: "IDE за замовчуванням для посилань на джерела:"
652
+ default_ide_label: "IDE за замовчуванням для посилань на джерела"
639
653
  ide_show_menu: "Показувати меню (за замовчуванням)"
640
654
  ide_vscode_wsl: "VS Code (WSL)"
641
655
  ide_vscode: "VS Code"
@@ -648,14 +662,27 @@ uk:
648
662
  update_interval: "Оновлення кожні 5с"
649
663
  toggle_title: "Перемкнути live-моніторинг"
650
664
  query_monitor_title: "Монітор SQL-запитів"
651
- session_label: "Сесія:"
665
+ auto_stop_label: "Зупиниться через"
652
666
  queries_label: "Запитів:"
653
667
  feed_empty: "Натисніть «Запустити моніторинг», щоб почати захоплення SQL-запитів"
654
668
  feed_no_queries: "Запитів поки не захоплено..."
655
669
  unknown_source: "Джерело невідоме"
656
- expand_collapse_title: "Розгорнути / згорнути"
657
- scope_note_short: "область: хост-застосунок"
658
- scope_note_long: "Монітор SQL-запитів підписаний на ActiveSupport::Notifications у процесі хост-застосунку та захоплює кожен SQL, який виконує застосунок, у тій базі, до якої він підключений. Селектор бази даних у дашборді на цю область не впливає."
670
+ col_time: "Час"
671
+ col_duration: "мс"
672
+ col_source: "Джерело"
673
+ col_sql: "SQL"
674
+ session_ended: "Моніторинг завершено"
675
+ scope_process_warning: "Відстежується лише цей процес сервера: Rails.cache — це NullStore або MemoryStore, тому інші процеси не бачать сесію. На запитах, що потрапили до іншого воркера, моніторинг виглядатиме зупиненим. Підключіть спільний кеш (Redis, Memcached) або задайте config.query_monitor_cache_store."
676
+ scope_note_short_host: "область: хост-застосунок"
677
+ scope_note_long_host: "Монітор записує SQL усіх процесів застосунку, що використовують спільний кеш: і веб-воркерів, і фонових задач — у тій базі, до якої підключено кожен із них. Перемикач бази в дашборді на це не впливає. Сесія зупиняється сама через config.query_monitor_max_duration."
678
+ scope_note_short_sampled: "вибірка з pg_stat_activity"
679
+ scope_note_long_sampled: "Кожні 0,5 с читається pg_stat_activity вибраної бази. Видно всіх її клієнтів, але backend зберігає лише останній запит: швидкі запити, що змінилися між двома читаннями, не потраплять до стрічки. Це вибірка, а не повний журнал. Сесія зупиняється сама через config.query_monitor_max_duration."
680
+ source_switch_label: "Спосіб запису"
681
+ source_notifications: "App"
682
+ source_activity: "Database"
683
+ source_notifications_title: "Усі запити цього застосунку з джерелом. Запити інших клієнтів не видно."
684
+ source_activity_title: "Вибірка запитів усіх клієнтів вибраної бази. Швидкі запити між читаннями губляться."
685
+ hidden_sessions_warning: "Приховано сесій: %{count} — цій ролі недоступні запити інших ролей. Надайте доступ: GRANT pg_read_all_stats TO your_role;"
659
686
  metrics:
660
687
  connections_label: "З'єднання"
661
688
  tps_label: "TPS"
@@ -747,20 +774,23 @@ uk:
747
774
  create_migration_failed: "Не вдалося створити міграцію"
748
775
  explain_disabled_toast: "EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
749
776
  execute_disabled_toast: "Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
750
- migration_disabled_toast: "Створення міграцій вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
777
+ migration_disabled_toast: "Створення міграцій вимкнено. Увімкніть у конфігурації: config.allow_migration_creation = true"
751
778
  query_monitoring_error: "Помилка моніторингу запитів"
752
779
  query_hash_required: "Потрібен хеш запиту"
753
780
  query_execution_disabled: "Виконання запитів із дашборду вимкнено. Увімкніть у конфігурації: 'config.allow_raw_query_execution = true'"
754
781
  query_not_found_expired: "Запит не знайдено або термін дії минув. Оновіть сторінку."
755
- query_monitor_unavailable_standalone: "SQL Query Monitor недоступний в автономному режимі — немає процесу основного застосунку для спостереження."
782
+ query_monitor_disabled: "SQL-монітор вимкнено налаштуванням config.query_monitor_enabled."
756
783
  query_timed_out: "Перевищено час очікування запиту (%{ms} мс, statement_timeout). Звузьте запит або збільшіть config.raw_query_statement_timeout_ms."
784
+ report_timed_out: "Звіт зупинено через %{ms} мс (statement_timeout). Якщо цій базі потрібно більше часу, збільште config.report_statement_timeout_ms."
785
+ report_lock_timed_out: "Звіту потрібне блокування, яке тримає інша сесія, тому його зупинено, а не очікує. Дивіться Blocking Queries і Locks."
757
786
  rate_limit_exceeded: "Забагато запитів. Зачекайте трохи і спробуйте знову."
758
787
  security_violation_prefix: "Порушення безпеки:"
759
788
  trigger_variables_not_allowed: "Не можна виконати EXPLAIN ANALYZE для запитів із тригерними змінними (NEW, OLD). Вони доступні лише в контексті тригерних функцій."
760
789
  missing_parameter_values: "Вкажіть значення для всіх плейсхолдерів параметрів ($1, $2 тощо)"
761
790
  migration_dev_only: "Створення міграцій дозволено лише в development-середовищі"
762
- filename_code_required: "Ім'я файлу та код обов'язкові"
763
- invalid_filename_format: "Невірний формат імені файлу міграції"
791
+ index_not_found: "Індекс не знайдено, або таке ім'я є в кількох схемах"
792
+ migration_exists: "Міграція для цього індексу вже є в db/migrate"
793
+ migration_primary_only: "Міграції можна створювати лише з основної бази застосунку"
764
794
  migrations_dir_not_found: "Каталог міграцій не знайдено"
765
795
  success:
766
796
  statistics_reset: "Статистику успішно скинуто"
@@ -793,6 +823,9 @@ uk:
793
823
  heavy_queries:
794
824
  name: "Часті запити"
795
825
  description: "Найчастіше викликані запити"
826
+ frequent_queries:
827
+ name: "Найчастіші запити"
828
+ description: "Запити з найбільшою частотою виконання (викликів за секунду)"
796
829
  expensive_queries:
797
830
  name: "Дорогі запити"
798
831
  description: "Запити з найбільшим сумарним часом"
data/config/routes.rb CHANGED
@@ -9,12 +9,14 @@ 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
15
16
  post "explain_analyze", to: "dashboard#explain_analyze", as: :explain_analyze
16
17
  post "execute_query", to: "dashboard#execute_query", as: :execute_query
17
18
  post "run_query", to: "dashboard#run_query", as: :run_query
19
+ post "migration_preview", to: "dashboard#migration_preview", as: :migration_preview
18
20
  post "create_migration", to: "dashboard#create_migration", as: :create_migration
19
21
 
20
22
  # Query monitoring
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgReports
4
+ # What happens when an endpoint has no credentials configured.
5
+ #
6
+ # Nothing is reachable by default except from the machine itself: a dashboard
7
+ # started for a quick look on a laptop stays open to that laptop, not to the
8
+ # network it happens to be bound to (`rails s -b 0.0.0.0`, a Docker port).
9
+ module AccessPolicy
10
+ # For dashboard_auth or grafana_metrics_token: the host application protects
11
+ # this route itself, so the check here must not turn anyone away.
12
+ HOST_MANAGED = :host_managed
13
+
14
+ DASHBOARD_DENIED = <<~MESSAGE
15
+ PgReports: access denied.
16
+
17
+ This dashboard has no authentication configured, so it only answers requests
18
+ from the machine it runs on, and never in production.
19
+
20
+ Set one of these in your PgReports.configure block:
21
+
22
+ config.dashboard_auth = -> { authenticate_or_request_with_http_basic { |u, p| ... } }
23
+ config.dashboard_auth = :host_managed # the route is already protected by your app
24
+ MESSAGE
25
+
26
+ # request.local? requires both the socket peer and the resolved client to be
27
+ # loopback, so an X-Forwarded-For header cannot talk its way past this.
28
+ #
29
+ # Production is refused outright because a local peer proves nothing there: a
30
+ # reverse proxy on the same host that sets no forwarding header looks local for
31
+ # every client. Standalone has no such proxy unless someone builds one, and
32
+ # RAILS_ENV says nothing about how it is deployed.
33
+ def self.open_access_allowed?(request)
34
+ return false if Rails.env.production? && !PgReports.config.standalone
35
+
36
+ request.local?
37
+ end
38
+ end
39
+ end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module PgReports
4
4
  # Checks runtime environment and warns about outdated or unsupported versions.
5
- # Called once at boot (Ruby/Rails) and lazily on first DB access (PostgreSQL).
5
+ # Ruby/Rails are checked at boot. PostgreSQL waits for the dashboard, so that
6
+ # booting the host application costs no query and no connection.
6
7
  module Compatibility
7
8
  # Keep in sync with gemspec constraints
8
9
  MINIMUM_RUBY_VERSION = "2.7"
9
- MINIMUM_RAILS_VERSION = "5.0"
10
+ MINIMUM_RAILS_VERSION = "6.1"
10
11
  MINIMUM_PG_VERSION = 12_00_00 # server_version_num format
11
12
  MINIMUM_PG_VERSION_LABEL = "12"
12
13
 
@@ -37,6 +38,13 @@ module PgReports
37
38
  "Some reports may return errors or incomplete data."
38
39
  end
39
40
 
41
+ def check_postgresql_once!
42
+ return if @postgresql_checked
43
+
44
+ @postgresql_checked = true
45
+ check_postgresql!
46
+ end
47
+
40
48
  def check_all!
41
49
  check_ruby!
42
50
  check_rails!