pg_reports 0.8.0 → 0.8.2

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.
data/bin/pg_reports ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Soft bundler shim: when run straight from the gem checkout (a Gemfile sits at
5
+ # the repo root, next to bin/), activate the bundle so the local `lib` and the
6
+ # dev-group web server are on the load path — letting `./bin/pg_reports` work
7
+ # without an explicit `bundle exec`. Installed as a gem there is no Gemfile
8
+ # alongside the executable, so this is skipped and RubyGems resolves everything.
9
+ gemfile = File.expand_path("../Gemfile", __dir__)
10
+ if File.exist?(gemfile) && !defined?(Bundler)
11
+ ENV["BUNDLE_GEMFILE"] ||= gemfile
12
+ begin
13
+ require "bundler/setup"
14
+ rescue LoadError
15
+ # Bundler unavailable — fall through and let RubyGems/$LOAD_PATH resolve.
16
+ end
17
+ end
18
+
19
+ require "optparse"
20
+
21
+ options = {
22
+ port: 4000,
23
+ host: "127.0.0.1",
24
+ mount: "/",
25
+ database_url: nil,
26
+ server: nil,
27
+ config_file: nil
28
+ }
29
+
30
+ # Per-setting overrides from flags. nil means "unset" — the config file / ENV
31
+ # value is kept. Populated below and forwarded to Standalone.run(overrides:).
32
+ overrides = {
33
+ allow_raw_query_execution: nil,
34
+ allow_migration_creation: nil,
35
+ load_external_fonts: nil
36
+ }
37
+
38
+ banner = <<~BANNER
39
+ pg_reports — self-contained PostgreSQL insights dashboard
40
+
41
+ Usage:
42
+ pg_reports server [options]
43
+
44
+ Connection is resolved from --database-url, else DATABASE_URL, else the
45
+ standard libpq env vars (PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE).
46
+
47
+ Configuration is layered, lowest to highest precedence:
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
50
+ plain Ruby calling `PgReports.configure` — the way to set every option that
51
+ has no flag (thresholds, Telegram, Grafana, dashboard_auth, …).
52
+
53
+ Options:
54
+ BANNER
55
+
56
+ parser = OptionParser.new do |o|
57
+ o.banner = banner
58
+ o.on("-p", "--port PORT", Integer, "Port to listen on (default: 4000)") { |v| options[:port] = v }
59
+ o.on("-b", "--host HOST", "Host/interface to bind (default: 127.0.0.1)") { |v| options[:host] = v }
60
+ o.on("-m", "--mount PATH", "Path to mount the dashboard at (default: /)") { |v| options[:mount] = v }
61
+ o.on("-d", "--database-url URL", "PostgreSQL connection URL") { |v| options[:database_url] = v }
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 }
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
+ o.on("--[no-]allow-migration-creation", "Allow the dashboard to write migration files to db/migrate/") { |v| overrides[:allow_migration_creation] = v }
66
+ o.on("--[no-]external-fonts", "Load Google Fonts in the dashboard (off by default)") { |v| overrides[:load_external_fonts] = v }
67
+ o.on("-h", "--help", "Show this help") do
68
+ puts o
69
+ exit
70
+ end
71
+ o.on("-v", "--version", "Show version") do
72
+ require "pg_reports/version"
73
+ puts PgReports::VERSION
74
+ exit
75
+ end
76
+ end
77
+
78
+ # parse! permutes, so options are recognized before or after the subcommand
79
+ # (e.g. `pg_reports server --port 4055`).
80
+ parser.parse!(ARGV)
81
+ command = ARGV.shift || "server"
82
+
83
+ case command
84
+ when "server"
85
+ require "pg_reports"
86
+ begin
87
+ PgReports::Standalone.run(
88
+ port: options[:port],
89
+ host: options[:host],
90
+ mount_path: options[:mount],
91
+ database_url: options[:database_url],
92
+ server: options[:server],
93
+ config_file: options[:config_file],
94
+ overrides: overrides
95
+ )
96
+ rescue PgReports::Standalone::ServerUnavailable => e
97
+ warn e.message
98
+ exit 1
99
+ rescue PgReports::Error => e
100
+ # Config-file and connection problems surface as PgReports::Error with a
101
+ # human-readable message — print it, not a backtrace.
102
+ warn "pg_reports: #{e.message}"
103
+ exit 1
104
+ rescue Interrupt
105
+ warn "\npg_reports: stopped"
106
+ end
107
+ else
108
+ warn "Unknown command: #{command.inspect}"
109
+ warn parser.help
110
+ exit 1
111
+ end
@@ -0,0 +1,74 @@
1
+ {
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": "Cross-Site Request Forgery",
28
+ "warning_code": 7,
29
+ "fingerprint": "b9682cc0e0a1cd86fddef96834d4f71c3558989c2136fe58b5ff51c75b83c8b4",
30
+ "check_name": "ForgerySetting",
31
+ "message": "`protect_from_forgery` should be called in `PgReports::MetricsController`",
32
+ "file": "app/controllers/pg_reports/metrics_controller.rb",
33
+ "line": 6,
34
+ "link": "https://brakemanscanner.org/docs/warning_types/cross-site_request_forgery/",
35
+ "code": null,
36
+ "render_path": null,
37
+ "location": {
38
+ "type": "controller",
39
+ "controller": "PgReports::MetricsController"
40
+ },
41
+ "user_input": null,
42
+ "confidence": "High",
43
+ "cwe_id": [
44
+ 352
45
+ ],
46
+ "note": "Reviewed & accepted: read-only Prometheus /metrics endpoint. Uses bearer-token auth (config.grafana_metrics_token) with constant-time compare; CSRF protection is not applicable to a GET scrape endpoint."
47
+ },
48
+ {
49
+ "warning_type": "SQL Injection",
50
+ "warning_code": 0,
51
+ "fingerprint": "f133378f2542f3c09b527aebcc0939a1d227c46b0ab124be613e1e5aac829c79",
52
+ "check_name": "SQL",
53
+ "message": "Possible SQL injection",
54
+ "file": "app/controllers/pg_reports/dashboard_controller.rb",
55
+ "line": 278,
56
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
57
+ "code": "ActiveRecord::Base.connection.execute(\"EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{substitute_params(retrieve_query_by_hash(params[:query_hash]), (params[:params] or {}))}\")",
58
+ "render_path": null,
59
+ "location": {
60
+ "type": "method",
61
+ "class": "PgReports::DashboardController",
62
+ "method": "explain_analyze"
63
+ },
64
+ "user_input": "params[:params]",
65
+ "confidence": "High",
66
+ "cwe_id": [
67
+ 89
68
+ ],
69
+ "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."
70
+ }
71
+ ],
72
+ "updated": "2026-07-04 20:27:49 +0300",
73
+ "brakeman_version": "8.0.5"
74
+ }
@@ -622,6 +622,7 @@ en:
622
622
  ide_settings_button_title: "IDE Settings"
