visage-app 0.9.4 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock ADDED
@@ -0,0 +1,62 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ builder (3.0.0)
5
+ cucumber (0.10.0)
6
+ builder (>= 2.1.2)
7
+ diff-lcs (~> 1.1.2)
8
+ gherkin (~> 2.3.2)
9
+ json (~> 1.4.6)
10
+ term-ansicolor (~> 1.0.5)
11
+ diff-lcs (1.1.2)
12
+ errand (0.7.2)
13
+ gherkin (2.3.3)
14
+ json (~> 1.4.6)
15
+ git (1.2.5)
16
+ haml (3.0.25)
17
+ jeweler (1.5.2)
18
+ bundler (~> 1.0.0)
19
+ git (>= 1.2.5)
20
+ rake
21
+ json (1.4.6)
22
+ nokogiri (1.4.4)
23
+ rack (1.2.1)
24
+ rack-test (0.5.7)
25
+ rack (>= 1.0)
26
+ rake (0.8.7)
27
+ rspec (2.4.0)
28
+ rspec-core (~> 2.4.0)
29
+ rspec-expectations (~> 2.4.0)
30
+ rspec-mocks (~> 2.4.0)
31
+ rspec-core (2.4.0)
32
+ rspec-expectations (2.4.0)
33
+ diff-lcs (~> 1.1.2)
34
+ rspec-mocks (2.4.0)
35
+ shotgun (0.8)
36
+ rack (>= 1.0)
37
+ sinatra (1.1.2)
38
+ rack (~> 1.1)
39
+ tilt (~> 1.2)
40
+ term-ansicolor (1.0.5)
41
+ tilt (1.2.2)
42
+ webrat (0.7.1)
43
+ nokogiri (>= 1.2.0)
44
+ rack (>= 1.0)
45
+ rack-test (>= 0.5.3)
46
+ yajl-ruby (0.7.9)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ cucumber
53
+ errand
54
+ haml
55
+ jeweler
56
+ rack-test
57
+ rspec
58
+ shotgun
59
+ sinatra
60
+ tilt
61
+ webrat
62
+ yajl-ruby
data/README.md CHANGED
@@ -35,6 +35,10 @@ On CentOS, to install dependencies run:
35
35
 
36
36
  $ sudo yum install -y ruby-RRDtool ruby rubygems collectd
37
37
 
38
+ If you are using Ruby Enterprise Edition, instead of installing librrd-ruby or ruby-RRDtool, run
39
+
40
+ $ gem install librrd
41
+
38
42
  Then install the app with:
39
43
 
40
44
  $ gem install visage-app
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.4
1
+ 0.9.5
@@ -36,32 +36,6 @@ Feature: Export data
36
36
  When I visit "cpu-0?callback=foobar" on the first available host
37
37
  Then I should receive JSON wrapped in a callback named "foobar"
38
38
 
39
- Scenario: Retrieve multiple plugin instances without color definition
40
- Given a list of hosts exist
41
- When I visit "memory" on the first available host
42
- Then the request should succeed
43
- Then I should receive valid JSON
44
- And each plugin instance should have a different color
45
-
46
- Scenario Outline: Return only one colour per metric
47
- Given a list of hosts exist
48
- When I visit "<path>" on the first available host
49
- Then the request should succeed
50
- Then I should receive valid JSON
51
- And each plugin instance should have a different color
52
-
53
- Examples:
54
- | path |
55
- | cpu-0/cpu-user |
56
- | memory/memory-used |
57
-
58
- Scenario: Retrieve single plugin instance with a color definition
59
- Given a list of hosts exist
60
- When I visit "load/load" on the first available host
61
- Then the request should succeed
62
- Then I should receive valid JSON
63
- And the plugin instance should have a color
64
-
65
39
  Scenario: Retrieve multiple plugins through a glob
66
40
  Given a list of hosts exist
67
41
  When I visit "disk*/disk_ops" on the first available host
@@ -0,0 +1,12 @@
1
+ Feature: Metric metadata
2
+ To view the data
3
+ In an identifiable way
4
+ A user
5
+ Needs meaningful labels
6
+
7
+ Scenario: Retreive a list of types
8
+ When I go to /meta/types
9
+ Then the request should succeed
10
+ Then I should receive valid JSON
11
+ And the JSON should have a list of types
12
+
@@ -5,6 +5,8 @@ Then /^I should receive valid JSON$/ do
5
5
  }.should_not raise_error
