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.
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgReports
4
+ module Connection
5
+ # Represents a single named PostgreSQL target (host+credentials).
6
+ # Holds a memoized AR-subclass-backed connection per database it has been
7
+ # asked for, so switching between databases on the same target reuses pools.
8
+ #
9
+ # The :primary target wraps ActiveRecord::Base directly when accessed at its
10
+ # default database — we don't open a parallel pool to the host app's DB.
11
+ # For non-default databases we create an isolated AR subclass that has its
12
+ # own pool, so the host application's pool is never affected.
13
+ class Target
14
+ class ConnectionFailed < PgReports::Error; end
15
+
16
+ attr_reader :name
17
+
18
+ def initialize(name, spec)
19
+ @name = name.to_sym
20
+ @spec = normalize_spec(spec)
21
+ @pools = {} # database (string) => AR class
22
+ end
23
+
24
+ # Configuration hash used as a template for derived databases.
25
+ def spec
26
+ @spec.dup
27
+ end
28
+
29
+ def default_database
30
+ @spec[:database]&.to_s
31
+ end
32
+
33
+ # Resolve the AR class backing the connection for `database` (nil = default).
34
+ # Returns a class responding to `.connection` (ActiveRecord::Base or subclass).
35
+ def ar_class_for(database = nil)
36
+ db = (database || default_database).to_s
37
+ raise ArgumentError, "Cannot resolve connection: target #{name.inspect} has no default database and none was given" if db.empty?
38
+
39
+ @pools[db] ||= build_pool_class(db)
40
+ end
41
+
42
+ # Returns the active PG connection for `database`, opening it if needed.
43
+ def connection_for(database = nil)
44
+ ar_class_for(database).connection
45
+ rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad => e
46
+ raise ConnectionFailed, "Cannot connect to #{name}/#{database || default_database}: #{e.message}"
47
+ end
48
+
49
+ # List all databases visible on this target's cluster (using pg_database).
50
+ # Result rows: { "name" => String, "size" => String, "current" => Boolean }
51
+ def list_databases(current: nil)
52
+ rows = connection_for.exec_query(<<~SQL, "PgReports").to_a
53
+ SELECT datname AS name,
54
+ pg_size_pretty(pg_database_size(datname)) AS size
55
+ FROM pg_database
56
+ WHERE datistemplate = false AND datallowconn = true
57
+ ORDER BY datname
58
+ SQL
59
+ current_db = (current || default_database).to_s
60
+ rows.each { |r| r["current"] = (r["name"].to_s == current_db) }
61
+ rows
62
+ end
63
+
64
+ # Close all derived pools we own. The :primary AR::Base pool is never touched.
65
+ def disconnect!
66
+ @pools.each_value do |klass|
67
+ next if klass.equal?(ActiveRecord::Base)
68
+ klass.connection_pool.disconnect! if klass.connection_pool.connected?
69
+ rescue
70
+ # Best-effort cleanup
71
+ end
72
+ @pools.clear
73
+ end
74
+
75
+ private
76
+
77
+ def normalize_spec(spec)
78
+ hash = case spec
79
+ when Hash
80
+ spec.transform_keys(&:to_sym)
81
+ when ActiveRecord::DatabaseConfigurations::HashConfig
82
+ spec.configuration_hash.transform_keys(&:to_sym)
83
+ else
84
+ raise ArgumentError, "Unsupported spec type: #{spec.class}"
85
+ end
86
+
87
+ # Ensure adapter defaults to postgresql; we only support PG.
88
+ hash[:adapter] ||= "postgresql"
89
+ hash
90
+ end
91
+
92
+ def build_pool_class(database)
93
+ # When asked for the primary target's default database, reuse AR::Base
94
+ # so we don't open a parallel pool to the same DB the host app uses.
95
+ if name == :primary && database == default_database
96
+ return ActiveRecord::Base
97
+ end
98
+
99
+ klass = Class.new(ActiveRecord::Base) { self.abstract_class = true }
100
+ const_name = "Pool_#{name}_#{database}".gsub(/\W/, "_")
101
+ if PgReports::Connection.const_defined?(const_name, false)
102
+ PgReports::Connection.send(:remove_const, const_name)
103
+ end
104
+ PgReports::Connection.const_set(const_name, klass)
105
+
106
+ klass.establish_connection(@spec.merge(database: database))
107
+ klass
108
+ end
109
+ end
110
+ end
111
+ end
@@ -277,8 +277,8 @@ module PgReports
277
277
  seq_scans: {name: "Sequential Scans", description: "Tables with high sequential scans"},