623
623
  explain_analyze: "📊 EXPLAIN ANALYZE"
624
624
  execute_query: "▶ Execute Query"
625
+ run_query: "SQL Console"
625
626
  create_migration_file: "📁 Create File & Open in IDE"
626
627
  start_monitoring: "▶ Start Monitoring"
627
628
  stop_monitoring: "⏹ Stop Monitoring"
@@ -633,11 +634,12 @@ en:
633
634
  save_for_comparison: "📌 Save for Comparison"
634
635
  saved_marker: "📌 Saved"
635
636
  status:
636
- pg_stat_ready: "pg_stat_statements ready"
637
- extension_installed: "Extension installed, not preloaded"
638
- preloaded: "Preloaded, extension not created"
639
- not_configured: "Not configured"
637
+ pg_stat_ready: "Active"
638
+ not_preloaded: "Preload required"
639
+ extension_missing: "Extension required"
640
+ disconnected: "No connection"
640
641
  monitoring_unavailable: "Status Unavailable"
642
+ click_for_details: "Click to see how to enable it"
641
643
  modals:
642
644
  enable_pg_stat_title: "Enable pg_stat_statements"
643
645
  enable_pg_stat_intro: "To enable pg_stat_statements, follow these steps:"
@@ -645,6 +647,9 @@ en:
645
647
  restart_postgresql: "Restart PostgreSQL:"