6
6
 
7
7
  case
8
+ when @response.class == Array
9
+ next
8
10
  when @response.keys.first == "hosts"
9
11
  @response["hosts"].should respond_to(:size)
10
12
  when @response[@response.keys.first].respond_to?(:size)
@@ -121,3 +123,13 @@ Then /^the JSON should have a list of plugins$/ do
121
123
  plugins = @response[host]
122
124
  plugins.size.should > 0
123
125
  end
126
+
127
+ Then /^the JSON should have a list of types$/ do
128
+ @response.size.should > 0
129
+ @response.each do |type|
130
+ %w(dataset datasource type min max).each do |attr|
131
+ type[attr].should_not be_nil
132
+ end
133
+ end
134
+ end
135
+
@@ -6,6 +6,11 @@ When /^I visit the first profile$/ do
6
6
  end
7
7
 
8
8
  Then /^I should see a list of graphs$/ do
9
+ begin
10
+ follow_redirect!
11
+ rescue Rack::Test::Error
12
+ end
13
+
9
14
  doc = Nokogiri::HTML(response_body)
10
15
  doc.search('div#profile div.graph').size.should > 0
11
16
  end
@@ -14,19 +14,19 @@ When /^I follow "(.*)"$/ do |link|
14
14
  end
15
15
 
16
16
  When /^I fill in "(.*)" with "(.*)"$/ do |field, value|
17
- fill_in(field, :with => value)
17
+ fill_in(field, :with => value)
18
18
  end
19
19
 
20
20
  When /^I select "(.*)" from "(.*)"$/ do |value, field|
21
- select(value, :from => field)
21
+ select(value, :from => field)
22
22
  end
23
23
 
24
24
  When /^I check "(.*)"$/ do |field|
25
- check(field)
25
+ check(field)
26
26
  end
27
27
 
28
28
  When /^I uncheck "(.*)"$/ do |field|
29
- uncheck(field)
29
+ uncheck(field)
30
30
  end
31
31
 
32
32
  When /^I choose "(.*)"$/ do |field|
@@ -32,6 +32,7 @@ class SinatraWorld
32
32
  use Visage::Profiles
33
33
  use Visage::Builder
34
34
  use Visage::JSON
35
+ use Visage::Meta
35
36
  run Sinatra::Application
36
37
  end
37
38
  end
@@ -14,6 +14,7 @@ class CollectdJSON
14
14
 
15
15
  def initialize(opts={})
16
16
  @rrddir = opts[:rrddir] || CollectdJSON.rrddir
17
+ @types = opts[:types] || CollectdJSON.types
17
18
  end
18
19
 
19
20
  # Entry point.
@@ -22,24 +23,40 @@ class CollectdJSON
22
23
  plugin = opts[:plugin]
23
24
  plugin_instances = opts[:plugin_instances][/\w.*/]
24
25
  instances = plugin_instances.blank? ? '*' : '{' + plugin_instances.split('/').join(',') + '}'
25
- @plugin_names = []
26
+ rrdglob = "#{@rrddir}/#{host}/#{plugin}/#{instances}.rrd"
27
+
28
+ start = case
29
+ when opts[:start] && opts[:start].index('.')
30
+ opts[:start].split('.').first
31
+ when opts[:start]
32
+ opts[:start]
33
+ else
34
+ (Time.now - 3600).to_i
35
+ end
26
36
 
27
- rrdglob = "#{@rrddir}/#{host}/#{plugin}/#{instances}.rrd"
28
- plugin_offset = @rrddir.size + 1 + host.size + 1
37
+ finish = case
38
+ when opts[:finish] && opts[:finish].index('.')
39
+ opts[:finish].split('.').first
40
+ when opts[:finish]
41
+ opts[:finish]
42
+ else
43
+ Time.now.to_i
44
+ end
29
45
 
30
46
  data = []
31
47
 
32
48
  Dir.glob(rrdglob).map do |rrdname|
