├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── app ├── assets │ ├── images │ │ └── pgbouncerhero │ │ │ └── short-paragraph.png │ ├── javascripts │ │ └── pgbouncerhero │ │ │ └── application.js │ └── stylesheets │ │ └── pgbouncerhero │ │ └── application.css.scss ├── controllers │ └── pg_bouncer_hero │ │ ├── application_controller.rb │ │ ├── database_controller.rb │ │ └── home_controller.rb ├── helpers │ └── pg_bouncer_hero │ │ └── application_helper.rb └── views │ ├── layouts │ └── pg_bouncer_hero │ │ └── application.html.erb │ ├── pg_bouncer_hero │ ├── database │ │ ├── _actions.html.erb │ │ ├── _clients.html.erb │ │ ├── _conf.html.erb │ │ ├── _databases.html.erb │ │ ├── _menu.html.erb │ │ ├── _pools.html.erb │ │ ├── _stats.html.erb │ │ ├── clients.html.erb │ │ ├── conf.html.erb │ │ ├── databases.html.erb │ │ ├── pools.html.erb │ │ ├── stats.html.erb │ │ └── summary.js.erb │ └── home │ │ ├── _card.html.erb │ │ ├── _card_content.html.erb │ │ ├── _card_loading_content.html.erb │ │ └── index.html.erb │ └── shared │ ├── _alert.html.erb │ └── _flash_messages.html.erb ├── config └── routes.rb ├── doc ├── screenshot-1.png ├── screenshot-2.png └── screenshot-3.png ├── lib ├── generators │ └── pgbouncerhero │ │ ├── config_generator.rb │ │ └── templates │ │ └── config.yml ├── pgbouncerhero.rb └── pgbouncerhero │ ├── connection.rb │ ├── database.rb │ ├── engine.rb │ ├── group.rb │ ├── methods │ └── basics.rb │ └── version.rb └── pgbouncerhero.gemspec /.gitignore: -------------------------------------------------------------------------------- 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 19 | .ruby-version 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 2 | 3 | - Easier setup 4 | - Automatically require jquery 5 | - Automatically require semantic-ui-sass 6 | - Use pgbouncerhero/application stylesheets and javascript 7 | 8 | ## 1.0.3 9 | 10 | - Drop Haml dependency 11 | 12 | ## 1.0.1 13 | 14 | - Explicitly require ApplicationController. Thanks @Tolsto 15 | 16 | ## 1.0.0 17 | 18 | - Lazy connection for index 19 | - Bug fixes 20 | 21 | ## 0.1.0 22 | 23 | - First major release 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in pgbouncerhero.gemspec 4 | gemspec -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Quentin Rousseau 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. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 | ``` 20 | 21 | And mount the engine in your `config/routes.rb`: 22 | 23 | ```ruby 24 | mount PgBouncerHero::Engine, at: "pgbouncerhero" 25 | ``` 26 | 27 | ### Basic Authentication 28 | 29 | Set the following variables in your environment or an initializer. 30 | 31 | ```ruby 32 | ENV["PGBOUNCERHERO_USERNAME"] = "zelda" 33 | ENV["PGBOUNCERHERO_PASSWORD"] = "triforce" 34 | ``` 35 | 36 | ### Devise 37 | 38 | ```ruby 39 | authenticate :user, -> (user) { user.admin? } do 40 | mount PgBouncerHero::Engine, at: "pgbouncerhero" 41 | end 42 | ``` 43 | 44 | ## One PgBouncer 45 | 46 | ```bash 47 | export PGBOUNCERHERO_DATABASE_URL=postgres://user:password@host:port/pgbouncer 48 | ``` 49 | 50 | ## Multiple PgBouncers 51 | 52 | Create `config/pgbouncerhero.yml` with: 53 | 54 | ```yml 55 | default: &default 56 | pgbouncers: 57 | production: 58 | master: 59 | url: <%= ENV["PGBOUNCER_PRODUCTION_MASTER_DATABASE_URL"] %> 60 | slave: 61 | url: <%= ENV["PGBOUNCER_PRODUCTION_SLAVE_DATABASE_URL"] %> 62 | staging: 63 | master: 64 | url: <%= ENV["PGBOUNCER_STAGING_MASTER_DATABASE_URL"] %> 65 | slave: 66 | url: <%= ENV["PGBOUNCER_STAGING_SLAVE_DATABASE_URL"] %> 67 | 68 | development: 69 | <<: *default 70 | 71 | production: 72 | <<: *default 73 | ``` 74 | 75 | # Authors 76 | 77 | - [Quentin Rousseau](https://github.com/kwent) 78 | 79 | # License 80 | 81 | ```plain 82 | Copyright (c) 2022 Quentin Rousseau 83 | 84 | MIT License 85 | 86 | Permission is hereby granted, free of charge, to any person 87 | obtaining a copy of this software and associated documentation 88 | files (the "Software"), to deal in the Software without 89 | restriction, including without limitation the rights to use, 90 | copy, modify, merge, publish, distribute, sublicense, and/or sell 91 | copies of the Software, and to permit persons to whom the 92 | Software is furnished to do so, subject to the following 93 | conditions: 94 | 95 | The above copyright notice and this permission notice shall be 96 | included in all copies or substantial portions of the Software. 97 | 98 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 99 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 100 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 101 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 102 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 103 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 104 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 105 | OTHER DEALINGS IN THE SOFTWARE. 106 | ``` 107 | -------------------------------------------------------------------------------- /app/assets/images/pgbouncerhero/short-paragraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwent/pgbouncerhero/ad025e93f81c5520b07f79decf097721a5c8fc63/app/assets/images/pgbouncerhero/short-paragraph.png -------------------------------------------------------------------------------- /app/assets/javascripts/pgbouncerhero/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | //= require semantic-ui 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/pgbouncerhero/application.css.scss: -------------------------------------------------------------------------------- 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 | } 16 | -------------------------------------------------------------------------------- /app/controllers/pg_bouncer_hero/application_controller.rb: -------------------------------------------------------------------------------- 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 28 | -------------------------------------------------------------------------------- /app/controllers/pg_bouncer_hero/database_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'pg_bouncer_hero/application_controller' 2 | 3 | module PgBouncerHero 4 | class DatabaseController < ApplicationController 5 | def summary 6 | if @database.connection 7 | @dbs = @database.summary 8 | else 9 | flash[:error] = "#{@database.name} does not look online." 10 | end 11 | end 12 | def databases 13 | if @database.connection 14 | @dbs = @database.databases 15 | else 16 | flash[:error] = "#{@database.name} does not look online." 17 | end 18 | end 19 | def stats 20 | if @database.connection 21 | @stats = @database.stats 22 | else 23 | flash[:error] = "#{@database.name} does not look online." 24 | end 25 | end 26 | def pools 27 | if @database.connection 28 | @pools = @database.pools 29 | else 30 | flash[:error] = "#{@database.name} does not look online." 31 | end 32 | end 33 | def clients 34 | if @database.connection 35 | @clients = @database.clients 36 | else 37 | flash[:error] = "#{@database.name} does not look online." 38 | end 39 | end 40 | def conf 41 | if @database.connection 42 | @conf = @database.conf 43 | else 44 | flash[:error] = "#{@database.name} does not look online." 45 | end 46 | end 47 | def reload 48 | if @database.connection 49 | @database.reload 50 | flash[:success] = "#{@database.name} has been reloaded." 51 | else 52 | flash[:error] = "#{@database.name} does not look online." 53 | end 54 | end 55 | def suspend 56 | if @database.connection 57 | @database.suspend 58 | flash[:success] = "#{@database.name} has been suspended." 59 | else 60 | flash[:error] = "#{@database.name} does not look online." 61 | end 62 | end 63 | def shutdown 64 | if @database.connection 65 | @database.shutdown 66 | flash[:success] = "#{@database.name} has been shutdown." 67 | else 68 | flash[:error] = "#{@database.name} does not look online." 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /app/controllers/pg_bouncer_hero/home_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'pg_bouncer_hero/application_controller' 2 | 3 | module PgBouncerHero 4 | class HomeController < ApplicationController 5 | def index 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/helpers/pg_bouncer_hero/application_helper.rb: -------------------------------------------------------------------------------- 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 30 | -------------------------------------------------------------------------------- /app/views/layouts/pg_bouncer_hero/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= [@groups.size > 1 ? "#{@database.group.name} | #{@database.name}" : "PgBouncerHero", @title].compact.join(" / ") %> 9 | 10 | <%= stylesheet_link_tag 'pgbouncerhero/application', media: 'all', 'data-turbolinks-track': true %> 11 | <%= javascript_include_tag "pgbouncerhero/application" %> 12 | <%= csrf_meta_tags %> 13 | 14 | 15 | 20 |
21 |
22 |
23 | <%= render partial: 'shared/flash_messages', flash: flash %> 24 |
25 | <%= yield %> 26 |
27 | <% if content_for? :inline_script %> 28 | <%= yield :inline_script %> 29 | <% end %> 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/_actions.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 |
Actions
4 |

