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.
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