elefant 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +13 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +65 -0
  9. data/Rakefile +8 -0
  10. data/bin/elefant-web +3 -0
  11. data/config.ru +4 -0
  12. data/elefant.gemspec +32 -0
  13. data/lib/elefant.rb +22 -0
  14. data/lib/elefant/connection_adapter.rb +122 -0
  15. data/lib/elefant/postgres/size_queries.rb +31 -0
  16. data/lib/elefant/postgres/stat_queries.rb +91 -0
  17. data/lib/elefant/stats.rb +56 -0
  18. data/lib/elefant/version.rb +3 -0
  19. data/lib/elefant/web.rb +86 -0
  20. data/test/ar_connection_test.rb +55 -0
  21. data/test/config.yml.example +4 -0
  22. data/test/fixtures/teardown.sql +4 -0
  23. data/test/fixtures/test_models.sql +11 -0
  24. data/test/pg_connection_test.rb +12 -0
  25. data/test/stats_test.rb +32 -0
  26. data/test/test_helper.rb +37 -0
  27. data/test/webapp_test.rb +46 -0
  28. data/watchr.rb +34 -0
  29. data/web/locales/en.yml +33 -0
  30. data/web/public/css/_fonts.scss +36 -0
  31. data/web/public/css/_lists.scss +16 -0
  32. data/web/public/css/_nav.scss +75 -0
  33. data/web/public/css/_normalize.scss +427 -0
  34. data/web/public/css/_skeleton.scss +418 -0
  35. data/web/public/css/_tables.scss +36 -0
  36. data/web/public/css/_typography.scss +24 -0
  37. data/web/public/css/_variables.scss +2 -0
  38. data/web/public/css/elefant.css +13 -0
  39. data/web/public/css/elefant.css.map +7 -0
  40. data/web/public/css/elefant.scss +45 -0
  41. data/web/public/fonts/ClearSans-Bold.eot +0 -0
  42. data/web/public/fonts/ClearSans-Bold.svg +22646 -0
  43. data/web/public/fonts/ClearSans-Bold.ttf +0 -0
  44. data/web/public/fonts/ClearSans-Bold.woff +0 -0
  45. data/web/public/fonts/ClearSans-Light.eot +0 -0
  46. data/web/public/fonts/ClearSans-Light.svg +22411 -0
  47. data/web/public/fonts/ClearSans-Light.ttf +0 -0
  48. data/web/public/fonts/ClearSans-Light.woff +0 -0
  49. data/web/public/fonts/LICENSE-2.0.txt +202 -0
  50. data/web/public/img/postgresql.ico +0 -0
  51. data/web/public/img/postgresql_logo.svg +22 -0
  52. data/web/public/img/screenshot.png +0 -0
  53. data/web/public/js/zepto.min.js +2 -0
  54. data/web/views/activity.erb +27 -0
  55. data/web/views/indices.erb +12 -0
  56. data/web/views/layout.erb +58 -0
  57. data/web/views/partials/field_table.erb +18 -0
  58. data/web/views/partials/generic_table.erb +23 -0
  59. data/web/views/partials/list.erb +6 -0
  60. data/web/views/size.erb +23 -0
  61. data/web/views/summary.erb +18 -0
  62. data/web/views/tables.erb +5 -0
  63. metadata +255 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 088dc5c6b9b4cbaa02e1497445dc9fb3cb5d1f83
