visage-app 0.9.4 → 0.9.5

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