fnordmetric 1.0.1 → 1.2.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 (71) hide show
  1. data/Rakefile +1 -1
  2. data/fnordmetric.gemspec +1 -0
  3. data/lib/fnordmetric.rb +6 -13
  4. data/lib/fnordmetric/acceptors/acceptor.rb +20 -7
  5. data/lib/fnordmetric/acceptors/amqp_acceptor.rb +56 -0
  6. data/lib/fnordmetric/acceptors/fyrehose_acceptor.rb +43 -0
  7. data/lib/fnordmetric/acceptors/stomp_acceptor.rb +71 -0
  8. data/lib/fnordmetric/acceptors/tcp_acceptor.rb +1 -0
  9. data/lib/fnordmetric/acceptors/udp_acceptor.rb +2 -1
  10. data/lib/fnordmetric/context.rb +21 -9
  11. data/lib/fnordmetric/defaults.rb +16 -3
  12. data/lib/fnordmetric/gauge.rb +8 -3
  13. data/lib/fnordmetric/gauge_calculations.rb +8 -2
  14. data/lib/fnordmetric/gauge_modifiers.rb +5 -4
  15. data/lib/fnordmetric/gauges/server_health_gauge.rb +13 -0
  16. data/lib/fnordmetric/namespace.rb +53 -17
  17. data/lib/fnordmetric/util.rb +25 -0
  18. data/lib/fnordmetric/version.rb +1 -1
  19. data/lib/fnordmetric/web/app.rb +7 -66
  20. data/lib/fnordmetric/web/reactor.rb +32 -2
  21. data/lib/fnordmetric/web/websocket.rb +1 -1
  22. data/lib/fnordmetric/widgets/bars_widget.rb +1 -1
  23. data/lib/fnordmetric/widgets/numbers_widget.rb +28 -4
  24. data/lib/fnordmetric/widgets/timeseries_widget.rb +19 -9
  25. data/lib/fnordmetric/widgets/toplist_widget.rb +8 -4
  26. data/lib/fnordmetric/worker.rb +5 -1
  27. data/lib/fnordmetric/zero_config_gauge.rb +138 -0
  28. data/spec/context_spec.rb +4 -4
  29. data/spec/event_spec.rb +11 -11
  30. data/spec/gauge_modifiers_spec.rb +135 -29
  31. data/spec/gauge_spec.rb +7 -2
  32. data/spec/namespace_spec.rb +8 -19
  33. data/spec/util_spec.rb +46 -0
  34. data/web/.gitignore +4 -0
  35. data/web/build.sh +34 -0
  36. data/web/{fnordmetric.css → css/fnordmetric.core.css} +121 -58
  37. data/web/haml/app.haml +4 -22
  38. data/web/{loader.gif → img/loader.gif} +0 -0
  39. data/web/js/fnordmetric.bars_widget.js +3 -3
  40. data/web/js/fnordmetric.dashboard_view.js +1 -1
  41. data/web/js/fnordmetric.gauge_explorer.js +173 -0
  42. data/web/js/fnordmetric.js +93 -33
  43. data/web/js/fnordmetric.numbers_widget.js +15 -14
  44. data/web/js/fnordmetric.session_view.js +0 -1
  45. data/web/js/fnordmetric.timeline_widget.js +3 -3
  46. data/web/js/fnordmetric.timeseries_widget.js +46 -29
  47. data/web/js/fnordmetric.toplist_widget.js +23 -16
  48. data/web/js/fnordmetric.util.js +12 -8
  49. data/web/vendor/font-awesome/css/font-awesome-ie7.min.css +22 -0
  50. data/web/vendor/font-awesome/css/font-awesome.css +522 -221
  51. data/web/vendor/font-awesome/css/font-awesome.min.css +33 -0
  52. data/web/vendor/font-awesome/font/FontAwesome.otf +0 -0
  53. data/web/vendor/font-awesome/font/fontawesome-webfont.eot +0 -0
  54. data/web/vendor/font-awesome/font/fontawesome-webfont.svg +278 -169
  55. data/web/vendor/font-awesome/font/fontawesome-webfont.ttf +0 -0
  56. data/web/vendor/font-awesome/font/fontawesome-webfont.woff +0 -0
  57. data/web/vendor/jquery-ui.min.js +6 -413
  58. data/web/vendor/jquery.combobox.js +129 -0
  59. metadata +55 -48
  60. data/doc/V1.0-ROADMAP +0 -97
  61. data/doc/full_example.rb +0 -224
  62. data/doc/legacy_example.rb +0 -640
  63. data/doc/minimal_example.rb +0 -26
  64. data/doc/preview1.png +0 -0
  65. data/doc/preview2.png +0 -0
  66. data/doc/preview3.png +0 -0
  67. data/readme.md +0 -365
  68. data/web/vendor/d3.v2.js +0 -9382
  69. data/web/vendor/font-awesome/font/fontawesome-webfont.svgz +0 -0
  70. data/web/vendor/rickshaw.css +0 -286
  71. data/web/vendor/rickshaw.fnordmetric.js +0 -2700