278
278
  tables_without_pk: {name: "No Primary Key", description: "Tables missing primary keys"},
279
279
  recently_modified: {name: "Recently Modified", description: "Tables with recent activity"},
280
- update_hotspots: {name: "Update Hotspots", description: "Same rows or indexed columns updated repeatedly", new: true},
281
- unused_tables: {name: "Unused Tables", description: "Tables never queried since the last stats reset", new: true}
280
+ update_hotspots: {name: "Update Hotspots", description: "Same rows or indexed columns updated repeatedly"},
281
+ unused_tables: {name: "Unused Tables", description: "Tables never queried since the last stats reset"}
282
282
  }
283
283
  },
284
284
  connections: {
@@ -316,14 +316,19 @@ module PgReports
316
316
  name: "Schema Analysis",
317
317
  icon: "🔍",
318
318
  color: "#06b6d4",
319
+ # These reports introspect the host application's ActiveRecord models,
320
+ # which are bound to the default database. Running them against a
321
+ # different database in the cluster returns rows that may not map to
322
+ # any model. The dashboard greys the category out in that case.
323
+ target_constraint: :primary_default_database_only,
319
324
  reports: {
320
325
  missing_validations: {name: "Missing Validations", description: "Unique indexes without model validations"},
321
- unused_columns: {name: "Unused Columns", description: "Columns that have only ever held a single value", new: true},
322
- always_null_columns: {name: "Always-NULL Columns", description: "Nullable columns that contain only NULL", new: true},
323
- polymorphic_without_index: {name: "Polymorphic Without Index", description: "Polymorphic associations missing composite index", new: true},
324
- counter_cache_issues: {name: "Counter Cache Issues", description: "counter_cache declarations whose target column is missing", new: true},
325
- soft_delete_without_scope: {name: "Soft Delete Without Scope", description: "Soft-delete columns with no model scope filtering them", new: true},
326
- orphan_tables: {name: "Orphan Tables", description: "DB tables without a corresponding Rails model", new: true}
326
+ unused_columns: {name: "Unused Columns", description: "Columns that have only ever held a single value"},
327
+ always_null_columns: {name: "Always-NULL Columns", description: "Nullable columns that contain only NULL"},
328
+ polymorphic_without_index: {name: "Polymorphic Without Index", description: "Polymorphic associations missing composite index"},
329
+ counter_cache_issues: {name: "Counter Cache Issues", description: "counter_cache declarations whose target column is missing"},
330
+ soft_delete_without_scope: {name: "Soft Delete Without Scope", description: "Soft-delete columns with no model scope filtering them"},
331
+ orphan_tables: {name: "Orphan Tables", description: "DB tables without a corresponding Rails model"}
327
332
  }
328
333
  }
329
334
  }.freeze
@@ -398,6 +403,15 @@ module PgReports
398
403
  def self.problem_fields(report)
399
404
  REPORT_CONFIG.dig(report.to_sym, :problem_fields) || []
400
405
  end
