elefant 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +65 -0
- data/Rakefile +8 -0
- data/bin/elefant-web +3 -0
- data/config.ru +4 -0
- data/elefant.gemspec +32 -0
- data/lib/elefant.rb +22 -0
- data/lib/elefant/connection_adapter.rb +122 -0
- data/lib/elefant/postgres/size_queries.rb +31 -0
- data/lib/elefant/postgres/stat_queries.rb +91 -0
- data/lib/elefant/stats.rb +56 -0
- data/lib/elefant/version.rb +3 -0
- data/lib/elefant/web.rb +86 -0
- data/test/ar_connection_test.rb +55 -0
- data/test/config.yml.example +4 -0
- data/test/fixtures/teardown.sql +4 -0
- data/test/fixtures/test_models.sql +11 -0
- data/test/pg_connection_test.rb +12 -0
- data/test/stats_test.rb +32 -0
- data/test/test_helper.rb +37 -0
- data/test/webapp_test.rb +46 -0
- data/watchr.rb +34 -0
- data/web/locales/en.yml +33 -0
- data/web/public/css/_fonts.scss +36 -0
- data/web/public/css/_lists.scss +16 -0
- data/web/public/css/_nav.scss +75 -0
- data/web/public/css/_normalize.scss +427 -0
- data/web/public/css/_skeleton.scss +418 -0
- data/web/public/css/_tables.scss +36 -0
- data/web/public/css/_typography.scss +24 -0
- data/web/public/css/_variables.scss +2 -0
- data/web/public/css/elefant.css +13 -0
- data/web/public/css/elefant.css.map +7 -0
- data/web/public/css/elefant.scss +45 -0
- data/web/public/fonts/ClearSans-Bold.eot +0 -0
- data/web/public/fonts/ClearSans-Bold.svg +22646 -0
- data/web/public/fonts/ClearSans-Bold.ttf +0 -0
- data/web/public/fonts/ClearSans-Bold.woff +0 -0
- data/web/public/fonts/ClearSans-Light.eot +0 -0
- data/web/public/fonts/ClearSans-Light.svg +22411 -0
- data/web/public/fonts/ClearSans-Light.ttf +0 -0
- data/web/public/fonts/ClearSans-Light.woff +0 -0
- data/web/public/fonts/LICENSE-2.0.txt +202 -0
- data/web/public/img/postgresql.ico +0 -0
- data/web/public/img/postgresql_logo.svg +22 -0
- data/web/public/img/screenshot.png +0 -0
- data/web/public/js/zepto.min.js +2 -0
- data/web/views/activity.erb +27 -0
- data/web/views/indices.erb +12 -0
- data/web/views/layout.erb +58 -0
- data/web/views/partials/field_table.erb +18 -0
- data/web/views/partials/generic_table.erb +23 -0
- data/web/views/partials/list.erb +6 -0
- data/web/views/size.erb +23 -0
- data/web/views/summary.erb +18 -0
- data/web/views/tables.erb +5 -0
- metadata +255 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -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
|
+
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
elefant-gem
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0-p645
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
data/bin/elefant-web
ADDED
data/config.ru
ADDED
data/elefant.gemspec
ADDED
@@ -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
|
data/lib/elefant.rb
ADDED
@@ -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
|