pgbouncerhero 3.1.0 → 3.3.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 +23 -0
- data/README.md +39 -0
- data/app/assets/builds/pgbouncerhero/application.css +2 -2
- data/app/assets/stylesheets/pgbouncerhero/application.css +11 -1
- data/app/controllers/pg_bouncer_hero/database_controller.rb +75 -11
- data/app/helpers/pg_bouncer_hero/application_helper.rb +54 -0
- data/app/javascript/pgbouncerhero/controllers/data_table_controller.js +183 -0
- data/app/javascript/pgbouncerhero/controllers/fleet_controller.js +52 -0
- data/app/javascript/pgbouncerhero/controllers/polling_controller.js +49 -5
- data/app/views/layouts/pg_bouncer_hero/application.html.erb +1 -1
- data/app/views/pg_bouncer_hero/database/_actions.html.erb +1 -1
- data/app/views/pg_bouncer_hero/database/_clients.html.erb +9 -8
- data/app/views/pg_bouncer_hero/database/_conf.html.erb +10 -9
- data/app/views/pg_bouncer_hero/database/_databases.html.erb +30 -9
- data/app/views/pg_bouncer_hero/database/_menu.html.erb +13 -4
- data/app/views/pg_bouncer_hero/database/_pools.html.erb +10 -9
- data/app/views/pg_bouncer_hero/database/_servers.html.erb +31 -0
- data/app/views/pg_bouncer_hero/database/_state.html.erb +1 -1
- data/app/views/pg_bouncer_hero/database/_stats.html.erb +12 -11
- data/app/views/pg_bouncer_hero/database/_users.html.erb +31 -0
- data/app/views/pg_bouncer_hero/database/clients.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/conf.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/databases.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/pools.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/servers.html.erb +6 -0
- data/app/views/pg_bouncer_hero/database/state.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/stats.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/users.html.erb +6 -0
- data/app/views/pg_bouncer_hero/home/_card.html.erb +5 -2
- data/app/views/pg_bouncer_hero/home/_card_content.html.erb +48 -31
- data/app/views/pg_bouncer_hero/home/index.html.erb +28 -8
- data/app/views/shared/_data_table_toolbar.html.erb +13 -0
- data/config/routes.rb +6 -0
- data/lib/generators/pgbouncerhero/templates/config.yml +3 -1
- data/lib/pgbouncerhero/database.rb +4 -0
- data/lib/pgbouncerhero/methods/basics.rb +68 -8
- data/lib/pgbouncerhero/version.rb +1 -1
- data/lib/pgbouncerhero.rb +16 -0
- metadata +22 -1
|
@@ -1,18 +1,38 @@
|
|
|
1
|
-
<div
|
|
2
|
-
<div class="
|
|
3
|
-
<
|
|
1
|
+
<div data-controller="fleet polling" data-polling-interval-value="60000" data-fleet-total-value="<%= @groups.values.sum { |group| group.databases.size } %>" data-action="turbo:frame-load->fleet#refresh turbo:frame-load->polling#refreshed">
|
|
2
|
+
<div class="bg-white rounded-lg border border-gray-200 shadow-xs mb-6">
|
|
3
|
+
<div class="px-4 py-3 border-b border-gray-200 flex flex-wrap items-center justify-between gap-2">
|
|
4
|
+
<div>
|
|
5
|
+
<h2 class="text-lg font-semibold text-gray-900">Fleet health</h2>
|
|
6
|
+
<p class="text-sm text-gray-500" data-fleet-target="headline" aria-live="polite">Checking <%= pluralize(@groups.values.sum { |group| group.databases.size }, "instance") %>…</p>
|
|
7
|
+
</div>
|
|
8
|
+
<span class="text-xs text-gray-400" data-polling-target="status">Loading current status…</span>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="grid grid-cols-1 sm:grid-cols-3 divide-y sm:divide-y-0 sm:divide-x divide-gray-200">
|
|
11
|
+
<div class="px-4 py-3">
|
|
12
|
+
<div class="text-xs font-medium uppercase tracking-wider text-gray-500">Offline</div>
|
|
13
|
+
<div class="text-xl font-semibold text-red-700" data-fleet-target="offline">—</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="px-4 py-3">
|
|
16
|
+
<div class="text-xs font-medium uppercase tracking-wider text-gray-500">Waiting clients</div>
|
|
17
|
+
<div class="text-xl font-semibold text-yellow-800" data-fleet-target="waiting">—</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="px-4 py-3">
|
|
20
|
+
<div class="text-xs font-medium uppercase tracking-wider text-gray-500">High utilization</div>
|
|
21
|
+
<div class="text-xl font-semibold text-amber-800" data-fleet-target="utilization">—</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
4
24
|
</div>
|
|
5
|
-
</div>
|
|
6
25
|
|
|
7
|
-
<div data-controller="polling" data-polling-interval-value="60000">
|
|
8
26
|
<% @groups.each do |_, group| %>
|
|
9
27
|
<div class="mb-8">
|
|
10
28
|
<h3 class="text-xl font-semibold text-gray-900 mb-4 pb-2 border-b border-gray-200">
|
|
11
29
|
<%= group.name %>
|
|
12
30
|
</h3>
|
|
13
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
14
|
-
<% group.databases.
|
|
15
|
-
|
|
31
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" data-fleet-target="group">
|
|
32
|
+
<% group.databases.each_with_index do |database, index| %>
|
|
33
|
+
<div data-fleet-target="item" data-fleet-original-index="<%= index %>">
|
|
34
|
+
<%= render partial: "card", locals: { database: database } %>
|
|
35
|
+
</div>
|
|
16
36
|
<% end %>
|
|
17
37
|
</div>
|
|
18
38
|
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="flex items-start gap-3 flex-wrap">
|
|
2
|
+
<label class="sr-only" for="<%= table_id %>_search">Search <%= title.downcase %></label>
|
|
3
|
+
<input id="<%= table_id %>_search" type="search" placeholder="Search <%= title.downcase %>…" class="w-56 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700" data-data-table-target="query" data-action="input->data-table#filter">
|
|
4
|
+
<details class="relative text-sm text-gray-700">
|
|
5
|
+
<summary class="cursor-pointer rounded-md border border-gray-300 bg-white px-3 py-1.5 font-medium">Columns</summary>
|
|
6
|
+
<div class="absolute right-0 z-20 mt-2 w-max min-w-64 rounded-md border border-gray-200 bg-white p-3 text-xs shadow-lg">
|
|
7
|
+
<div class="grid grid-cols-2 gap-x-4 gap-y-2" data-data-table-target="columnMenu"></div>
|
|
8
|
+
<button type="button" class="mt-3 w-full border-t border-gray-200 pt-2 text-left font-medium text-gray-600 hover:text-gray-900" data-action="data-table#resetColumns">Reset columns</button>
|
|
9
|
+
</div>
|
|
10
|
+
</details>
|
|
11
|
+
<span class="py-1.5 text-xs text-gray-500" data-data-table-target="status"><%= row_count %> rows</span>
|
|
12
|
+
</div>
|
|
13
|
+
<p class="mt-3 text-sm text-gray-500" data-data-table-target="empty" hidden>No rows match this search.</p>
|
data/config/routes.rb
CHANGED
|
@@ -13,11 +13,17 @@ PgBouncerHero::Engine.routes.draw do
|
|
|
13
13
|
get :stats, controller: :database
|
|
14
14
|
get :pools, controller: :database
|
|
15
15
|
get :clients, controller: :database
|
|
16
|
+
get :servers, controller: :database
|
|
17
|
+
get :users, controller: :database
|
|
16
18
|
get :conf, controller: :database
|
|
17
19
|
get :state, controller: :database
|
|
18
20
|
post :reload, controller: :database
|
|
19
21
|
post :suspend, controller: :database
|
|
20
22
|
post :resume, controller: :database
|
|
23
|
+
post :pause_database, controller: :database
|
|
24
|
+
post :reconnect_database, controller: :database
|
|
25
|
+
post :wait_close_database, controller: :database
|
|
26
|
+
post :resume_database, controller: :database
|
|
21
27
|
post :shutdown, controller: :database
|
|
22
28
|
end
|
|
23
29
|
end
|
|
@@ -5,6 +5,7 @@ pgbouncers:
|
|
|
5
5
|
# url: <%%= ENV["PGBOUNCER_PRODUCTION_PRIMARY_DATABASE_URL"] %>
|
|
6
6
|
# pool_size: 5
|
|
7
7
|
# pool_timeout: 5
|
|
8
|
+
# read_only: true
|
|
8
9
|
# Add more databases
|
|
9
10
|
# replica:
|
|
10
11
|
# url: <%%= ENV["PGBOUNCER_PRODUCTION_REPLICA_DATABASE_URL"] %>
|
|
@@ -17,5 +18,6 @@ pgbouncers:
|
|
|
17
18
|
# Time zone (defaults to app time zone)
|
|
18
19
|
# time_zone: "Pacific Time (US & Canada)"
|
|
19
20
|
|
|
20
|
-
# Hide and reject
|
|
21
|
+
# Hide and reject administrative commands for every PgBouncer unless an
|
|
22
|
+
# individual PgBouncer sets read_only explicitly
|
|
21
23
|
# read_only: true
|
|
@@ -12,9 +12,12 @@ module PgBouncerHero
|
|
|
12
12
|
if connection
|
|
13
13
|
l = lists
|
|
14
14
|
d = databases
|
|
15
|
+
p = pools
|
|
15
16
|
l = l.as_json
|
|
16
17
|
d = d.as_json.reject { |a| a["name"] == "pgbouncer" }
|
|
18
|
+
p = p.as_json
|
|
17
19
|
l.push({ databases_details: d })
|
|
20
|
+
l.push({ pools_details: p })
|
|
18
21
|
l
|
|
19
22
|
end
|
|
20
23
|
end
|
|
@@ -33,6 +36,12 @@ module PgBouncerHero
|
|
|
33
36
|
def clients
|
|
34
37
|
execute("SHOW clients")
|
|
35
38
|
end
|
|
39
|
+
def servers
|
|
40
|
+
execute("SHOW servers")
|
|
41
|
+
end
|
|
42
|
+
def users
|
|
43
|
+
execute("SHOW users")
|
|
44
|
+
end
|
|
36
45
|
def conf
|
|
37
46
|
execute("SHOW config")
|
|
38
47
|
end
|
|
@@ -40,19 +49,70 @@ module PgBouncerHero
|
|
|
40
49
|
execute("SHOW state")
|
|
41
50
|
end
|
|
42
51
|
def reload
|
|
43
|
-
execute("RELOAD")
|
|
52
|
+
instrument_admin_command(:reload) { execute("RELOAD") }
|
|
44
53
|
end
|
|
45
54
|
def suspend
|
|
46
|
-
execute("SUSPEND")
|
|
55
|
+
instrument_admin_command(:suspend) { execute("SUSPEND") }
|
|
56
|
+
end
|
|
57
|
+
def pause(database_name)
|
|
58
|
+
execute_database_command(:pause, "PAUSE", database_name)
|
|
47
59
|
end
|
|
48
|
-
def
|
|
49
|
-
|
|
60
|
+
def reconnect(database_name)
|
|
61
|
+
execute_database_command(:reconnect, "RECONNECT", database_name)
|
|
62
|
+
end
|
|
63
|
+
def wait_close(database_name)
|
|
64
|
+
execute_database_command(:wait_close, "WAIT_CLOSE", database_name)
|
|
65
|
+
end
|
|
66
|
+
def resume(database_name = nil)
|
|
67
|
+
return instrument_admin_command(:resume) { execute("RESUME") } if database_name.nil?
|
|
68
|
+
|
|
69
|
+
execute_database_command(:resume, "RESUME", database_name)
|
|
50
70
|
end
|
|
51
71
|
def shutdown(mode = nil)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
instrument_admin_command(:shutdown, mode: mode || :immediate) do
|
|
73
|
+
suffix = SHUTDOWN_MODES.fetch(mode)
|
|
74
|
+
execute("SHUTDOWN#{suffix}")
|
|
75
|
+
rescue KeyError
|
|
76
|
+
raise ArgumentError, "unsupported shutdown mode: #{mode.inspect}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def execute_database_command(action, command, database_name)
|
|
83
|
+
name = database_name.to_s
|
|
84
|
+
instrument_admin_command(action, target_database: name) do
|
|
85
|
+
raise ArgumentError, "database name must not be empty" if name.empty?
|
|
86
|
+
|
|
87
|
+
execute("#{command} #{PG::Connection.quote_ident(name)}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def instrument_admin_command(action, target_database: nil, **attributes)
|
|
92
|
+
payload = {
|
|
93
|
+
group: group.name,
|
|
94
|
+
database: name,
|
|
95
|
+
action: action,
|
|
96
|
+
target_database: target_database,
|
|
97
|
+
**attributes
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
error = nil
|
|
101
|
+
result = ActiveSupport::Notifications.instrument(PgBouncerHero::ADMIN_COMMAND_EVENT, payload) do
|
|
102
|
+
begin
|
|
103
|
+
command_result = yield
|
|
104
|
+
payload[:outcome] = command_result.nil? ? :unavailable : :success
|
|
105
|
+
command_result
|
|
106
|
+
rescue StandardError => command_error
|
|
107
|
+
error = command_error
|
|
108
|
+
payload[:outcome] = :error
|
|
109
|
+
payload[:error_class] = command_error.class.name
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
raise error if error
|
|
114
|
+
|
|
115
|
+
result
|
|
56
116
|
end
|
|
57
117
|
end
|
|
58
118
|
end
|
data/lib/pgbouncerhero.rb
CHANGED
|
@@ -3,6 +3,7 @@ require "monitor"
|
|
|
3
3
|
require "pathname"
|
|
4
4
|
require "yaml"
|
|
5
5
|
require "rails"
|
|
6
|
+
require "active_support/notifications"
|
|
6
7
|
require "pg"
|
|
7
8
|
require "importmap-rails"
|
|
8
9
|
require "turbo-rails"
|
|
@@ -15,6 +16,8 @@ require "pgbouncerhero/group"
|
|
|
15
16
|
require "pgbouncerhero/engine"
|
|
16
17
|
|
|
17
18
|
module PgBouncerHero
|
|
19
|
+
ADMIN_COMMAND_EVENT = "admin_command.pgbouncerhero"
|
|
20
|
+
|
|
18
21
|
class ConfigurationError < StandardError; end
|
|
19
22
|
|
|
20
23
|
BOOLEAN_SETTINGS = {
|
|
@@ -79,6 +82,17 @@ module PgBouncerHero
|
|
|
79
82
|
|
|
80
83
|
def read_only?
|
|
81
84
|
value = ENV.fetch("PGBOUNCERHERO_READ_ONLY") { config.fetch("read_only", false) }
|
|
85
|
+
parse_read_only(value)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def read_only_for?(database)
|
|
89
|
+
value = ENV.fetch("PGBOUNCERHERO_READ_ONLY") do
|
|
90
|
+
database.config.fetch("read_only") { config.fetch("read_only", false) }
|
|
91
|
+
end
|
|
92
|
+
parse_read_only(value)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parse_read_only(value)
|
|
82
96
|
BOOLEAN_SETTINGS.fetch(value.is_a?(String) ? value.downcase : value)
|
|
83
97
|
rescue KeyError
|
|
84
98
|
raise ConfigurationError, "read_only must be a boolean"
|
|
@@ -100,6 +114,8 @@ module PgBouncerHero
|
|
|
100
114
|
end
|
|
101
115
|
end
|
|
102
116
|
|
|
117
|
+
private :parse_read_only
|
|
118
|
+
|
|
103
119
|
private
|
|
104
120
|
|
|
105
121
|
def default_config_path
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgbouncerhero
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Quentin Rousseau
|
|
@@ -247,6 +247,20 @@ dependencies:
|
|
|
247
247
|
- - "~>"
|
|
248
248
|
- !ruby/object:Gem::Version
|
|
249
249
|
version: '0.8'
|
|
250
|
+
- !ruby/object:Gem::Dependency
|
|
251
|
+
name: json
|
|
252
|
+
requirement: !ruby/object:Gem::Requirement
|
|
253
|
+
requirements:
|
|
254
|
+
- - "<"
|
|
255
|
+
- !ruby/object:Gem::Version
|
|
256
|
+
version: '4'
|
|
257
|
+
type: :development
|
|
258
|
+
prerelease: false
|
|
259
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
260
|
+
requirements:
|
|
261
|
+
- - "<"
|
|
262
|
+
- !ruby/object:Gem::Version
|
|
263
|
+
version: '4'
|
|
250
264
|
description: A Rails engine providing a web dashboard for monitoring and managing
|
|
251
265
|
one or multiple PgBouncer connection poolers.
|
|
252
266
|
email:
|
|
@@ -265,6 +279,8 @@ files:
|
|
|
265
279
|
- app/controllers/pg_bouncer_hero/home_controller.rb
|
|
266
280
|
- app/helpers/pg_bouncer_hero/application_helper.rb
|
|
267
281
|
- app/javascript/pgbouncerhero/application.js
|
|
282
|
+
- app/javascript/pgbouncerhero/controllers/data_table_controller.js
|
|
283
|
+
- app/javascript/pgbouncerhero/controllers/fleet_controller.js
|
|
268
284
|
- app/javascript/pgbouncerhero/controllers/polling_controller.js
|
|
269
285
|
- app/views/layouts/pg_bouncer_hero/application.html.erb
|
|
270
286
|
- app/views/pg_bouncer_hero/database/_actions.html.erb
|
|
@@ -273,20 +289,25 @@ files:
|
|
|
273
289
|
- app/views/pg_bouncer_hero/database/_databases.html.erb
|
|
274
290
|
- app/views/pg_bouncer_hero/database/_menu.html.erb
|
|
275
291
|
- app/views/pg_bouncer_hero/database/_pools.html.erb
|
|
292
|
+
- app/views/pg_bouncer_hero/database/_servers.html.erb
|
|
276
293
|
- app/views/pg_bouncer_hero/database/_state.html.erb
|
|
277
294
|
- app/views/pg_bouncer_hero/database/_stats.html.erb
|
|
295
|
+
- app/views/pg_bouncer_hero/database/_users.html.erb
|
|
278
296
|
- app/views/pg_bouncer_hero/database/clients.html.erb
|
|
279
297
|
- app/views/pg_bouncer_hero/database/conf.html.erb
|
|
280
298
|
- app/views/pg_bouncer_hero/database/databases.html.erb
|
|
281
299
|
- app/views/pg_bouncer_hero/database/pools.html.erb
|
|
300
|
+
- app/views/pg_bouncer_hero/database/servers.html.erb
|
|
282
301
|
- app/views/pg_bouncer_hero/database/state.html.erb
|
|
283
302
|
- app/views/pg_bouncer_hero/database/stats.html.erb
|
|
284
303
|
- app/views/pg_bouncer_hero/database/summary.html.erb
|
|
304
|
+
- app/views/pg_bouncer_hero/database/users.html.erb
|
|
285
305
|
- app/views/pg_bouncer_hero/home/_card.html.erb
|
|
286
306
|
- app/views/pg_bouncer_hero/home/_card_content.html.erb
|
|
287
307
|
- app/views/pg_bouncer_hero/home/_card_loading_content.html.erb
|
|
288
308
|
- app/views/pg_bouncer_hero/home/index.html.erb
|
|
289
309
|
- app/views/shared/_alert.html.erb
|
|
310
|
+
- app/views/shared/_data_table_toolbar.html.erb
|
|
290
311
|
- app/views/shared/_flash_messages.html.erb
|
|
291
312
|
- config/importmap.rb
|
|
292
313
|
- config/routes.rb
|