406
+
407
+ # Returns the target constraint declared on a category, or nil.
408
+ # Currently the only constraint is :primary_default_database_only, which
409
+ # means "only meaningful when the dashboard is pointing at the host app's
410
+ # primary target on its default database" — used by Schema Analysis,
411
+ # which depends on ActiveRecord::Base.descendants of the host app.
412
+ def self.target_constraint(category)
413
+ REPORTS.dig(category.to_sym, :target_constraint)
414
+ end
401
415
  end
402
416
  end
403
417
  end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgReports
4
- # Executes SQL queries and returns results
4
+ # Executes SQL queries and returns results.
5
+ #
6
+ # The connection is resolved lazily on every #execute call so that thread-local
7
+ # context set by PgReports.with_target / with_database is honored even when an
8
+ # Executor instance has been memoized at the module level.
5
9
  class Executor
6
10
  def initialize(connection: nil)
7
- @connection = connection || PgReports.config.connection
11
+ @connection_override = connection
8
12
  end
9
13
 
10
14
  # Execute SQL from a file and return results as array of hashes
@@ -13,13 +17,23 @@ module PgReports
13
17
  execute(sql, **params)
14
18
  end
15
19
 
16
- # Execute raw SQL and return results as array of hashes
20
+ # Execute raw SQL and return results as array of hashes.
21
+ #
22
+ # Every query is tagged with the "PgReports" AR statement name so the Query
23
+ # Monitor can skip our own queries by name (see QueryMonitor#should_skip?),
24
+ # reliably and independent of backtrace depth — the internal live_metrics /
25
+ # status polling would otherwise leak into the monitor's history.
17
26
  def execute(sql, **params)
18
27
  processed_sql = interpolate_params(sql, params)
19
- result = @connection.exec_query(processed_sql)
28
+ result = connection.exec_query(processed_sql, "PgReports")
20
29
  result.to_a
21
30
  end
22
31
 
32
+ # Resolved on every call: explicit override > thread-local > registry default.
33
+ def connection
34
+ @connection_override || PgReports.config.connection
35
+ end
36
+
23
37
  private
24
38
 
25
39
  # Simple parameter interpolation for SQL
@@ -40,11 +54,11 @@ module PgReports
40
54
  when Integer, Float
41
55
  value.to_s
42
56
  when String
43
- @connection.quote(value)
57
+ connection.quote(value)
44
58
  when Array
45
59
  "(#{value.map { |v| quote_value(v) }.join(", ")})"
46
60
  else
47
- @connection.quote(value.to_s)
61
+ connection.quote(value.to_s)
48
62
  end
49
63
  end
50
64
  end
@@ -44,13 +44,37 @@ module PgReports
44
44
  false
45
45
  end
46
46
 
47
- # Get pg_stat_statements status details
47
+ # Whether the database connection can execute a basic query.
48
+ # Used to tell "the connection itself is down" apart from
49
+ # "connected, but pg_stat_statements isn't set up yet".
50
+ # @return [Boolean]
51
+ def connected?
52
+ executor.execute("SELECT 1")
53
+ true
54
+ rescue
55
+ false
56
+ end
57
+
58
+ # Get pg_stat_statements status details.
59
+ #
60
+ # Note: whether pg_stat_statements is in shared_preload_libraries cannot be
61
+ # read by a plain monitoring role (that requires the pg_read_all_settings
62
+ # role), so we never look at the setting. Instead we derive the state from
63
+ # signals every role can observe: can we run a query at all, does the
64
+ # extension exist in pg_extension, and is its view queryable.
65
+ #
48
66
  # @return [Hash] Status information
49
67
  def pg_stat_statements_status
68
+ unless connected?
69
+ return {connected: false, extension_installed: false, preloaded: false, ready: false}
70
+ end
71
+
72
+ installed = pg_stat_statements_available?
50
73
  {
51
- extension_installed: pg_stat_statements_available?,
74
+ connected: true,
75
+ extension_installed: installed,
52
76
  preloaded: pg_stat_statements_preloaded?,
53
- ready: pg_stat_statements_available? && pg_stat_statements_preloaded?
77
+ ready: installed && pg_stat_statements_preloaded?
54
78
  }
