fnordmetric 0.3.2 → 0.5.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 (59) hide show
  1. data/Gemfile +6 -0
  2. data/Gemfile.lock +21 -0
  3. data/Procfile +1 -2
  4. data/VERSION +1 -1
  5. data/_spec/app_spec.rb +178 -0
  6. data/{spec → _spec}/cache_spec.rb +0 -0
  7. data/{spec → _spec}/combine_metric_spec.rb +0 -0
  8. data/{spec → _spec}/core_spec.rb +0 -0
  9. data/{spec → _spec}/count_metric_spec.rb +0 -0
  10. data/_spec/dashboard_spec.rb +67 -0
  11. data/_spec/event_spec.rb +46 -0
  12. data/{spec → _spec}/metric_spec.rb +0 -0
  13. data/{spec → _spec}/report_spec.rb +0 -0
  14. data/{spec → _spec}/sum_metric_spec.rb +0 -0
  15. data/_spec/widget_spec.rb +107 -0
  16. data/doc/import_dump.rb +26 -0
  17. data/em_runner.rb +33 -0
  18. data/fnordmetric.gemspec +59 -20
  19. data/haml/app.haml +26 -12
  20. data/lib/fnordmetric.rb +150 -15
  21. data/lib/fnordmetric/app.rb +70 -11
  22. data/lib/fnordmetric/cache.rb +4 -4
  23. data/lib/fnordmetric/context.rb +65 -0
  24. data/lib/fnordmetric/dashboard.rb +16 -12
  25. data/lib/fnordmetric/event.rb +65 -15
  26. data/lib/fnordmetric/gauge.rb +46 -0
  27. data/lib/fnordmetric/gauge_calculations.rb +43 -0
  28. data/lib/fnordmetric/gauge_modifiers.rb +43 -0
  29. data/lib/fnordmetric/inbound_stream.rb +66 -0
  30. data/lib/fnordmetric/logger.rb +38 -0
  31. data/lib/fnordmetric/namespace.rb +120 -0
  32. data/lib/fnordmetric/numbers_widget.rb +29 -11
  33. data/lib/fnordmetric/session.rb +131 -0
  34. data/lib/fnordmetric/standalone.rb +31 -0
  35. data/lib/fnordmetric/timeline_widget.rb +29 -9
  36. data/lib/fnordmetric/widget.rb +50 -45
  37. data/lib/fnordmetric/worker.rb +80 -0
  38. data/pub/fnordmetric/fnordmetric.css +76 -9
  39. data/pub/fnordmetric/fnordmetric.js +541 -42
  40. data/pub/raphael-min.js +8 -0
  41. data/pub/raphael-utils.js +221 -0
  42. data/readme.rdoc +172 -27
  43. data/server.rb +22 -0
  44. data/spec/app_spec.rb +359 -117
  45. data/spec/context_spec.rb +42 -0
  46. data/spec/dashboard_spec.rb +7 -47
  47. data/spec/event_spec.rb +114 -33
  48. data/spec/gauge_modifiers_spec.rb +276 -0
  49. data/spec/gauge_spec.rb +128 -0
  50. data/spec/namespace_spec.rb +104 -0
  51. data/spec/session_spec.rb +231 -0
  52. data/spec/spec_helper.rb +27 -4
  53. data/spec/widget_spec.rb +81 -75
  54. data/spec/worker_spec.rb +37 -0
  55. data/test_stream.sh +187 -0
  56. data/ulm_stats.rb +198 -0
  57. metadata +114 -35
  58. data/lib/fnordmetric/core.rb +0 -66
  59. data/lib/fnordmetric/engine.rb +0 -3
@@ -1,7 +1,11 @@
1
+ # encoding: utf-8
2
+
1
3
  class FnordMetric::App < Sinatra::Base
2
4
 
3
5
  @@sessions = Hash.new
4
6
 
7
+ Encoding.default_external = Encoding::UTF_8
8
+
5
9
  #use Rack::Reloader, 0