@@ -0,0 +1,13 @@
1
+ # ServerHealthGauge
2
+ # here be dragons....
3
+ #
4
+ # This gauge listens for JSON events that look like this:
5
+ #
6
+ # {
7
+ # "hostname": "my-hostname",
8
+ # "uptime": 1231523,
9
+ # "load_avg": [0.32, 0.16, 0.13],
10
+ # "mem": { "total": 4069, "free": 1096 },
11
+ # "swap": { "total": 0, "free": 0 },
12
+ # "disk_usage": "43%"
13
+ # }
@@ -1,39 +1,54 @@
1
1
  class FnordMetric::Namespace
2
-
2
+
3
3
  attr_reader :handlers, :gauges, :opts, :key, :dashboards, :flags
4
4
 
5
- @@opts = [:event, :gauge, :widget, :set_title, :hide_active_users, :hide_overview, :dashboard]
5
+ @@opts = [:event, :gauge, :widget, :set_title, :hide_active_users, :hide_overview,
6
+ :hide_gauge_explorer, :dashboard]
7
+
6
8
  @@multi_gauges = [:timeseries_gauge, :toplist_gauge, :distribution_gauge]
7
9
 
8
10
  def initialize(key, opts)
9
11
  @gauges = Hash.new
10
12
  @dashboards = Hash.new
11
- @handlers = Hash.new
12
- @flags = Hash.new
13
+ @handlers = Hash.new.with_indifferent_access
13
14
  @title = key
14
- @active_users_available = true
15
15
  @opts = opts
16
16
  @key = key
17
+
18
+ @flags = {
19
+ :hide_active_users => (FnordMetric.options[:enable_active_users] == false),
20
+ :hide_gauge_explorer => (FnordMetric.options[:enable_gauge_explorer] == false)
21
+ }
17
22
  end
18
23
 
19
- def ready!(redis)
24
+ def ready!(redis, sync_redis = nil)
20
25
  @redis = redis
26
+ @sync_redis = sync_redis
27
+ load_gauges
21
28
  self
22
29
  end
23
30
 
24
31
  def announce(event)
25
- announce_to_timeline(event)
26
- announce_to_typelist(event)
32
+ if !@flags[:hide_active_users]
33
+ announce_to_timeline(event)
34
+ announce_to_typelist(event)
35
+ end
27
36
 
28
37
  if event[:_session]
29
38
  event[:_session_key] = announce_to_session(event).session_key
30
39
  end
31
40
 
41
+ if FnordMetric::ZeroConfigGauge::TYPES.include?(event[:_type].to_sym)
42
+ ctx = FnordMetric::Context.new(opts, FnordMetric::ZeroConfigGauge::Handler)
43
+ ctx.call(event, @redis, self)
44
+ return self
45
+ end
46
+
32
47
  res = [
33
48
  @handlers[event[:_type].to_s],
34
49
  @handlers["*"]
35
50
  ].flatten.compact.each do |context|
36
- context.call(event, @redis)
51
+ context.call(event, @redis, self)
37
52
  end.size
38
53
 
39
54
  if res == 0
@@ -75,10 +90,6 @@ class FnordMetric::Namespace
75
90
  @title
76
91
  end
