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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgReports
4
- VERSION = "0.8.0"
4
+ VERSION = "0.8.2"
5
5
  end
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.0
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: 4.0.4
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: []