pg_reports 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +36 -150
- data/app/controllers/pg_reports/dashboard_controller.rb +143 -3
- data/app/views/layouts/pg_reports/application.html.erb +213 -6
- data/app/views/pg_reports/dashboard/_database_selector.html.erb +39 -0
- data/app/views/pg_reports/dashboard/_target_selector.html.erb +27 -0
- data/app/views/pg_reports/dashboard/index.html.erb +143 -50
- data/app/views/pg_reports/dashboard/show.html.erb +4 -0
- data/bin/pg_reports +85 -0
- data/config/locales/en.yml +18 -7
- data/config/locales/ru.yml +18 -7
- data/config/locales/uk.yml +18 -7
- data/config/routes.rb +3 -0
- data/lib/pg_reports/configuration.rb +32 -2
- data/lib/pg_reports/connection/error_translator.rb +109 -0
- data/lib/pg_reports/connection/registry.rb +150 -0
- data/lib/pg_reports/connection/target.rb +111 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +22 -8
- data/lib/pg_reports/executor.rb +20 -6
- data/lib/pg_reports/modules/system.rb +32 -5
- data/lib/pg_reports/query_monitor.rb +10 -7
- data/lib/pg_reports/standalone.rb +152 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +57 -0
- metadata +11 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ff4489af5016c4bfbe14f7187daa5ca2ea279b15887bab928c8effefc4b2dad
|
|
4
|
+
data.tar.gz: 26815859090bbf8a84000acb3545c1eecb982887f8d0f07d9ab0e582f2380c58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f94d1f175001f3a8140d8ef9b826d353bb20963d3b75487aace882fde54186466be500840252bc9e87268e55244bb96746f59fd82b6ba648271a828b596b47d4
|
|
7
|
+
data.tar.gz: f89ff6622a18f3daf33fabf898a8736da510e2ecf84f4455e5720a8a07f3a219ed1336a1f1a286437a23da505560e724679bb27957b74b501567db2cfaee9f17
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.1] - 2026-07-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Standalone mode — run the dashboard without a host Rails app.** A new `pg_reports server` executable (and `rake pg_reports:server` task) boots a minimal Rails application that mounts the engine at `/` and serves it on port **4000**, straight from the gem's root folder. The connection is resolved from `--database-url`, then `DATABASE_URL`, then libpq-style `PG*` env vars; flags cover `--port`, `--host`, `--mount`, and `--server`. All existing multi-database / multi-cluster switching works unchanged, since the connection registry auto-registers the standalone connection as the `:primary` target.
|
|
15
|
+
- **No new runtime dependencies.** `rack` and `rackup` already ship transitively via `actionpack`/`railties`; the web server (Puma, WEBrick, …) is resolved at run time and is not a hard dependency — the runner uses whichever is installed and prints a clear message if none is.
|
|
16
|
+
- New `PgReports::Standalone` module encapsulating app construction, connection resolution, and server boot.
|
|
17
|
+
- `./bin/pg_reports` runs from a checkout without `bundle exec` via a soft bundler shim (activated only when a Gemfile sits next to the executable; skipped for the installed gem).
|
|
18
|
+
- **[docs/standalone.md](docs/standalone.md).**
|
|
19
|
+
- **Dashboard footer** with links to the project on GitHub and a contact address.
|
|
20
|
+
- **ESC closes any open modal.** A single global handler triggers the modal's own close button, so per-modal cleanup still runs.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Redesigned the pg_stat_statements status badge.** The header badge now reports four clearly distinguished states with plain-language labels instead of a raw identifier glued to an adjective (`pg_stat_statements готовий`):
|
|
25
|
+
- 🟢 **Active** — monitoring works.
|
|
26
|
+
- 🟡 **Preload required** — the extension exists but isn't in `shared_preload_libraries`; clicking the badge opens the setup instructions.
|
|
27
|
+
- 🟡 **Extension required** — the library is loaded but the extension isn't created; clicking opens a modal with a one-click **Create extension** button.
|
|
28
|
+
- 🔴 **No connection** — the database itself is unreachable.
|
|
29
|
+
Warning/error badges are now clickable and self-explanatory. The redundant `?` info button and the header **Create extension** button were removed in favor of the badge-driven modals. Labels no longer use a negative "Not …" framing. Translations updated for `en` / `uk` / `ru`.
|
|
30
|
+
- **pg_stat_statements status detection no longer reads `shared_preload_libraries`.** That setting requires the `pg_read_all_settings` role and is unreadable by a typical monitoring user, which made the "preloaded but extension missing" state unreachable. State is now derived entirely from signals every role can observe: connectivity (`SELECT 1`), extension presence in `pg_extension`, and whether the `pg_stat_statements` view is queryable. `pg_stat_statements_status` gains a `connected:` key, and `PgReports.system.connected?` is a new public helper.
|
|
31
|
+
- **Primary buttons toned down.** `btn-primary` (Start monitoring, Run report, Create extension, …) switched from a solid accent fill to a subtle tinted style, so it reads as the accent action without dominating the page.
|
|
32
|
+
- **Live-metrics "long queries" threshold lowered from 60s to 5s** — the default the top-of-dashboard tile counts against.
|
|
33
|
+
- **Header/layout polish** — the status badge, settings button, and Reset button are unified to the same height; the settings button is now square; the pg_stat_statements category warning banner no longer overhangs its card; and the "scope: host application" note in the query monitor uses a real styled tooltip (hover + keyboard focus) instead of the unreliable native `title`.
|
|
34
|
+
- **README trimmed**: standalone, Telegram, and Grafana/Prometheus details moved to dedicated docs ([docs/standalone.md](docs/standalone.md), [docs/telegram.md](docs/telegram.md), [docs/grafana.md](docs/grafana.md)).
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **Query Monitor no longer records pg_reports' own queries.** Internal queries (live-metrics polling, status checks, database listing) run through `Executor`/`Target` are now tagged with a `"PgReports"` statement name, so `QueryMonitor#should_skip?` filters them by name regardless of backtrace depth. Previously the deep `ActiveSupport::Notifications` stack could push the pg_reports frames past the 30-frame backtrace scan, leaking these queries into the monitor history.
|
|
39
|
+
- **Dashboard requests broke when the engine is mounted at the root path** (e.g. standalone). The client base path resolved to `/`, so `fetch` URLs became `//live_metrics` — a protocol-relative URL the browser sent to a bogus host, breaking live metrics and report runs. The base now strips a trailing slash.
|
|
40
|
+
|
|
41
|
+
## [0.8.0] - 2026-05-01
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **Multi-database dashboard.** Switch between databases on the connected cluster from a dropdown in the dashboard header — no configuration required. The selected database persists across requests in the session and applies to every report on every page.
|
|
46
|
+
- New `PgReports::Connection::Registry` with auto-registered `:primary` target derived from `ActiveRecord::Base.connection_db_config`. Existing setups need no changes.
|
|
47
|
+
- New block-scoped APIs: `PgReports.with_target(:name, database: ...)`, `PgReports.with_database("name")`. Honored by `Executor` even when memoized inside modules — the connection is now resolved on every call rather than at construction.
|
|
48
|
+
- New helpers: `PgReports.list_databases`, `PgReports.list_targets`, `PgReports.current_target_name`, `PgReports.current_database_name`.
|
|
49
|
+
- New `Configuration#add_target(name, spec)` and `default_target=` for explicitly registering additional targets (host/port/user/database) when the dashboard should reach databases the host app cannot.
|
|
50
|
+
- Database switching opens an isolated AR connection pool per `(target, database)` so the host application's pool is never disturbed; the primary target's default database keeps using `ActiveRecord::Base` directly.
|
|
51
|
+
- **Human-readable connection errors.** `PgReports::Connection::ErrorTranslator` maps `PG::Error` SQLSTATEs (`42501`, `3D000`, `28P01`, `08006`, `53300`) and AR-wrapped variants into a `{ title, detail, hint, code }` hash. Permission errors include a concrete `GRANT ...` remediation hint. The dashboard renders the translation as a banner on the index when it can't list databases.
|
|
52
|
+
- **Schema Analysis category gated to the primary target.** When the dashboard is pointed at a non-primary database (where the host app's models don't apply), the Schema Analysis category is greyed out with an explanation, and direct URL access redirects to the index with a flash message. Same gating extends to the JSON endpoints (`run`, `download`, `send_to_telegram`).
|
|
53
|
+
- **Configuration reference** moved to [docs/configuration.md](docs/configuration.md).
|
|
54
|
+
|
|
55
|
+
### Security
|
|
56
|
+
|
|
57
|
+
- **CSRF protection** is now enforced on the dashboard controller (`protect_from_forgery with: :exception`). Previously the controller inherited from `ActionController::Base` without opting in — every state-changing endpoint (`switch_database`, `switch_target`, `execute_query`, `explain_analyze`, `create_migration`, `reset_statistics`, telegram delivery, query_monitor start/stop) was reachable cross-origin from any logged-in user's browser. The dashboard already shipped `authenticity_token` in forms and `X-CSRF-Token` in XHR — only the server enforcement was missing.
|
|
58
|
+
- **`create_migration` is now opt-in via `config.allow_migration_creation`** (default `Rails.env.development?` to preserve prior behavior; toggleable via `PG_REPORTS_ALLOW_MIGRATION_CREATION`). The endpoint writes Ruby code into `db/migrate/`. With CSRF fixed and a denied-by-default flag, a dashboard exposed without auth no longer trivially leads to RCE on next `rails db:migrate`. Combine with `dashboard_auth` for layered defense.
|
|
59
|
+
|
|
10
60
|
## [0.7.0] - 2026-04-26
|
|
11
61
|
|
|
12
62
|
### Added
|
data/README.md
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
[](https://rubyonrails.org/)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
A comprehensive PostgreSQL monitoring and analysis library for Rails applications. Get insights into query performance, index usage, table statistics, connection health, and more. Includes a beautiful web dashboard and Telegram
|
|
8
|
+
A comprehensive PostgreSQL monitoring and analysis library for Rails applications. Get insights into query performance, index usage, table statistics, connection health, and more — across **every database on the cluster**, switchable from the dashboard with no extra configuration. Includes a beautiful web dashboard, a Grafana / Prometheus exporter, and Telegram delivery.
|
|
9
9
|
|
|
10
10
|

|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
|
+
- 🗄️ **Multi-database** - Auto-discovers every database on the cluster and lets you switch from a dropdown in the dashboard. No configuration required.
|
|
14
15
|
- 📊 **Query Analysis** - Identify slow, heavy, and expensive queries using `pg_stat_statements`
|
|
15
16
|
- 📇 **Index Analysis** - Find unused, duplicate, invalid, and missing indexes
|
|
16
17
|
- 📋 **Table Statistics** - Monitor table sizes, bloat, vacuum needs, and cache hit ratios
|
|
@@ -58,7 +59,18 @@ end
|
|
|
58
59
|
|
|
59
60
|
Visit `http://localhost:3000/pg_reports`.
|
|
60
61
|
|
|
61
|
-
For query analysis, also enable `pg_stat_statements` — see [setup](#pg_stat_statements-setup)
|
|
62
|
+
For query analysis, also enable `pg_stat_statements` — see [setup instructions in docs/configuration.md](docs/configuration.md#pg_stat_statements-setup).
|
|
63
|
+
|
|
64
|
+
## Standalone (no host app)
|
|
65
|
+
|
|
66
|
+
You can also run the dashboard on its own, straight from the gem's root folder — no Rails app to mount it in. It serves at `/` on port **4000** and connects via `DATABASE_URL` or libpq env vars:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
./bin/pg_reports server # from a checkout; no `bundle exec` needed
|
|
70
|
+
DATABASE_URL=postgres://user:pass@localhost/myapp bundle exec pg_reports server
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Adds no runtime dependencies to the gem. **[Standalone guide → docs/standalone.md](docs/standalone.md)**
|
|
62
74
|
|
|
63
75
|
## Usage
|
|
64
76
|
|
|
@@ -72,114 +84,45 @@ report = PgReports.expensive_queries
|
|
|
72
84
|
report.to_text
|
|
73
85
|
report.to_csv
|
|
74
86
|
report.to_a
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**[Full list of reports →](docs/reports.md)** · **[Send reports to Telegram →](docs/telegram.md)**
|
|
90
|
+
|
|
91
|
+
## Multi-database
|
|
92
|
+
|
|
93
|
+
The dashboard auto-discovers every database on the cluster you're connected to and shows a dropdown next to the *Status* panel. Switching is zero-config — credentials and host come from your existing `database.yml`. Schema-analysis reports stay scoped to the primary database (they introspect the host app's models); the dropdown greys them out elsewhere.
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
Programmatic access:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
PgReports.with_database("logs") { PgReports.table_sizes }
|
|
99
|
+
PgReports.with_target(:analytics) { PgReports.slow_queries }
|
|
78
100
|
```
|
|
79
101
|
|
|
80
|
-
**[
|
|
102
|
+
For multi-cluster setups (separate analytics warehouse, replica with different credentials, etc.), register additional targets explicitly. **[Multi-database reference in docs/configuration.md →](docs/configuration.md#multi-database-support)**
|
|
81
103
|
|
|
82
104
|
## Configuration
|
|
83
105
|
|
|
106
|
+
PgReports works out of the box once mounted. Common options:
|
|
107
|
+
|
|
84
108
|
```ruby
|
|
85
109
|
# config/initializers/pg_reports.rb
|
|
86
110
|
PgReports.configure do |config|
|
|
87
|
-
# Telegram (optional)
|
|
88
|
-
config.telegram_bot_token = ENV["PG_REPORTS_TELEGRAM_TOKEN"]
|
|
89
|
-
config.telegram_chat_id = ENV["PG_REPORTS_TELEGRAM_CHAT_ID"]
|
|
90
|
-
|
|
91
|
-
# Thresholds
|
|
92
111
|
config.slow_query_threshold_ms = 100
|
|
93
|
-
config.heavy_query_threshold_calls = 1000
|
|
94
|
-
config.expensive_query_threshold_ms = 10_000
|
|
95
112
|
config.unused_index_threshold_scans = 50
|
|
96
113
|
config.bloat_threshold_percent = 20
|
|
97
|
-
config.dead_rows_threshold = 10_000
|
|
98
|
-
|
|
99
|
-
# Output
|
|
100
|
-
config.max_query_length = 200
|
|
101
114
|
|
|
102
|
-
#
|
|
115
|
+
# Strongly recommended in production
|
|
103
116
|
config.dashboard_auth = -> {
|
|
104
117
|
authenticate_or_request_with_http_basic do |user, pass|
|
|
105
118
|
user == ENV["PG_REPORTS_USER"] && pass == ENV["PG_REPORTS_PASSWORD"]
|
|
106
119
|
end
|
|
107
120
|
}
|
|
108
|
-
|
|
109
|
-
# Google Fonts (default: false — no external requests)
|
|
110
|
-
config.load_external_fonts = false
|
|
111
121
|
end
|
|
112
122
|
```
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
PgReports follows your application's `I18n.locale`. Set it the way you set it for the rest of the app — there's no PgReports-specific knob. The dashboard supports `en`, `ru`, and `uk` out of the box.
|
|
118
|
-
|
|
119
|
-
</details>
|
|
120
|
-
|
|
121
|
-
<details>
|
|
122
|
-
<summary><strong>Raw query execution (EXPLAIN ANALYZE / Execute Query)</strong></summary>
|
|
123
|
-
|
|
124
|
-
⚠️ Disabled by default. The dashboard's "Execute Query" and "EXPLAIN ANALYZE" buttons require this opt-in.
|
|
125
|
-
|
|
126
|
-
```ruby
|
|
127
|
-
PgReports.configure do |config|
|
|
128
|
-
config.allow_raw_query_execution = Rails.env.development? || Rails.env.staging?
|
|
129
|
-
end
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
</details>
|
|
133
|
-
|
|
134
|
-
<details>
|
|
135
|
-
<summary><strong>Query source tracking (Rails query logs)</strong></summary>
|
|
136
|
-
|
|
137
|
-
PgReports parses query annotations to show **where queries originated**. On Rails 7.0+ use the built-in `ActiveRecord::QueryLogs` (no extra gem needed). On older Rails, install [Marginalia](https://github.com/basecamp/marginalia) — PgReports auto-detects both formats.
|
|
138
|
-
|
|
139
|
-
Minimal setup — adds controller/action:
|
|
140
|
-
|
|
141
|
-
```ruby
|
|
142
|
-
# config/application.rb
|
|
143
|
-
config.active_record.query_log_tags_enabled = true
|
|
144
|
-
config.active_record.query_log_tags = [:controller, :action]
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
To also surface **file path and line number** (so source links jump to the actual call site, not just the controller), add a custom `source_location` lambda that walks `caller_locations` and skips gem/framework frames:
|
|
148
|
-
|
|
149
|
-
```ruby
|
|
150
|
-
# config/application.rb
|
|
151
|
-
config.active_record.query_log_tags_enabled = true
|
|
152
|
-
config.active_record.query_log_tags = [
|
|
153
|
-
:controller,
|
|
154
|
-
:action,
|
|
155
|
-
:job,
|
|
156
|
-
{
|
|
157
|
-
source_location: -> {
|
|
158
|
-
ignore = %r{/(gems|active_record|active_support|active_model|railties|
|
|
159
|
-
action_controller|action_view|action_pack|action_dispatch|
|
|
160
|
-
rack|core_ext|relation|associations|scoping|connection_adapters)/}x
|
|
161
|
-
loc = caller_locations.find { |l| !l.path.match?(ignore) }
|
|
162
|
-
"#{loc.path}:#{loc.lineno}" if loc
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
]
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
PgReports recognizes the `source_location` tag and splits it into file and line for the **source** column.
|
|
169
|
-
|
|
170
|
-
</details>
|
|
171
|
-
|
|
172
|
-
## pg_stat_statements setup
|
|
173
|
-
|
|
174
|
-
1. Edit `postgresql.conf`:
|
|
175
|
-
```
|
|
176
|
-
shared_preload_libraries = 'pg_stat_statements'
|
|
177
|
-
pg_stat_statements.track = all
|
|
178
|
-
```
|
|
179
|
-
2. Restart PostgreSQL: `sudo systemctl restart postgresql`
|
|
180
|
-
3. Create the extension (via dashboard button or `PgReports.enable_pg_stat_statements!`).
|
|
181
|
-
|
|
182
|
-
> PgReports does **not** require the `pg_read_all_settings` role — extension availability is detected directly. Works with CloudnativePG, managed databases, and other restricted environments.
|
|
124
|
+
**Multi-database, thresholds, query monitor, raw query execution, source tracking, locale —**
|
|
125
|
+
**[full reference in docs/configuration.md →](docs/configuration.md)** · **[Telegram](docs/telegram.md)** · **[Grafana / Prometheus](docs/grafana.md)**
|
|
183
126
|
|
|
184
127
|
## Report object
|
|
185
128
|
|
|
@@ -303,66 +246,9 @@ The Export dropdown includes **Copy Prompt** (visible on actionable reports). It
|
|
|
303
246
|
<details>
|
|
304
247
|
<summary><strong>Grafana / Prometheus exporter</strong></summary>
|
|
305
248
|
|
|
306
|
-
Expose selected reports at `<mount_point>/metrics` in Prometheus exposition format
|
|
307
|
-
|
|
308
|
-
```ruby
|
|
309
|
-
PgReports.configure do |config|
|
|
310
|
-
config.grafana_favorites = [
|
|
311
|
-
:slow_queries,
|
|
312
|
-
:unused_indexes,
|
|
313
|
-
:bloated_tables,
|
|
314
|
-
:missing_validations,
|
|
315
|
-
:polymorphic_without_index
|
|
316
|
-
]
|
|
317
|
-
config.grafana_metrics_token = ENV["PG_REPORTS_METRICS_TOKEN"] # optional bearer token
|
|
318
|
-
config.grafana_cache_ttl = 60 # seconds
|
|
319
|
-
end
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
Scrape with Prometheus:
|
|
323
|
-
|
|
324
|
-
```yaml
|
|
325
|
-
scrape_configs:
|
|
326
|
-
- job_name: pg_reports
|
|
327
|
-
metrics_path: /pg_reports/metrics # adjust to your Engine mount point
|
|
328
|
-
scrape_interval: 60s
|
|
329
|
-
authorization: { credentials: "${PG_REPORTS_METRICS_TOKEN}" }
|
|
330
|
-
static_configs:
|
|
331
|
-
- targets: ["app.internal:3000"]
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
> [!WARNING]
|
|
335
|
-
> Reports are cached via `Rails.cache` for `grafana_cache_ttl` so frequent scrapes don't hammer the database. Without it, Prometheus' default 15s scrape interval against heavy reports like `missing_validations` will DDoS your own DB. Always set a TTL ≥ scrape interval, and consider a longer per-report TTL for expensive reports.
|
|
336
|
-
|
|
337
|
-
The exporter also emits a `pg_reports_row` series per report row (each column becomes a Prometheus label), so the auto-generated dashboard can show a **table panel** with the actual rows that need fixing — not just an aggregate count.
|
|
338
|
-
|
|
339
|
-
Generate a matching Grafana dashboard from the same favorites:
|
|
340
|
-
|
|
341
|
-
```bash
|
|
342
|
-
bundle exec rake pg_reports:grafana:dashboard
|
|
343
|
-
# writes pg_reports.json in pwd; then Dashboards → Import in Grafana
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
**[Full Grafana integration guide →](docs/grafana.md)** · **[Local Prometheus + Grafana without Docker →](docs/grafana-local-setup.md)**
|
|
347
|
-
|
|
348
|
-
</details>
|
|
349
|
-
|
|
350
|
-
<details>
|
|
351
|
-
<summary><strong>Telegram delivery</strong></summary>
|
|
352
|
-
|
|
353
|
-
Get a bot token from [@BotFather](https://t.me/BotFather) and your chat ID from [@userinfobot](https://t.me/userinfobot), then:
|
|
354
|
-
|
|
355
|
-
```ruby
|
|
356
|
-
PgReports.configure do |config|
|
|
357
|
-
config.telegram_bot_token = "123456:ABC-DEF..."
|
|
358
|
-
config.telegram_chat_id = "-1001234567890"
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
PgReports.slow_queries.send_to_telegram
|
|
362
|
-
PgReports.health_report.send_to_telegram_as_file
|
|
363
|
-
```
|
|
249
|
+
Expose selected reports at `<mount_point>/metrics` in Prometheus exposition format, with severity (`ok` / `warning` / `critical`) derived automatically from each report's thresholds. Reports are cached per a configurable TTL so frequent scrapes don't hammer the database, and a matching Grafana dashboard can be generated from the same favorites (`rake pg_reports:grafana:dashboard`).
|
|
364
250
|
|
|
365
|
-
|
|
251
|
+
**[Grafana / Prometheus integration guide →](docs/grafana.md)** · **[Local Prometheus + Grafana without Docker →](docs/grafana-local-setup.md)**
|
|
366
252
|
|
|
367
253
|
</details>
|
|
368
254
|
|
|
@@ -386,8 +272,8 @@ bundle exec rubocop
|
|
|
386
272
|
|
|
387
273
|
## License
|
|
388
274
|
|
|
389
|
-
|
|
275
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
390
276
|
|
|
391
277
|
## Acknowledgments
|
|
392
278
|
|
|
393
|
-
Inspired by [rails-pg-extras](https://github.com/pawurb/rails-pg-extras)
|
|
279
|
+
Inspired by [rails-pg-extras](https://github.com/pawurb/rails-pg-extras) and built with ❤️ for the Rails community.
|
|
@@ -4,14 +4,59 @@ module PgReports
|
|
|
4
4
|
class DashboardController < ActionController::Base
|
|
5
5
|
layout "pg_reports/application"
|
|
6
6
|
|
|
7
|
+
# CSRF protection. ActionController::Base does NOT enforce this by default;
|
|
8
|
+
# we opt in explicitly because every state-changing endpoint here
|
|
9
|
+
# (switch_*, execute_query, create_migration, reset_statistics, telegram
|
|
10
|
+
# delivery, query_monitor start/stop) is sensitive. The dashboard already
|
|
11
|
+
# ships authenticity_token in forms and X-CSRF-Token in XHR.
|
|
12
|
+
protect_from_forgery with: :exception
|
|
13
|
+
|
|
7
14
|
before_action :authenticate_dashboard!, if: -> { PgReports.config.dashboard_auth.present? }
|
|
8
15
|
before_action :set_categories
|
|
16
|
+
before_action :resolve_database_selection
|
|
17
|
+
around_action :within_selected_database
|
|
18
|
+
|
|
19
|
+
helper_method :category_disabled_reason, :category_disabled?
|
|
9
20
|
|
|
10
21
|
def index
|
|
11
22
|
@pg_stat_status = PgReports.pg_stat_statements_status
|
|
12
23
|
@current_database = PgReports.system.current_database
|
|
13
24
|
end
|
|
14
25
|
|
|
26
|
+
# POST /switch_database
|
|
27
|
+
# Persists the chosen database in session and redirects back. The actual
|
|
28
|
+
# connection switch happens on the next request via #within_selected_database.
|
|
29
|
+
def switch_database
|
|
30
|
+
requested = params[:database].to_s
|
|
31
|
+
|
|
32
|
+
if requested.empty?
|
|
33
|
+
session.delete(:pg_reports_database)
|
|
34
|
+
elsif valid_database?(requested)
|
|
35
|
+
session[:pg_reports_database] = requested
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
redirect_back fallback_location: root_path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# POST /switch_target
|
|
42
|
+
# Persists the chosen target in session, clears the database choice (each
|
|
43
|
+
# target has its own list of databases), and redirects back.
|
|
44
|
+
def switch_target
|
|
45
|
+
requested = params[:target].to_s
|
|
46
|
+
|
|
47
|
+
if requested.empty?
|
|
48
|
+
session.delete(:pg_reports_target)
|
|
49
|
+
session.delete(:pg_reports_database)
|
|
50
|
+
elsif PgReports.connection_registry.target?(requested)
|
|
51
|
+
session[:pg_reports_target] = requested
|
|
52
|
+
# Database list is target-specific; reset so the next request picks the
|
|
53
|
+
# new target's default rather than carrying a stale name.
|
|
54
|
+
session.delete(:pg_reports_database)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
redirect_back fallback_location: root_path
|
|
58
|
+
end
|
|
59
|
+
|
|
15
60
|
def enable_pg_stat_statements
|
|
16
61
|
result = PgReports.enable_pg_stat_statements!
|
|
17
62
|
render json: result
|
|
@@ -25,7 +70,7 @@ module PgReports
|
|
|
25
70
|
end
|
|
26
71
|
|
|
27
72
|
def live_metrics
|
|
28
|
-
threshold = params[:long_query_threshold]&.to_i ||
|
|
73
|
+
threshold = params[:long_query_threshold]&.to_i || 5
|
|
29
74
|
|
|
30
75
|
# Check if we have access to required statistics
|
|
31
76
|
begin
|
|
@@ -72,6 +117,12 @@ module PgReports
|
|
|
72
117
|
return
|
|
73
118
|
end
|
|
74
119
|
|
|
120
|
+
reason = category_disabled_reason(@category)
|
|
121
|
+
if reason
|
|
122
|
+
redirect_to root_path, alert: reason
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
75
126
|
# Get documentation for the report
|
|
76
127
|
@documentation = Dashboard::ReportsRegistry.documentation(@report_key)
|
|
77
128
|
@thresholds = Dashboard::ReportsRegistry.thresholds(@report_key)
|
|
@@ -322,8 +373,7 @@ module PgReports
|
|
|
322
373
|
end
|
|
323
374
|
|
|
324
375
|
def create_migration
|
|
325
|
-
|
|
326
|
-
unless Rails.env.development?
|
|
376
|
+
unless PgReports.config.allow_migration_creation
|
|
327
377
|
render json: {
|
|
328
378
|
success: false,
|
|
329
379
|
error: I18n.t("pg_reports.ui.errors.migration_dev_only")
|
|
@@ -492,6 +542,68 @@ module PgReports
|
|
|
492
542
|
@categories = Dashboard::ReportsRegistry.all
|
|
493
543
|
end
|
|
494
544
|
|
|
545
|
+
# Resolves which target/database every action should run against, based on:
|
|
546
|
+
# 1. session[:pg_reports_target] (set by #switch_target)
|
|
547
|
+
# 2. session[:pg_reports_database] (set by #switch_database)
|
|
548
|
+
# Falls back to the registry's default target and that target's default
|
|
549
|
+
# database when either is missing or invalid.
|
|
550
|
+
#
|
|
551
|
+
# Exposes @selected_target / @available_targets / @selected_database /
|
|
552
|
+
# @available_databases / @target_default_database / @database_error for the
|
|
553
|
+
# layout to render the selectors. Connection errors are swallowed so the
|
|
554
|
+
# dashboard still renders with a banner instead of a 500.
|
|
555
|
+
def resolve_database_selection
|
|
556
|
+
registry = PgReports.connection_registry
|
|
557
|
+
|
|
558
|
+
# Target resolution (must come first — database list depends on it)
|
|
559
|
+
@available_targets = registry.targets
|
|
560
|
+
requested_target = session[:pg_reports_target].to_s
|
|
561
|
+
@selected_target = if requested_target.present? && registry.target?(requested_target)
|
|
562
|
+
requested_target.to_sym
|
|
563
|
+
else
|
|
564
|
+
registry.default_name
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Resolve database list under the chosen target so list_databases hits
|
|
568
|
+
# the right cluster.
|
|
569
|
+
@available_databases = []
|
|
570
|
+
@database_error = nil
|
|
571
|
+
@target_default_database = registry.fetch(@selected_target).default_database
|
|
572
|
+
|
|
573
|
+
begin
|
|
574
|
+
registry.with_context(target: @selected_target) do
|
|
575
|
+
@available_databases = registry.fetch(@selected_target).list_databases(current: @target_default_database)
|
|
576
|
+
end
|
|
577
|
+
rescue => e
|
|
578
|
+
@database_error = PgReports::Connection::ErrorTranslator.translate(e)
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
requested_database = session[:pg_reports_database].to_s
|
|
582
|
+
@selected_database = if requested_database.present? && @available_databases.any? { |d| d["name"] == requested_database }
|
|
583
|
+
requested_database
|
|
584
|
+
else
|
|
585
|
+
@target_default_database
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def within_selected_database
|
|
590
|
+
database_override = (@selected_database && @selected_database != @target_default_database) ? @selected_database : nil
|
|
591
|
+
target_override = (@selected_target && @selected_target != PgReports.connection_registry.default_name) ? @selected_target : nil
|
|
592
|
+
|
|
593
|
+
if target_override || database_override
|
|
594
|
+
PgReports.with_target(target_override || PgReports.connection_registry.default_name,
|
|
595
|
+
database: database_override) { yield }
|
|
596
|
+
else
|
|
597
|
+
yield
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def valid_database?(name)
|
|
602
|
+
# Reuse the list already fetched in #resolve_database_selection so the
|
|
603
|
+
# switch_database POST hits pg_database once per request, not twice.
|
|
604
|
+
Array(@available_databases).any? { |row| row["name"] == name }
|
|
605
|
+
end
|
|
606
|
+
|
|
495
607
|
def load_report_filters(category, report_key)
|
|
496
608
|
definition = ReportLoader.get(category.to_s, report_key.to_s)
|
|
497
609
|
return {} unless definition
|
|
@@ -530,6 +642,9 @@ module PgReports
|
|
|
530
642
|
end
|
|
531
643
|
|
|
532
644
|
def execute_report(category, report_key, **filter_params)
|
|
645
|
+
reason = category_disabled_reason(category)
|
|
646
|
+
raise ArgumentError, reason if reason
|
|
647
|
+
|
|
533
648
|
mod = case category
|
|
534
649
|
when :queries then Modules::Queries
|
|
535
650
|
when :indexes then Modules::Indexes
|
|
@@ -547,6 +662,31 @@ module PgReports
|
|
|
547
662
|
mod.public_send(report_key, **filter_params)
|
|
548
663
|
end
|
|
549
664
|
|
|
665
|
+
# nil if the category is available in the current target/database context,
|
|
666
|
+
# otherwise a localized string explaining why it is disabled. Exposed to
|
|
667
|
+
# views via helper_method.
|
|
668
|
+
def category_disabled_reason(category)
|
|
669
|
+
constraint = Dashboard::ReportsRegistry.target_constraint(category)
|
|
670
|
+
return nil unless constraint == :primary_default_database_only
|
|
671
|
+
return nil if on_primary_default_database?
|
|
672
|
+
|
|
673
|
+
I18n.t("pg_reports.ui.categories.primary_only_reason",
|
|
674
|
+
default: "This report category only runs on the primary database " \
|
|
675
|
+
"because it inspects the host application's models. Switch back to " \
|
|
676
|
+
"the default database to use it.")
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def category_disabled?(category)
|
|
680
|
+
category_disabled_reason(category).present?
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def on_primary_default_database?
|
|
684
|
+
return true if @target_default_database.nil?
|
|
685
|
+
|
|
686
|
+
@selected_target == PgReports.connection_registry.default_name &&
|
|
687
|
+
@selected_database == @target_default_database
|
|
688
|
+
end
|
|
689
|
+
|
|
550
690
|
def substitute_params(query, params_hash)
|
|
551
691
|
result = query.dup
|
|
552
692
|
|