77
92
 
78
- def active_users_available
79
- @active_users_available
80
- end
81
-
82
93
  def dashboards(name=nil, opts = {})
83
94
  return @dashboards unless name
84
95
  dash = FnordMetric::Dashboard.new(opts.merge(:title => name))
@@ -105,6 +116,10 @@ class FnordMetric::Namespace
105
116
  @flags[:hide_active_users] = true
106
117
  end
107
118
 
119
+ def opt_hide_gauge_explorer
120
+ @flags[:hide_gauge_explorer] = true
121
+ end
122
+
108
123
  def opt_hide_overview
109
124
  @flags[:hide_overview] = true
110
125
  end
@@ -113,8 +128,7 @@ class FnordMetric::Namespace
113
128
  @title = title
114
129
  end
115
130
 
116
- def opt_event(event_type, opts={}, &block)
117
- opts.merge!(:redis => @redis, :gauges => @gauges)
131
+ def opt_event(event_type, opts={}, &block)
118
132
  FnordMetric::Context.new(opts, block).tap do |context|
119
133
  @handlers[event_type.to_s] ||= []
120
134
  @handlers[event_type.to_s] << context
@@ -122,7 +136,9 @@ class FnordMetric::Namespace
122
136
  end
123
137
 
124
138
  def opt_gauge(gauge_key, opts={})
125
- opts.merge!(:key => gauge_key, :key_prefix => key_prefix)
139
+ opts.merge!(:key => gauge_key)
140
+ store_gauge(gauge_key, opts) if opts[:zero_config]
141
+ opts.merge!(:key_prefix => key_prefix)
126
142
  klass = "FnordMetric::#{(opts[:type] || "").to_s.camelize}Gauge".constantize
127
143
  @gauges[gauge_key] ||= klass.new(opts)
128
144
  end
@@ -143,11 +159,31 @@ class FnordMetric::Namespace
143
159
  end
144
160
 
145
161
  def build_widget(opts)
146
- _gauges = [opts[:gauges]].flatten.map{ |g| @gauges.fetch(g) }
162
+ _gauges = [opts[:gauges]].flatten.map do |g|
163
+ @gauges[g] || FnordMetric::ZeroConfigGauge.new(g, self)
164
+ end
147
165
  widget_klass = "FnordMetric::#{opts.fetch(:type).to_s.capitalize}Widget"
148
166
  widget_klass.constantize.new(opts.merge(:gauges => _gauges))
149
167
  end
150
168
 
169
+ def store_gauge(gauge_key, opts)
170
+ gaugelist_key = key_prefix("zero-config-gauges")
171
+ sync_redis.hset(gaugelist_key, gauge_key, opts.to_json)
172
+ end
173
+
174
+ def load_gauges
175
+ gaugelist_key = key_prefix("zero-config-gauges")
176
+ sync_redis.hgetall(gaugelist_key).each do |gauge_key, gauge_opts|
177
+ gopts = JSON.parse(gauge_opts).symbolize_keys
178
+ gopts.delete(:zero_config)
179
+ opt_gauge(gauge_key.to_sym, gopts)
180
+ end
181
+ end
182
+
183
+ def sync_redis
184
+ @sync_redis || @redis
185
+ end
186
+
151
187
  def extend_opts(opts)