646
648
  create_extension_step: "Create extension:"
647
649
  enable_button_note: "Or click \"Enable\" button after restart."
650
+ create_extension_title: "Create pg_stat_statements extension"
651
+ create_extension_intro: "The pg_stat_statements extension isn't created in this database yet. Click below to create it:"
652
+ create_extension_note: "If the library isn't preloaded, you'll also need to add it to shared_preload_libraries and restart PostgreSQL."
648
653
  reset_stats_title: "⚠️ Reset Statistics"
649
654
  reset_stats_confirm: "Are you sure you want to reset pg_stat_statements statistics?"
650
655
  reset_stats_warning: "This action will clear all collected query statistics and cannot be undone."
@@ -659,6 +664,9 @@ en:
659
664
  migration_warning_dev_only: "This operation should only be performed in a local development environment."
660
665
  query_execution_disabled_title: "⚠️ Query execution is disabled"
661
666
  query_execution_disabled_intro: "To enable this feature, add to your configuration:"
667
+ query_execution_disabled_env_note: "...or set the following environment variable instead:"
668
+ run_query_title: "SQL Console"
669
+ run_query_placeholder: "SELECT * FROM users LIMIT 10;"
662
670
  settings:
663
671
  default_ide_label: "Default IDE for source links:"
664
672
  ide_show_menu: "Show menu (default)"
@@ -691,7 +699,7 @@ en:
691
699
  cache_hit_detail: "heap blocks from cache"
692
700
  long_queries_label: "Long Queries"
693
701
  queries_unit: "queries"
694
- long_running_threshold: "> 60s runtime"
702
+ long_running_threshold: "> 5s runtime"
695
703
  blocked_label: "Blocked"
696
704
  processes_unit: "processes"
697
705
  waiting_for_locks: "waiting for locks"
@@ -700,6 +708,7 @@ en:
700
708
  requires_pg_stat: "🔒 Requires pg_stat_statements"
701
709
  reports_count_suffix: "reports"
702
710
  primary_only_reason: "🔒 This category only runs on the primary database — it inspects the host application's models. Switch back to the default database to use it."
711
+ standalone_no_host_app_reason: "🔒 Not available in standalone mode — this category inspects the host application's models, and there is no host app when running the dashboard on its own."
703
712
  documentation:
704
713
  toggle_title: "📖 What does this report show?"
705
714
  what_section: "📋 What"
@@ -764,6 +773,8 @@ en:
764
773
  decode_query_failed: "Failed to decode query"
765
774
  explain_analyze_failed: "Failed to run EXPLAIN ANALYZE"
766
775
  execute_query_failed: "Failed to execute query"
776
+ run_query_failed: "Failed to run query"
777
+ query_required: "Please enter a query"
767
778
  create_migration_failed: "Failed to create migration"
768
779
  explain_disabled_toast: "⚠️ EXPLAIN ANALYZE is disabled. Enable in configuration: config.allow_raw_query_execution = true"
769
780
  execute_disabled_toast: "⚠️ Query execution is disabled. Enable in configuration: config.allow_raw_query_execution = true"
@@ -771,6 +782,9 @@ en:
771
782
  query_hash_required: "Query hash is required"
772
783
  query_execution_disabled: "Query execution from dashboard is disabled. Enable it in configuration with 'config.allow_raw_query_execution = true'"
773
784
  query_not_found_expired: "Query not found or expired. Please refresh the page."
785
+ query_monitor_unavailable_standalone: "SQL Query Monitor isn't available in standalone mode — there is no host application process to observe."
786
+ query_timed_out: "Query timed out after %{ms}ms (statement_timeout). Try narrowing the query or increase config.raw_query_statement_timeout_ms."
787
+ rate_limit_exceeded: "Too many requests. Please wait a bit before trying again."
774
788
  security_violation_prefix: "Security violation:"
