visage-app 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.gitignore +9 -0
  2. data/README.md +138 -0
  3. data/Rakefile +33 -0
  4. data/VERSION +1 -0
  5. data/bin/visage +17 -0
  6. data/config.ru +7 -0
  7. data/features/json.feature +66 -0
  8. data/features/site.feature +9 -0
  9. data/features/step_definitions/form_steps.rb +6 -0
  10. data/features/step_definitions/json_steps.rb +79 -0
  11. data/features/step_definitions/result_steps.rb +19 -0
  12. data/features/step_definitions/site_steps.rb +4 -0
  13. data/features/step_definitions/visage_steps.rb +20 -0
  14. data/features/step_definitions/webrat_steps.rb +42 -0
  15. data/features/support/env.rb +36 -0
  16. data/features/visage.feature +11 -0
  17. data/lib/visage/collectd/json.rb +142 -0
  18. data/lib/visage/collectd/profile.rb +36 -0
  19. data/lib/visage/config/fallback-colors.yaml +82 -0
  20. data/lib/visage/config/init.rb +33 -0
  21. data/lib/visage/config/plugin-colors.yaml +63 -0
  22. data/lib/visage/config/profiles.yaml +35 -0
  23. data/lib/visage/config/profiles.yaml.sample +33 -0
  24. data/lib/visage/config.rb +51 -0
  25. data/lib/visage/patches.rb +18 -0
  26. data/lib/visage/public/favicon.gif +0 -0
  27. data/lib/visage/public/javascripts/application.js +4 -0
  28. data/lib/visage/public/javascripts/g.line.js +217 -0
  29. data/lib/visage/public/javascripts/g.raphael.js +7 -0
  30. data/lib/visage/public/javascripts/graph.js +510 -0
  31. data/lib/visage/public/javascripts/mootools-1.2.3-core.js +4036 -0
  32. data/lib/visage/public/javascripts/mootools-1.2.3.1-more.js +104 -0
  33. data/lib/visage/public/javascripts/raphael-min.js +7 -0
  34. data/lib/visage/public/javascripts/raphael.js +3215 -0
  35. data/lib/visage/public/stylesheets/screen.css +96 -0
  36. data/lib/visage/views/index.haml +48 -0
  37. data/lib/visage/views/layout.haml +22 -0
  38. data/lib/visage/views/single.haml +43 -0
  39. data/lib/visage-app.rb +81 -0
  40. metadata +142 -0
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ config/profiles.yaml
2
+ .DS_Store
3
+ log/*
4
+ tmp/*
5
+ pkg/*
6
+ *~
7
+ .#*
8
+ webrat*
9
+ visage-app.gemspec
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,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $0 = "visage"
4
+
5
+ require 'lib/visage-app'
6
+ run Sinatra::Application
7
+
@@ -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,9 @@
1
+ Feature: Visit site
2
+ To find out how systems are performing
3
+ A user
4
+ Must be able to visualise the data
5
+
6
+ Scenario: Show available hosts
7
+ When I go to /
8
+ Then I should see a list of available hosts
9
+ When I follow the first host
@@ -0,0 +1,6 @@
1
+
2
+ Then /^the "(.+)" select should have "(.+)" selected$/ do |input, value|
3
+ response_body.to_s.should have_xpath("//select[@name='#{input}']//option[contains(.,'#{value}')][@selected]")
4
+ end
5
+
6
+
@@ -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,4 @@
1
+ Then /^I should see a list of available hosts$/ do
2
+ doc = Nokogiri::HTML(response_body)
3
+ doc.search('div#hosts li').size.should > 0
4
+ 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