55
79
  end
56
80
 
@@ -58,7 +82,7 @@ module PgReports
58
82
  # @param long_query_threshold [Integer] Threshold in seconds for long queries
59
83
  # @return [Hash] Metrics data
60
84
  # @raise [StandardError] If no data is returned
61
- def live_metrics(long_query_threshold: 60)
85
+ def live_metrics(long_query_threshold: 5)
62
86
  data = executor.execute_from_file(:system, :live_metrics,
63
87
  long_query_threshold: long_query_threshold)
64
88
 
@@ -173,7 +197,10 @@ module PgReports
173
197
  private
174
198
 
175
199
  def pg_version
176
- @pg_version ||= begin
200
+ # Cache per-connection so switching targets/databases re-resolves the version.
201
+ cache = (Thread.current[:pg_reports_pg_version_cache] ||= {})
202
+ key = executor.connection.object_id
203
+ cache[key] ||= begin
177
204
  result = executor.execute("SELECT current_setting('server_version_num')::int AS v")
178
205
  result.first&.fetch("v", 0).to_i
179
206
  end
@@ -290,13 +290,16 @@ module PgReports
290
290
  # when gem is installed from RubyGems
291
291
  next if path.include?("/query_monitor.rb")
292
292
 
293
- # Filter queries from pg_reports internal modules only:
294
- # - Installed gem: /gems/pg_reports-X.Y.Z/lib/
295
- # - Local gem: /pg_reports/lib/pg_reports/modules/
296
- # Note: We intentionally DO NOT filter dashboard_controller.rb
297
- # to allow monitoring of user application queries made during dashboard page loads
298
- path.match?(%r{/gems/pg_reports[-\d.]+/lib/}) ||
299
- path.match?(%r{/pg_reports/lib/pg_reports/modules/})
293
+ # Filter queries from pg_reports internal code:
294
+ # - Installed gem: /gems/pg_reports-X.Y.Z/lib/ or /gems/pg_reports-X.Y.Z/app/
295
+ # - Local gem: /pg_reports/lib/pg_reports/ or /pg_reports/app/(controllers|views)/pg_reports/
296
+ # This includes the dashboard controller's own SQL execution endpoints
297
+ # (execute_query, explain_analyze) which call AR directly without going
298
+ # through Executor — without this match nothing in the caller stack
299
+ # would identify the query as ours.
300
+ path.match?(%r{/gems/pg_reports[-\d.]+/(lib|app)/}) ||
301
+ path.match?(%r{/pg_reports/lib/pg_reports/}) ||
302
+ path.match?(%r{/pg_reports/app/(controllers|views)/pg_reports/})
300
303
  end
301
304
  end
302
305
 