775
789
  trigger_variables_not_allowed: "Cannot EXPLAIN ANALYZE queries with trigger variables (NEW, OLD). These are only available within trigger functions."
776
790
  missing_parameter_values: "Please provide values for all parameter placeholders ($1, $2, etc.)"
@@ -587,6 +587,7 @@ ru:
587
587
  ide_settings_button_title: "Настройки IDE"
588
588
  explain_analyze: "📊 EXPLAIN ANALYZE"
589
589
  execute_query: "▶ Выполнить запрос"
590
+ run_query: "SQL-консоль"
590
591
  create_migration_file: "📁 Создать файл и открыть в IDE"
591
592
  start_monitoring: "▶ Запустить мониторинг"
592
593
  stop_monitoring: "⏹ Остановить мониторинг"
@@ -598,11 +599,12 @@ ru:
598
599
  save_for_comparison: "📌 Сохранить для сравнения"
599
600
  saved_marker: "📌 Сохранено"
600
601
  status:
601
- pg_stat_ready: "pg_stat_statements готов"
602
- extension_installed: "Расширение установлено, не предзагружено"
603
- preloaded: "Предзагружено, расширение не создано"
604
- not_configured: "Не настроено"
602
+ pg_stat_ready: "Активно"
603
+ not_preloaded: "Требуется предзагрузка"
604
+ extension_missing: "Требуется расширение"
605
+ disconnected: "Нет соединения"
605
606
  monitoring_unavailable: "Статус недоступен"
607
+ click_for_details: "Нажмите, чтобы узнать, как включить"
606
608
  modals:
607
609
  enable_pg_stat_title: "Включение pg_stat_statements"
608
610
  enable_pg_stat_intro: "Чтобы включить pg_stat_statements, выполните следующие шаги:"
@@ -610,6 +612,9 @@ ru:
610
612
  restart_postgresql: "Перезапустите PostgreSQL:"
611
613
  create_extension_step: "Создайте расширение:"
612
614
  enable_button_note: "Или нажмите кнопку «Создать расширение» после перезапуска."
615
+ create_extension_title: "Создание расширения pg_stat_statements"
616
+ create_extension_intro: "Расширение pg_stat_statements ещё не создано в этой базе данных. Нажмите, чтобы создать его:"
617
+ create_extension_note: "Если библиотека не предзагружена, её также нужно добавить в shared_preload_libraries и перезапустить PostgreSQL."
613
618
  reset_stats_title: "⚠️ Сброс статистики"
614
619
  reset_stats_confirm: "Вы уверены, что хотите сбросить статистику pg_stat_statements?"
615
620
  reset_stats_warning: "Это действие очистит всю собранную статистику запросов и не может быть отменено."
@@ -624,6 +629,9 @@ ru:
624
629
  migration_warning_dev_only: "Эту операцию следует выполнять только в локальном dev-окружении."
625
630
  query_execution_disabled_title: "⚠️ Выполнение запросов отключено"
626
631
  query_execution_disabled_intro: "Чтобы включить эту функцию, добавьте в конфигурацию:"
632
+ query_execution_disabled_env_note: "...или вместо этого установите переменную окружения:"
633
+ run_query_title: "SQL-консоль"
634
+ run_query_placeholder: "SELECT * FROM users LIMIT 10;"
627
635
  settings:
628
636
  default_ide_label: "IDE по умолчанию для ссылок на исходники:"
629
637
  ide_show_menu: "Показывать меню (по умолчанию)"
@@ -656,7 +664,7 @@ ru:
656
664
  cache_hit_detail: "блоки heap из кэша"
657
665
  long_queries_label: "Долгие запросы"
658
666
  queries_unit: "запросов"
659
- long_running_threshold: "> 60с выполнения"
667
+ long_running_threshold: "> 5с выполнения"
660
668
  blocked_label: "Заблокировано"
661
669
  processes_unit: "процессов"
662
670
  waiting_for_locks: "ждут блокировок"
@@ -665,6 +673,7 @@ ru:
665
673
  requires_pg_stat: "🔒 Требуется pg_stat_statements"
