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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +12 -8
- data/app/controllers/pg_reports/dashboard_controller.rb +277 -221
- data/app/controllers/pg_reports/metrics_controller.rb +13 -4
- data/app/views/layouts/pg_reports/application.html.erb +222 -93
- data/app/views/pg_reports/dashboard/_settings_modal.html.erb +55 -0
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +1 -42
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +75 -105
- data/app/views/pg_reports/dashboard/index.html.erb +346 -238
- data/app/views/pg_reports/dashboard/show.html.erb +1 -1
- data/bin/pg_reports +2 -2
- data/config/brakeman.ignore +10 -56
- data/config/locales/en.yml +45 -11
- data/config/locales/ru.yml +44 -11
- data/config/locales/uk.yml +44 -11
- data/config/routes.rb +2 -0
- data/lib/pg_reports/access_policy.rb +39 -0
- data/lib/pg_reports/compatibility.rb +10 -2
- data/lib/pg_reports/configuration.rb +33 -2
- data/lib/pg_reports/connection/error_translator.rb +21 -0
- data/lib/pg_reports/connection/target.rb +43 -10
- data/lib/pg_reports/csv_cell.rb +20 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +37 -30
- data/lib/pg_reports/definitions/connections/connection_stats.yml +4 -0
- data/lib/pg_reports/definitions/connections/idle_connections.yml +4 -0
- data/lib/pg_reports/definitions/connections/locks.yml +1 -0
- data/lib/pg_reports/definitions/connections/long_running_queries.yml +0 -1
- data/lib/pg_reports/definitions/indexes/bloated_indexes.yml +1 -2
- data/lib/pg_reports/definitions/indexes/index_correlation.yml +2 -1
- data/lib/pg_reports/definitions/indexes/missing_indexes.yml +1 -0
- data/lib/pg_reports/definitions/queries/frequent_queries.yml +50 -0
- data/lib/pg_reports/definitions/system/cache_stats.yml +2 -1
- data/lib/pg_reports/definitions/tables/bloated_tables.yml +1 -2
- data/lib/pg_reports/definitions/tables/recently_modified.yml +3 -3
- data/lib/pg_reports/definitions/tables/unused_tables.yml +1 -1
- data/lib/pg_reports/drop_index_migration.rb +56 -0
- data/lib/pg_reports/engine.rb +4 -3
- data/lib/pg_reports/executor.rb +46 -9
- data/lib/pg_reports/modules/queries.rb +1 -0
- data/lib/pg_reports/query_monitor.rb +478 -192
- data/lib/pg_reports/rate_limiter.rb +63 -0
- data/lib/pg_reports/report.rb +1 -1
- data/lib/pg_reports/report_definition.rb +11 -1
- data/lib/pg_reports/sql/connections/connection_stats.sql +2 -1
- data/lib/pg_reports/sql/connections/idle_connections.sql +1 -0
- data/lib/pg_reports/sql/connections/locks.sql +1 -0
- data/lib/pg_reports/sql/indexes/bloated_indexes.sql +16 -8
- data/lib/pg_reports/sql/queries/activity_sample.sql +22 -0
- data/lib/pg_reports/sql/queries/frequent_queries.sql +42 -0
- data/lib/pg_reports/sql_redactor.rb +32 -0
- data/lib/pg_reports/standalone.rb +83 -20
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +7 -2
- 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="
|
|
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,
|
|
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
|
|
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 }
|
data/config/brakeman.ignore
CHANGED
|
@@ -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": "
|
|
28
|
+
"fingerprint": "d79e7adc175add96cdc7054b79ce7861dca0ce3dfd0bd8ec025f953b62806ae3",
|
|
75
29
|
"check_name": "SQL",
|
|
76
30
|
"message": "Possible SQL injection",
|
|
77
|
-
"file": "
|
|
78
|
-
"line":
|
|
31
|
+
"file": "lib/pg_reports/executor.rb",
|
|
32
|
+
"line": 42,
|
|
79
33
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
80
|
-
"code": "
|
|
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::
|
|
85
|
-
"method": "
|
|
38
|
+
"class": "PgReports::Executor",
|
|
39
|
+
"method": "execute"
|
|
86
40
|
},
|
|
87
|
-
"user_input": "params
|
|
88
|
-
"confidence": "
|
|
41
|
+
"user_input": "interpolate_params(sql, params)",
|
|
42
|
+
"confidence": "Medium",
|
|
89
43
|
"cwe_id": [
|
|
90
44
|
89
|
|
91
45
|
],
|
|
92
|
-
"note": "Reviewed & accepted:
|
|
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
|
|
49
|
+
"updated": "2026-11-07 12:00:00 +0300",
|
|
96
50
|
"brakeman_version": "8.0.5"
|
|
97
51
|
}
|
data/config/locales/en.yml
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
798
|
-
|
|
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"
|
data/config/locales/ru.yml
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
665
|
+
auto_stop_label: "Остановится через"
|
|
652
666
|
queries_label: "Запросов:"
|
|
653
667
|
feed_empty: "Нажмите «Запустить мониторинг», чтобы начать захват SQL-запросов"
|
|
654
668
|
feed_no_queries: "Запросы пока не захвачены..."
|
|
655
669
|
unknown_source: "Источник неизвестен"
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
763
|
-
|
|
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: "Запросы с наибольшим суммарным временем"
|
data/config/locales/uk.yml
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
665
|
+
auto_stop_label: "Зупиниться через"
|
|
652
666
|
queries_label: "Запитів:"
|
|
653
667
|
feed_empty: "Натисніть «Запустити моніторинг», щоб почати захоплення SQL-запитів"
|
|
654
668
|
feed_no_queries: "Запитів поки не захоплено..."
|
|
655
669
|
unknown_source: "Джерело невідоме"
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
763
|
-
|
|
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
|
-
#
|
|
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 = "
|
|
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!
|