33
49
  parts = rrdname.gsub(/#{@rrddir}\//, '').split('/')
34
- host = parts[0]
50
+ host_name = parts[0]
35
51
  plugin_name = parts[1]
36
- instance_name = parts[2].split('.').first
52
+ instance_name = File.basename(parts[2], '.rrd')
37
53
  rrd = Errand.new(:filename => rrdname)
38
54
 
55
+
39
56
  data << { :plugin => plugin_name, :instance => instance_name,
40
- :host => host,
41
- :start => opts[:start] || (Time.now - 3600).to_i,
42
- :finish => opts[:finish] || Time.now.to_i,
57
+ :host => host_name,
58
+ :start => start,
59
+ :finish => finish,
43
60
  :rrd => rrd }
44
61
  end
45
62
 
@@ -105,6 +122,10 @@ class CollectdJSON
105
122
  @rrddir ||= Visage::Config.rrddir
106
123
  end
107
124
 
125
+ def types
126
+ @types ||= Visage::Config.types
127
+ end
128
+
108
129
  def hosts
109
130
  if @rrddir
110
131
  Dir.glob("#{@rrddir}/*").map {|e| e.split('/').last }.sort
@@ -31,7 +31,6 @@ module Visage
31
31
  when opts[:metrics].blank?
32
32
  glob = "*/*"
33
33
  when opts[:metrics] =~ /,/
34
- puts "\n" * 4
35
34
  glob = "{" + opts[:metrics].split(/\s*,\s*/).map { |m|
36
35
  m =~ /\// ? m : ["*/#{m}", "#{m}/*"]
37
36
  }.join(',').gsub(/,$/, '') + "}"
@@ -19,7 +19,7 @@ module Visage
19
19
  end
20
20
 
21
21
  def self.load(filename, opts={})
22
- unless path = self.find(filename, opts)
22
+ if not path = self.find(filename, opts)
23
23
  if opts[:create]
24
24
  path = @@config_directories.first.join(filename)
25
25
  begin
@@ -41,7 +41,7 @@ module Visage
41
41
  end
42
42
 
43
43
  def initialize(filename, opts={})
44
- unless ::File.exists?(filename)
44
+ if not ::File.exists?(filename)
45
45
  path = @@config_directories.first.join(filename)
46
46
  FileUtils.touch(path)
47
47
  end
@@ -10,4 +10,5 @@ require 'lib/visage-app'
10
10
  use Visage::Profiles
11
11
  use Visage::Builder
12
12
  use Visage::JSON
13
+ use Visage::Meta
13
14
  run Sinatra::Base
@@ -1,10 +1,10 @@
1
- # extracted from Extlib.
1
+ # extracted from Extlib.
2
2
  # FIXME: what's the licensing here?
3
3
  class String
4
- def camel_case
5
- return self if self !~ /_/ && self =~ /[A-Z]+.*/
6
- split('_').map{|e| e.capitalize}.join
7
- end
4
+ def camel_case
5
+ return self if self !~ /_/ && self =~ /[A-Z]+.*/
6
+ split('_').map{|e| e.capitalize}.join
7
+ end
8
8
 
9
9
  def blank?
10
10
  strip.empty?
@@ -24,7 +24,7 @@ module Visage
24
24
  def self.all(opts={})
25
25
  sort = opts[:sort]
26
26
  profiles = self.load
27
- profiles = sort == "name" ? profiles.sort.map {|i| i.last } : profiles.values
27
+ profiles = sort == "name" ? profiles.sort_by {|k,v| v[:profile_name]}.map {|i| i.last } : profiles.values
28
28
  profiles.map { |prof| self.new(prof) }
29
29
  end
30
30
 
@@ -185,10 +185,10 @@ var visageBase = new Class({
185
185
  },
186
186
  getData: function() {
187
187
  this.request = new Request.JSONP({
188
- url: this.dataURL(),
189
- data: this.requestData,
190
- secure: this.options.secureJSON,
191
- method: this.options.httpMethod,
188
+ url: this.dataURL(),
189
+ data: this.requestData,
190
+ secure: this.options.secureJSON,
191
+ method: this.options.httpMethod,
192
192
  onComplete: function(json) {
193
193
  this.graphData(json);
194
194
  }.bind(this),
@@ -228,13 +228,33 @@ var visageGraph = new Class({
228
228
  graphData: function(data) {
229
229
  this.response = data
230
230
  this.buildDataStructures()
231
+ this.lastStart = this.series[0].data[0][0]
232
+ this.lastFinish = this.series[0].data.getLast()[0]
233
+
234
+ switch(true) {
235
+ case $defined(this.chart) && this.requestData['live']:
236
+ this.series.each(function(series, index) {
237
+ var point = series.data[1];
238
+ this.chart.series[index].addPoint(point, false);
239
+ }, this);
240
+ this.chart.redraw();
241
+ break;
242
+ case $defined(this.chart):
243
+ this.series.each(function(series, index) {
244
+ this.chart.series[index].setData(series.data, false);
245
+ }, this);
246
+
247
+ /* Reset the zoom */
248
+ this.chart.toolbar.remove('zoom');
249
+ this.chart.xAxis.concat(this.chart.yAxis).each(function(axis) {
250
+ axis.setExtremes(null,null,false);
251
+ });
231
252
 
232
- if ( $defined(this.chart) ) {
233
- this.series.each(function(series, index) {
234
- this.chart.series[index].setData(series.data, true)
235
- }, this);
236
- } else {
237
- this.drawChart()
253
+ this.chart.redraw();
254
+ break;
255
+ default:
256
+ this.drawChart()
257
+ break;
238
258
  }
239
259
  },
240
260
  buildDataStructures: function (data) {
@@ -294,6 +314,10 @@ var visageGraph = new Class({
294
314
  load: function(e) {
295
315
  setInterval(function() {
296
316
  if (this.options.live) {
317
+ var data = { 'start': this.lastFinish,
318
+ 'finish': this.lastFinish + 10,
319
+ 'live': true };
320
+ this.requestData = data;
297
321
  this.getData()
298
322
  }
299
323
  }.bind(this), 10000);
@@ -411,11 +435,10 @@ var visageGraph = new Class({
411
435
  'submit': function(e, foo) {
412
436
  e.stop();
413
437
  e.target.getElement('select').getSelected().each(function(option) {
414
- data = new Hash()
415
- split = option.value.split('=')
416
- data.set(split[0], split[1])
438
+ value = parseInt(option.value.split('=')[1])
439
+ data = { 'start': value }
417
440
  });
418
- this.requestData = data
441
+ this.requestData = data;
419
442
 
420
443
  /* Draw everything again. */
421
444
  this.getData();
@@ -446,6 +469,7 @@ var visageGraph = new Class({
446
469
  'type': 'checkbox',
447
470
  'id': this.parentElement + '-live',
448
471
  'name': 'live',
472
+ 'checked': this.options.live,
449
473
  'events': {
450
474
  'click': function() {
451
475
  this.options.live = !this.options.live
@@ -478,12 +502,15 @@ var visageGraph = new Class({
478
502
  },
479
503
  'events': {
480
504
  'mouseover': function(e) {
481
- var url = e.target.get('href')
482
- var options = this.requestData.toQueryString()
483
-
484
- if ( options != '' && ! url.contains('?') ) {
485
- url += '?' + options
486
- }
505
+ e.stop();
506
+ var url = e.target.get('href'),
507
+ extremes = this.chart.xAxis[0].getExtremes(),
508
+ options = { 'start': extremes.dataMin,
509
+ 'finish': parseInt(extremes.dataMax) };
510
+
511
+ var options = new Hash(options).toQueryString(),
512
+ baseurl = this.dataURL(),
513
+ url = baseurl += '?' + options;
487
514
 
488
515
  e.target.set('href', url)
489
516
  }.bind(this)
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Visage
4
+ class Types
5
+ attr_reader :types
6
+
7
+ def initialize(opts={})
8
+ @filename = opts[:filename] || "/usr/share/collectd/types.db"
9
+ @types = []
10
+ build
11
+ end
12
+
13
+ def to_json
14
+ @types.to_json
15
+ end
16
+
17
+ private
18
+ def build
19
+ file = File.new(@filename)
20
+ file.each_line do |line|
21
+ next if line =~ /^#/
22
+ next if line =~ /^\s*$/
23
+ attrs = {}
24
+ spec = line.strip.split(/\t+|,\s+/)
25
+ dataset = spec.shift
26
+ spec.each do |source|
27
+ parts = source.split(':')
28
+ @types << { "dataset" => dataset,
29
+ "datasource" => parts[0],
30
+ "type" => parts[1],
31
+ "min" => parts[2],
32
+ "max" => parts[3] }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -16,6 +16,7 @@
16
16
  pluginInstance: '#{instance}',
17
17
  start: '#{@start}',
18
18
  finish: '#{@finish}',
19
+ live: #{@live},
19
20
  #{ "baseurl: '" + ENV['BASE_URL'].gsub(/^\//, '') if ENV['BASE_URL'] }
20
21
  });
21
22
  });
data/lib/visage-app.rb CHANGED
@@ -12,6 +12,7 @@ require 'lib/visage-app/config'
12
12
  require 'lib/visage-app/config/file'
13
13
  require 'lib/visage-app/collectd/rrds'
14
14
  require 'lib/visage-app/collectd/json'
15
+ require 'lib/visage-app/types'
15
16
  require 'yajl/json_gem'
16
17
 
17
18
  module Visage
@@ -27,7 +28,11 @@ module Visage
27
28
  Visage::Config.use do |c|
28
29
  # FIXME: make this configurable through file
29
30
  c['rrddir'] = ENV["RRDDIR"] ? Pathname.new(ENV["RRDDIR"]).expand_path : Pathname.new("/var/lib/collectd/rrd").expand_path
31
+ c['types'] = ENV["TYPES"] ? Visage::Types.new(:filename => ENV["TYPES"]) : Visage::Types.new
30
32
  end
33
+
34
+ # Load up the profile.yaml. Creates it if it doesn't already exist.
35
+ Visage::Profile.load
31
36
  end
32
37
  end
33
38
 
@@ -41,6 +46,7 @@ module Visage
41
46
  raise Sinatra::NotFound unless @profile
42
47
  @start = params[:start]
43
48
  @finish = params[:finish]
49
+ @live = params[:live] ? true : false
44
50
  haml :profile
45
51
  end
46
52
 
@@ -83,19 +89,27 @@ module Visage
83
89
  class JSON < Application
84
90
 
85
91
  # JSON data backend
92
+ mime_type :json, "application/json"
93
+ mime_type :jsonp, "text/javascript"
94
+
95
+ before do
96
+ content_type :jsonp
97
+ end
86
98
 
87
99
  # /data/:host/:plugin/:optional_plugin_instance
88
100
  get %r{/data/([^/]+)/([^/]+)((/[^/]+)*)} do
89
101
  host = params[:captures][0].gsub("\0", "")
90
102
  plugin = params[:captures][1].gsub("\0", "")
91
103
  plugin_instances = params[:captures][2].gsub("\0", "")
104
+ start = params[:start]
105
+ finish = params[:finish]
92
106
 
93
107
  collectd = CollectdJSON.new(:rrddir => Visage::Config.rrddir)
94
- json = collectd.json(:host => host,
95
- :plugin => plugin,
108
+ json = collectd.json(:host => host,
109
+ :plugin => plugin,
96
110
  :plugin_instances => plugin_instances,
97
- :start => params[:start],
98
- :finish => params[:finish])
111
+ :start => start,
112
+ :finish => finish)
99
113
  # if the request is cross-domain, we need to serve JSONP
100
114
  maybe_wrap_with_callback(json)
101
115
  end
@@ -120,4 +134,10 @@ module Visage
120
134
  end
121
135
 
122
136
  end
137
+
138
+ class Meta < Application
139
+ get '/meta/types' do
140
+ Visage::Config.types.to_json
141
+ end
142
+ end
123
143
  end
data/visage-app.gemspec CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{visage-app}
8
- s.version = "0.9.3"
8
+ s.version = "0.9.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Lindsay Holmwood"]
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 9
8
- - 4
9
- version: 0.9.4
8
+ - 5
9
+ version: 0.9.5
10
10
  platform: ruby
11
11
  authors:
12
12
  - Lindsay Holmwood
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-03-03 00:00:00 +11:00
17
+ date: 2011-05-08 00:00:00 +10:00
18
18
  default_executable: visage-app
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -230,6 +230,7 @@ extra_rdoc_files:
230
230
  files:
231
231
  - AUTHORS
232
232
  - Gemfile
233
+ - Gemfile.lock
233
234
  - LICENCE
234
235
  - README.md
235
236
  - Rakefile
@@ -239,6 +240,7 @@ files:
239
240
  - features/cli.feature
240
241
  - features/data/config/with_no_profiles/.stub
241
242
  - features/json.feature
243
+ - features/meta.feature
242
244
  - features/profiles.feature
243
245
  - features/step_definitions/form_steps.rb
244
246
  - features/step_definitions/json_steps.rb
@@ -272,6 +274,7 @@ files:
272
274
  - lib/visage-app/public/javascripts/mootools-1.2.3-core.js
273
275
  - lib/visage-app/public/javascripts/mootools-1.2.5.1-more.js
274
276
  - lib/visage-app/public/stylesheets/screen.css
277
+ - lib/visage-app/types.rb
275
278
  - lib/visage-app/views/builder.haml
276
279
  - lib/visage-app/views/layout.haml
277
280
  - lib/visage-app/views/profile.haml