666
674
  reports_count_suffix: "отчётов"
667
675
  primary_only_reason: "🔒 Эта категория работает только на первичной базе — она проверяет модели хост-приложения. Чтобы пользоваться, переключитесь обратно на базу по умолчанию."
676
+ standalone_no_host_app_reason: "🔒 Недоступно в автономном режиме — эта категория проверяет модели хост-приложения, а при самостоятельном запуске дашборда хост-приложения нет."
668
677
  documentation:
669
678
  toggle_title: "📖 Что показывает этот отчёт?"
670
679
  what_section: "📋 Что"
@@ -729,6 +738,8 @@ ru:
729
738
  decode_query_failed: "Не удалось декодировать запрос"
730
739
  explain_analyze_failed: "Не удалось выполнить EXPLAIN ANALYZE"
731
740
  execute_query_failed: "Не удалось выполнить запрос"
741
+ run_query_failed: "Не удалось выполнить запрос"
742
+ query_required: "Введите запрос"
732
743
  create_migration_failed: "Не удалось создать миграцию"
733
744
  explain_disabled_toast: "⚠️ EXPLAIN ANALYZE отключен. Включите в конфигурации: config.allow_raw_query_execution = true"
734
745
  execute_disabled_toast: "⚠️ Выполнение запросов отключено. Включите в конфигурации: config.allow_raw_query_execution = true"
@@ -736,6 +747,9 @@ ru:
736
747
  query_hash_required: "Требуется хэш запроса"
737
748
  query_execution_disabled: "Выполнение запросов из дашборда отключено. Включите в конфигурации: 'config.allow_raw_query_execution = true'"
738
749
  query_not_found_expired: "Запрос не найден или истёк срок действия. Обновите страницу."
750
+ query_monitor_unavailable_standalone: "SQL Query Monitor недоступен в автономном режиме — нет процесса основного приложения для наблюдения."
751
+ query_timed_out: "Превышено время ожидания запроса (%{ms} мс, statement_timeout). Сузьте запрос или увеличьте config.raw_query_statement_timeout_ms."
752
+ rate_limit_exceeded: "Слишком много запросов. Подождите немного и попробуйте снова."
739
753
  security_violation_prefix: "Нарушение безопасности:"
740
754
  trigger_variables_not_allowed: "Нельзя выполнить EXPLAIN ANALYZE для запросов с триггерными переменными (NEW, OLD). Они доступны только в контексте триггерных функций."
741
755
  missing_parameter_values: "Укажите значения для всех плейсхолдеров параметров ($1, $2 и т.д.)"
@@ -587,6 +587,7 @@ uk:
587
587
  ide_settings_button_title: "Налаштування IDE"
588
588
  explain_analyze: "📊 EXPLAIN ANALYZE"
589
589
  execute_query: "▶ Виконати запит"
590
+ run_query: "SQL-консоль"
590
591
  create_migration_file: "📁 Створити файл і відкрити в IDE"
591
592
  start_monitoring: "▶ Запустити моніторинг"
592
593
  stop_monitoring: "⏹ Зупинити моніторинг"
@@ -598,11 +599,12 @@ uk:
598
599
  save_for_comparison: "📌 Зберегти для порівняння"
599
600
  saved_marker: "📌 Збережено"
600
601
  status:
601
- pg_stat_ready: "pg_stat_statements готовий"
602
- extension_installed: "Розширення встановлено, не передзавантажено"
603
- preloaded: "Передзавантажено, розширення не створено"
604
- not_configured: "Не налаштовано"
602
+ pg_stat_ready: "Активно"
603
+ not_preloaded: "Потрібне передзавантаження"
604
+ extension_missing: "Потрібне розширення"
605
+ disconnected: "Немає з'єднання"
605
606
  monitoring_unavailable: "Статус недоступний"
607
+ click_for_details: "Натисніть, щоб дізнатися, як увімкнути"
606
608
  modals:
607
609
  enable_pg_stat_title: "Увімкнення pg_stat_statements"
