visage-app 1.0.0 → 2.0.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 (42) hide show
  1. data/.gitignore +10 -0
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile +1 -15
  4. data/Gemfile.lock +44 -42
  5. data/README.md +123 -49
  6. data/Rakefile +16 -26
  7. data/bin/visage-app +17 -4
  8. data/features/cli.feature +10 -3
  9. data/features/json.feature +37 -0
  10. data/features/step_definitions/{visage_steps.rb → cli_steps.rb} +1 -1
  11. data/features/step_definitions/json_steps.rb +50 -8
  12. data/features/step_definitions/site_steps.rb +1 -1
  13. data/features/support/config/default/profiles.yaml +335 -0
  14. data/features/{data → support}/config/with_no_profiles/.stub +0 -0
  15. data/features/support/config/with_no_profiles/profiles.yaml +0 -0
  16. data/features/support/config/with_old_profile_yaml/profiles.yaml +116 -0
  17. data/features/support/env.rb +2 -3
  18. data/lib/visage-app.rb +35 -25
  19. data/lib/visage-app/collectd/json.rb +115 -118
  20. data/lib/visage-app/collectd/rrds.rb +25 -19
  21. data/lib/visage-app/helpers.rb +17 -0
  22. data/lib/visage-app/profile.rb +18 -25
  23. data/lib/visage-app/public/images/caution.png +0 -0
  24. data/lib/visage-app/public/images/ok.png +0 -0
  25. data/lib/visage-app/public/images/questions.png +0 -0
  26. data/lib/visage-app/public/javascripts/builder.js +607 -0
  27. data/lib/visage-app/public/javascripts/graph.js +179 -142
  28. data/lib/visage-app/public/javascripts/message.js +520 -0
  29. data/lib/visage-app/public/javascripts/mootools-core-1.4.0-full-compat.js +6285 -0
  30. data/lib/visage-app/public/javascripts/mootools-more-1.4.0.1.js +6399 -0
  31. data/lib/visage-app/public/stylesheets/message.css +61 -0
  32. data/lib/visage-app/public/stylesheets/screen.css +149 -38
  33. data/lib/visage-app/version.rb +5 -0
  34. data/lib/visage-app/views/builder.haml +38 -49
  35. data/lib/visage-app/views/builder_form.haml +14 -0
  36. data/lib/visage-app/views/layout.haml +5 -2
  37. data/lib/visage-app/views/profile.haml +44 -25
  38. data/visage-app.gemspec +29 -132
  39. metadata +93 -150
  40. data/VERSION +0 -1
  41. data/features/builder.feature +0 -16
  42. data/lib/visage-app/collectd/profile.rb +0 -36