6
10
 
7
11
  enable :session
@@ -9,6 +13,17 @@ class FnordMetric::App < Sinatra::Base
9
13
  set :haml, :format => :html5
10
14
  set :views, ::File.expand_path('../../../haml', __FILE__)
11
15
  set :public, ::File.expand_path('../../../pub', __FILE__)
16
+
17
+ def initialize(namespaces, opts)
18
+ @namespaces = {}
19
+ @redis = Redis.new
20
+ namespaces.each do |key, block|
21
+ @namespaces[key] = FnordMetric::Namespace.new(key, opts.clone)
22
+ @namespaces[key].instance_eval(&block)
23
+ @namespaces[key].ready!(@redis.clone)
24
+ end
25
+ super(nil)
26
+ end
12
27
 
13
28
  helpers do
14
29
  include Rack::Utils
@@ -18,6 +33,12 @@ class FnordMetric::App < Sinatra::Base
18
33
  request.env["SCRIPT_NAME"]
19
34
  end
20
35
 
36
+ def current_namespace
37
+ @namespaces[@namespaces.keys.detect{ |k|
38
+ k.to_s == params[:namespace]
39
+ }.try(:intern)]
40
+ end
41
+
21
42
  end
22
43
 
23
44
  if ENV['RACK_ENV'] == "test"
@@ -25,24 +46,62 @@ class FnordMetric::App < Sinatra::Base
25
46
  end
26
47
 
27
48
  get '/' do
28
- redirect "#{request.env["SCRIPT_NAME"]}/dashboard/default"
49
+ redirect "#{path_prefix}/#{@namespaces.keys.first}"
29
50
  end
30
51
 
31
- get '/dashboard/:name' do
32
- @dashboard = FnordMetric.dashboards.detect{|d| d.token == params[:name] }
33
- @dashboard ||= FnordMetric.dashboards.first
52
+ get '/:namespace' do
34
53
  haml :app
35
54
  end
36
55
 
37
- get '/metric/:name' do
38
- content_type 'application/json'
39
- FnordMetric::MetricAPI.new(params).render
56
+ get '/favicon.ico' do
57
+ ""
58
+ end
59
+
60
+ #get '/metric/:name' do
61
+ # content_type 'application/json'
62
+ # FnordMetric::MetricAPI.new(params).render
63
+ #end
64
+
65
+ #get '/widget/:name' do
66
+ # @dashboard = FnordMetric.dashboards.first
67
+ # @widget = @dashboard.widgets.first
68
+ # haml :widget
69
+ #end
70
+
71
+ get '/:namespace/sessions' do
72
+
73
+ sessions = current_namespace.sessions(:all, :limit => 100).map do |session|
74
+ session.fetch_data!
75
+ session.to_json
76
+ end
77
+
78
+ { :sessions => sessions }.to_json
79
+ end
80
+
81
+ get '/:namespace/events' do
82
+
83
+ events = if params[:type]
84
+ current_namespace.events(:by_type, :type => params[:type])
85
+ else
86
+ find_opts = { :limit => 100 }
87
+ find_opts.merge!(:since => params[:since].to_i+1) if params[:since]
88
+ current_namespace.events(:all, find_opts)
89
+ end
90
+
91
+ { :events => events.map(&:to_json) }.to_json
40
92
  end
41
93
 
42
- get '/widget/:name' do
43
- @dashboard = FnordMetric.dashboards.first
44
- @widget = @dashboard.widgets.first
45
- haml :widget
94
+ get '/:namespace/event_types' do
95
+ types_key = current_namespace.key_prefix("type-")
96
+ keys = @redis.keys("#{types_key}*").map{ |k| k.gsub(types_key,'') }
97
+
98
+ { :types => keys }.to_json
99
+ end
100
+
101
+ get '/:namespace/dashboard/:dashboard' do
102
+ dashboard = current_namespace.dashboards.fetch(params[:dashboard])
103
+
104
+ dashboard.to_json
46
105
  end