608
610
  enable_pg_stat_intro: "Щоб увімкнути pg_stat_statements, виконайте такі кроки:"
@@ -610,6 +612,9 @@ uk:
610
612
  restart_postgresql: "Перезапустіть PostgreSQL:"
611
613
  create_extension_step: "Створіть розширення:"
612
614
  enable_button_note: "Або натисніть кнопку «Створити розширення» після перезапуску."
615
+ create_extension_title: "Створення розширення pg_stat_statements"
616
+ create_extension_intro: "Розширення pg_stat_statements ще не створено в цій базі даних. Натисніть, щоб створити його:"
617
+ create_extension_note: "Якщо бібліотеку не передзавантажено, її також потрібно додати до shared_preload_libraries та перезапустити PostgreSQL."
613
618
  reset_stats_title: "⚠️ Скидання статистики"
614
619
  reset_stats_confirm: "Ви впевнені, що хочете скинути статистику pg_stat_statements?"
615
620
  reset_stats_warning: "Ця дія очистить усю зібрану статистику запитів і не може бути скасована."
@@ -624,6 +629,9 @@ uk:
624
629
  migration_warning_dev_only: "Цю операцію слід виконувати лише в локальному dev-середовищі."
625
630
  query_execution_disabled_title: "⚠️ Виконання запитів вимкнено"
626
631
  query_execution_disabled_intro: "Щоб увімкнути цю функцію, додайте до конфігурації:"
632
+ query_execution_disabled_env_note: "...або замість цього встановіть змінну середовища:"
633
+ run_query_title: "SQL-консоль"
634
+ run_query_placeholder: "SELECT * FROM users LIMIT 10;"
627
635
  settings:
628
636
  default_ide_label: "IDE за замовчуванням для посилань на джерела:"
629
637
  ide_show_menu: "Показувати меню (за замовчуванням)"
@@ -656,7 +664,7 @@ uk:
656
664
  cache_hit_detail: "блоки heap із кешу"
657
665
  long_queries_label: "Довгі запити"
658
666
  queries_unit: "запитів"
659
- long_running_threshold: "> 60с виконання"
667
+ long_running_threshold: "> 5с виконання"
660
668
  blocked_label: "Заблоковано"
661
669
  processes_unit: "процесів"
662
670
  waiting_for_locks: "чекають блокувань"
@@ -665,6 +673,7 @@ uk:
665
673
  requires_pg_stat: "🔒 Потрібен pg_stat_statements"
666
674
  reports_count_suffix: "звітів"
667
675
  primary_only_reason: "🔒 Ця категорія працює лише на первинній базі — вона перевіряє моделі хост-застосунку. Щоб користуватися, перемкніться на базу за замовчуванням."
676
+ standalone_no_host_app_reason: "🔒 Недоступно в автономному режимі — ця категорія перевіряє моделі хост-застосунку, а при самостійному запуску дашборда хост-застосунку немає."
668
677
  documentation:
669
678
  toggle_title: "📖 Що показує цей звіт?"
670
679
  what_section: "📋 Що"
@@ -729,6 +738,8 @@ uk:
729
738
  decode_query_failed: "Не вдалося декодувати запит"
730
739
  explain_analyze_failed: "Не вдалося виконати EXPLAIN ANALYZE"
731
740
  execute_query_failed: "Не вдалося виконати запит"
741
+ run_query_failed: "Не вдалося виконати запит"
742
+ query_required: "Введіть запит"
732
743
  create_migration_failed: "Не вдалося створити міграцію"
733
744
  explain_disabled_toast: "⚠️ EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
734
745
  execute_disabled_toast: "⚠️ Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
@@ -736,6 +747,9 @@ uk:
736
747
  query_hash_required: "Потрібен хеш запиту"
737
748
  query_execution_disabled: "Виконання запитів із дашборду вимкнено. Увімкніть у конфігурації: 'config.allow_raw_query_execution = true'"
738
749
  query_not_found_expired: "Запит не знайдено або термін дії минув. Оновіть сторінку."
