visage-app 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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