pgbouncerhero 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +24 -0
- data/README.md +125 -0
- data/app/assets/images/pgbouncerhero/short-paragraph.png +0 -0
- data/app/assets/javascripts/pgbouncerhero/application.js +2 -0
- data/app/assets/stylesheets/pgbouncerhero/application.css.scss +15 -0
- data/app/controllers/pg_bouncer_hero/application_controller.rb +27 -0
- data/app/controllers/pg_bouncer_hero/database_controller.rb +70 -0
- data/app/controllers/pg_bouncer_hero/home_controller.rb +6 -0
- data/app/helpers/pg_bouncer_hero/application_helper.rb +29 -0
- data/app/views/layouts/pg_bouncer_hero/application.html.haml +20 -0
- data/app/views/pg_bouncer_hero/database/_actions.html.haml +10 -0
- data/app/views/pg_bouncer_hero/database/_clients.html.haml +15 -0
- data/app/views/pg_bouncer_hero/database/_conf.html.haml +15 -0
- data/app/views/pg_bouncer_hero/database/_databases.html.haml +15 -0
- data/app/views/pg_bouncer_hero/database/_menu.html.haml +15 -0
- data/app/views/pg_bouncer_hero/database/_pools.html.haml +15 -0
- data/app/views/pg_bouncer_hero/database/_stats.html.haml +20 -0
- data/app/views/pg_bouncer_hero/database/clients.html.haml +4 -0
- data/app/views/pg_bouncer_hero/database/conf.html.haml +4 -0
- data/app/views/pg_bouncer_hero/database/databases.html.haml +5 -0
- data/app/views/pg_bouncer_hero/database/pools.html.haml +4 -0
- data/app/views/pg_bouncer_hero/database/stats.html.haml +4 -0
- data/app/views/pg_bouncer_hero/database/summary.js.haml +3 -0
- data/app/views/pg_bouncer_hero/home/_card.html.haml +15 -0
- data/app/views/pg_bouncer_hero/home/_card_content.html.haml +26 -0
- data/app/views/pg_bouncer_hero/home/_card_loading_content.html.haml +5 -0
- data/app/views/pg_bouncer_hero/home/index.html.haml +31 -0
- data/app/views/shared/_alert.html.haml +4 -0
- data/app/views/shared/_flash_messages.html.haml +2 -0
- data/config/routes.rb +16 -0
- data/doc/screenshot-1.png +0 -0
- data/doc/screenshot-2.png +0 -0
- data/doc/screenshot-3.png +0 -0
- data/lib/generators/pgbouncerhero/config_generator.rb +13 -0
- data/lib/generators/pgbouncerhero/templates/config.yml +16 -0
- data/lib/pgbouncerhero.rb +65 -0
- data/lib/pgbouncerhero/connection.rb +29 -0
- data/lib/pgbouncerhero/database.rb +55 -0
- data/lib/pgbouncerhero/engine.rb +20 -0
- data/lib/pgbouncerhero/group.rb +21 -0
- data/lib/pgbouncerhero/methods/basics.rb +41 -0
- data/lib/pgbouncerhero/version.rb +3 -0
- data/pgbouncerhero.gemspec +32 -0
- metadata +188 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 054de6f1d8d3846419f355c4512129e06385c8af
|
4
|
+
data.tar.gz: 1254bddb23ead79c688f283b07e179b41a328fd9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cf35ed3c426b20262a6be30a555eba7547c8cd1c89db7fe200a76babe5f4842298df9b0b7eb4ce0a9b3efb555d46c392c67627f4aed826417997ff4830fe9a71
|
7
|
+
data.tar.gz: fb73c61c6dda241124a59db1820083cfcf24d4cf25ae16f3c524feb7f05485d1054ec74bed889a2faad6847773603aec8d565003f4d81f8468e92de8c9374204
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Copyright (c) 2017 Quentin Rousseau <contact@quent.in>
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person
|
6
|
+
obtaining a copy of this software and associated documentation
|
7
|
+
files (the "Software"), to deal in the Software without
|
8
|
+
restriction, including without limitation the rights to use,
|
9
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the
|
11
|
+
Software is furnished to do so, subject to the following
|
12
|
+
conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
19
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
21
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
22
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
23
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
24
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# PgBouncerHero
|
2
|
+
|
3
|
+
A graphical user interface for your PGBouncers.
|
4
|
+
|
5
|
+
[See it in action](https://pgbouncerhero-demo.herokuapp.com/). [Source Code](https://github.com/kwent/pgbouncerhero-demo).
|
6
|
+
|
7
|
+
[![Screenshot1](https://github.com/kwent/pgbouncerhero/blob/master/doc/screenshot-1.png?raw=true)](https://pgbouncerhero-demo.herokuapp.com/)
|
8
|
+
[![Screenshot2](https://github.com/kwent/pgbouncerhero/blob/master/doc/screenshot-2.png?raw=true)](https://pgbouncerhero-demo.herokuapp.com/)
|
9
|
+
[![Screenshot2](https://github.com/kwent/pgbouncerhero/blob/master/doc/screenshot-3.png?raw=true)](https://pgbouncerhero-demo.herokuapp.com/)
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
PgBouncerHero is available as a Rails engine.
|
14
|
+
|
15
|
+
Add those dependencies to your application’s Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'pgbouncerhero'
|
19
|
+
gem 'jquery-rails'
|
20
|
+
gem 'sass-rails'
|
21
|
+
gem 'semantic-ui-sass'
|
22
|
+
gem 'haml-rails'
|
23
|
+
gem 'pg'
|
24
|
+
```
|
25
|
+
|
26
|
+
And mount the engine in your `config/routes.rb`:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
mount PgBouncerHero::Engine, at: "pgbouncerhero"
|
30
|
+
```
|
31
|
+
|
32
|
+
Add the following line in your `application.css`:
|
33
|
+
|
34
|
+
```
|
35
|
+
*= require pgbouncerhero/application
|
36
|
+
```
|
37
|
+
|
38
|
+
Add the following lines in your `application.js`:
|
39
|
+
|
40
|
+
```
|
41
|
+
//= require jquery
|
42
|
+
//= require jquery_ujs
|
43
|
+
//= require pgbouncerhero/application
|
44
|
+
```
|
45
|
+
|
46
|
+
### Basic Authentication
|
47
|
+
|
48
|
+
Set the following variables in your environment or an initializer.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
ENV["PGBOUNCERHERO_USERNAME"] = "zelda"
|
52
|
+
ENV["PGBOUNCERHERO_PASSWORD"] = "triforce"
|
53
|
+
```
|
54
|
+
|
55
|
+
### Devise
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
authenticate :user, -> (user) { user.admin? } do
|
59
|
+
mount PgBouncerHero::Engine, at: "pgbouncerhero"
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
## One PgBouncer
|
64
|
+
|
65
|
+
```bash
|
66
|
+
export PGBOUNCERHERO_DATABASE_URL=postgres://user:password@host:port/pgbouncer
|
67
|
+
```
|
68
|
+
|
69
|
+
## Multiple PgBouncers
|
70
|
+
|
71
|
+
Create `config/pgbouncerhero.yml` with:
|
72
|
+
|
73
|
+
```yml
|
74
|
+
default: &default
|
75
|
+
pgbouncers:
|
76
|
+
production:
|
77
|
+
master:
|
78
|
+
url: <%= ENV["PGBOUNCER_PRODUCTION_MASTER_DATABASE_URL"] %>
|
79
|
+
slave:
|
80
|
+
url: <%= ENV["PGBOUNCER_PRODUCTION_SLAVE_DATABASE_URL"] %>
|
81
|
+
staging:
|
82
|
+
master:
|
83
|
+
url: <%= ENV["PGBOUNCER_STAGING_MASTER_DATABASE_URL"] %>
|
84
|
+
slave:
|
85
|
+
url: <%= ENV["PGBOUNCER_STAGING_SLAVE_DATABASE_URL"] %>
|
86
|
+
|
87
|
+
development:
|
88
|
+
<<: *default
|
89
|
+
|
90
|
+
production:
|
91
|
+
<<: *default
|
92
|
+
```
|
93
|
+
|
94
|
+
# Authors
|
95
|
+
|
96
|
+
- [Quentin Rousseau](https://github.com/kwent)
|
97
|
+
|
98
|
+
# License
|
99
|
+
|
100
|
+
```plain
|
101
|
+
Copyright (c) 2017 Quentin Rousseau <contact@quent.in>
|
102
|
+
|
103
|
+
MIT License
|
104
|
+
|
105
|
+
Permission is hereby granted, free of charge, to any person
|
106
|
+
obtaining a copy of this software and associated documentation
|
107
|
+
files (the "Software"), to deal in the Software without
|
108
|
+
restriction, including without limitation the rights to use,
|
109
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
110
|
+
copies of the Software, and to permit persons to whom the
|
111
|
+
Software is furnished to do so, subject to the following
|
112
|
+
conditions:
|
113
|
+
|
114
|
+
The above copyright notice and this permission notice shall be
|
115
|
+
included in all copies or substantial portions of the Software.
|
116
|
+
|
117
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
118
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
119
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
120
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
121
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
122
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
123
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
124
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
125
|
+
```
|
Binary file
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
class ApplicationController < ActionController::Base
|
3
|
+
layout "pg_bouncer_hero/application"
|
4
|
+
|
5
|
+
protect_from_forgery
|
6
|
+
|
7
|
+
http_basic_authenticate_with name: ENV["PGBOUNCERHERO_USERNAME"], password: ENV["PGBOUNCERHERO_PASSWORD"] if ENV["PGBOUNCERHERO_PASSWORD"]
|
8
|
+
|
9
|
+
if respond_to?(:before_action)
|
10
|
+
before_action :set_database
|
11
|
+
else
|
12
|
+
before_filter :set_database
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def set_database
|
18
|
+
@groups = PgBouncerHero.groups
|
19
|
+
if params[:group] && params[:database]
|
20
|
+
@database = PgBouncerHero.groups[params[:group]].databases.find { |db| db.id.to_s == params[:database].to_s }
|
21
|
+
else
|
22
|
+
@group = @groups.first
|
23
|
+
@database = @groups.first.last.databases.first
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
class DatabaseController < ApplicationController
|
3
|
+
def summary
|
4
|
+
if @database.connection
|
5
|
+
@dbs = @database.summary
|
6
|
+
else
|
7
|
+
flash[:error] = "#{@database.name} does not look online."
|
8
|
+
end
|
9
|
+
end
|
10
|
+
def databases
|
11
|
+
if @database.connection
|
12
|
+
@dbs = @database.databases
|
13
|
+
else
|
14
|
+
flash[:error] = "#{@database.name} does not look online."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
def stats
|
18
|
+
if @database.connection
|
19
|
+
@stats = @database.stats
|
20
|
+
else
|
21
|
+
flash[:error] = "#{@database.name} does not look online."
|
22
|
+
end
|
23
|
+
end
|
24
|
+
def pools
|
25
|
+
if @database.connection
|
26
|
+
@pools = @database.pools
|
27
|
+
else
|
28
|
+
flash[:error] = "#{@database.name} does not look online."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
def clients
|
32
|
+
if @database.connection
|
33
|
+
@clients = @database.clients
|
34
|
+
else
|
35
|
+
flash[:error] = "#{@database.name} does not look online."
|
36
|
+
end
|
37
|
+
end
|
38
|
+
def conf
|
39
|
+
if @database.connection
|
40
|
+
@conf = @database.conf
|
41
|
+
else
|
42
|
+
flash[:error] = "#{@database.name} does not look online."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
def reload
|
46
|
+
if @database.connection
|
47
|
+
@database.reload
|
48
|
+
flash[:success] = "#{@database.name} has been reloaded."
|
49
|
+
else
|
50
|
+
flash[:error] = "#{@database.name} does not look online."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
def suspend
|
54
|
+
if @database.connection
|
55
|
+
@database.suspend
|
56
|
+
flash[:success] = "#{@database.name} has been suspended."
|
57
|
+
else
|
58
|
+
flash[:error] = "#{@database.name} does not look online."
|
59
|
+
end
|
60
|
+
end
|
61
|
+
def shutdown
|
62
|
+
if @database.connection
|
63
|
+
@database.shutdown
|
64
|
+
flash[:success] = "#{@database.name} has been shutdown."
|
65
|
+
else
|
66
|
+
flash[:error] = "#{@database.name} does not look online."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
module ApplicationHelper
|
3
|
+
def is_active(action_name)
|
4
|
+
params[:action] == action_name ? 'active' : nil
|
5
|
+
end
|
6
|
+
def alert_class_for(flash_type)
|
7
|
+
case flash_type
|
8
|
+
when 'success'
|
9
|
+
'success'
|
10
|
+
when 'error'
|
11
|
+
'error'
|
12
|
+
when 'notice'
|
13
|
+
'info'
|
14
|
+
when 'warning'
|
15
|
+
'warning'
|
16
|
+
else
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
def humanize_ms(millis)
|
21
|
+
[[1000, :ms], [60, :s], [60, :min], [24, :h], [1000, :d]].map{ |count, name|
|
22
|
+
if millis > 0
|
23
|
+
millis, n = millis.divmod(count)
|
24
|
+
"#{n.to_i} #{name}"
|
25
|
+
end
|
26
|
+
}.compact.reverse.join(' ')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
!!!
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%meta{charset: "utf-8"}
|
5
|
+
%meta{content: "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible"}
|
6
|
+
%meta{content: "width=device-width, initial-scale=1.0, maximum-scale=1.0", name: "viewport"}
|
7
|
+
%title= [@groups.size > 1 ? "#{@database.group.name} | #{@database.name}" : "PgBouncerHero", @title].compact.join(" / ")
|
8
|
+
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': true
|
9
|
+
= csrf_meta_tags
|
10
|
+
%body
|
11
|
+
.ui.fixed.menu
|
12
|
+
.ui.container
|
13
|
+
= link_to'PGBouncerHero', root_path, class: 'header item'
|
14
|
+
.pusher
|
15
|
+
.ui.main.container
|
16
|
+
#flash.row= render partial: 'shared/flash_messages', flash: flash
|
17
|
+
= yield
|
18
|
+
= javascript_include_tag 'application'
|
19
|
+
- if content_for? :inline_script
|
20
|
+
= yield :inline_script
|
@@ -0,0 +1,10 @@
|
|
1
|
+
.ui.active.segment
|
2
|
+
|
3
|
+
%h4.ui.header
|
4
|
+
.content Actions
|
5
|
+
|
6
|
+
%button.ui.negative.basic.button Pause
|
7
|
+
%button.ui.positive.basic.button Resume
|
8
|
+
%button.ui.negative.basic.button Disable
|
9
|
+
%button.ui.positive.basic.button Enable
|
10
|
+
%button.ui.negative.basic.button Kill
|
@@ -0,0 +1,15 @@
|
|
1
|
+
.ui.pointing.menu
|
2
|
+
.header.item
|
3
|
+
= "#{database.group.name} > #{database.name}"
|
4
|
+
= link_to "Databases", databases_path(database: database.name.parameterize), class: "item #{is_active('databases')}"
|
5
|
+
= link_to "Stats", stats_path(database: database.name.parameterize), class: "item #{is_active('stats')}"
|
6
|
+
= link_to "Pools", pools_path(database: database.name.parameterize), class: "item #{is_active('pools')}"
|
7
|
+
= link_to "Clients", clients_path(database: database.name.parameterize), class: "item #{is_active('clients')}"
|
8
|
+
= link_to "Configuration", conf_path(database: database.name.parameterize), class: "item #{is_active('conf')}"
|
9
|
+
.right.menu
|
10
|
+
.item
|
11
|
+
= link_to 'Reload', reload_path(database: database.name.parameterize), remote: :true, method: :post, class: 'ui button basic positive button', data: { confirm: 'Are you sure?' }
|
12
|
+
.item
|
13
|
+
= link_to 'Suspend', suspend_path(database: database.name.parameterize), remote: :true, method: :post, class: 'ui button basic primary button', data: { confirm: 'Are you sure?' }
|
14
|
+
.item
|
15
|
+
= link_to 'Shutdown', shutdown_path(database: database.name.parameterize), remote: :true, method: :post, class: 'ui button basic negative button', data: { confirm: 'Are you sure?' }
|
@@ -0,0 +1,20 @@
|
|
1
|
+
.ui.active.segment
|
2
|
+
|
3
|
+
%h4.ui.header
|
4
|
+
.content Stats
|
5
|
+
|
6
|
+
%table.ui.compact.table
|
7
|
+
%thead
|
8
|
+
%tr
|
9
|
+
- stats.first.keys.each do |key|
|
10
|
+
%th= key.titleize
|
11
|
+
%tbody
|
12
|
+
- stats.each do |row|
|
13
|
+
%tr
|
14
|
+
- row.each do |k,v|
|
15
|
+
- if ['total_received', 'total_sent', 'avg_recv', 'avg_sent'].include? k
|
16
|
+
%td= number_to_human_size(v)
|
17
|
+
- elsif ['total_query_time', 'avg_query'].include? k
|
18
|
+
%td= humanize_ms(v.to_i)
|
19
|
+
- else
|
20
|
+
%td= v
|
@@ -0,0 +1,3 @@
|
|
1
|
+
$("#segment_content_#{@database.group.name.parameterize}_#{@database.name.parameterize}").replaceWith("#{j(render partial: 'pg_bouncer_hero/home/card_content', locals: {database: @database, id: "segment_content_#{@database.group.name.parameterize}_#{@database.name.parameterize}"})}")
|
2
|
+
$("#segment_refreshed_#{@database.group.name.parameterize}_#{@database.name.parameterize}").css('display', '')
|
3
|
+
$("#segment_content_#{@database.group.name.parameterize}_#{@database.name.parameterize} .ui.progress").each(function(idx, el) { $(el).progress({ text: {active: '{value} of {total} connections'}, autoSuccess: false }); });
|
@@ -0,0 +1,15 @@
|
|
1
|
+
- connected = database.connection
|
2
|
+
.eight.wide.column{id: "segment_href_#{database.group.name.parameterize}_#{database.name.parameterize}", data: {href: summary_path(group: database.group.name.parameterize, database: database.name.parameterize)}}
|
3
|
+
= link_to databases_path(group: database.group.name.parameterize, database: database.name.parameterize), style: 'text-decoration: none !important; color: inherit !important' do
|
4
|
+
.ui.segments{style: 'margin-bottom: 10px;'}
|
5
|
+
.ui.segment{style: 'height: 60px;'}
|
6
|
+
%div{style: 'float: left;'}
|
7
|
+
%div{style: 'font-size: 16px; font-weight: bold;'}= database.name
|
8
|
+
%div{style: 'font-size: 12px; color: grey'}= database.host
|
9
|
+
%button.ui.button.basic.button{style: 'float: right;', class: connected ? 'positive': 'negative'}= connected ? 'Online' : 'Offline'
|
10
|
+
- if connected
|
11
|
+
= render partial: partial, locals: {database: database, id: "segment_content_#{database.group.name.parameterize}_#{database.name.parameterize}"}
|
12
|
+
.ui.segment{id: "segment_refreshed_#{database.group.name.parameterize}_#{database.name.parameterize}", style: 'height: 35px; display: none;'}
|
13
|
+
%span.right.floated{id: "segment_refreshed_span_#{database.group.name.parameterize}_#{database.name.parameterize}", style: 'float:right; margin-top: -6px; color: grey'}
|
14
|
+
Last refresh:
|
15
|
+
= Time.now.strftime("%H:%M:%S GMT%z (%Z)") # Same as new Date().toTimeString() in JS
|
@@ -0,0 +1,26 @@
|
|
1
|
+
.ui.horizontal.segments{id: id, style: 'background-color: white !important;'}
|
2
|
+
.ui.segment
|
3
|
+
- summary = database.summary
|
4
|
+
%div
|
5
|
+
%i.users.icon
|
6
|
+
- number = summary.select { |row| row['list'] == 'users' }.first['items']
|
7
|
+
= pluralize(number, 'user')
|
8
|
+
%div
|
9
|
+
%i.database.icon
|
10
|
+
- number = summary.select { |row| row['list'] == 'databases' }.first['items'].to_i - 1 # Removed pgbouncer
|
11
|
+
= pluralize(number, 'database')
|
12
|
+
%div
|
13
|
+
%i.plug.icon
|
14
|
+
- number = summary.select { |row| row['list'] == 'pools' }.first['items'].to_i - 1 # Removed pgbouncer
|
15
|
+
= pluralize(number, 'pool')
|
16
|
+
.ui.segment
|
17
|
+
%ul{style: 'margin: 0; padding-left: 5px;'}
|
18
|
+
- summary.select{ |row| row.key?(:databases_details) }.first[:databases_details].each do |db|
|
19
|
+
%li{style: 'margin: 0; list-style-type: none;'}
|
20
|
+
%span
|
21
|
+
%i.database.icon
|
22
|
+
%strong=db['name']
|
23
|
+
.ui.progress.small.success{data:{value: db['current_connections'], total: db['max_connections']}, style: 'float:right; width: 150px; margin-top:4px;'}
|
24
|
+
.bar
|
25
|
+
.progress
|
26
|
+
.label Connections
|
@@ -0,0 +1,31 @@
|
|
1
|
+
.ui.pointing.menu
|
2
|
+
.header.item
|
3
|
+
Overview
|
4
|
+
|
5
|
+
.ui.grid
|
6
|
+
- @groups.each do |_, group|
|
7
|
+
.sixteen.wide.row
|
8
|
+
.sixteen.wide.column
|
9
|
+
%h3.ui.dividing.header{style: 'margin-bottom: 10px;'}= group.name
|
10
|
+
= render partial: "card", collection: group.databases, as: :database, locals: {partial: 'card_loading_content'}
|
11
|
+
|
12
|
+
:javascript
|
13
|
+
document.addEventListener("DOMContentLoaded", function(event) {
|
14
|
+
// First
|
15
|
+
$("[id^='segment_href']").each(function(idx, el) {
|
16
|
+
$.get($(el).data('href'));
|
17
|
+
});
|
18
|
+
// Refresh every minutes
|
19
|
+
setInterval(function (){
|
20
|
+
$("[id^='segment_href']").each(function(idx, el) {
|
21
|
+
var el = $(el)
|
22
|
+
var id = el.attr('id');
|
23
|
+
var href = el.data('href');
|
24
|
+
var refreshed_id = id.replace('href', 'refreshed_span');
|
25
|
+
document.getElementById(refreshed_id).innerHTML = 'Refreshing...';
|
26
|
+
$.get(href, function(data) {
|
27
|
+
document.getElementById(refreshed_id).innerHTML = 'Last refresh: ' + new Date().toTimeString();
|
28
|
+
});
|
29
|
+
});
|
30
|
+
}, 60 * 1000)
|
31
|
+
});
|
data/config/routes.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
PgBouncerHero::Engine.routes.draw do
|
2
|
+
root to: "home#index"
|
3
|
+
scope path: ":group", constraints: proc { |req| (PgBouncerHero.groups.keys.map(&:parameterize) + [nil]).include?(req.params[:group]) } do
|
4
|
+
scope path: ":database", constraints: proc { |req| (PgBouncerHero.groups[req.params[:group]].databases.map(&:name).map(&:parameterize) + [nil]).include?(req.params[:database]) } do
|
5
|
+
get :summary, controller: :database
|
6
|
+
get :databases, controller: :database
|
7
|
+
get :stats, controller: :database
|
8
|
+
get :pools, controller: :database
|
9
|
+
get :clients, controller: :database
|
10
|
+
get :conf, controller: :database
|
11
|
+
post :reload, controller: :database
|
12
|
+
post :suspend, controller: :database
|
13
|
+
post :shutdown, controller: :database
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module PgBouncerHero
|
4
|
+
module Generators
|
5
|
+
class ConfigGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def create_initializer
|
9
|
+
template "config.yml", "config/pgbouncerhero.yml"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
pgbouncers:
|
2
|
+
production:
|
3
|
+
master:
|
4
|
+
# eg. postgres://user:password@host:port/pgbouncer
|
5
|
+
# url: <%= ENV["PGBOUNCER_PRODUCTION_MASTER_DATABASE_URL"] %>
|
6
|
+
# Add more databases
|
7
|
+
# slave:
|
8
|
+
# url: <%= ENV["PGBOUNCER_PRODUCTION_SLAVE_DATABASE_URL"] %>
|
9
|
+
# staging:
|
10
|
+
# master:
|
11
|
+
# url: <%= ENV["PGBOUNCER_STAGING_MASTER_DATABASE_URL"] %>
|
12
|
+
# slave:
|
13
|
+
# url: <%= ENV["PGBOUNCER_STAGING_SLAVE_DATABASE_URL"] %>
|
14
|
+
|
15
|
+
# Time zone (defaults to app time zone)
|
16
|
+
# time_zone: "Pacific Time (US & Canada)"
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "pgbouncerhero/version"
|
2
|
+
require "active_record"
|
3
|
+
|
4
|
+
# methods
|
5
|
+
require "pgbouncerhero/methods/basics"
|
6
|
+
|
7
|
+
require "pgbouncerhero/group"
|
8
|
+
require "pgbouncerhero/engine" if defined?(Rails)
|
9
|
+
|
10
|
+
# models
|
11
|
+
require "pgbouncerhero/connection"
|
12
|
+
|
13
|
+
module PgBouncerHero
|
14
|
+
# settings
|
15
|
+
class << self
|
16
|
+
attr_accessor :env
|
17
|
+
end
|
18
|
+
self.env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
19
|
+
|
20
|
+
class << self
|
21
|
+
extend Forwardable
|
22
|
+
|
23
|
+
def time_zone=(time_zone)
|
24
|
+
@time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
|
25
|
+
end
|
26
|
+
|
27
|
+
def time_zone
|
28
|
+
@time_zone || Time.zone
|
29
|
+
end
|
30
|
+
|
31
|
+
def config
|
32
|
+
Thread.current[:PgBouncerHero_config] ||= begin
|
33
|
+
path = "config/pgbouncerhero.yml"
|
34
|
+
|
35
|
+
config = YAML.load(ERB.new(File.read(path)).result) if File.exist?(path)
|
36
|
+
config ||= {}
|
37
|
+
|
38
|
+
if config[env]
|
39
|
+
config[env]
|
40
|
+
elsif config["pgbouncers"] # preferred format
|
41
|
+
config
|
42
|
+
else
|
43
|
+
{
|
44
|
+
"pgbouncers" => {
|
45
|
+
"default" => {
|
46
|
+
"primary" => {
|
47
|
+
"url" => ENV["PGBOUNCERHERO_DATABASE_URL"]
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def groups
|
57
|
+
@groups ||= begin
|
58
|
+
mapped = config['pgbouncers'].map do |group_id, hash|
|
59
|
+
[group_id.parameterize, PgBouncerHero::Group.new(group_id, config['pgbouncers'])]
|
60
|
+
end
|
61
|
+
Hash[mapped]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
class Connection
|
3
|
+
|
4
|
+
def initialize(host, port, user, password, dbname)
|
5
|
+
@host = host
|
6
|
+
@port = port
|
7
|
+
@user = user
|
8
|
+
@password = password
|
9
|
+
@dbname = dbname
|
10
|
+
end
|
11
|
+
|
12
|
+
def connection
|
13
|
+
begin
|
14
|
+
PG.connect(
|
15
|
+
host: @host,
|
16
|
+
port: @port,
|
17
|
+
user: @user,
|
18
|
+
password: @password,
|
19
|
+
dbname: @dbname,
|
20
|
+
connect_timeout: 5
|
21
|
+
)
|
22
|
+
rescue => e
|
23
|
+
Rails.logger.info(e)
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
class Database
|
3
|
+
|
4
|
+
include Methods::Basics
|
5
|
+
|
6
|
+
attr_reader :id, :config, :group
|
7
|
+
|
8
|
+
def initialize(group, id, config)
|
9
|
+
@id = id
|
10
|
+
@config = config || {}
|
11
|
+
@url = URI.parse(config["url"])
|
12
|
+
@group = group
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
@name ||= id.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def connection
|
20
|
+
connection_model.new(host, port, user, password, dbname).connection
|
21
|
+
end
|
22
|
+
|
23
|
+
def host
|
24
|
+
@url.host if @url
|
25
|
+
end
|
26
|
+
|
27
|
+
def port
|
28
|
+
@url.port if @url
|
29
|
+
end
|
30
|
+
|
31
|
+
def user
|
32
|
+
@url.user if @url
|
33
|
+
end
|
34
|
+
|
35
|
+
def password
|
36
|
+
@url.password if @url
|
37
|
+
end
|
38
|
+
|
39
|
+
def dbname
|
40
|
+
@url.path[1..-1] if @url
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def connection_model
|
46
|
+
@connection_model ||= begin
|
47
|
+
Class.new(PgBouncerHero::Connection) do
|
48
|
+
def self.name
|
49
|
+
"PgBouncerHero::Connection::Database#{object_id}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace PgBouncerHero
|
4
|
+
|
5
|
+
initializer "pgbouncerhero", group: :all do |app|
|
6
|
+
if defined?(Sprockets) && Sprockets::VERSION >= "4"
|
7
|
+
app.config.assets.precompile << "pgbouncerhero/application.js"
|
8
|
+
app.config.assets.precompile << "pgbouncerhero/application.css"
|
9
|
+
app.config.assets.precompile << "pgbouncerhero/short-paragraph.png"
|
10
|
+
else
|
11
|
+
# use a proc instead of a string
|
12
|
+
app.config.assets.precompile << proc { |path| path == "pgbouncerhero/application.js" }
|
13
|
+
app.config.assets.precompile << proc { |path| path == "pgbouncerhero/application.css" }
|
14
|
+
app.config.assets.precompile << proc { |path| path == "pgbouncerhero/short-paragraph.png" }
|
15
|
+
end
|
16
|
+
|
17
|
+
PgBouncerHero.time_zone = PgBouncerHero.config["time_zone"] if PgBouncerHero.config["time_zone"]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "pgbouncerhero/database"
|
2
|
+
|
3
|
+
module PgBouncerHero
|
4
|
+
class Group
|
5
|
+
|
6
|
+
attr_reader :id, :config, :databases
|
7
|
+
|
8
|
+
def initialize(id, config)
|
9
|
+
@id = id
|
10
|
+
@config = config || {}
|
11
|
+
@databases = config[id].map do |k, v|
|
12
|
+
PgBouncerHero::Database.new(self, k, config[id][k])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
@name ||= id.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module PgBouncerHero
|
2
|
+
module Methods
|
3
|
+
module Basics
|
4
|
+
def summary
|
5
|
+
l = lists
|
6
|
+
d = databases
|
7
|
+
l = l.as_json
|
8
|
+
d = d.as_json.reject { |a| a['name'] == 'pgbouncer' }
|
9
|
+
l.push({databases_details: d})
|
10
|
+
l
|
11
|
+
end
|
12
|
+
def databases
|
13
|
+
connection.exec("SHOW databases")
|
14
|
+
end
|
15
|
+
def stats
|
16
|
+
connection.exec("SHOW stats")
|
17
|
+
end
|
18
|
+
def lists
|
19
|
+
connection.exec("SHOW lists")
|
20
|
+
end
|
21
|
+
def pools
|
22
|
+
connection.exec("SHOW pools")
|
23
|
+
end
|
24
|
+
def clients
|
25
|
+
connection.exec("SHOW clients")
|
26
|
+
end
|
27
|
+
def conf
|
28
|
+
connection.exec("SHOW config")
|
29
|
+
end
|
30
|
+
def reload
|
31
|
+
connection.exec("RELOAD")
|
32
|
+
end
|
33
|
+
def suspend
|
34
|
+
connection.exec("SUSPEND")
|
35
|
+
end
|
36
|
+
def shutdown
|
37
|
+
connection.exec("SHUTDOWN")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "pgbouncerhero/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pgbouncerhero"
|
8
|
+
spec.version = PgBouncerHero::VERSION
|
9
|
+
spec.authors = ["Quentin Rousseau"]
|
10
|
+
spec.email = ["contact@quent.in"]
|
11
|
+
spec.summary = "A graphical user interface for your PGBouncers"
|
12
|
+
spec.description = "A graphical user interface for your PGBouncers"
|
13
|
+
spec.homepage = "https://github.com/kwent/pgbouncerhero"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "minitest"
|
23
|
+
spec.add_runtime_dependency "sass-rails"
|
24
|
+
spec.add_runtime_dependency "semantic-ui-sass"
|
25
|
+
spec.add_runtime_dependency "haml-rails"
|
26
|
+
|
27
|
+
if RUBY_PLATFORM == "java"
|
28
|
+
spec.add_runtime_dependency "pg_jruby"
|
29
|
+
else
|
30
|
+
spec.add_runtime_dependency "pg"
|
31
|
+
end
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pgbouncerhero
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Quentin Rousseau
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-02-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sass-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: semantic-ui-sass
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: haml-rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pg
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: A graphical user interface for your PGBouncers
|
112
|
+
email:
|
113
|
+
- contact@quent.in
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- CHANGELOG.md
|
120
|
+
- Gemfile
|
121
|
+
- LICENSE.txt
|
122
|
+
- README.md
|
123
|
+
- app/assets/images/pgbouncerhero/short-paragraph.png
|
124
|
+
- app/assets/javascripts/pgbouncerhero/application.js
|
125
|
+
- app/assets/stylesheets/pgbouncerhero/application.css.scss
|
126
|
+
- app/controllers/pg_bouncer_hero/application_controller.rb
|
127
|
+
- app/controllers/pg_bouncer_hero/database_controller.rb
|
128
|
+
- app/controllers/pg_bouncer_hero/home_controller.rb
|
129
|
+
- app/helpers/pg_bouncer_hero/application_helper.rb
|
130
|
+
- app/views/layouts/pg_bouncer_hero/application.html.haml
|
131
|
+
- app/views/pg_bouncer_hero/database/_actions.html.haml
|
132
|
+
- app/views/pg_bouncer_hero/database/_clients.html.haml
|
133
|
+
- app/views/pg_bouncer_hero/database/_conf.html.haml
|
134
|
+
- app/views/pg_bouncer_hero/database/_databases.html.haml
|
135
|
+
- app/views/pg_bouncer_hero/database/_menu.html.haml
|
136
|
+
- app/views/pg_bouncer_hero/database/_pools.html.haml
|
137
|
+
- app/views/pg_bouncer_hero/database/_stats.html.haml
|
138
|
+
- app/views/pg_bouncer_hero/database/clients.html.haml
|
139
|
+
- app/views/pg_bouncer_hero/database/conf.html.haml
|
140
|
+
- app/views/pg_bouncer_hero/database/databases.html.haml
|
141
|
+
- app/views/pg_bouncer_hero/database/pools.html.haml
|
142
|
+
- app/views/pg_bouncer_hero/database/stats.html.haml
|
143
|
+
- app/views/pg_bouncer_hero/database/summary.js.haml
|
144
|
+
- app/views/pg_bouncer_hero/home/_card.html.haml
|
145
|
+
- app/views/pg_bouncer_hero/home/_card_content.html.haml
|
146
|
+
- app/views/pg_bouncer_hero/home/_card_loading_content.html.haml
|
147
|
+
- app/views/pg_bouncer_hero/home/index.html.haml
|
148
|
+
- app/views/shared/_alert.html.haml
|
149
|
+
- app/views/shared/_flash_messages.html.haml
|
150
|
+
- config/routes.rb
|
151
|
+
- doc/screenshot-1.png
|
152
|
+
- doc/screenshot-2.png
|
153
|
+
- doc/screenshot-3.png
|
154
|
+
- lib/generators/pgbouncerhero/config_generator.rb
|
155
|
+
- lib/generators/pgbouncerhero/templates/config.yml
|
156
|
+
- lib/pgbouncerhero.rb
|
157
|
+
- lib/pgbouncerhero/connection.rb
|
158
|
+
- lib/pgbouncerhero/database.rb
|
159
|
+
- lib/pgbouncerhero/engine.rb
|
160
|
+
- lib/pgbouncerhero/group.rb
|
161
|
+
- lib/pgbouncerhero/methods/basics.rb
|
162
|
+
- lib/pgbouncerhero/version.rb
|
163
|
+
- pgbouncerhero.gemspec
|
164
|
+
homepage: https://github.com/kwent/pgbouncerhero
|
165
|
+
licenses:
|
166
|
+
- MIT
|
167
|
+
metadata: {}
|
168
|
+
post_install_message:
|
169
|
+
rdoc_options: []
|
170
|
+
require_paths:
|
171
|
+
- lib
|
172
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
173
|
+
requirements:
|
174
|
+
- - ">="
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '0'
|
177
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
requirements: []
|
183
|
+
rubyforge_project:
|
184
|
+
rubygems_version: 2.5.1
|
185
|
+
signing_key:
|
186
|
+
specification_version: 4
|
187
|
+
summary: A graphical user interface for your PGBouncers
|
188
|
+
test_files: []
|