pgbouncerhero 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/CHANGELOG.md +3 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +24 -0
  6. data/README.md +125 -0
  7. data/app/assets/images/pgbouncerhero/short-paragraph.png +0 -0
  8. data/app/assets/javascripts/pgbouncerhero/application.js +2 -0
  9. data/app/assets/stylesheets/pgbouncerhero/application.css.scss +15 -0
  10. data/app/controllers/pg_bouncer_hero/application_controller.rb +27 -0
  11. data/app/controllers/pg_bouncer_hero/database_controller.rb +70 -0
  12. data/app/controllers/pg_bouncer_hero/home_controller.rb +6 -0
  13. data/app/helpers/pg_bouncer_hero/application_helper.rb +29 -0
  14. data/app/views/layouts/pg_bouncer_hero/application.html.haml +20 -0
  15. data/app/views/pg_bouncer_hero/database/_actions.html.haml +10 -0
  16. data/app/views/pg_bouncer_hero/database/_clients.html.haml +15 -0
  17. data/app/views/pg_bouncer_hero/database/_conf.html.haml +15 -0
  18. data/app/views/pg_bouncer_hero/database/_databases.html.haml +15 -0
  19. data/app/views/pg_bouncer_hero/database/_menu.html.haml +15 -0
  20. data/app/views/pg_bouncer_hero/database/_pools.html.haml +15 -0
  21. data/app/views/pg_bouncer_hero/database/_stats.html.haml +20 -0
  22. data/app/views/pg_bouncer_hero/database/clients.html.haml +4 -0
  23. data/app/views/pg_bouncer_hero/database/conf.html.haml +4 -0
  24. data/app/views/pg_bouncer_hero/database/databases.html.haml +5 -0
  25. data/app/views/pg_bouncer_hero/database/pools.html.haml +4 -0
  26. data/app/views/pg_bouncer_hero/database/stats.html.haml +4 -0
  27. data/app/views/pg_bouncer_hero/database/summary.js.haml +3 -0
  28. data/app/views/pg_bouncer_hero/home/_card.html.haml +15 -0
  29. data/app/views/pg_bouncer_hero/home/_card_content.html.haml +26 -0
  30. data/app/views/pg_bouncer_hero/home/_card_loading_content.html.haml +5 -0
  31. data/app/views/pg_bouncer_hero/home/index.html.haml +31 -0
  32. data/app/views/shared/_alert.html.haml +4 -0
  33. data/app/views/shared/_flash_messages.html.haml +2 -0
  34. data/config/routes.rb +16 -0
  35. data/doc/screenshot-1.png +0 -0
  36. data/doc/screenshot-2.png +0 -0
  37. data/doc/screenshot-3.png +0 -0
  38. data/lib/generators/pgbouncerhero/config_generator.rb +13 -0
  39. data/lib/generators/pgbouncerhero/templates/config.yml +16 -0
  40. data/lib/pgbouncerhero.rb +65 -0
  41. data/lib/pgbouncerhero/connection.rb +29 -0
  42. data/lib/pgbouncerhero/database.rb +55 -0
  43. data/lib/pgbouncerhero/engine.rb +20 -0
  44. data/lib/pgbouncerhero/group.rb +21 -0
  45. data/lib/pgbouncerhero/methods/basics.rb +41 -0
  46. data/lib/pgbouncerhero/version.rb +3 -0
  47. data/pgbouncerhero.gemspec +32 -0
  48. metadata +188 -0