5 | 6 | 7 | 8 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/_clients.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 |
Clients
4 |

5 | 6 | 7 | 8 | <% clients.first.keys.each do |key| %> 9 | 12 | <% end %> 13 | 14 | 15 | 16 | <% clients.each do |row| %> 17 | 18 | <% row.each do |k,v| %> 19 | 22 | <% end %> 23 | 24 | <% end %> 25 | 26 |
10 | <%= key.titleize %> 11 |
20 | <%= v %> 21 |
27 |
28 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/_conf.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 |
Confs
4 |

5 | 6 | 7 | 8 | <% conf.first.keys.each do |key| %> 9 | 12 | <% end %> 13 | 14 | 15 | 16 | <% conf.each do |row| %> 17 | 18 | <% row.each do |k,v| %> 19 | 22 | <% end %> 23 | 24 | <% end %> 25 | 26 |
10 | <%= key.titleize %> 11 |
20 | <%= v %> 21 |
27 |
28 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/_databases.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 |
Databases
4 |

5 | 6 | 7 | 8 | <% databases.first.keys.each do |key| %> 9 | 12 | <% end %> 13 | 14 | 15 | 16 | <% databases.each do |row| %> 17 | 18 | <% row.each do |k,v| %> 19 | 22 | <% end %> 23 | 24 | <% end %> 25 | 26 |
10 | <%= key.titleize %> 11 |
20 | <%= v %> 21 |
27 |
28 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/_menu.html.erb: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/_pools.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 |
Pools
4 |