47
106
 
48
107
  post '/events' do
@@ -1,10 +1,10 @@
1
1
  class FnordMetric::Cache
2
- include Mongoid::Document
2
+ # include Mongoid::Document
3
3
 
4
- self.collection_name = 'fnordmetric_cache'
4
+ # self.collection_name = 'fnordmetric_cache'
5
5
 
6
- field :cache_key, :type => String
7
- field :data, :type => Hash
6
+ # field :cache_key, :type => String
7
+ # field :data, :type => Hash
8
8
 
9
9
  def self.store!(cache_key, data)
10
10
  data = { :value => data } unless data.is_a?(Hash)
@@ -0,0 +1,65 @@
1
+ class FnordMetric::Context
2
+
3
+ include FnordMetric::GaugeModifiers
4
+
5
+ def initialize(opts, block)
6
+ @block = block
7
+ @opts = opts
8
+ end
9
+
10
+ def call(event, redis)
11
+ @redis = redis
12
+ @event = event
13
+ self.instance_eval(&@block)
14
+ rescue Exception => e
15
+ raise e if ENV['FNORDMETRIC_ENV'] == 'test'
16
+ puts "error: #{e.message}"
17
+ end
18
+
19
+ private
20
+
21
+ def session_key
22
+ @event[:_session_key]
23
+ end
24
+
25
+ def data
26
+ @event
27
+ end
28
+
29
+ def key(gauge)
30
+ fetch_gauge(gauge).key
31
+ end
32
+
33
+ def time
34
+ @event[:_time].to_i
35
+ end
36
+
37
+ protected
38
+
39
+ def fetch_gauge(_gauge)
40
+ _gauge.is_a?(FnordMetric::Gauge) ? _gauge : @opts[:gauges].fetch(_gauge)
41
+ rescue
42
+ error! "error: gauge '#{_gauge}' is undefined"
43
+ end
44
+
45
+ def error!(msg)
46
+ FnordMetric.error!(msg)
47
+ end
48
+
49
+ def assure_two_dimensional!(gauge)
50
+ return true if gauge.two_dimensional?
51
+ error! "error: #{caller[0].split(" ")[-1]} can only be used with 2-dimensional gauges"
52
+ end
53
+
54
+ def assure_three_dimensional!(gauge)
55
+ return true unless gauge.two_dimensional?
56
+ error! "error: #{caller[0].split(" ")[-1]} can only be used with 3-dimensional gauges"
57
+ end
58
+
59
+ def assure_non_progressive!(gauge)
60
+ return true unless gauge.progressive?
61
+ error! "error: #{caller[0].split(" ")[-1]} can only be used with non-progressive gauges"
62
+ end
63
+
64
+ end
65
+
@@ -1,17 +1,15 @@
1
1
  class FnordMetric::Dashboard
2
2
 
3
- attr_accessor :widgets, :report
3
+ attr_accessor :widgets
4
4
 
5
- def initialize(options, _block=nil, &block)
6
- @options = options.to_options
5
+ def initialize(options={})
6
+ raise "please provide a :title" unless options[:title]
7
7
  @widgets = Array.new
8
- raise "please provide a :title" unless @options[:title]
9
- (_block||block).call(self)
10
- add_report(@options[:report]) if @options[:report]
8
+ @options = options
11
9
  end
12
10
 
13
11
  def add_widget(w)
14
- @widgets << (w.is_a?(FnordMetric::Widget) ? w : FnordMetric.widgets.fetch(w))
12
+ @widgets << w
15
13
  end
16
14
 
17
15
  def title
@@ -21,10 +19,16 @@ class FnordMetric::Dashboard
21
19
  def token
22
20
  title.to_s.gsub(/[\W]/, '')
23
21
  end
24
-
25
- def add_report(report)
26
- @report = report
27
- @widgets.each{ |w| w.add_report(report) }
28
- end
29
22
 