@@ -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
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ *.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ tmp
14
+ *.bundle
15
+ *.so
16
+ *.o
17
+ *.a
18
+ mkmf.log
@@ -0,0 +1,3 @@
1
+ ## 0.1.0
2
+
3
+ - First major release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in pgbouncerhero.gemspec
4
+ gemspec
@@ -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.
@@ -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
+ ```
@@ -0,0 +1,2 @@
1
+ //= require jquery_ujs
2
+ //= require semantic-ui
@@ -0,0 +1,15 @@
1
+ @import "semantic-ui";
2
+
3
+ body {
4
+ margin: 0;
5
+ padding-top: 50px;
6
+ background-color: #eee;
7
+ }
8
+
9
+ .ui.main.container {
10
+ padding: 10px;
11
+ }
12
+
13
+ .wireframe.image {
14
+ opacity: 0.5;
15
+ }
@@ -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,6 @@
1
+ module PgBouncerHero
2
+ class HomeController < ApplicationController
3
+ def index
4
+ end
5
+ end
6
+ 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.active.segment
2
+
3
+ %h4.ui.header
4
+ .content Clients
5
+
6
+ %table.ui.compact.table
7
+ %thead
8
+ %tr
9
+ - clients.first.keys.each do |key|
10
+ %th= key.titleize
11
+ %tbody
12
+ - clients.each do |row|
13
+ %tr
14
+ - row.each do |k,v|
15
+ %td= v
@@ -0,0 +1,15 @@
1
+ .ui.active.segment
2
+
3
+ %h4.ui.header
4
+ .content Confs
5
+
6
+ %table.ui.compact.table
7
+ %thead
8
+ %tr
9
+ - conf.first.keys.each do |key|
10
+ %th= key.titleize
11
+ %tbody
12
+ - conf.each do |row|
13
+ %tr
14
+ - row.each do |k,v|
15
+ %td= v
@@ -0,0 +1,15 @@
1
+ .ui.active.segment
2
+
3
+ %h4.ui.header
4
+ .content Databases
5
+
6
+ %table.ui.compact.table
7
+ %thead
8
+ %tr
9
+ - databases.first.keys.each do |key|
10
+ %th= key.titleize
11
+ %tbody
12
+ - databases.each do |row|
13
+ %tr
14
+ - row.each do |k,v|
15
+ %td= v
@@ -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,15 @@
1
+ .ui.active.segment
2
+
3
+ %h4.ui.header
4
+ .content Pools
5
+
6
+ %table.ui.compact.table
7
+ %thead
8
+ %tr
9
+ - pools.first.keys.each do |key|
10
+ %th= key.titleize
11
+ %tbody
12
+ - pools.each do |row|
13
+ %tr
14
+ - row.each do |k,v|
15
+ %td= v
@@ -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,4 @@
1
+ = render partial: "menu", locals: {database: @database}
2
+ -# = render partial: "actions", locals: {database: @database}
3
+ - if @clients
4
+ = render partial: "clients", locals: {database: @database, clients: @clients}
@@ -0,0 +1,4 @@
1
+ = render partial: "menu", locals: {database: @database}
2
+ -# = render partial: "actions", locals: {database: @database}
3
+ - if @conf
4
+ = render partial: "conf", locals: {database: @database, conf: @conf}
@@ -0,0 +1,5 @@
1
+ = render partial: "menu", locals: {database: @database}
2
+ -# = render partial: "actions", locals: {database: @database}
3
+
4
+ - if @dbs
5
+ = render partial: "databases", locals: {database: @database, databases: @dbs}
@@ -0,0 +1,4 @@
1
+ = render partial: "menu", locals: {database: @database}
2
+ -# = render partial: "actions", locals: {database: @database}
3
+ - if @pools
4
+ = render partial: "pools", locals: {database: @database, pools: @pools}
@@ -0,0 +1,4 @@
1
+ = render partial: "menu", locals: {database: @database}
2
+ -# = render partial: "actions", locals: {database: @database}
3
+ - if @stats
4
+ = render partial: "stats", locals: {database: @database, stats: @stats}
@@ -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,5 @@
1
+ .ui.segment{id: id}
2
+ .ui.active.inverted.dimmer
3
+ .ui.medium.text.loader Loading...
4
+ = image_tag('pgbouncerhero/short-paragraph.png', class: 'ui wireframe image')
5
+ = image_tag('pgbouncerhero/short-paragraph.png', class: 'ui wireframe image')
@@ -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
+ });
@@ -0,0 +1,4 @@
1
+ .alert{class: "ui #{class_name} message"}
2
+ - if defined?(closable) && closable && closable == true
3
+ %i.close.icon
4
+ = message.html_safe
@@ -0,0 +1,2 @@
1
+ - flash.each do |type, message|
2
+ = render partial: 'shared/alert', locals: {class_name: alert_class_for(type), message: message}
@@ -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,3 @@
1
+ module PgBouncerHero
2
+ VERSION = "0.1.0"
3
+ 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: []