4
+ data.tar.gz: f7cf82d109b27295d28a8ed2c9726fa5c7eb0965
5
+ SHA512:
6
+ metadata.gz: 7f57ab871b8301a5671b8bbdaa914b5d90502ec160ab48694d6c0e7b904322db390ae9302453284cc02f1315dae0d02e9c893c7d029619084c522f8348e92a36
7
+ data.tar.gz: e84818a8ba32bb9bf34c1fba56d26eb7e16d4b79ae85d1e7d983c8fcc612acd070b2694c063ee7205db5f2baa219fddb222b717a7e380bf99e2063994b5e851f
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ test/config.yml
19
+ .sass-cache
20
+ **/.DS_Store
21
+
@@ -0,0 +1 @@
1
+ elefant-gem
@@ -0,0 +1 @@
1
+ 2.0.0-p645
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ - 2.1.5
5
+ - 2.0.0
6
+ - 1.9.3
7
+
8
+ addons:
9
+ postgresql: "9.3"
10
+
11
+ before_script:
12
+ - psql -c 'create database elefant_test;' -U postgres
13
+ - cp test/config.yml.example test/config.yml
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in elefant.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Christoph Sassenberg
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,65 @@
1
+ # Elefant
2
+
3
+ [![Build Status](https://travis-ci.org/defsprite/elefant.svg?branch=master)](https://travis-ci.org/defsprite/elefant)
4
+
5
+ When you are running a small / medium sized project, there is usually not a dedicated DBA and you rarely care about the database analytics that PostgreSQL gives you for free.
6
+ Elefant tries to help a little with that by providing web interface for some basic database analytics as a small mountable rack application.
7
+
8
+ ![Screenshot](https://raw.githubusercontent.com/defsprite/elefant/master/web/public/img/screenshot.png)
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'elefant'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install elefant
23
+
24
+ ## Usage
25
+
26
+ ### Standalone
27
+
28
+ Run the web interface as a standalone rack application:
29
+
30
+ $ DATABASE_URL=postgres:///some_db_connection_string elefant-web
31
+
32
+ or if you want to use a different database specifically for elefant:
33
+
34
+ $ ELEFANT_DATABASE_URL=postgres:///some_db_connection_string elefant-web
35
+
36
+ ### Rails
37
+
38
+ You can mount the web interface in a rails application by adding this to `config/routes.rb`:
39
+
40
+ ```ruby
41
+ require 'elefant/web'
42
+ mount Elefant::Web => '/elefant'
43
+ ```
44
+ It will pick up a connection from the `ActiveRecord` connection pool.
45
+
46
+ :warning: There is no authentication built in! Mounting it as described above is probably a bad idea! :warning:
47
+
48
+ In case you are using [Devise](https://github.com/plataformatec/devise), here is an example of mounting only for
49
+ authenticated users of the `:admin` scope:
50
+
51
+ ```ruby
52
+ authenticate(:admin) do
53
+ require 'elefant/web'
54
+ mount Elefant::Web => '/elefant'
55
+ end
56
+ ```
57
+
58
+
59
+ ## Contributing
60
+
61
+ 1. Fork it
62
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
63
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
64
+ 4. Push to the branch (`git push origin my-new-feature`)
65
+ 5. Create new Pull Request
@@ -0,0 +1,8 @@
1
+ require "rake/testtask"
2
+
3
+ Rake::TestTask.new do |task|
4
+ task.libs << %w(test lib)
5
+ task.pattern = "test/*_test.rb"
6
+ end
7
+
8
+ task :default => :test
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ app = File.expand_path('../../config.ru', __FILE__)
3
+ system("bundle exec rackup '#{app}'")
@@ -0,0 +1,4 @@
1
+ require 'elefant'
2
+ require 'elefant/web'
3
+
4
+ run Elefant::Web
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'elefant/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "elefant"
8
+ gem.version = Elefant::VERSION
9
+ gem.authors = ["Christoph Sassenberg"]
10
+ gem.email = ["christoph.sassenberg@googlemail.com"]
11
+ gem.description = %q{The elefant gem gives you a few insights analytics about your PostgreSQL database}
12
+ gem.summary = %q{Know your PostgreSQL database}
13
+ gem.homepage = "http://github.com/defsprite/elefant"
14
+ gem.licenses = ["MIT"]
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_dependency 'pg', '~> 0.15'
22
+ gem.add_dependency 'sinatra', '~> 1.4'
23
+ gem.add_dependency 'sinatra-partial', '~> 0.4'
24
+ gem.add_dependency 'i18n', '~> 0.7'
25
+
26
+ gem.add_development_dependency 'rake', '~> 10.4'
27
+ gem.add_development_dependency 'minitest', '~> 5.5'
28
+ gem.add_development_dependency 'rack-test', '~> 0.6'
29
+ gem.add_development_dependency 'watchr', '~> 0.7'
30
+ gem.add_development_dependency 'activerecord', '~> 4.2'
31
+ gem.add_development_dependency 'sass', '~> 3.4'
32
+ end
@@ -0,0 +1,22 @@
1
+ require "elefant/version"
2
+ require "elefant/stats"
3
+
4
+ module Elefant
5
+
6
+ def self.configuration
7
+ @configuration ||= Configuration.new
8
+ end
9
+
10
+ def self.configure
11
+ configuration = self.configuration
12
+ yield(configuration)
13
+ end
14
+
15
+ class Configuration
16
+ attr_accessor :disable_ar
17
+
18
+ def initialize
19
+ @disable_ar = false
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,122 @@
1
+ require "uri"
2
+ require "pg"
3
+
4
+ module Elefant
5
+ class ConnectionAdapter
6
+
7
+ attr_accessor :connection
8
+
9
+ def initialize(connection=nil)
10
+ @connection = connection.nil? ? establish_new : validate!(connection)
11
+ end
12
+
13
+ def execute(stmt, params = [])
14
+ begin
15
+ params = nil if params.empty?
16
+ r = @connection.exec(stmt, params)
17
+ result = []
18
+ r.each do |t|
19
+ result << t
20
+ end
21
+ result
22
+ rescue PGError => e
23
+ @connection.reset
24
+ raise e
25
+ end
26
+ end
27
+
28
+ def disconnect
29
+ begin
30
+ if active_record?
31
+ @connection = nil
32
+ ActiveRecord::Base.clear_active_connections!
33
+ else
34
+ @connection.close
35
+ end
36
+ rescue => e
37
+ # TODO: log something and do sensible stuff
38
+ raise e
39
+ end
40
+ end
41
+
42
+ def alive?
43
+ @connection.query "SELECT 1"
44
+ true
45
+ rescue PGError
46
+ false
47
+ end
48
+
49
+ def info
50
+ @info ||= {
51
+ db_name: @connection.db,
52
+ server_version: version_str(connection.server_version),
53
+ client_version: (PG.respond_to?( :library_version ) ? version_str(PG.library_version) : 'unknown')
54
+ }
55
+ end
56
+
57
+ def db_name
58
+ @connection.db
59
+ end
60
+
61
+ def active_record?
62
+ defined?(ActiveRecord::Base) == "constant" && ActiveRecord::Base.class == Class && !Elefant.configuration.disable_ar
63
+ end
64
+
65
+ private
66
+
67
+ def version_str(number)
68
+ number.to_s.tr('0','.').gsub(/(\.)+\z/, '')
69
+ end
70
+
71
+ def validate!(c)
72
+ return c if c.is_a?(PG::Connection)
73
+ err = "connection must be an instance of PG::Connection, but was #{c.class}"
74
+ raise(ArgumentError, err)
75
+ end
76
+
77
+ def establish_new
78
+ #QC.log(:at => "establish_conn")
79
+ connection = if active_record?
80
+ establish_ar
81
+ else
82
+ establish_pg
83
+ end
84
+ connection.exec("SET application_name = 'Elefant Stats #{Elefant::VERSION}'")
85
+ connection
86
+ end
87
+
88
+ def establish_pg
89
+ PGconn.connect(*normalize_db_url(db_url))
90
+ # if conn.status != PGconn::CONNECTION_OK
91
+ # log(:error => conn.error)
92
+ # end
93
+ end
94
+
95
+ def establish_ar
96
+ ActiveRecord::Base.connection.raw_connection
97
+ end
98
+
99
+ def normalize_db_url(url)
100
+ host = url.host
101
+ host = host.gsub(/%2F/i, '/') if host
102
+
103
+ [
104
+ host, # host or percent-encoded socket path
105
+ url.port || 5432,
106
+ nil, "", #opts, tty
107
+ url.path.gsub('/', ''), # database name
108
+ url.user,
109
+ url.password
110
+ ]
111
+ end
112
+
113
+ def db_url
114
+ return @db_url if defined?(@db_url) && @db_url
115
+
116
+ url = ENV["ELEFANT_DATABASE_URL"] ||
117
+ ENV["DATABASE_URL"] ||
118
+ raise(ArgumentError, "missing ELEFANT_DATABASE_URL or DATABASE_URL")
119
+ @db_url = URI.parse(url)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,31 @@
1
+ module Elefant
2
+ module Postgres
3
+ module SizeQueries
4
+
5
+ def top_sizes(limit = 20)
6
+ exec %Q{
7
+ SELECT
8
+ relname AS name,
9
+ relkind AS kind,
10
+ pg_size_pretty(pg_relation_size(pg_class.oid)) AS size
11
+ FROM
12
+ pg_class
13
+ ORDER BY
14
+ pg_relation_size(pg_class.oid) DESC
15
+ LIMIT #{limit}
16
+ }
17
+ end
18
+
19
+ def size
20
+ exec %Q{
21
+ SELECT
22
+ '#{@connection.db_name}' AS db_name,
23
+ count(oid) AS num_rels,
24
+ pg_size_pretty(pg_database_size('#{@connection.db_name}')) AS dbsize
25
+ FROM
26
+ pg_class
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,91 @@
1
+ module Elefant::Postgres
2
+ module StatQueries
3
+
4
+ def initialize(connection)
5
+ @connection = connection
6
+ end
7
+
8
+ def activity
9
+ exec %Q{
10
+ SELECT
11
+ -- datid
12
+ -- datname
13
+ -- pid
14
+ -- usesysid
15
+ usename AS act_user,
16
+ application_name AS act_app,
17
+ client_addr AS act_c_addr,
18
+ client_hostname AS act_c_host,
19
+ client_port AS act_c_port,
20
+ backend_start AS act_bknd_start,
21
+ xact_start AS act_tx_start,
22
+ query_start AS act_q_start,
23
+ state_change AS act_st_chng,
24
+ waiting AS act_wtng,
25
+ state AS act_state,
26
+ query AS act_qry
27
+ FROM
28
+ pg_stat_activity
29
+ WHERE
30
+ datname = '%s';
31
+ } % [@connection.db_name]
32
+ end
33
+
34
+ def user_tables
35
+ exec %Q{
36
+ SELECT
37
+ relname AS rel_name,
38
+ heap_blks_read AS heap_blks_rd,
39
+ heap_blks_hit AS heap_blks_ht,
40
+ idx_blks_read AS idx_blks_rd,
41
+ idx_blks_hit AS idx_blks_ht
42
+ FROM
43
+ pg_statio_user_tables;
44
+ }
45
+ end
46
+
47
+ def user_indexes
48
+ exec %Q{
49
+ SELECT
50
+ -- relid
51
+ -- indexrelid
52
+ -- schemaname
53
+ relname AS rel_name,
54
+ indexrelname AS idx_name,
55
+ idx_scan AS idx_scn,
56
+ idx_tup_read AS idx_tup_rd,
57
+ idx_tup_fetch AS idx_tup_ftch
58
+ FROM
59
+ pg_stat_user_indexes;
60
+ }
61
+ end
62
+
63
+ def summary
64
+ exec %Q{
65
+ SELECT
66
+ now() AS db_time,
67
+ MAX(stat_db.xact_commit) AS commits,
68
+ MAX(stat_db.xact_rollback) AS rollbks,
69
+ MAX(stat_db.blks_read) AS blksrd,
70
+ MAX(stat_db.blks_hit) AS blkshit,
71
+ MAX(stat_db.numbackends) AS bkends,
72
+ SUM(stat_tables.seq_scan) AS seqscan,
73
+ SUM(stat_tables.seq_tup_read) AS seqtprd,
74
+ SUM(stat_tables.idx_scan) AS idxscn,
75
+ SUM(stat_tables.idx_tup_fetch) AS idxtrd,
76
+ SUM(stat_tables.n_tup_ins) AS ins,
77
+ SUM(stat_tables.n_tup_upd) AS upd,
78
+ SUM(stat_tables.n_tup_del) AS del,
79
+ MAX(stat_locks.locks) AS locks,
80
+ MAX(activity.sess) AS activeq
81
+ FROM
82
+ pg_stat_database AS stat_db,
83
+ pg_stat_user_tables AS stat_tables,
84
+ (SELECT COUNT(*) AS locks FROM pg_locks ) AS stat_locks,
85
+ (SELECT COUNT(*) AS sess FROM pg_stat_activity WHERE query <> '<IDLE>') AS activity
86
+ WHERE
87
+ stat_db.datname = '%s';
88
+ } % [@connection.db_name]
89
+ end
90
+ end
91
+ end