5 | 6 | 7 | 8 | <% pools.first.keys.each do |key| %> 9 | 12 | <% end %> 13 | 14 | 15 | 16 | <% pools.each do |row| %> 17 | 18 | <% row.each do |k,v| %> 19 | 22 | <% end %> 23 | 24 | <% end %> 25 | 26 |
10 | <%= key.titleize %> 11 |
20 | <%= v %> 21 |
27 |
28 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/_stats.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 |
Stats
4 |

5 | 6 | 7 | 8 | <% stats.first.keys.each do |key| %> 9 | 12 | <% end %> 13 | 14 | 15 | 16 | <% stats.each do |row| %> 17 | 18 | <% row.each do |k,v| %> 19 | <% if ['total_received', 'total_sent', 'avg_recv', 'avg_sent'].include? k %> 20 | 23 | <% elsif ['total_query_time', 'avg_query'].include? k %> 24 | 27 | <% else %> 28 | 31 | <% end %> 32 | <% end %> 33 | 34 | <% end %> 35 | 36 |
10 | <%= key.titleize %> 11 |
21 | <%= number_to_human_size(v) %> 22 | 25 | <%= humanize_ms(v.to_i) %> 26 | 29 | <%= v %> 30 |
37 |
38 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/clients.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "menu", locals: {database: @database} %> 2 | <% if @clients %> 3 | <%= render partial: "clients", locals: {database: @database, clients: @clients} %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/conf.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "menu", locals: {database: @database} %> 2 | <% if @conf %> 3 | <%= render partial: "conf", locals: {database: @database, conf: @conf} %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/databases.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "menu", locals: {database: @database} %> 2 | <% if @dbs %> 3 | <%= render partial: "databases", locals: {database: @database, databases: @dbs} %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/pools.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "menu", locals: {database: @database} %> 2 | <% if @pools %> 3 | <%= render partial: "pools", locals: {database: @database, pools: @pools} %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/stats.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: "menu", locals: {database: @database} %> 2 | <% if @stats %> 3 | <%= render partial: "stats", locals: {database: @database, stats: @stats} %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/database/summary.js.erb: -------------------------------------------------------------------------------- 1 | $("#segment_status_<%= @database.group.name.parameterize %>_<%= @database.name.parameterize %>").html("<%= j(@database.connection ? "Online" : "Offline") %>") 2 | $("#segment_status_<%= @database.group.name.parameterize %>_<%= @database.name.parameterize %>").removeClass("grey negative positive").addClass("<%= j(@database.connection ? "positive" : "negative") %>") 3 | $("#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}"}) %>") 4 | $("#segment_refreshed_<%= @database.group.name.parameterize %>_<%= @database.name.parameterize %>").css('display', '') 5 | $("#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 }); }); 6 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/home/_card.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to databases_path(group: database.group.name.parameterize, database: database.name.parameterize), style: 'text-decoration: none !important; color: inherit !important' do %> 3 |
4 |
5 |
6 |
7 | <%= database.name %> 8 |
9 |
10 | <%= database.host %> 11 |
12 |
13 | 16 |
17 | <%= render partial: partial, locals: {database: database, id: "segment_content_#{database.group.name.parameterize}_#{database.name.parameterize}"} %> 18 | 24 |
25 | <% end %> 26 |
27 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/home/_card_content.html.erb: -------------------------------------------------------------------------------- 1 | <% connected = database.connection %> 2 | <% if connected %> 3 |
4 |
5 | <% summary = database.summary %> 6 |
7 | 8 | <% number = summary.select { |row| row['list'] == 'users' }.first['items'] %> 9 | <%= pluralize(number, 'user') %> 10 |
11 |
12 | 13 | <% number = summary.select { |row| row['list'] == 'databases' }.first['items'].to_i - 1 %> 14 | <%= pluralize(number, 'database') %> 15 |
16 |
17 | 18 | <% number = summary.select { |row| row['list'] == 'pools' }.first['items'].to_i - 1 %> 19 | <%= pluralize(number, 'pool') %> 20 |
21 |
22 |
23 |
    24 | <% summary.select{ |row| row.key?(:databases_details) }.first[:databases_details].each do |db| %> 25 |
  • 26 | 27 | 28 | 29 | <%= db['name'] %> 30 | 31 |
    32 |
    33 |
    34 |
    35 |
    Connections
    36 |
    37 |
    38 |
  • 39 | <% end %> 40 |