23
+ def to_json
24
+ {
25
+ :title => title,
26
+ :widgets => {}.tap { |wids|
27
+ @widgets.each do |w|
28
+ wids[w.token] = w.render
29
+ end
30
+ }
31
+ }.to_json
32
+ end
33
+
30
34
  end
@@ -1,28 +1,78 @@
1
1
  class FnordMetric::Event
2
- include Mongoid::Document
3
2
 
4
- self.collection_name = 'fnordmetric_event'
3
+ attr_accessor :time, :type, :event_id
5
4
 
6
- field :type, :type => String
7
- field :client, :type => Integer
8
- field :data, :type => Hash
5
+ #def self.track!(event_type, event_data)
6
+ #end
9
7
 
10
- def self.track!(event_type, event_data)
11
- event_data.to_options!
12
- event_time = (event_data.delete(:time) || Time.now.getutc).to_i
13
- self.create(:type => event_type, :time => event_time, :data => event_data)
8
+ def self.all(opts)
9
+ range_opts = { :withscores => true }
10
+ range_opts.merge!(:limit => [0,opts[:limit]]) if opts[:limit]
11
+ opts[:redis].zrevrangebyscore(
12
+ "#{opts[:namespace_prefix]}-timeline",
13
+ '+inf', opts[:since]||'0',
14
+ range_opts
15
+ ).in_groups_of(2).map do |event_id, ts|
16
+ next if event_id.blank?
17
+ find(event_id, opts).tap{ |e| e.time = ts }
18
+ end
14
19
  end
15
20
 
16
- def time
17
- read_attribute(:time).to_i
21
+ def self.by_type(_type, opts)
22
+ opts[:redis].lrange(
23
+ "#{opts[:namespace_prefix]}-type-#{_type}",
24
+ 0, 200).map do |event_id|
25
+ next if event_id.blank?
26
+ find(event_id, opts).tap{ |e| }
27
+ end
18
28
  end
19
29
 
20
- def [](key)
21
- send(key)
30
+ def self.find(event_id, opts)
31
+ self.new(event_id, opts).tap do |event|
32
+ event.fetch!
33
+ end
22
34
  end
23
35
 
24
- def method_missing(method)
25
- data[method.to_s]
36
+ def initialize(event_id, opts)
37
+ @opts = opts
38
+ @event_id = event_id
39
+ end
40
+
41
+ def fetch!
42
+ @data = JSON.parse(fetch_json).tap do |event|
43
+ @type = event.delete("_type")
44
+ end
45
+ end
46
+
47
+ def fetch_json
48
+ @opts[:redis].get(redis_key) || "{}"
49
+ end
50
+
51
+ def redis_key
52
+ [@opts[:redis_prefix], :event, @event_id].join("-")
53
+ end
54
+
55
+ def session_key
56
+ @data["_session"] ? Digest::MD5.hexdigest(@data["_session"]) : nil
57
+ end
58
+
59
+ def id
60
+ @event_id
61
+ end
62
+
63
+ def data(key=nil)
64
+ key ? @data[key.to_s] : @data
65
+ end
66
+
67
+ alias :[] :data
68
+
69
+ def to_json
70
+ @data.merge!(
71
+ :_type => @type,
72
+ :_session_key => session_key,
73
+ :_eid => @event_id,
74
+ :_time => @time
75
+ )
26
76
  end
27
77
 
28
78
  end