152
188
  opts.merge(
153
189
  :namespace_prefix => key_prefix,
@@ -0,0 +1,25 @@
1
+ class FnordMetric::Util
2
+
3
+ def self.parse_time(str)
4
+ str = str.downcase
5
+
6
+ if (str == "now")
7
+ Time.now.to_i
8
+ elsif str =~ /^([0-9]+(?:\.[0-9]+)?)$/
9
+ $1.to_i
10
+ elsif str =~ /^-([0-9]+(?:\.[0-9]+)?)$/
11
+ Time.now.to_i - $1.to_i
12
+ elsif str =~ /^-([0-9]+(?:\.[0-9]+)?)s(ec(ond)?(s?))?$/
13
+ Time.now.to_i - $1.to_f
14
+ elsif str =~ /^-([0-9]+(?:\.[0-9]+)?)m(in(ute)?(s?))?$/
15
+ Time.now.to_i - ($1.to_f * 60)
16
+ elsif str =~ /^-([0-9]+(?:\.[0-9]+)?)h(our(s?))?$/
17
+ Time.now.to_i - ($1.to_f * 3600)
18
+ elsif str =~ /^-([0-9]+(?:\.[0-9]+)?)d(ay(s?))?$/
19
+ Time.now.to_i - ($1.to_i * 86400)
20
+ else
21
+ raise "invalid time specifiation: #{str}"
22
+ end
23
+ end
24
+
25
+ end
@@ -1,3 +1,3 @@
1
1
  module FnordMetric
2
- VERSION = "1.0.1"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -22,6 +22,11 @@ class FnordMetric::App < Sinatra::Base
22
22
  include FnordMetric::AppHelpers
23
23
  end
24
24
 
25
+ %w(fnordmetric-ui.js fnordmetric-ui.css fnordmetric-core.css fnordmetric-core.js).each do |f|
26
+ next if ::File.exists?(::File.expand_path("../../../../web/#{f}", __FILE__))
27
+ raise "error: file 'web/#{f}' does not exist, please run build.sh in web/"
28
+ end
29
+
25
30
  def initialize(opts = {})
26
31
  @opts = FnordMetric.default_options(opts)
27
32
 
@@ -37,10 +42,12 @@ class FnordMetric::App < Sinatra::Base
37
42
 
38
43
  get '/:namespace' do
39
44
  pass unless current_namespace
45
+ current_namespace.ready!(@redis)
40
46
  haml :app
41
47
  end
42
48
 
43
49
  post '/events' do
50
+ params = JSON.parse(request.body.read) unless params
44
51
  halt 400, 'please specify the event_type (_type)' unless params["_type"]
45
52
  track_event((8**32).to_s(36), parse_params(params))
46
53
  end
@@ -52,71 +59,5 @@ class FnordMetric::App < Sinatra::Base
52
59
  dashboard.to_json
53
60
  end
54
61
 
55
-
56
- # get '/favicon.ico' do
57
- # ""
58
- # end
59
-
60
- # get '/:namespace/gauge/:name' do
61
- # gauge = if params[:name].include?("++")
62
- # parts = params[:name].split("++")
63
- # current_namespace.gauges[parts.first.to_sym].fetch_gauge(parts.last, params[:tick].to_i)
64
- # else
65
- # current_namespace.gauges[params[:name].to_sym]
66
- # end
67
-
68
- # if !gauge
69
- # status 404
70
- # return ""
71
- # end
72
-
73
- # data = if gauge.three_dimensional?
74
- # _t = (params[:at] || Time.now).to_i
75
- # { :count => gauge.field_values_total(_t), :values => gauge.field_values_at(_t) }
76
- # elsif params[:at] && params[:at] =~ /^[0-9]+$/
77
- # { (_t = gauge.tick_at(params[:at].to_i)) => gauge.value_at(_t) }
78
- # elsif params[:at] && params[:at] =~ /^([0-9]+)-([0-9]+)$/
79
- # _range = params[:at].split("-").map(&:to_i)
80
- # _values = gauge.values_in(_range.first.._range.last)
81
- # params[:sum] ? { :sum => _values.values.compact.map(&:to_i).sum } : _values
82
- # else
83
- # { (_t = gauge.tick_at(Time.now.to_i-gauge.tick)) => gauge.value_at(_t) }
84
- # end
85
-
86
- # data.to_json
87
- # end
88
-
89
- # get '/:namespace/sessions' do
90
- # sessions = current_namespace.sessions(:all, :limit => 100).map do |session|
91
- # session.fetch_data!
92
- # session.to_json
93
- # end
94
-
95
- # { :sessions => sessions }.to_json
96
- # end
97
-
98
- # get '/:namespace/events' do
99
- # events = if params[:type]
100
- # current_namespace.events(:by_type, :type => params[:type])
101
- # elsif params[:session_key]
102
- # current_namespace.events(:by_session_key, :session_key => params[:session_key])
103
- # else
104
- # find_opts = { :limit => 100 }
105
- # find_opts.merge!(:since => params[:since].to_i+1) if params[:since]
106
- # current_namespace.events(:all, find_opts)
107
- # end
108
-
109
- # { :events => events.map(&:to_json) }.to_json
110
- # end
111
-
112
- # get '/:namespace/event_types' do
113
- # types_key = current_namespace.key_prefix("type-")
114
- # keys = @redis.keys("#{types_key}*").map{ |k| k.gsub(types_key,'') }
115
-
116
- # { :types => keys }.to_json
117
- # end
118
-
119
-
120
-
121
62
  end
122
63
 
@@ -22,11 +22,23 @@ private
22
22
  messages << widget(ns, event) if event["type"] == "widget_request"
23
23
  messages << gauge(ns, event) if event["type"] == "render_request"
24
24
  messages << active_users(ns, event) if event["type"] == "active_users_request"
25
- messages.flatten.compact
25
+ messages << gauge_list(ns, event) if event["type"] == "gauge_list_request"
26
+
27
+ messages.flatten.compact.map do |m|
28
+ m["namespace"] = event["namespace"]; m
29
+ end
26
30
  end
27
31
 
28
32
  def widget(namespace, event)
29
- "FnordMetric::#{event["klass"]}".constantize.execute(namespace, event) # FIXPAUL
33
+ klass = if event["klass"] == "generic" && event["cmd"] == "values_for"
34
+ FnordMetric::NumbersWidget
35
+ elsif event["klass"] == "generic" && event["cmd"] == "values_at"
36
+ FnordMetric::TimeseriesWidget
37
+ else
38
+ "FnordMetric::#{event["klass"]}".constantize
39
+ end
40
+
41
+ klass.execute(namespace, event)
30
42
  end
31
43
 
32
44
  def gauge(namespace, event)
@@ -40,6 +52,8 @@ private
40
52
  end
41
53
 
42
54
  def discover(namespace)
55
+ namespace.ready!(@redis)
56
+
43
57
  [namespace.dashboards.map do |dash_key, dash|
44
58
  { "type" => "discover_response", "gauge_key" => dash_key, "view" => "dashboard",
45
59
  "group" => dash.group }
@@ -84,4 +98,20 @@ private
84
98
  }
85
99
  end
86
100
 
101
+ def gauge_list(namespace, event)
102
+ namespace.ready!(@redis)
103
+
104
+ gauges = namespace.gauges.map do |name, gauge|
105
+ {
106
+ "key" => gauge.name,
107
+ "title" => gauge.title
108
+ }
109
+ end
110
+
111
+ {
112
+ :type => "gauge_list_response",
113
+ :gauges => gauges
114
+ }
115
+ end
116
+
87
117
  end
@@ -35,4 +35,4 @@ class FnordMetric::WebSocket < Rack::WebSocket::Application
35
35
  rand(8**64).to_s(36)
36
36
  end
37
37
 
38
- end
38
+ end
@@ -11,7 +11,7 @@ class FnordMetric::BarsWidget < FnordMetric::Widget
11
11
  return false unless resp
12
12
 
13
13
  resp.merge(
14
- :class => "widget_response",
14
+ :type => "widget_response",
15
15
  :widget_key => event["widget_key"]
16
16
  )
17
17
  end
@@ -8,14 +8,36 @@ class FnordMetric::NumbersWidget < FnordMetric::Widget
8
8
  return false unless resp
9
9
 
10
10
  resp.merge(
11
- :class => "widget_response",
11
+ :type => "widget_response",
12
12
  :widget_key => event["widget_key"]
13
13
  )
14
14
  end
15
15
 
16
16
  def self.execute_values_for(gauge, event)
17
+ unless gauge
18
+ return { "error" => "gauge not found..." }
19
+ end
20
+
17
21
  _t = Time.now.to_i
18
22
 
23
+ if at = event["at"]
24
+ value = if at =~ /sum\((.+)\)/
25
+ vals = gauge.values_in(FnordMetric::Util.parse_time($1).._t+gauge.tick)
26
+ vals.values.compact.map(&:to_f).inject(&:+)
27
+ elsif at =~ /avg\((.+)\)/
28
+ vals = gauge.values_in(FnordMetric::Util.parse_time($1).._t+gauge.tick)
29
+ (vals.values.compact.map(&:to_f).inject(&:+) || 0) / vals.size.to_f
30
+ else
31
+ gauge.value_at(FnordMetric::Util.parse_time(at)).to_i
32
+ end
33
+
34
+ return({
35
+ "cmd" => "values_for",
36
+ "at" => event["at"],
37
+ "gauge" => gauge.name,
38
+ "value" => value })
39
+ end
40
+
19
41
  values = {}.tap do |out|
20
42
  event["offsets"].each do |off|
21
43
  if off.to_s.starts_with?("s")
@@ -36,14 +58,16 @@ class FnordMetric::NumbersWidget < FnordMetric::Widget
36
58
  end
37
59
  end
38
60
 
39
- { "cmd" => "values_for",
40
- "series" => gauge.name,
61
+ { "cmd" => "values_for",
62
+ "gauge" => gauge.name,
41
63
  "values" => values }
42
64
  end
43
65
 
44
66
  def data
45
67
  super.merge(
46
68
  :series => gauges.map(&:name),
69
+ :series_titles => gauges.map(&:title),
70
+ :series_units => Hash[gauges.map{ |g| [g.name, g.unit] }],
47
71
  :offsets => (@opts[:offsets] || [0, 1, "s30"]),
48
72
  :autoupdate => (@opts[:autoupdate] || 60)
49
73
  )
@@ -53,4 +77,4 @@ class FnordMetric::NumbersWidget < FnordMetric::Widget
53
77
  false
54
78
  end
55
79
 
56
- end
80
+ end
@@ -5,8 +5,16 @@ class FnordMetric::TimeseriesWidget < FnordMetric::Widget
5
5
  {
6
6
  :cmd => :values_at,
7
7
  :gauges => event["gauges"].map{ |gkey|
8
- vals = namespace.gauges[gkey.to_sym].values_in(event["since"]..event["until"])
9
- { :key => gkey, :vals => vals }
8
+ _gauge = namespace.gauges[gkey.to_sym]
9
+ unless _gauge
10
+ return { :error => "gauge not found: #{gkey}" }
11
+ end
12
+
13
+ t_since = FnordMetric::Util.parse_time(event["since"].to_s)
14
+ t_until = FnordMetric::Util.parse_time(event["until"].to_s)
15
+
16
+ vals = _gauge.values_in(t_since..t_until)
17
+ { :key => gkey, :vals => vals, :title => _gauge.title }
10
18
  }
11
19
  }
12
20
  end
@@ -14,17 +22,18 @@ class FnordMetric::TimeseriesWidget < FnordMetric::Widget
14
22
  return false unless resp
15
23
 
16
24
  resp.merge(
17
- :class => "widget_response",
25
+ :type => "widget_response",
18
26
  :widget_key => event["widget_key"]
19
27
  )
20
28
  end
21
29
 
22
- def data
30
+ def data
23
31
  super.merge(
24
32
  :series => series,
25
33
  :gauges => gauges.map(&:name),
26
34
  :start_timestamp => ticks.first,
27
35
  :end_timestamp => ticks.last,
36
+ :xticks => (@opts[:xticks] || 30),
28
37
  :autoupdate => (@opts[:autoupdate] || 60),
29
38
  :include_current => !!@opts[:include_current],
30
39
  :default_style => (@opts[:plot_style] || 'line'),
@@ -35,12 +44,13 @@ class FnordMetric::TimeseriesWidget < FnordMetric::Widget
35
44
 
36
45
  def series
37
46
  colors = FnordMetric::COLORS.dup
38
-
47
+
39
48
  gauges.map do |gauge|
40
- {
41
- :name => gauge.name,
42
- :data => [{:x => ticks.first, :y => 0}],
43
- :color => colors.unshift(colors.pop).first
49
+ {
50
+ :name => gauge.name,
51
+ :title => gauge.title,
52
+ :data => [{:x => ticks.first, :y => 0}],
53
+ :color => colors.unshift(colors.pop).first
44
54
  }
45
55
  end
46
56
  end