41 |
42 |
43 | <% end %> 44 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/home/_card_loading_content.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Loading...
4 |
5 | <%= image_tag('pgbouncerhero/short-paragraph.png', class: 'ui wireframe image') %> 6 | <%= image_tag('pgbouncerhero/short-paragraph.png', class: 'ui wireframe image') %> 7 |
8 | -------------------------------------------------------------------------------- /app/views/pg_bouncer_hero/home/index.html.erb: -------------------------------------------------------------------------------- 1 | 6 |
7 | <% @groups.each do |_, group| %> 8 |
9 |
10 |

11 | <%= group.name %> 12 |

13 |
14 | <%= render partial: "card", collection: group.databases, as: :database, locals: {partial: 'card_loading_content', lazy: true} %> 15 |
16 | <% end %> 17 |
18 | 45 | -------------------------------------------------------------------------------- /app/views/shared/_alert.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if defined?(closable) && closable && closable == true %> 3 | 4 | <% end %> 5 | <%= message.html_safe %> 6 |
7 | -------------------------------------------------------------------------------- /app/views/shared/_flash_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |type, message| %> 2 | <%= render partial: 'shared/alert', locals: {class_name: alert_class_for(type), message: message} %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 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 17 | -------------------------------------------------------------------------------- /doc/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwent/pgbouncerhero/ad025e93f81c5520b07f79decf097721a5c8fc63/doc/screenshot-1.png -------------------------------------------------------------------------------- /doc/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwent/pgbouncerhero/ad025e93f81c5520b07f79decf097721a5c8fc63/doc/screenshot-2.png -------------------------------------------------------------------------------- /doc/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwent/pgbouncerhero/ad025e93f81c5520b07f79decf097721a5c8fc63/doc/screenshot-3.png -------------------------------------------------------------------------------- /lib/generators/pgbouncerhero/config_generator.rb: -------------------------------------------------------------------------------- 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 14 | -------------------------------------------------------------------------------- /lib/generators/pgbouncerhero/templates/config.yml: -------------------------------------------------------------------------------- 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)" 17 | -------------------------------------------------------------------------------- /lib/pgbouncerhero.rb: -------------------------------------------------------------------------------- 1 | require "pgbouncerhero/version" 2 | 3 | # methods 4 | require "pgbouncerhero/methods/basics" 5 | 6 | require "pgbouncerhero/group" 7 | require "pgbouncerhero/engine" if defined?(Rails) 8 | 9 | # models 10 | require "pgbouncerhero/connection" 11 | 12 | # others 13 | require "jquery-rails" 14 | require "semantic-ui-sass" 15 | 16 | module PgBouncerHero 17 | # settings 18 | class << self 19 | attr_accessor :env 20 | end 21 | self.env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" 22 | 23 | class << self 24 | extend Forwardable 25 | 26 | def time_zone=(time_zone) 27 | @time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s] 28 | end 29 | 30 | def time_zone 31 | @time_zone || Time.zone 32 | end 33 | 34 | def config 35 | Thread.current[:PgBouncerHero_config] ||= begin 36 | path = "config/pgbouncerhero.yml" 37 | 38 | config = YAML.load(ERB.new(File.read(path)).result) if File.exist?(path) 39 | config ||= {} 40 | 41 | if config[env] 42 | config[env] 43 | elsif config["pgbouncers"] # preferred format 44 | config 45 | else 46 | { 47 | "pgbouncers" => { 48 | "default" => { 49 | "primary" => { 50 | "url" => ENV["PGBOUNCERHERO_DATABASE_URL"] 51 | } 52 | } 53 | } 54 | } 55 | end 56 | end 57 | end 58 | 59 | def groups 60 | @groups ||= begin 61 | mapped = config['pgbouncers'].map do |group_id, hash| 62 | [group_id.parameterize, PgBouncerHero::Group.new(group_id, config['pgbouncers'])] 63 | end 64 | Hash[mapped] 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/pgbouncerhero/connection.rb: -------------------------------------------------------------------------------- 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 | @timeout = ENV["PGBOUNCERHERO_TIMEOUT"] || 5 11 | end 12 | 13 | def connection 14 | @connection ||= begin 15 | begin 16 | PG.connect( 17 | host: @host, 18 | port: @port, 19 | user: @user, 20 | password: @password, 21 | dbname: @dbname, 22 | connect_timeout: @timeout 23 | ) 24 | rescue Exception => e 25 | Rails.logger.error("[PGBouncerHero] Host:#{@host} | Database Name:#{@dbname} | Timeout: #{@timeout}s => #{e}") 26 | nil 27 | end 28 | end 29 | end 30 | 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/pgbouncerhero/database.rb: -------------------------------------------------------------------------------- 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 ||= 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 56 | -------------------------------------------------------------------------------- /lib/pgbouncerhero/engine.rb: -------------------------------------------------------------------------------- 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 21 | -------------------------------------------------------------------------------- /lib/pgbouncerhero/group.rb: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /lib/pgbouncerhero/methods/basics.rb: -------------------------------------------------------------------------------- 1 | module PgBouncerHero 2 | module Methods 3 | module Basics 4 | def summary 5 | if connection 6 | l = lists 7 | d = databases 8 | l = l.as_json 9 | d = d.as_json.reject { |a| a['name'] == 'pgbouncer' } 10 | l.push({databases_details: d}) 11 | l 12 | end 13 | end 14 | def databases 15 | connection.exec("SHOW databases") 16 | end 17 | def stats 18 | connection.exec("SHOW stats") 19 | end 20 | def lists 21 | connection.exec("SHOW lists") 22 | end 23 | def pools 24 | connection.exec("SHOW pools") 25 | end 26 | def clients 27 | connection.exec("SHOW clients") 28 | end 29 | def conf 30 | connection.exec("SHOW config") 31 | end 32 | def reload 33 | connection.exec("RELOAD") 34 | end 35 | def suspend 36 | connection.exec("SUSPEND") 37 | end 38 | def shutdown 39 | connection.exec("SHUTDOWN") 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/pgbouncerhero/version.rb: -------------------------------------------------------------------------------- 1 | module PgBouncerHero 2 | VERSION = "2.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /pgbouncerhero.gemspec: -------------------------------------------------------------------------------- 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 "jquery-rails" 25 | spec.add_runtime_dependency "semantic-ui-sass" 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 33 | --------------------------------------------------------------------------------