750
+ query_monitor_unavailable_standalone: "SQL Query Monitor недоступний в автономному режимі — немає процесу основного застосунку для спостереження."
751
+ query_timed_out: "Перевищено час очікування запиту (%{ms} мс, statement_timeout). Звузьте запит або збільшіть config.raw_query_statement_timeout_ms."
752
+ rate_limit_exceeded: "Забагато запитів. Зачекайте трохи і спробуйте знову."
739
753
  security_violation_prefix: "Порушення безпеки:"
740
754
  trigger_variables_not_allowed: "Не можна виконати EXPLAIN ANALYZE для запитів із тригерними змінними (NEW, OLD). Вони доступні лише в контексті тригерних функцій."
741
755
  missing_parameter_values: "Вкажіть значення для всіх плейсхолдерів параметрів ($1, $2 тощо)"
data/config/routes.rb CHANGED
@@ -14,6 +14,7 @@ PgReports::Engine.routes.draw do
14
14
  post "reset_statistics", to: "dashboard#reset_statistics", as: :reset_statistics
15
15
  post "explain_analyze", to: "dashboard#explain_analyze", as: :explain_analyze
16
16
  post "execute_query", to: "dashboard#execute_query", as: :execute_query
17
+ post "run_query", to: "dashboard#run_query", as: :run_query
17
18
  post "create_migration", to: "dashboard#create_migration", as: :create_migration
18
19
 
19
20
  # Query monitoring
@@ -27,6 +27,7 @@ module PgReports
27
27
 
28
28
  # Dashboard settings
29
29
  attr_accessor :dashboard_auth # Proc for dashboard authentication
30
+ attr_accessor :standalone # True when running via PgReports::Standalone (no host app)
30
31
 
31
32
  # Assets / privacy settings
32
33
  attr_accessor :load_external_fonts # When true, loads Google Fonts in the dashboard layout
@@ -42,6 +43,9 @@ module PgReports
42
43
  # Security settings
43
44
  attr_accessor :allow_raw_query_execution # Allow execute_query and explain_analyze from dashboard
44
45
  attr_accessor :allow_migration_creation # Allow dashboard's "Generate Migration" button to write files into db/migrate/
46
+ attr_accessor :raw_query_statement_timeout_ms # Postgres statement_timeout (ms) applied to Execute Query / EXPLAIN ANALYZE / SQL Console. 0 disables it.
47
+ attr_accessor :raw_query_rate_limit # Max privileged requests (raw query execution / Generate Migration) per client IP per window. nil disables rate limiting.
48
+ attr_accessor :raw_query_rate_limit_window_seconds # Window size (seconds) for raw_query_rate_limit
45
49
 
46
50
  # Grafana / Prometheus exporter settings
47
51
  attr_accessor :grafana_favorites # Reports exposed at /metrics (Array of keys or Hash with per-report opts)
@@ -74,6 +78,7 @@ module PgReports
74
78
 
75
79
  # Dashboard
76
80
  @dashboard_auth = nil
81
+ @standalone = false
77
82
 
78
83
  # Assets / privacy
79
84
  @load_external_fonts = ActiveModel::Type::Boolean.new.cast(ENV.fetch("PG_REPORTS_LOAD_EXTERNAL_FONTS", false))
@@ -101,6 +106,9 @@ module PgReports
101
106
  else
102
107
  ActiveModel::Type::Boolean.new.cast(env_override)
103
108
  end
109
+ @raw_query_statement_timeout_ms = ENV.fetch("PG_REPORTS_RAW_QUERY_STATEMENT_TIMEOUT_MS", 5_000).to_i
110
+ @raw_query_rate_limit = ENV.fetch("PG_REPORTS_RAW_QUERY_RATE_LIMIT", 30).to_i
111
+ @raw_query_rate_limit_window_seconds = ENV.fetch("PG_REPORTS_RAW_QUERY_RATE_LIMIT_WINDOW_SECONDS", 60).to_i
104
112
 
105
113
  # Grafana / Prometheus exporter
106
114
  @grafana_favorites = []
@@ -49,7 +49,7 @@ module PgReports
49
49
  # List all databases visible on this target's cluster (using pg_database).