@@ -0,0 +1,46 @@
1
+ class FnordMetric::Gauge
2
+
3
+ include FnordMetric::GaugeCalculations
4
+
5
+ def initialize(opts)
6
+ opts.fetch(:key) && opts.fetch(:key_prefix)
7
+ @opts = opts
8
+ end
9
+
10
+ def tick
11
+ (@opts[:tick] || 3600).to_i
12
+ end
13
+
14
+ def tick_at(time)
15
+ (time/tick.to_f).floor*tick
16
+ end
17
+
18
+ def name
19
+ @opts[:key]
20
+ end
21
+
22
+ def key(_append=nil)
23
+ [@opts[:key_prefix], "gauge", name, tick, _append].flatten.compact.join("-")
24
+ end
25
+
26
+ def tick_key(_time, _append=nil)
27
+ key([(progressive? ? :progressive : tick_at(_time).to_s), _append])
28
+ end
29
+
30
+ def two_dimensional?
31
+ !@opts[:three_dimensional]
32
+ end
33
+
34
+ def progressive?
35
+ !!@opts[:progressive]
36
+ end
37
+
38
+ def unique?
39
+ !!@opts[:unique]
40
+ end
41
+
42
+ def add_redis(_redis)
43
+ @opts[:redis] = _redis
44
+ end
45
+
46
+ end
@@ -0,0 +1,43 @@
1
+ module FnordMetric::GaugeCalculations
2
+
3
+ @@avg_per_session_proc = proc{ |_v, _t|
4
+ #raise redis.get(tick_key(_t, :"sessions-count")).inspect
5
+ (_v.to_f / (redis.get(tick_key(_t, :"sessions-count"))||0).to_i)
6
+ }
7
+
8
+ def value_at(time, opts={}, &block)
9
+ _t = tick_at(time)
10
+ _v = redis.hget(key, _t)
11
+
12
+ calculate_value(_v, _t, opts, block)
13
+ end
14
+
15
+ def values_at(times, opts={}, &block)
16
+ times = times.map{ |_t| tick_at(_t) }
17
+ Hash.new.tap do |ret|
18
+ redis.hmget(key, *times).each_with_index do |_v, _n|
19
+ _t = times[_n]
20
+ ret[_t] = calculate_value(_v, _t, opts, block)
21
+ end
22
+ end
23
+ end
24
+
25
+ def values_in(range, opts={}, &block)
26
+ values_at((tick_at(range.first)..range.last).step(tick))
27
+ end
28
+
29
+ def calculate_value(_v, _t, opts, block)
30
+ block = @@avg_per_session_proc if opts[:avg_per_session]
31
+
32
+ if block
33
+ instance_exec(_v, _t, &block)
34
+ else
35
+ _v
36
+ end
37
+ end
38
+
39
+ def redis
40
+ @opts[:redis]
41
+ end
42
+
43
+ end
@@ -0,0 +1,43 @@
1
+ module FnordMetric::GaugeModifiers
2
+
3
+ def incr(gauge_name, value=1)
4
+ gauge = fetch_gauge(gauge_name)
5
+ assure_two_dimensional!(gauge)
6
+ if gauge.unique?
7
+ incr_uniq(gauge, value)
8
+ else
9
+ incr_tick(gauge, value)
10
+ end
11
+ end
12
+
13
+ def incr_tick(gauge, value)
14
+ if gauge.progressive?
15
+ @redis.incrby(gauge.key(:head), value).callback do |head|
16
+ @redis.hsetnx(gauge.key, gauge.tick_at(time), head).callback do |_new|
17
+ @redis.hincrby(gauge.key, gauge.tick_at(time), value) unless _new
18
+ end
19
+ end
20
+ else
21
+ @redis.hincrby(gauge.key, gauge.tick_at(time), value)
22
+ end
23
+ end
24
+
25
+ def incr_uniq(gauge, value)
26
+ return false unless session_key
27
+ @redis.sadd(gauge.tick_key(time, :sessions), session_key).callback do |_new|
28
+ @redis.expire(gauge.tick_key(time, :sessions), gauge.tick)
29
+ if _new
30
+ @redis.incr(gauge.tick_key(time, :"sessions-count")).callback do |sc|
31
+ incr_tick(gauge, value)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def incr_field(gauge_name, field_name, value=1)
38
+ gauge = fetch_gauge(gauge_name)
39
+ assure_three_dimensional!(gauge)
40
+ # here be dragons
41
+ end
42
+
43
+ end