visage-app 0.1.0
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.
- 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
|