visage-app 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/README.md +138 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/bin/visage +17 -0
- data/config.ru +7 -0
- data/features/json.feature +66 -0
- data/features/site.feature +9 -0
- data/features/step_definitions/form_steps.rb +6 -0
- data/features/step_definitions/json_steps.rb +79 -0
- data/features/step_definitions/result_steps.rb +19 -0
- data/features/step_definitions/site_steps.rb +4 -0
- data/features/step_definitions/visage_steps.rb +20 -0
- data/features/step_definitions/webrat_steps.rb +42 -0
- data/features/support/env.rb +36 -0
- data/features/visage.feature +11 -0
- data/lib/visage/collectd/json.rb +142 -0
- data/lib/visage/collectd/profile.rb +36 -0
- data/lib/visage/config/fallback-colors.yaml +82 -0
- data/lib/visage/config/init.rb +33 -0
- data/lib/visage/config/plugin-colors.yaml +63 -0
- data/lib/visage/config/profiles.yaml +35 -0
- data/lib/visage/config/profiles.yaml.sample +33 -0
- data/lib/visage/config.rb +51 -0
- data/lib/visage/patches.rb +18 -0
- data/lib/visage/public/favicon.gif +0 -0
- data/lib/visage/public/javascripts/application.js +4 -0
- data/lib/visage/public/javascripts/g.line.js +217 -0
- data/lib/visage/public/javascripts/g.raphael.js +7 -0
- data/lib/visage/public/javascripts/graph.js +510 -0
- data/lib/visage/public/javascripts/mootools-1.2.3-core.js +4036 -0
- data/lib/visage/public/javascripts/mootools-1.2.3.1-more.js +104 -0
- data/lib/visage/public/javascripts/raphael-min.js +7 -0
- data/lib/visage/public/javascripts/raphael.js +3215 -0
- data/lib/visage/public/stylesheets/screen.css +96 -0
- data/lib/visage/views/index.haml +48 -0
- data/lib/visage/views/layout.haml +22 -0
- data/lib/visage/views/single.haml +43 -0
- data/lib/visage-app.rb +81 -0
- metadata +142 -0
data/.gitignore
ADDED
data/README.md
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
Visage
|
2
|
+
======
|
3
|
+
|
4
|
+
Visage is a web interface for viewing [collectd](http://collectd.org) statistics.
|
5
|
+
|
6
|
+
It also provides a [JSON](http://json.org) interface onto `collectd`'s RRD data,
|
7
|
+
giving you an easy way to mash up the data.
|
8
|
+
|
9
|
+
Features
|
10
|
+
--------
|
11
|
+
|
12
|
+
* renders graphs in the browser, and retrieves data asynchronously
|
13
|
+
* interactive graph keys, to highlight lines and toggle line visibility
|
14
|
+
* drop-down or mouse selection of timeframes (also rendered asynchronously)
|
15
|
+
* JSON interface onto `collectd` RRDs
|
16
|
+
|
17
|
+
Check out [a demo](http://visage.unstated.net/nadia/cpu+load).
|
18
|
+
|
19
|
+
Installing
|
20
|
+
----------
|
21
|
+
|
22
|
+
On Ubuntu, to install dependencies run:
|
23
|
+
|
24
|
+
$ sudo apt-get install -y librrd-ruby ruby rubygems
|
25
|
+
|
26
|
+
On CentOS, to install dependencies run:
|
27
|
+
|
28
|
+
$ sudo yum install -y rrdtool ruby rubygems
|
29
|
+
|
30
|
+
Then install the app with:
|
31
|
+
|
32
|
+
$ gem install visage
|
33
|
+
|
34
|
+
|
35
|
+
Running
|
36
|
+
-------
|
37
|
+
|
38
|
+
You can check out the application quickly with:
|
39
|
+
|
40
|
+
$ visage start
|
41
|
+
|
42
|
+
|
43
|
+
Configuring
|
44
|
+
-----------
|
45
|
+
|
46
|
+
Config lives in several files under `config/`.
|
47
|
+
|
48
|
+
* `profiles.yaml` - groups of graphs Visage is to display
|
49
|
+
* `plugin-colors.yaml` - colors for specific plugins/plugin instances
|
50
|
+
* `fallback-colors.yaml` - ordered list of fallback colors
|
51
|
+
* `init.rb` - bootstrapping code, specifies collectd's RRD directory
|
52
|
+
|
53
|
+
`profiles.yaml` isn't setup by default, but you can copy `profiles.yaml.sample`
|
54
|
+
across and edit to taste. The plugins are in the format of
|
55
|
+
`plugin/plugin-instance`, with `plugins-instance` being optional.
|
56
|
+
|
57
|
+
If you don't specify a `plugin-instance` Visage will attempt to graph all plugin
|
58
|
+
instances under the specified `plugin`, e.g. `cpu-0` will display `cpu-idle`,
|
59
|
+
`cpu-interrupt`, `cpu-nice`, etc, whereas `cpu-0/cpu-wait` will only show
|
60
|
+
`cpu-wait`. You can also choose a specific group of plugin instances to graph,
|
61
|
+
with something like `cpu-0/cpu-system/cpu-user/cpu-wait`.
|
62
|
+
|
63
|
+
It should be pretty easy to deduce the config format from the existing file
|
64
|
+
(it's simple nested key-value data).
|
65
|
+
|
66
|
+
Make sure collectd's RRD directory is readable by whatever user the web server
|
67
|
+
is running as. You can specify where collectd's rrd directory is in `init.rb`,
|
68
|
+
with the `c['rrddir']` key.
|
69
|
+
|
70
|
+
Deploying
|
71
|
+
---------
|
72
|
+
|
73
|
+
With Passenger, create an Apache vhost with the `DocumentRoot` set to the
|
74
|
+
`public/` directory of where you have deployed the checked out code, e.g.
|
75
|
+
|
76
|
+
<VirtualHost *>
|
77
|
+
ServerName visage.example.org
|
78
|
+
ServerAdmin contact@visage.example.org
|
79
|
+
|
80
|
+
DocumentRoot /srv/www/visage.example.org/root/public/
|
81
|
+
|
82
|
+
<Directory "/srv/www/visage.example.org/root/public/">
|
83
|
+
Options FollowSymLinks Indexes
|
84
|
+
AllowOverride None
|
85
|
+
Order allow,deny
|
86
|
+
Allow from all
|
87
|
+
</Directory>
|
88
|
+
|
89
|
+
ErrorLog /srv/www/visage.example.org/log/apache_errors_log
|
90
|
+
CustomLog /srv/www/visage.example.org/log/apache_app_log combined
|
91
|
+
|
92
|
+
</VirtualHost>
|
93
|
+
|
94
|
+
This assumes you have a checkout of the code at `/srv/www/visage.example.org/root`.
|
95
|
+
|
96
|
+
If you don't want to use Apache + Passenger, you can install the `thin` or
|
97
|
+
`mongrel` gems and run up a web server yourself.
|
98
|
+
|
99
|
+
Ubuntu users looking for Passenger packages should add John Ferlito's
|
100
|
+
[mod-passenger PPA](https://launchpad.net/~johnf-inodes/+archive/mod-passenger)
|
101
|
+
to their apt sources.
|
102
|
+
|
103
|
+
Developing + testing
|
104
|
+
--------------------
|
105
|
+
|
106
|
+
Check out the code with:
|
107
|
+
|
108
|
+
$ git clone git://github.com/auxesis/visage.git
|
109
|
+
|
110
|
+
Install the development dependencies with
|
111
|
+
|
112
|
+
$ gem install shotgun rack-test rspec cucumber webrat
|
113
|
+
|
114
|
+
And run the app with:
|
115
|
+
|
116
|
+
$ shotgun visage.rb
|
117
|
+
|
118
|
+
Create and install a new gem from the current source tree:
|
119
|
+
|
120
|
+
$ rake install
|
121
|
+
|
122
|
+
Run all cucumber features:
|
123
|
+
|
124
|
+
$ rake cucumber
|
125
|
+
|
126
|
+
Specific features:
|
127
|
+
|
128
|
+
$ bin/cucumber --require features/ features/something.feature
|
129
|
+
|
130
|
+
TODO
|
131
|
+
----
|
132
|
+
|
133
|
+
* refactor tests to work on hosts other than my laptop
|
134
|
+
* interface to build custom graph profiles
|
135
|
+
* combine graphs from different hosts
|
136
|
+
* detailed point-in-time data on hover
|
137
|
+
* comment on time periods
|
138
|
+
* view list of comments
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'cucumber/rake/task'
|
7
|
+
|
8
|
+
Cucumber::Rake::Task.new do |t|
|
9
|
+
t.binary = "bin/cucumber"
|
10
|
+
t.cucumber_opts = "--require features/ features/"
|
11
|
+
end
|
12
|
+
rescue LoadError
|
13
|
+
end
|
14
|
+
|
15
|
+
begin
|
16
|
+
require 'jeweler'
|
17
|
+
Jeweler::Tasks.new do |gemspec|
|
18
|
+
gemspec.name = "visage-app"
|
19
|
+
gemspec.summary = "a web (interface | service) for viewing collectd statistics"
|
20
|
+
gemspec.description = "Visage is a web interface for viewing collectd statistics. It also provides a JSON interface onto collectd's RRD data, giving you an easy way to mash up the data."
|
21
|
+
gemspec.email = "lindsay@holmwood.id.au"
|
22
|
+
gemspec.homepage = "http://github.com/auxesis/visage"
|
23
|
+
gemspec.authors = ["Lindsay Holmwood"]
|
24
|
+
|
25
|
+
gemspec.add_dependency "sinatra", "1.0"
|
26
|
+
gemspec.add_dependency "tilt", "1.0.1"
|
27
|
+
gemspec.add_dependency "haml", "3.0.13"
|
28
|
+
gemspec.add_dependency "errand", "0.7.2"
|
29
|
+
gemspec.add_dependency "yajl-ruby", "0.7.6"
|
30
|
+
end
|
31
|
+
rescue LoadError
|
32
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
33
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/visage
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
@root = Pathname.new(File.dirname(__FILE__)).parent.expand_path
|
6
|
+
action = ARGV[0]
|
7
|
+
|
8
|
+
case action
|
9
|
+
when "start"
|
10
|
+
require 'rubygems'
|
11
|
+
require 'rack'
|
12
|
+
config = @root.join('config.ru').to_s
|
13
|
+
Rack::Server.start(:config => config, :Port => 9292)
|
14
|
+
else
|
15
|
+
puts "Usage: visage start"
|
16
|
+
end
|
17
|
+
|
data/config.ru
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
Feature: Export data
|
2
|
+
To perform analysis
|
3
|
+
A user
|
4
|
+
Must be able to extract data
|
5
|
+
From the application
|
6
|
+
|
7
|
+
Scenario: Retrieve single plugin instance
|
8
|
+
When I go to /data/theodor/memory/memory-free
|
9
|
+
Then the request should succeed
|
10
|
+
Then I should receive valid JSON
|
11
|
+
And the JSON should have a plugin instance named "memory-free"
|
12
|
+
|
13
|
+
Scenario: Retrieve multiple plugin instances
|
14
|
+
When I go to /data/theodor/memory
|
15
|
+
Then the request should succeed
|
16
|
+
Then I should receive valid JSON
|
17
|
+
And the JSON should have a plugin named "memory"
|
18
|
+
And the JSON should have multiple plugin instances under the "memory" plugin
|
19
|
+
|
20
|
+
Scenario: Make cross-domain requests
|
21
|
+
When I go to /data/theodor/cpu-0?callback=foobar
|
22
|
+
Then I should receive JSON wrapped in a callback named "foobar"
|
23
|
+
|
24
|
+
Scenario: Retrieve multiple plugin instances without color definition
|
25
|
+
When I go to /data/theodor/memory
|
26
|
+
Then the request should succeed
|
27
|
+
Then I should receive valid JSON
|
28
|
+
And each plugin instance should have a different color
|
29
|
+
|
30
|
+
Scenario Outline: Return only one colour per metric
|
31
|
+
When I go to /data/theodor/<path>
|
32
|
+
Then the request should succeed
|
33
|
+
Then I should receive valid JSON
|
34
|
+
And each plugin instance should have a different color
|
35
|
+
|
36
|
+
Examples:
|
37
|
+
| path |
|
38
|
+
| cpu-0/cpu-user |
|
39
|
+
| df/df-root |
|
40
|
+
|
41
|
+
Scenario: Retrieve single plugin instance with a color definition
|
42
|
+
When I go to /data/theodor/tcpconns-80-local/tcp_connections-LISTEN
|
43
|
+
Then the request should succeed
|
44
|
+
Then I should receive valid JSON
|
45
|
+
And the plugin instance should have a color
|
46
|
+
|
47
|
+
Scenario: Retrieve multiple plugins through a glob
|
48
|
+
Given I have the "tcpconns" plugin collecting data on multiple ports
|
49
|
+
When I go to /data/theodor/tcpconns-*-local/tcp_connections-LISTEN
|
50
|
+
Then the request should succeed
|
51
|
+
Then I should receive valid JSON
|
52
|
+
And I should see multiple plugins
|
53
|
+
|
54
|
+
Scenario Outline: Retrieve multple hosts through a glob
|
55
|
+
Given I have the "memory" plugin collecting data on multiple ports
|
56
|
+
When I go to /data/<host>/libvirt/virt_cpu_total
|
57
|
+
Then the request should succeed
|
58
|
+
Then I should receive valid JSON
|
59
|
+
And I should see multiple hosts
|
60
|
+
|
61
|
+
Examples:
|
62
|
+
| host |
|
63
|
+
| * |
|
64
|
+
| %7Brgh,flapjack-test%7D |
|
65
|
+
|
66
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
Then /^I should receive valid JSON$/ do
|
2
|
+
yajl = Yajl::Parser.new
|
3
|
+
lambda {
|
4
|
+
@response = yajl.parse(response_body)
|
5
|
+
}.should_not raise_error
|
6
|
+
|
7
|
+
host = @response.keys.first
|
8
|
+
plugin = @response[host].keys.first
|
9
|
+
metric = @response[host][plugin].keys.first
|
10
|
+
|
11
|
+
host.should_not be_nil
|
12
|
+
plugin.should_not be_nil
|
13
|
+
metric.should_not be_nil
|
14
|
+
|
15
|
+
data = @response[host][plugin][metric]["data"]
|
16
|
+
#.values.first.size.should > 0
|
17
|
+
end
|
18
|
+
|
19
|
+
Then /^I should receive JSON wrapped in a callback named "([^\"]*)"$/ do |callback|
|
20
|
+
response_body.should =~ /^#{callback}\(.+\)$/
|
21
|
+
end
|
22
|
+
|
23
|
+
Then /^the JSON should have a plugin instance named "([^\"]*)"$/ do |plugin_instance|
|
24
|
+
yajl = Yajl::Parser.new
|
25
|
+
data = yajl.parse(response_body)
|
26
|
+
|
27
|
+
data.values.map { |k,v| k.values }.map {|k,v| k.keys }.flatten.include?(plugin_instance).should be_true
|
28
|
+
end
|
29
|
+
|
30
|
+
Then /^the JSON should have a plugin named "([^\"]*)"$/ do |plugin|
|
31
|
+
yajl = Yajl::Parser.new
|
32
|
+
data = yajl.parse(response_body)
|
33
|
+
|
34
|
+
data.values.map { |k,v| k.keys }.flatten.include?(plugin).should be_true
|
35
|
+
end
|
36
|
+
|
37
|
+
Then /^the JSON should have multiple plugin instances under the "([^\"]*)" plugin$/ do |arg1|
|
38
|
+
yajl = Yajl::Parser.new
|
39
|
+
data = yajl.parse(response_body)
|
40
|
+
|
41
|
+
data.values.map { |k,v| k[arg1] }.map { |k,v| k.keys }.flatten.size.should > 1
|
42
|
+
end
|
43
|
+
|
44
|
+
Then /^each plugin instance should have a different color$/ do
|
45
|
+
yajl = Yajl::Parser.new
|
46
|
+
data = yajl.parse(response_body)
|
47
|
+
|
48
|
+
@colours = []
|
49
|
+
data.values.map { |k,v| k.values }.map {|k,v| k.values }.map {|k,v| k.values }.map do |a|
|
50
|
+
a.each do |b|
|
51
|
+
string = b["color"]
|
52
|
+
string.should =~ /^#[0-9a-fA-F]+$/
|
53
|
+
@colours << string
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@colours.uniq.size.should == @colours.size
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
Then /^the plugin instance should have a color$/ do
|
62
|
+
Then "each plugin instance should have a different color"
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
Given /^I have the "([^\"]*)" plugin collecting data on multiple ports$/ do |plugin|
|
67
|
+
Dir.glob("/var/lib/collectd/rrd/theodor/tcpconns*").size.should > 1
|
68
|
+
end
|
69
|
+
|
70
|
+
Then /^I should see multiple plugins$/ do
|
71
|
+
@response.should_not be_nil
|
72
|
+
host = @response.keys.first
|
73
|
+
@response[host].keys.size.should > 1
|
74
|
+
end
|
75
|
+
|
76
|
+
Then /^I should see multiple hosts$/ do
|
77
|
+
@response.should_not be_nil
|
78
|
+
@response.keys.size.should > 1
|
79
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Then /^I should see "(.*)"$/ do |text|
|
2
|
+
response_body.to_s.should =~ /#{text}/m
|
3
|
+
end
|
4
|
+
|
5
|
+
Then /^I should not see "(.*)"$/ do |text|
|
6
|
+
response_body.to_s.should_not =~ /#{text}/m
|
7
|
+
end
|
8
|
+
|
9
|
+
Then /^I should see an? (\w+) message$/ do |message_type|
|
10
|
+
response.should have_xpath("//*[@class='#{message_type}']")
|
11
|
+
end
|
12
|
+
|
13
|
+
Then /^the (.*) ?request should succeed/ do |_|
|
14
|
+
response_code.should < 400
|
15
|
+
end
|
16
|
+
|
17
|
+
Then /^the (.*) ?request should fail/ do |_|
|
18
|
+
response_code.should >= 400
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Given /^the "([^"]*)" gem is installed$/ do |name|
|
2
|
+
`gem list #{name} |grep #{name}`.size.should > 0
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^I start the visage server helper with "([^"]*)"$/ do |cmd|
|
6
|
+
@root = Pathname.new(File.dirname(__FILE__)).parent.parent.expand_path
|
7
|
+
command = "#{@root.join('bin')}/#{cmd}"
|
8
|
+
|
9
|
+
pipe = IO.popen(command)
|
10
|
+
sleep 2 # so the visage server has a chance to boot
|
11
|
+
|
12
|
+
# clean up the visage server when the tests finish
|
13
|
+
at_exit do
|
14
|
+
Process.kill("KILL", pipe.pid)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Then /^a visage web server should be running$/ do
|
19
|
+
`ps -eo cmd |grep ^visage`.size.should > 0
|
20
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Commonly used webrat steps
|
2
|
+
# http://github.com/brynary/webrat
|
3
|
+
|
4
|
+
When /^I go to (.*)$/ do |path|
|
5
|
+
visit path
|
6
|
+
end
|
7
|
+
|
8
|
+
When /^I press "(.*)"$/ do |button|
|
9
|
+
click_button(button)
|
10
|
+
end
|
11
|
+
|
12
|
+
When /^I follow "(.*)"$/ do |link|
|
13
|
+
click_link(link)
|
14
|
+
end
|
15
|
+
|
16
|
+
When /^I fill in "(.*)" with "(.*)"$/ do |field, value|
|
17
|
+
fill_in(field, :with => value)
|
18
|
+
end
|
19
|
+
|
20
|
+
When /^I select "(.*)" from "(.*)"$/ do |value, field|
|
21
|
+
select(value, :from => field)
|
22
|
+
end
|
23
|
+
|
24
|
+
When /^I check "(.*)"$/ do |field|
|
25
|
+
check(field)
|
26
|
+
end
|
27
|
+
|
28
|
+
When /^I uncheck "(.*)"$/ do |field|
|
29
|
+
uncheck(field)
|
30
|
+
end
|
31
|
+
|
32
|
+
When /^I choose "(.*)"$/ do |field|
|
33
|
+
choose(field)
|
34
|
+
end
|
35
|
+
|
36
|
+
When /^I attach the file at "(.*)" to "(.*)" $/ do |path, field|
|
37
|
+
attach_file(field, path)
|
38
|
+
end
|
39
|
+
|
40
|
+
Then /^show me the page$/ do
|
41
|
+
save_and_open_page
|
42
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
@root = Pathname.new(File.dirname(__FILE__)).parent.parent.expand_path
|
6
|
+
app_file = @root.join('lib/visage')
|
7
|
+
|
8
|
+
require 'rubygems'
|
9
|
+
require 'spec/expectations'
|
10
|
+
require 'rack/test'
|
11
|
+
require 'webrat'
|
12
|
+
|
13
|
+
require app_file
|
14
|
+
# Force the application name because polyglot breaks the auto-detection logic.
|
15
|
+
Sinatra::Application.app_file = app_file
|
16
|
+
|
17
|
+
Webrat.configure do |config|
|
18
|
+
config.mode = :rack
|
19
|
+
end
|
20
|
+
|
21
|
+
class SinatraWorld
|
22
|
+
include Rack::Test::Methods
|
23
|
+
include Webrat::Methods
|
24
|
+
include Webrat::Matchers
|
25
|
+
|
26
|
+
Webrat::Methods.delegate_to_session :response_code, :response_body
|
27
|
+
|
28
|
+
def app
|
29
|
+
Sinatra::Application
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
World do
|
34
|
+
SinatraWorld.new
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Feature: command line utility
|
2
|
+
As a systems administrator
|
3
|
+
Or a hard core developer
|
4
|
+
I want to get Visage up and running
|
5
|
+
With the least hassle possible
|
6
|
+
|
7
|
+
Scenario: Command line tool
|
8
|
+
Given the "visage" gem is installed
|
9
|
+
When I start the visage server helper with "visage start"
|
10
|
+
Then a visage web server should be running
|
11
|
+
|
@@ -0,0 +1,142 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.expand_path(File.dirname(__FILE__))
|
4
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
5
|
+
|
6
|
+
require 'errand'
|
7
|
+
require 'yajl'
|
8
|
+
require 'patches'
|
9
|
+
|
10
|
+
# Exposes RRDs as JSON.
|
11
|
+
#
|
12
|
+
# A loose shim onto RRDtool, with some extra logic to normalise the data.
|
13
|
+
# Also provides a recommended color for rendering the data in a line graph.
|
14
|
+
#
|
15
|
+
class CollectdJSON
|
16
|
+
|
17
|
+
def initialize(opts={})
|
18
|
+
@rrddir = opts[:rrddir] || CollectdJSON.rrddir
|
19
|
+
@fallback_colors = opts[:fallback_colors] || {}
|
20
|
+
@used_fallbacks = []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Entry point.
|
24
|
+
def json(opts={})
|
25
|
+
host = opts[:host]
|
26
|
+
plugin = opts[:plugin]
|
27
|
+
plugin_instances = opts[:plugin_instances][/\w.*/]
|
28
|
+
instances = plugin_instances.blank? ? '*' : '{' + plugin_instances.split('/').join(',') + '}'
|
29
|
+
@colors = opts[:plugin_colors]
|
30
|
+
@plugin_names = []
|
31
|
+
|
32
|
+
rrdglob = "#{@rrddir}/#{host}/#{plugin}/#{instances}.rrd"
|
33
|
+
plugin_offset = @rrddir.size + 1 + host.size + 1
|
34
|
+
|
35
|
+
data = []
|
36
|
+
|
37
|
+
Dir.glob(rrdglob).map do |rrdname|
|
38
|
+
parts = rrdname.gsub(/#{@rrddir}\//, '').split('/')
|
39
|
+
host = parts[0]
|
40
|
+
plugin_name = parts[1]
|
41
|
+
instance_name = parts[2].split('.').first
|
42
|
+
rrd = Errand.new(:filename => rrdname)
|
43
|
+
|
44
|
+
data << { :plugin => plugin_name, :instance => instance_name,
|
45
|
+
:host => host,
|
46
|
+
:start => opts[:start] || (Time.now - 3600).to_i,
|
47
|
+
:finish => opts[:finish] || Time.now.to_i,
|
48
|
+
:rrd => rrd }
|
49
|
+
end
|
50
|
+
|
51
|
+
encode(data)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
# Attempt to structure the JSON reasonably sanely, so the consumer (i.e. a
|
56
|
+
# browser) doesn't have to do a lot of computationally expensive work.
|
57
|
+
def encode(datas)
|
58
|
+
|
59
|
+
structure = {}
|
60
|
+
datas.each do |data|
|
61
|
+
fetch = data[:rrd].fetch(:function => "AVERAGE",
|
62
|
+
:start => data[:start],
|
63
|
+
:finish => data[:finish])
|
64
|
+
rrd_data = fetch[:data]
|
65
|
+
|
66
|
+
# A single rrd can have multiple data sets (multiple metrics within
|
67
|
+
# the same file). Separate the metrics.
|
68
|
+
rrd_data.each_pair do |source, metric|
|
69
|
+
|
70
|
+
# filter out NaNs, so yajl doesn't choke
|
71
|
+
metric.map! do |datapoint|
|
72
|
+
(!datapoint || datapoint.nan?) ? 0.0 : datapoint
|
73
|
+
end
|
74
|
+
|
75
|
+
color = color_for(:host => data[:host],
|
76
|
+
:plugin => data[:plugin],
|
77
|
+
:instance => data[:instance],
|
78
|
+
:metric => source)
|
79
|
+
|
80
|
+
structure[data[:host]] ||= {}
|
81
|
+
structure[data[:host]][data[:plugin]] ||= {}
|
82
|
+
structure[data[:host]][data[:plugin]][data[:instance]] ||= {}
|
83
|
+
structure[data[:host]][data[:plugin]][data[:instance]][source] ||= {}
|
84
|
+
structure[data[:host]][data[:plugin]][data[:instance]][source][:start] ||= data[:start]
|
85
|
+
structure[data[:host]][data[:plugin]][data[:instance]][source][:finish] ||= data[:finish]
|
86
|
+
structure[data[:host]][data[:plugin]][data[:instance]][source][:data] ||= metric
|
87
|
+
structure[data[:host]][data[:plugin]][data[:instance]][source][:color] ||= color
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
encoder = Yajl::Encoder.new
|
92
|
+
encoder.encode(structure)
|
93
|
+
end
|
94
|
+
|
95
|
+
# We append the recommended line color onto data set, so the javascript
|
96
|
+
# doesn't try and have to work it out. This lets us use all sorts of funky
|
97
|
+
# fallback logic when determining what colours should be used.
|
98
|
+
def color_for(opts={})
|
99
|
+
|
100
|
+
plugin = opts[:plugin]
|
101
|
+
instance = opts[:instance]
|
102
|
+
metric = opts[:metric]
|
103
|
+
|
104
|
+
return fallback_color unless plugin
|
105
|
+
return color_for(opts.merge(:plugin => plugin[/(.+)-.+$/, 1])) unless @colors[plugin]
|
106
|
+
return color_for(opts.merge(:instance => instance[/(.+)-.+$/, 1])) unless @colors[plugin][instance]
|
107
|
+
return @colors[plugin][instance][metric] if @colors[plugin][instance]
|
108
|
+
return fallback_color
|
109
|
+
end
|
110
|
+
|
111
|
+
def fallback_color
|
112
|
+
fallbacks = @fallback_colors.to_a.sort_by {|pair| pair[1]['fallback_order'] }
|
113
|
+
fallback = fallbacks.find { |color| !@used_fallbacks.include?(color) }
|
114
|
+
unless fallback
|
115
|
+
@used_fallbacks = []
|
116
|
+
fallback = fallbacks.find { |color| !@used_fallbacks.include?(color) }
|
117
|
+
end
|
118
|
+
@used_fallbacks << fallback
|
119
|
+
fallback[1]['color'] || "#000"
|
120
|
+
end
|
121
|
+
|
122
|
+
class << self
|
123
|
+
attr_writer :rrddir
|
124
|
+
|
125
|
+
def rrddir
|
126
|
+
@rrddir || @rrddir = "/var/lib/collectd/rrd"
|
127
|
+
end
|
128
|
+
|
129
|
+
def hosts
|
130
|
+
if @rrddir
|
131
|
+
Dir.glob("#{@rrddir}/*").map {|e| e.split('/').last }.sort
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def plugins(opts={})
|
136
|
+
host = opts[:host] || '*'
|
137
|
+
Dir.glob("#{@rrddir}/#{host}/*").map {|e| e.split('/').last }.sort
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|