50
50
  # Result rows: { "name" => String, "size" => String, "current" => Boolean }
51
51
  def list_databases(current: nil)
52
- rows = connection_for.exec_query(<<~SQL).to_a
52
+ rows = connection_for.exec_query(<<~SQL, "PgReports").to_a
53
53
  SELECT datname AS name,
54
54
  pg_size_pretty(pg_database_size(datname)) AS size
55
55
  FROM pg_database
@@ -17,10 +17,15 @@ module PgReports
17
17
  execute(sql, **params)
18
18
  end
19
19
 
20
- # Execute raw SQL and return results as array of hashes
20
+ # Execute raw SQL and return results as array of hashes.
21
+ #
22
+ # Every query is tagged with the "PgReports" AR statement name so the Query
23
+ # Monitor can skip our own queries by name (see QueryMonitor#should_skip?),
24
+ # reliably and independent of backtrace depth — the internal live_metrics /
25
+ # status polling would otherwise leak into the monitor's history.
21
26
  def execute(sql, **params)
22
27
  processed_sql = interpolate_params(sql, params)
23
- result = connection.exec_query(processed_sql)
28
+ result = connection.exec_query(processed_sql, "PgReports")
24
29
  result.to_a
25
30
  end
26
31
 
@@ -105,7 +105,11 @@ module PgReports
105
105
  mod = module_for(key) or raise ArgumentError, "Unknown report: #{key}"
106
106
 
107
107
  started = @clock.now
108
- report = mod.public_send(key, **report_args(opts))
108
+ args = report_args(opts)
109
+ # Call with no arguments when there are no kwargs to forward. On Ruby 2.7
110
+ # `public_send(key, **{})` does not reliably elide to a no-arg call, which
111
+ # breaks report methods (and `have_received(...).with(no_args)` matchers).
112
+ report = args.empty? ? mod.public_send(key) : mod.public_send(key, **args)
109
113
  finished = @clock.now
110
114
 
111
115
  {
@@ -44,13 +44,37 @@ module PgReports
44
44
  false
45
45
  end
46
46
 
47
- # Get pg_stat_statements status details
47
+ # Whether the database connection can execute a basic query.
48
+ # Used to tell "the connection itself is down" apart from
49
+ # "connected, but pg_stat_statements isn't set up yet".
50
+ # @return [Boolean]
51
+ def connected?
52
+ executor.execute("SELECT 1")
53
+ true
54
+ rescue
55
+ false
56
+ end
57
+
58
+ # Get pg_stat_statements status details.
59
+ #
60
+ # Note: whether pg_stat_statements is in shared_preload_libraries cannot be
61
+ # read by a plain monitoring role (that requires the pg_read_all_settings
62
+ # role), so we never look at the setting. Instead we derive the state from
63
+ # signals every role can observe: can we run a query at all, does the
64
+ # extension exist in pg_extension, and is its view queryable.
65
+ #
48
66
  # @return [Hash] Status information
49
67
  def pg_stat_statements_status
68
+ unless connected?
69
+ return {connected: false, extension_installed: false, preloaded: false, ready: false}
70
+ end
71
+
72
+ installed = pg_stat_statements_available?
50
73
  {
51
- extension_installed: pg_stat_statements_available?,
74
+ connected: true,
75
+ extension_installed: installed,
52
76
  preloaded: pg_stat_statements_preloaded?,
53
- ready: pg_stat_statements_available? && pg_stat_statements_preloaded?
77
+ ready: installed && pg_stat_statements_preloaded?
54
78
  }
55
79
  end
56
80
 
@@ -58,7 +82,7 @@ module PgReports
58
82
  # @param long_query_threshold [Integer] Threshold in seconds for long queries
59
83
  # @return [Hash] Metrics data
60
84
  # @raise [StandardError] If no data is returned
61
- def live_metrics(long_query_threshold: 60)
85
+ def live_metrics(long_query_threshold: 5)
62
86
  data = executor.execute_from_file(:system, :live_metrics,
63
87
  long_query_threshold: long_query_threshold)
64
88