@@ -0,0 +1,152 @@
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
+ class ServerUnavailable < PgReports::Error; end
28
+
29
+ # Boot the app and start a (blocking) web server.
30
+ #
31
+ # @param port [Integer]
32
+ # @param host [String]
33
+ # @param mount_path [String] where the engine is mounted (default "/")
34
+ # @param database_url [String, nil] explicit connection URL; otherwise resolved
35
+ # from DATABASE_URL or libpq-style PG* env vars
36
+ # @param server [String, nil] Rack handler name to force (e.g. "puma")
37
+ def run(port: DEFAULT_PORT, host: DEFAULT_HOST, mount_path: DEFAULT_MOUNT,
38
+ database_url: nil, server: nil)
39
+ # Rails' ActiveRecord railtie reads the connection from DATABASE_URL when no
40
+ # config/database.yml exists — so we route our resolved connection through
41
+ # it. The connection registry then auto-registers it as the :primary target,
42
+ # and database switching / multi-cluster all work unchanged.
43
+ ENV["DATABASE_URL"] = connection_url(database_url)
44
+
45
+ app = build_application(mount_path)
46
+ app.initialize!
47
+ verify_connection!
48
+
49
+ handler_name, handler = resolve_server(server)
50
+ banner(host: host, port: port, server: handler_name)
51
+ handler.run(app, Host: host, Port: port)
52
+ end
53
+
54
+ # Resolve the connection URL. Priority: explicit url > DATABASE_URL >
55
+ # libpq-style PG* env vars (PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE).
56
+ def connection_url(explicit = nil)
57
+ return explicit if explicit && !explicit.empty?
58
+ return ENV["DATABASE_URL"] if ENV["DATABASE_URL"] && !ENV["DATABASE_URL"].empty?
59
+
60
+ require "erb"
61
+ user = ENV["PGUSER"] || ENV["USER"]
62
+ password = ENV["PGPASSWORD"]
63
+ host = ENV["PGHOST"] || "localhost"
64
+ port = ENV["PGPORT"] || 5432
65
+ database = ENV["PGDATABASE"] || "postgres"
66
+
67
+ userinfo = +""
68
+ if user && !user.empty?
69
+ userinfo << ERB::Util.url_encode(user)
70
+ userinfo << ":#{ERB::Util.url_encode(password)}" if password && !password.empty?
71
+ userinfo << "@"
72
+ end
73
+
74
+ "postgresql://#{userinfo}#{host}:#{port}/#{ERB::Util.url_encode(database)}"
75
+ end
76
+
77
+ private
78
+
79
+ # Build (and register as Rails.application) a minimal Rails app that mounts
80
+ # the engine. Kept intentionally small: no asset pipeline (views are inline),
81
+ # cookie sessions for the dashboard's database selector + CSRF.
82
+ def build_application(mount_path)
83
+ require "rails"
84
+ require "action_controller/railtie"
85
+ require "active_record/railtie"
86
+ require "tmpdir"
87
+ # pg_reports.rb only requires the engine when Rails::Engine is already
88
+ # defined; when loaded outside a Rails app that guard was false, so load it
89
+ # now that the railties are present. This also registers its initializers.
90
+ require "pg_reports/engine"
91
+
92
+ target_mount = mount_path
93
+ # A throwaway, empty app root. We must NOT use the gem root here — Rails
94
+ # would then load the gem's engine config/routes.rb (and config/locales) as
95
+ # the *application's* own, which double-loads and breaks. The engine loads
96
+ # those itself relative to its own root; the app only needs the mount below.
97
+ app_root = Dir.mktmpdir("pg_reports-standalone")
98
+ at_exit { FileUtils.remove_entry(app_root, true) }
99
+
100
+ Class.new(Rails::Application) do
101
+ config.root = app_root
102
+ config.eager_load = false
103
+ config.consider_all_requests_local = true
104
+ config.secret_key_base = ENV["SECRET_KEY_BASE"] || SecureRandom.hex(64)
105
+ config.session_store :cookie_store, key: "_pg_reports_session"
106
+ config.hosts.clear # local tool: don't block by Host header
107
+ config.logger = ::Logger.new($stdout)
108
+ config.log_level = (ENV["LOG_LEVEL"] || "info").to_sym
109
+ config.active_support.report_deprecations = false
110
+
111
+ routes.append do
112
+ mount PgReports::Engine, at: target_mount, as: "pg_reports"
113
+ end
114
+ end
115
+ end
116
+
117
+ # Force an actual connection so the user gets a clear error at startup rather
118
+ # than a 500 on the first request when the database is unreachable.
119
+ def verify_connection!
120
+ ActiveRecord::Base.connection
121
+ rescue => e
122
+ raise PgReports::Error, "Cannot connect to the database (#{ENV["DATABASE_URL"]}): #{e.message}"
123
+ end
124
+
125
+ # Find a usable Rack handler. Honors an explicit name, otherwise tries the
126
+ # candidates in order and uses the first that is installed.
127
+ def resolve_server(name)
128
+ require "rackup"
129
+
130
+ candidates = name ? [name] : CANDIDATE_SERVERS
131
+ candidates.each do |candidate|
132
+ handler = begin
133
+ Rackup::Handler.get(candidate)
134
+ rescue LoadError, NameError
135
+ nil
136
+ end
137
+ return [candidate, handler] if handler
138
+ end
139
+
140
+ raise ServerUnavailable, <<~MSG.strip
141
+ No web server found (tried: #{candidates.join(", ")}).
142
+ Add one to run the standalone dashboard, e.g. `gem install puma`
143
+ (or add `gem "puma"` to your Gemfile).
144
+ MSG
145
+ end
146
+
147
+ def banner(host:, port:, server:)
148
+ url_host = (host == "0.0.0.0") ? "localhost" : host
149
+ warn "pg_reports: serving dashboard via #{server} on http://#{url_host}:#{port} (Ctrl-C to stop)"
150
+ end
151
+ end
152
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgReports
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.1"
5
5
  end
data/lib/pg_reports.rb CHANGED
@@ -6,6 +6,9 @@ require "active_record"
6
6
 
7
7
  require_relative "pg_reports/version"
8
8
  require_relative "pg_reports/error"
9
+ require_relative "pg_reports/connection/target"
10
+ require_relative "pg_reports/connection/registry"
11
+ require_relative "pg_reports/connection/error_translator"
9
12
  require_relative "pg_reports/compatibility"
10
13
  require_relative "pg_reports/configuration"
11
14
  require_relative "pg_reports/sql_loader"
@@ -40,6 +43,10 @@ require_relative "pg_reports/grafana/dashboard_builder"
40
43
  # Rails Engine
41
44
  require_relative "pg_reports/engine" if defined?(Rails::Engine)
42
45
 
46
+ # Standalone runner (no host app). Only defines methods; the heavy Rails/web
47
+ # requires happen lazily inside PgReports::Standalone.run.
48
+ require_relative "pg_reports/standalone"
49
+
43
50
  module PgReports
44
51
  class << self
45
52
  # Query analysis methods
@@ -138,6 +145,56 @@ module PgReports
138
145
  ReportLoader.reload!
139
146
  ModuleGenerator.generate!
140
147
  end
148
+
149
+ # Connection registry — multi-target / multi-database support.
150
+ # The :primary target is auto-discovered from ActiveRecord on first access.
151
+ def connection_registry
152
+ @connection_registry ||= Connection::Registry.new
153
+ end
154
+
155
+ # Run a block against a specific target (and optionally a specific database
156
+ # on that target). Honored by Executor and any code routing through
157
+ # PgReports.config.connection.
158
+ #
159
+ # PgReports.with_target(:analytics) { PgReports.slow_queries }
160
+ # PgReports.with_target(:primary, database: "logs") { PgReports.table_sizes }
161
+ def with_target(name, database: nil, &block)
162
+ connection_registry.with_context(target: name, database: database, &block)
163
+ end
164
+
165
+ # Switch only the database on whatever target is currently active
166
+ # (defaults to the registry's default target).
167
+ #
168
+ # PgReports.with_database("reporting") { PgReports.database_sizes }
169
+ def with_database(database, &block)
170
+ target = connection_registry.current_name || connection_registry.default_name
171
+ connection_registry.with_context(target: target, database: database, &block)
172
+ end
173
+
174
+ # Name of the currently effective target (taking with_target into account).
175
+ def current_target_name
176
+ connection_registry.current_name || connection_registry.default_name
177
+ end
178
+
179
+ # Name of the currently effective database (taking with_database into account).
180
+ def current_database_name
181
+ connection_registry.current_database_name
182
+ end
183
+
184
+ # List databases on the currently active target's cluster.
185
+ # Each row: { "name" => String, "size" => String, "current" => Boolean }
186
+ def list_databases
187
+ target = connection_registry.fetch
188
+ target.list_databases(current: current_database_name)
189
+ end
190
+
191
+ # List of registered targets, each as { name:, default_database:, current: }.
192
+ def list_targets
193
+ current = current_target_name
194
+ connection_registry.targets.map do |t|
195
+ {name: t.name, default_database: t.default_database, current: t.name == current}
196
+ end
197
+ end
141
198
  end
142
199
  end
143
200
 
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.7.0
4
+ version: 0.8.1
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:
@@ -150,12 +151,15 @@ files:
150
151
  - app/controllers/pg_reports/dashboard_controller.rb
151
152
  - app/controllers/pg_reports/metrics_controller.rb
152
153
  - app/views/layouts/pg_reports/application.html.erb
154
+ - app/views/pg_reports/dashboard/_database_selector.html.erb
153
155
  - app/views/pg_reports/dashboard/_fake_source_data.html.erb
154
156
  - app/views/pg_reports/dashboard/_show_modals.html.erb
155
157
  - app/views/pg_reports/dashboard/_show_scripts.html.erb
156
158
  - app/views/pg_reports/dashboard/_show_styles.html.erb
159
+ - app/views/pg_reports/dashboard/_target_selector.html.erb
157
160
  - app/views/pg_reports/dashboard/index.html.erb
158
161
  - app/views/pg_reports/dashboard/show.html.erb
162
+ - bin/pg_reports
159
163
  - config/locales/en.yml
160
164
  - config/locales/ru.yml
161
165
  - config/locales/uk.yml
@@ -164,6 +168,9 @@ files:
164
168
  - lib/pg_reports/annotation_parser.rb
165
169
  - lib/pg_reports/compatibility.rb
166
170
  - lib/pg_reports/configuration.rb
171
+ - lib/pg_reports/connection/error_translator.rb
172
+ - lib/pg_reports/connection/registry.rb
173
+ - lib/pg_reports/connection/target.rb
167
174
  - lib/pg_reports/dashboard/reports_registry.rb
168
175
  - lib/pg_reports/definitions/connections/active_connections.yml
169
176
  - lib/pg_reports/definitions/connections/blocking_queries.yml
@@ -279,6 +286,7 @@ files:
279
286
  - lib/pg_reports/sql/tables/update_hotspots.sql
280
287
  - lib/pg_reports/sql/tables/vacuum_needed.sql
281
288
  - lib/pg_reports/sql_loader.rb
289
+ - lib/pg_reports/standalone.rb
282
290
  - lib/pg_reports/telegram_sender.rb
283
291
  - lib/pg_reports/version.rb
284
292
  - lib/tasks/pg_reports.rake
@@ -289,16 +297,6 @@ metadata:
289
297
  homepage_uri: https://github.com/deadalice/pg_reports
290
298
  source_code_uri: https://github.com/deadalice/pg_reports
291
299
  changelog_uri: https://github.com/deadalice/pg_reports/blob/main/CHANGELOG.md
292
- post_install_message: |
293
- Thanks for installing pg_reports v0.7.0!
294
-
295
- New in 0.7.0 — experimental Grafana / Prometheus support
296
- ────────────────────────────────────────────────────────
297
- Expose selected reports at <mount_point>/metrics in Prometheus exposition
298
- format and import a ready-to-use Grafana dashboard:
299
-
300
- Setup guide:
301
- https://github.com/deadalice/pg_reports/blob/main/docs/grafana.md
302
300
  rdoc_options: []
303
301
  require_paths:
304
302
  - lib
@@ -313,7 +311,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
313
311
  - !ruby/object:Gem::Version
314
312
  version: '0'
315
313
  requirements: []
316
- rubygems_version: 4.0.4
314
+ rubygems_version: 3.6.9
317
315
  specification_version: 4
318
316
  summary: PostgreSQL analysis and reporting tool with Telegram integration
319
317
  test_files: []