pgbouncerhero 3.1.0 → 3.2.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 +17 -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 +8 -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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Quentin Rousseau
|
|
@@ -265,6 +265,8 @@ files:
|
|
|
265
265
|
- app/controllers/pg_bouncer_hero/home_controller.rb
|
|
266
266
|
- app/helpers/pg_bouncer_hero/application_helper.rb
|
|
267
267
|
- app/javascript/pgbouncerhero/application.js
|
|
268
|
+
- app/javascript/pgbouncerhero/controllers/data_table_controller.js
|
|
269
|
+
- app/javascript/pgbouncerhero/controllers/fleet_controller.js
|
|
268
270
|
- app/javascript/pgbouncerhero/controllers/polling_controller.js
|
|
269
271
|
- app/views/layouts/pg_bouncer_hero/application.html.erb
|
|
270
272
|
- app/views/pg_bouncer_hero/database/_actions.html.erb
|
|
@@ -273,20 +275,25 @@ files:
|
|
|
273
275
|
- app/views/pg_bouncer_hero/database/_databases.html.erb
|
|
274
276
|
- app/views/pg_bouncer_hero/database/_menu.html.erb
|
|
275
277
|
- app/views/pg_bouncer_hero/database/_pools.html.erb
|
|
278
|
+
- app/views/pg_bouncer_hero/database/_servers.html.erb
|
|
276
279
|
- app/views/pg_bouncer_hero/database/_state.html.erb
|
|
277
280
|
- app/views/pg_bouncer_hero/database/_stats.html.erb
|
|
281
|
+
- app/views/pg_bouncer_hero/database/_users.html.erb
|
|
278
282
|
- app/views/pg_bouncer_hero/database/clients.html.erb
|
|
279
283
|
- app/views/pg_bouncer_hero/database/conf.html.erb
|
|
280
284
|
- app/views/pg_bouncer_hero/database/databases.html.erb
|
|
281
285
|
- app/views/pg_bouncer_hero/database/pools.html.erb
|
|
286
|
+
- app/views/pg_bouncer_hero/database/servers.html.erb
|
|
282
287
|
- app/views/pg_bouncer_hero/database/state.html.erb
|
|
283
288
|
- app/views/pg_bouncer_hero/database/stats.html.erb
|
|
284
289
|
- app/views/pg_bouncer_hero/database/summary.html.erb
|
|
290
|
+
- app/views/pg_bouncer_hero/database/users.html.erb
|
|
285
291
|
- app/views/pg_bouncer_hero/home/_card.html.erb
|
|
286
292
|
- app/views/pg_bouncer_hero/home/_card_content.html.erb
|
|
287
293
|
- app/views/pg_bouncer_hero/home/_card_loading_content.html.erb
|
|
288
294
|
- app/views/pg_bouncer_hero/home/index.html.erb
|
|
289
295
|
- app/views/shared/_alert.html.erb
|
|
296
|
+
- app/views/shared/_data_table_toolbar.html.erb
|
|
290
297
|
- app/views/shared/_flash_messages.html.erb
|
|
291
298
|
- config/importmap.rb
|
|
292
299
|
- config/routes.rb
|