@@ -0,0 +1,116 @@
1
+ ---
2
+ zend+tail+on+ubuntu:
3
+ :metrics: tail*/*
4
+ :hosts: ubuntu.localdomain
5
+ :url: zend+tail+on+ubuntu
6
+ :profile_name: zend tail on ubuntu
7
+ load+on+ubuntu+localdomain:
8
+ :metrics: load/*
9
+ :hosts: ubuntu.localdomain
10
+ :url: load+on+ubuntu+localdomain
11
+ :profile_name: load on ubuntu.localdomain
12
+ apache+on+blah:
13
+ :metrics: apache/*
14
+ :hosts: blah
15
+ :url: apache+on+blah
16
+ :profile_name: apache on blah
17
+ interfaces+on+blah:
18
+ :metrics: interface/*
19
+ :hosts: blah
20
+ :url: interfaces+on+blah
21
+ :profile_name: interfaces on blah
22
+ all+on+all:
23
+ :metrics: "*"
24
+ :hosts: "*"
25
+ :url: all+on+all
26
+ :profile_name: all on all
27
+ object+space+on+ree:
28
+ :metrics: curl_json-object_space/*
29
+ :hosts: ubuntu*
30
+ :url: object+space+on+ree
31
+ :profile_name: Object space on REE
32
+ disk+sda+on+ubuntu:
33
+ :metrics: disk-sda/*
34
+ :hosts: ubuntu.localdomain
35
+ :url: disk+sda+on+ubuntu
36
+ :profile_name: disk-sda on ubuntu
37
+ gc+on+ree:
38
+ :metrics: curl_json-gc/*
39
+ :hosts: ubuntu*
40
+ :url: gc+on+ree
41
+ :profile_name: GC on REE
42
+ gateway+ping+from+blah:
43
+ :metrics: ping/*
44
+ :hosts: blah
45
+ :url: gateway+ping+from+blah
46
+ :profile_name: gateway ping from blah
47
+ tcpconns+on+ubuntu:
48
+ :metrics: tcpconns*/*
49
+ :hosts: ubuntu*
50
+ :url: tcpconns+on+ubuntu
51
+ :profile_name: tcpconns on ubuntu
52
+ entropy+on+blah:
53
+ :metrics: entropy/*
54
+ :hosts: blah
55
+ :url: entropy+on+blah
56
+ :profile_name: entropy on blah
57
+ memory+on+ubunttu:
58
+ :metrics: memory/*
59
+ :hosts: ubuntu*
60
+ :url: memory+on+ubunttu
61
+ :profile_name: memory on ubunttu
62
+ processes+on+blah:
63
+ :metrics: processes/*
64
+ :hosts: blah
65
+ :url: processes+on+blah
66
+ :profile_name: processes on blah
67
+ ruby+gc+on+ubuntu+localdomain:
68
+ :metrics: curl_json-gc/*
69
+ :hosts: ubuntu*
70
+ :url: ruby+gc+on+ubuntu+localdomain
71
+ :profile_name: Ruby GC on ubuntu.localdomain
72
+ cpu+statistics+for+ubuntu+localdomain:
73
+ :metrics: cpu*/*
74
+ :hosts: ubuntu*
75
+ :url: cpu+statistics+for+ubuntu+localdomain
76
+ :profile_name: CPU statistics for ubuntu.localdomain
77
+ swap+on+blah:
78
+ :metrics: swap/*
79
+ :hosts: blah
80
+ :url: swap+on+blah
81
+ :profile_name: swap on blah
82
+ cpu+and+load+on+ubuntu:
83
+ :metrics: cpu*/*,load/*
84
+ :hosts: ubuntu*
85
+ :url: cpu+and+load+on+ubuntu
86
+ :profile_name: cpu and load on ubuntu
87
+ cpu+on+flapjack+workers:
88
+ :metrics: cpu*/*
89
+ :hosts: flapjack-*
90
+ :url: cpu+on+flapjack+workers
91
+ :profile_name: cpu on flapjack workers
92
+ memory+on+flapjack+workers+and+blah:
93
+ :metrics: memory/*
94
+ :hosts: flapjack-worker*,blah
95
+ :url: memory+on+flapjack+workers+and+blah
96
+ :profile_name: memory on flapjack workers and blah
97
+ uptime+on+blah:
98
+ :metrics: uptime/*
99
+ :hosts: blah
100
+ :url: uptime+on+blah
101
+ :profile_name: uptime on blah
102
+ users+on+blah:
103
+ :metrics: users/*
104
+ :hosts: blah
105
+ :url: users+on+blah
106
+ :profile_name: users on blah
107
+ irqs+on+blah:
108
+ :metrics: irq/*
109
+ :hosts: blah
110
+ :url: irqs+on+blah
111
+ :profile_name: irqs on blah
112
+ vmem+on+blah:
113
+ :metrics: vmem/*
114
+ :hosts: blah
115
+ :url: vmem+on+blah
116
+ :profile_name: vmem on blah
@@ -1,16 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'rubygems'
3
4
  require 'pathname'
4
5
 
5
6
  @root = Pathname.new(File.dirname(__FILE__)).parent.parent.expand_path
6
7
  app_file = @root.join('lib/visage-app')
7
8
 
8
- require 'rubygems'
9
- require 'spec/expectations'
10
9
  require 'rack/test'
11
10
  require 'webrat'
12
11
 
13
- ENV['CONFIG_PATH'] = @root.join('features/data/config/default')
12
+ ENV['CONFIG_PATH'] = @root.join('features/support/config/default')
14
13
 
15
14
  require app_file
16
15
  # Force the application name because polyglot breaks the auto-detection logic.
@@ -18,20 +18,21 @@ require 'yajl/json_gem'
18
18
  module Visage
19
19
  class Application < Sinatra::Base
20
20
  @root = Pathname.new(File.dirname(__FILE__)).parent.expand_path
21
- set :public, @root.join('lib/visage-app/public')
22
- set :views, @root.join('lib/visage-app/views')
21
+ set :public_folder, @root.join('lib/visage-app/public')
22
+ set :views, @root.join('lib/visage-app/views')
23
23
 
24
24
  helpers Sinatra::LinkToHelper
25
25
  helpers Sinatra::PageTitleHelper
26
+ helpers Sinatra::RequireJSHelper
26
27
 
27
28
  configure do
28
29
  Visage::Config.use do |c|
29
30
  # FIXME: make this configurable through file
30
31
  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
32
+ c['types'] = ENV["TYPES"] ? Visage::Types.new(:filename => ENV["TYPES"]) : Visage::Types.new
32
33
  end
33
34
 
34
- # Load up the profile.yaml. Creates it if it doesn't already exist.
35
+ # Load up the profiles.yaml. Creates it if it doesn't already exist.
35
36
  Visage::Profile.load
36
37
  end
37
38
  end
@@ -44,9 +45,6 @@ module Visage
44
45
  get '/profiles/:url' do
45
46
  @profile = Visage::Profile.get(params[:url])
46
47
  raise Sinatra::NotFound unless @profile
47
- @start = params[:start]
48
- @finish = params[:finish]
49
- @live = params[:live] ? true : false
50
48
  haml :profile
51
49
  end
52
50
 
@@ -59,6 +57,17 @@ module Visage
59
57
 
60
58
  class Builder < Application
61
59
 
60
+ post '/builder' do
61
+ @profile = Visage::Profile.new(params)
62
+
63
+ if @profile.save
64
+ {'status' => 'ok'}.to_json
65
+ else
66
+ status 400 # Bad Request
67
+ {'status' => 'error', 'errors' => @profile.errors}.to_json
68
+ end
69
+ end
70
+
62
71
  get "/builder" do
63
72
  if params[:submit] == "create"
64
73
  @profile = Visage::Profile.new(params)
@@ -75,7 +84,7 @@ module Visage
75
84
  end
76
85
  end
77
86
 
78
- # infrastructure for embedding
87
+ # Infrastructure for embedding.
79
88
  get '/javascripts/visage.js' do
80
89
  javascript = ""
81
90
  %w{raphael-min g.raphael g.line mootools-1.2.3-core mootools-1.2.3.1-more graph}.each do |js|
@@ -98,27 +107,28 @@ module Visage
98
107
 
99
108
  # /data/:host/:plugin/:optional_plugin_instance
100
109
  get %r{/data/([^/]+)/([^/]+)((/[^/]+)*)} do
101
- host = params[:captures][0].gsub("\0", "")
102
- plugin = params[:captures][1].gsub("\0", "")
103
- plugin_instances = params[:captures][2].gsub("\0", "")
104
- start = params[:start]
105
- finish = params[:finish]
106
-
107
- collectd = CollectdJSON.new(:rrddir => Visage::Config.rrddir)
108
- json = collectd.json(:host => host,
109
- :plugin => plugin,
110
- :plugin_instances => plugin_instances,
111
- :start => start,
112
- :finish => finish)
113
- # if the request is cross-domain, we need to serve JSONP
110
+ host = params[:captures][0].gsub("\0", "")
111
+ plugin = params[:captures][1].gsub("\0", "")
112
+ instances = params[:captures][2].gsub("\0", "")
113
+ start = params[:start]
114
+ finish = params[:finish]
115
+
116
+ collectd = Visage::Collectd::JSON.new(:rrddir => Visage::Config.rrddir)
117
+ json = collectd.json(:host => host,
118
+ :plugin => plugin,
119
+ :instances => instances,
120
+ :start => start,
121
+ :finish => finish)
122
+
123
+ # If the request is cross-domain, we need to serve JSON-P.
114
124
  maybe_wrap_with_callback(json)
115
125
  end
116
126
 
117
127
  get %r{/data/([^/]+)} do
118
- host = params[:captures][0].gsub("\0", "")
119
- metrics = Visage::Collectd::RRDs.metrics(:host => host)
128
+ hosts = params[:captures][0].gsub("\0", "")
129
+ metrics = Visage::Collectd::RRDs.metrics(:hosts => hosts)
120
130
 
121
- json = { host => metrics }.to_json
131
+ json = { :metrics => metrics }.to_json
122
132
  maybe_wrap_with_callback(json)
123
133
  end
124
134
 
@@ -128,7 +138,7 @@ module Visage
128
138
  maybe_wrap_with_callback(json)
129
139
  end
130
140
 
131
- # wraps json with a callback method that JSONP clients can call
141
+ # Wraps json with a callback method that JSON-P clients can call.
132
142
  def maybe_wrap_with_callback(json)
133
143
  params[:callback] ? params[:callback] + '(' + json + ')' : json
134
144
  end
@@ -10,133 +10,130 @@ require 'yajl'
10
10
  #
11
11
  # A loose shim onto RRDtool, with some extra logic to normalise the data.
12
12
  #
13
- class CollectdJSON
14
-
15
- def initialize(opts={})
16
- @rrddir = opts[:rrddir] || CollectdJSON.rrddir
17
- @types = opts[:types] || CollectdJSON.types
18
- end
19
-
20
- # Entry point.
21
- def json(opts={})
22
- host = opts[:host]
23
- plugin = opts[:plugin]
24
- plugin_instances = opts[:plugin_instances][/\w.*/]
25
- instances = plugin_instances.blank? ? '*' : '{' + plugin_instances.split('/').join(',') + '}'
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
36
-
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
45
-
46
- data = []
47
-
48
- Dir.glob(rrdglob).map do |rrdname|
49
- parts = rrdname.gsub(/#{@rrddir}\//, '').split('/')
50
- host_name = parts[0]
51
- plugin_name = parts[1]
52
- instance_name = File.basename(parts[2], '.rrd')
53
- rrd = Errand.new(:filename => rrdname)
54
-
55
-
56
- data << { :plugin => plugin_name, :instance => instance_name,
57
- :host => host_name,
58
- :start => start,
59
- :finish => finish,
60
- :rrd => rrd }
61
- end
62
-
63
- encode(data)
64
- end
65
-
66
- private
67
- # Attempt to structure the JSON reasonably sanely, so the consumer (i.e. a
68
- # browser) doesn't have to do a lot of computationally expensive work.
69
- def encode(datas)
70
-
71
- structure = {}
72
- datas.each do |data|
73
- fetch = data[:rrd].fetch(:function => "AVERAGE",
74
- :start => data[:start],
75
- :finish => data[:finish])
76
- rrd_data = fetch[:data]
77
-
78
- # A single rrd can have multiple data sets (multiple metrics within
79
- # the same file). Separate the metrics.
80
- rrd_data.each_pair do |source, metric|
81
-
82
- # Filter out NaNs and weirdly massive values so yajl doesn't choke
83
- metric.map! do |datapoint|
84
- case
85
- when datapoint && datapoint.nan?
86
- @tripped = true
87
- @last_valid
88
- when @tripped
89
- @last_valid
90
- else
91
- @last_valid = datapoint
92
- end
93
- end
13
+ module Visage
14
+ module Collectd
15
+ class JSON
94
16
 
95
- # Last value is always wack. Set to 0, so the timescale isn't off by 1.
96
- metric[-1] = 0.0
97
- host = data[:host]
98
- plugin = data[:plugin]
99
- instance = data[:instance]
100
- start = data[:start].to_i
101
- finish = data[:finish].to_i
102
-
103
- structure[host] ||= {}
104
- structure[host][plugin] ||= {}
105
- structure[host][plugin][instance] ||= {}
106
- structure[host][plugin][instance][source] ||= {}
107
- structure[host][plugin][instance][source][:start] ||= start
108
- structure[host][plugin][instance][source][:finish] ||= finish
109
- structure[host][plugin][instance][source][:data] ||= metric
17
+ def initialize(opts={})
18
+ @rrddir = opts[:rrddir] || Visage::Collectd::JSON.rrddir
19
+ @types = opts[:types] || Visage::Collectd::JSON.types
20
+ end
110
21
 
22
+ def parse_time(time, opts={})
23
+ case
24
+ when time && time.index('.')
25
+ time.split('.').first.to_i
26
+ when time
27
+ time.to_i
28
+ else
29
+ opts[:default] || Time.now.to_i
30
+ end
111
31
  end
112
- end
113
32
 
114
- encoder = Yajl::Encoder.new
115
- encoder.encode(structure)
116
- end
33
+ # Entry point.
34
+ def json(opts={})
35
+ host = opts[:host]
36
+ plugin = opts[:plugin]
37
+ instances = opts[:instances][/\w.*/]
38
+ instances = instances.blank? ? '*' : '{' + instances.split('/').join(',') + '}'
39
+ rrdglob = "#{@rrddir}/#{host}/#{plugin}/#{instances}.rrd"
40
+ finish = parse_time(opts[:finish])
41
+ start = parse_time(opts[:start], :default => (finish - 3600 || (Time.now - 3600).to_i))
42
+ data = []
43
+
44
+ Dir.glob(rrdglob).map do |rrdname|
45
+ parts = rrdname.gsub(/#{@rrddir}\//, '').split('/')
46
+ host_name = parts[0]
47
+ plugin_name = parts[1]
48
+ instance_name = File.basename(parts[2], '.rrd')
49
+ rrd = Errand.new(:filename => rrdname)
50
+
51
+ data << { :plugin => plugin_name, :instance => instance_name,
52
+ :host => host_name,
53
+ :start => start,
54
+ :finish => finish,
55
+ :rrd => rrd }
56
+ end
117
57
 
118
- class << self
119
- attr_writer :rrddir
58
+ encode(data)
59
+ end
120
60
 
121
- def rrddir
122
- @rrddir ||= Visage::Config.rrddir
123
- end
61
+ private
62
+ # Attempt to structure the JSON reasonably sanely, so the consumer (i.e. a
63
+ # browser) doesn't have to do a lot of computationally expensive work.
64
+ def encode(datas)
65
+
66
+ structure = {}
67
+ datas.each do |data|
68
+ fetch = data[:rrd].fetch(:function => "AVERAGE",
69
+ :start => data[:start],
70
+ :finish => data[:finish])
71
+ rrd_data = fetch[:data]
72
+
73
+ # A single rrd can have multiple data sets (multiple metrics within
74
+ # the same file). Separate the metrics.
75
+ rrd_data.each_pair do |source, metric|
76
+
77
+ # Filter out NaNs and weirdly massive values so yajl doesn't choke
78
+ metric.map! do |datapoint|
79
+ case
80
+ when datapoint && datapoint.nan?
81
+ @tripped = true
82
+ @last_valid
83
+ when @tripped
84
+ @last_valid
85
+ else
86
+ @last_valid = datapoint
87
+ end
88
+ end
89
+
90
+ # Last value is always wack. Set to 0, so the timescale isn't off by 1.
91
+ metric[-1] = 0.0
92
+ host = data[:host]
93
+ plugin = data[:plugin]
94
+ instance = data[:instance]
95
+ start = data[:start].to_i
96
+ finish = data[:finish].to_i
97
+
98
+ structure[host] ||= {}
99
+ structure[host][plugin] ||= {}
100
+ structure[host][plugin][instance] ||= {}
101
+ structure[host][plugin][instance][source] ||= {}
102
+ structure[host][plugin][instance][source][:start] ||= start
103
+ structure[host][plugin][instance][source][:finish] ||= finish
104
+ structure[host][plugin][instance][source][:data] ||= metric
124
105
 
125
- def types
126
- @types ||= Visage::Config.types
127
- end
106
+ end
107
+ end
128
108
 
129
- def hosts
130
- if @rrddir
131
- Dir.glob("#{@rrddir}/*").map {|e| e.split('/').last }.sort
109
+ encoder = Yajl::Encoder.new
110
+ encoder.encode(structure)
132
111
  end
133
- end
134
112
 
135
- def plugins(opts={})
136
- host = opts[:host] || '*'
137
- Dir.glob("#{@rrddir}/#{host}/*").map {|e| e.split('/').last }.sort
138
- end
113
+ class << self
114
+ attr_writer :rrddir
115
+
116
+ def rrddir
117
+ @rrddir ||= Visage::Config.rrddir
118
+ end
119
+
120
+ def types
121
+ @types ||= Visage::Config.types
122
+ end
139
123
 
140
- end
124
+ def hosts
125
+ if @rrddir
126
+ Dir.glob("#{@rrddir}/*").map {|e| e.split('/').last }.sort
127
+ end
128
+ end
129
+
130
+ def plugins(opts={})
131
+ host = opts[:host] || '*'
132
+ Dir.glob("#{@rrddir}/#{host}/*").map {|e| e.split('/').last }.sort
133
+ end
134
+
135
+ end
141
136
 
142
- end
137
+ end # class JSON
138
+ end # module Collectd
139
+ end # module Visage