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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +40 -70
- data/app/controllers/pg_reports/dashboard_controller.rb +185 -19
- data/app/views/layouts/pg_reports/application.html.erb +88 -6
- data/app/views/pg_reports/dashboard/index.html.erb +391 -23
- data/bin/pg_reports +111 -0
- data/config/brakeman.ignore +74 -0
- data/config/locales/en.yml +19 -5
- data/config/locales/ru.yml +19 -5
- data/config/locales/uk.yml +19 -5
- data/config/routes.rb +1 -0
- data/lib/pg_reports/configuration.rb +8 -0
- data/lib/pg_reports/connection/target.rb +1 -1
- data/lib/pg_reports/executor.rb +7 -2
- data/lib/pg_reports/grafana/exporter.rb +5 -1
- data/lib/pg_reports/modules/system.rb +28 -4
- data/lib/pg_reports/standalone.rb +210 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +9 -0
- metadata +7 -3
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module PgReports
|
|
6
|
+
# Runs the dashboard as a self-contained application, without a host Rails app.
|
|
7
|
+
#
|
|
8
|
+
# It boots a minimal Rails::Application that mounts PgReports::Engine and points
|
|
9
|
+
# ActiveRecord::Base at a PostgreSQL database, then serves it over HTTP. This is
|
|
10
|
+
# what powers the `pg_reports server` executable and the `pg_reports:server`
|
|
11
|
+
# rake task, so the project can be launched straight from the gem's root folder.
|
|
12
|
+
#
|
|
13
|
+
# Dependency note: this relies only on gems already pulled in transitively by
|
|
14
|
+
# the gem's runtime deps (rack via actionpack, rackup via railties). The actual
|
|
15
|
+
# web server (puma / webrick) is resolved at run time and is NOT a hard
|
|
16
|
+
# dependency — installed-gem users bring their own.
|
|
17
|
+
module Standalone
|
|
18
|
+
extend self
|
|
19
|
+
|
|
20
|
+
DEFAULT_PORT = 4000
|
|
21
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
22
|
+
DEFAULT_MOUNT = "/"
|
|
23
|
+
|
|
24
|
+
# Rack handlers tried, in order, when none is named explicitly.
|
|
25
|
+
CANDIDATE_SERVERS = %w[puma webrick].freeze
|
|
26
|
+
|
|
27
|
+
# Config files auto-loaded (first that exists), relative to the working
|
|
28
|
+
# directory, when no explicit --config path is given.
|
|
29
|
+
DEFAULT_CONFIG_FILES = %w[pg_reports.rb config/pg_reports.rb].freeze
|
|
30
|
+
|
|
31
|
+
class ServerUnavailable < PgReports::Error; end
|
|
32
|
+
|
|
33
|
+
# Boot the app and start a (blocking) web server.
|
|
34
|
+
#
|
|
35
|
+
# @param port [Integer]
|
|
36
|
+
# @param host [String]
|
|
37
|
+
# @param mount_path [String] where the engine is mounted (default "/")
|
|
38
|
+
# @param database_url [String, nil] explicit connection URL; otherwise resolved
|
|
39
|
+
# from DATABASE_URL or libpq-style PG* env vars
|
|
40
|
+
# @param server [String, nil] Rack handler name to force (e.g. "puma")
|
|
41
|
+
# @param config_file [String, nil] path to a Ruby config file that calls
|
|
42
|
+
# `PgReports.configure`; auto-detected from DEFAULT_CONFIG_FILES otherwise
|
|
43
|
+
# @param overrides [Hash] per-setting overrides applied last (from CLI flags);
|
|
44
|
+
# nil values are ignored. Keys are Configuration attribute names.
|
|
45
|
+
def run(port: DEFAULT_PORT, host: DEFAULT_HOST, mount_path: DEFAULT_MOUNT,
|
|
46
|
+
database_url: nil, server: nil, config_file: nil, overrides: {})
|
|
47
|
+
# Rails' ActiveRecord railtie reads the connection from DATABASE_URL when no
|
|
48
|
+
# config/database.yml exists — so we route our resolved connection through
|
|
49
|
+
# it. The connection registry then auto-registers it as the :primary target,
|
|
50
|
+
# and database switching / multi-cluster all work unchanged.
|
|
51
|
+
ENV["DATABASE_URL"] = connection_url(database_url)
|
|
52
|
+
|
|
53
|
+
# Mark this process as standalone so the dashboard can hide reports that
|
|
54
|
+
# only make sense with a host app (e.g. Schema Analysis, which introspects
|
|
55
|
+
# the host application's ActiveRecord models — there are none here).
|
|
56
|
+
PgReports.config.standalone = true
|
|
57
|
+
|
|
58
|
+
# Layer settings on top of the ENV-derived defaults: config file first, then
|
|
59
|
+
# explicit CLI overrides win.
|
|
60
|
+
apply_configuration(config_file: config_file, overrides: overrides)
|
|
61
|
+
|
|
62
|
+
app = build_application(mount_path)
|
|
63
|
+
app.initialize!
|
|
64
|
+
verify_connection!
|
|
65
|
+
|
|
66
|
+
handler_name, handler = resolve_server(server)
|
|
67
|
+
banner(host: host, port: port, server: handler_name)
|
|
68
|
+
handler.run(app, Host: host, Port: port)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Resolve the connection URL. Priority: explicit url > DATABASE_URL >
|
|
72
|
+
# libpq-style PG* env vars (PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE).
|
|
73
|
+
def connection_url(explicit = nil)
|
|
74
|
+
return explicit if explicit && !explicit.empty?
|
|
75
|
+
return ENV["DATABASE_URL"] if ENV["DATABASE_URL"] && !ENV["DATABASE_URL"].empty?
|
|
76
|
+
|
|
77
|
+
require "erb"
|
|
78
|
+
user = ENV["PGUSER"] || ENV["USER"]
|
|
79
|
+
password = ENV["PGPASSWORD"]
|
|
80
|
+
host = ENV["PGHOST"] || "localhost"
|
|
81
|
+
port = ENV["PGPORT"] || 5432
|
|
82
|
+
database = ENV["PGDATABASE"] || "postgres"
|
|
83
|
+
|
|
84
|
+
userinfo = +""
|
|
85
|
+
if user && !user.empty?
|
|
86
|
+
userinfo << ERB::Util.url_encode(user)
|
|
87
|
+
userinfo << ":#{ERB::Util.url_encode(password)}" if password && !password.empty?
|
|
88
|
+
userinfo << "@"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
"postgresql://#{userinfo}#{host}:#{port}/#{ERB::Util.url_encode(database)}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Apply configuration in increasing order of precedence:
|
|
97
|
+
# ENV vars (already read when Configuration was built)
|
|
98
|
+
# < config file (full Ruby, calls PgReports.configure)
|
|
99
|
+
# < CLI overrides (individual flags)
|
|
100
|
+
#
|
|
101
|
+
# The config file is the escape hatch for every setting that has no dedicated
|
|
102
|
+
# flag — thresholds, Telegram, Grafana favorites, even the dashboard_auth
|
|
103
|
+
# proc. CLI flags cover only the handful of common security toggles.
|
|
104
|
+
def apply_configuration(config_file:, overrides:)
|
|
105
|
+
path = config_file || detect_config_file
|
|
106
|
+
load_config_file(path) if path
|
|
107
|
+
|
|
108
|
+
overrides.each do |key, value|
|
|
109
|
+
next if value.nil? # unset flag — leave the file/ENV value in place
|
|
110
|
+
PgReports.config.public_send(:"#{key}=", value)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# First existing DEFAULT_CONFIG_FILES entry (relative to the working dir), or
|
|
115
|
+
# nil when the user keeps no config file.
|
|
116
|
+
def detect_config_file
|
|
117
|
+
DEFAULT_CONFIG_FILES
|
|
118
|
+
.map { |f| File.expand_path(f, Dir.pwd) }
|
|
119
|
+
.find { |f| File.file?(f) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Evaluate a Ruby config file. A missing explicit path is an error (the user
|
|
123
|
+
# asked for it); a broken file is reported with context rather than a raw
|
|
124
|
+
# backtrace.
|
|
125
|
+
def load_config_file(path)
|
|
126
|
+
full = File.expand_path(path)
|
|
127
|
+
raise PgReports::Error, "Config file not found: #{path}" unless File.file?(full)
|
|
128
|
+
|
|
129
|
+
warn "pg_reports: loading config from #{full}"
|
|
130
|
+
load full
|
|
131
|
+
rescue PgReports::Error
|
|
132
|
+
raise
|
|
133
|
+
rescue => e
|
|
134
|
+
raise PgReports::Error, "Failed to load config file #{full}: #{e.message}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Build (and register as Rails.application) a minimal Rails app that mounts
|
|
138
|
+
# the engine. Kept intentionally small: no asset pipeline (views are inline),
|
|
139
|
+
# cookie sessions for the dashboard's database selector + CSRF.
|
|
140
|
+
def build_application(mount_path)
|
|
141
|
+
require "rails"
|
|
142
|
+
require "action_controller/railtie"
|
|
143
|
+
require "active_record/railtie"
|
|
144
|
+
require "tmpdir"
|
|
145
|
+
# pg_reports.rb only requires the engine when Rails::Engine is already
|
|
146
|
+
# defined; when loaded outside a Rails app that guard was false, so load it
|
|
147
|
+
# now that the railties are present. This also registers its initializers.
|
|
148
|
+
require "pg_reports/engine"
|
|
149
|
+
|
|
150
|
+
target_mount = mount_path
|
|
151
|
+
# A throwaway, empty app root. We must NOT use the gem root here — Rails
|
|
152
|
+
# would then load the gem's engine config/routes.rb (and config/locales) as
|
|
153
|
+
# the *application's* own, which double-loads and breaks. The engine loads
|
|
154
|
+
# those itself relative to its own root; the app only needs the mount below.
|
|
155
|
+
app_root = Dir.mktmpdir("pg_reports-standalone")
|
|
156
|
+
at_exit { FileUtils.remove_entry(app_root, true) }
|
|
157
|
+
|
|
158
|
+
Class.new(Rails::Application) do
|
|
159
|
+
config.root = app_root
|
|
160
|
+
config.eager_load = false
|
|
161
|
+
config.consider_all_requests_local = true
|
|
162
|
+
config.secret_key_base = ENV["SECRET_KEY_BASE"] || SecureRandom.hex(64)
|
|
163
|
+
config.session_store :cookie_store, key: "_pg_reports_session"
|
|
164
|
+
config.hosts.clear # local tool: don't block by Host header
|
|
165
|
+
config.logger = ::Logger.new($stdout)
|
|
166
|
+
config.log_level = (ENV["LOG_LEVEL"] || "info").to_sym
|
|
167
|
+
config.active_support.report_deprecations = false
|
|
168
|
+
|
|
169
|
+
routes.append do
|
|
170
|
+
mount PgReports::Engine, at: target_mount, as: "pg_reports"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Force an actual connection so the user gets a clear error at startup rather
|
|
176
|
+
# than a 500 on the first request when the database is unreachable.
|
|
177
|
+
def verify_connection!
|
|
178
|
+
ActiveRecord::Base.connection
|
|
179
|
+
rescue => e
|
|
180
|
+
raise PgReports::Error, "Cannot connect to the database (#{ENV["DATABASE_URL"]}): #{e.message}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Find a usable Rack handler. Honors an explicit name, otherwise tries the
|
|
184
|
+
# candidates in order and uses the first that is installed.
|
|
185
|
+
def resolve_server(name)
|
|
186
|
+
require "rackup"
|
|
187
|
+
|
|
188
|
+
candidates = name ? [name] : CANDIDATE_SERVERS
|
|
189
|
+
candidates.each do |candidate|
|
|
190
|
+
handler = begin
|
|
191
|
+
Rackup::Handler.get(candidate)
|
|
192
|
+
rescue LoadError, NameError
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
return [candidate, handler] if handler
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
raise ServerUnavailable, <<~MSG.strip
|
|
199
|
+
No web server found (tried: #{candidates.join(", ")}).
|
|
200
|
+
Add one to run the standalone dashboard, e.g. `gem install puma`
|
|
201
|
+
(or add `gem "puma"` to your Gemfile).
|
|
202
|
+
MSG
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def banner(host:, port:, server:)
|
|
206
|
+
url_host = (host == "0.0.0.0") ? "localhost" : host
|
|
207
|
+
warn "pg_reports: serving dashboard via #{server} on http://#{url_host}:#{port} (Ctrl-C to stop)"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
data/lib/pg_reports/version.rb
CHANGED
data/lib/pg_reports.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# ActiveSupport < 7.1 relies on Ruby's `logger` being loaded implicitly, which
|
|
4
|
+
# concurrent-ruby dropped in 1.3.5. Require it up front so pg_reports loads on
|
|
5
|
+
# Rails 6.1/7.0 (and any bundle with a modern concurrent-ruby). Harmless on
|
|
6
|
+
# newer Rails, which require it themselves.
|
|
7
|
+
require "logger"
|
|
3
8
|
require "active_support"
|
|
4
9
|
require "active_support/core_ext"
|
|
5
10
|
require "active_record"
|
|
@@ -43,6 +48,10 @@ require_relative "pg_reports/grafana/dashboard_builder"
|
|
|
43
48
|
# Rails Engine
|
|
44
49
|
require_relative "pg_reports/engine" if defined?(Rails::Engine)
|
|
45
50
|
|
|
51
|
+
# Standalone runner (no host app). Only defines methods; the heavy Rails/web
|
|
52
|
+
# requires happen lazily inside PgReports::Standalone.run.
|
|
53
|
+
require_relative "pg_reports/standalone"
|
|
54
|
+
|
|
46
55
|
module PgReports
|
|
47
56
|
class << self
|
|
48
57
|
# Query analysis methods
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pg_reports
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eldar Avatov
|
|
@@ -140,7 +140,8 @@ description: A comprehensive PostgreSQL monitoring and analysis library that pro
|
|
|
140
140
|
a beautiful web dashboard and Telegram notifications.
|
|
141
141
|
email:
|
|
142
142
|
- eldar.avatov@gmail.com
|
|
143
|
-
executables:
|
|
143
|
+
executables:
|
|
144
|
+
- pg_reports
|
|
144
145
|
extensions: []
|
|
145
146
|
extra_rdoc_files: []
|
|
146
147
|
files:
|
|
@@ -158,6 +159,8 @@ files:
|
|
|
158
159
|
- app/views/pg_reports/dashboard/_target_selector.html.erb
|
|
159
160
|
- app/views/pg_reports/dashboard/index.html.erb
|
|
160
161
|
- app/views/pg_reports/dashboard/show.html.erb
|
|
162
|
+
- bin/pg_reports
|
|
163
|
+
- config/brakeman.ignore
|
|
161
164
|
- config/locales/en.yml
|
|
162
165
|
- config/locales/ru.yml
|
|
163
166
|
- config/locales/uk.yml
|
|
@@ -284,6 +287,7 @@ files:
|
|
|
284
287
|
- lib/pg_reports/sql/tables/update_hotspots.sql
|
|
285
288
|
- lib/pg_reports/sql/tables/vacuum_needed.sql
|
|
286
289
|
- lib/pg_reports/sql_loader.rb
|
|
290
|
+
- lib/pg_reports/standalone.rb
|
|
287
291
|
- lib/pg_reports/telegram_sender.rb
|
|
288
292
|
- lib/pg_reports/version.rb
|
|
289
293
|
- lib/tasks/pg_reports.rake
|
|
@@ -308,7 +312,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
308
312
|
- !ruby/object:Gem::Version
|
|
309
313
|
version: '0'
|
|
310
314
|
requirements: []
|
|
311
|
-
rubygems_version:
|
|
315
|
+
rubygems_version: 3.6.9
|
|
312
316
|
specification_version: 4
|
|
313
317
|
summary: PostgreSQL analysis and reporting tool with Telegram integration
|
|
314
318
|
test_files: []
|