fnordmetric 0.3.2

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 (61) hide show
  1. data/.document +5 -0
  2. data/Gemfile +18 -0
  3. data/Gemfile.lock +69 -0
  4. data/Procfile +2 -0
  5. data/Rakefile +28 -0
  6. data/VERSION +1 -0
  7. data/doc/example_server.rb +56 -0
  8. data/fnordmetric.gemspec +145 -0
  9. data/haml/app.haml +48 -0
  10. data/haml/widget.haml +9 -0
  11. data/lib/fnordmetric.rb +21 -0
  12. data/lib/fnordmetric/app.rb +70 -0
  13. data/lib/fnordmetric/average_metric.rb +7 -0
  14. data/lib/fnordmetric/cache.rb +20 -0
  15. data/lib/fnordmetric/combine_metric.rb +7 -0
  16. data/lib/fnordmetric/core.rb +66 -0
  17. data/lib/fnordmetric/count_metric.rb +13 -0
  18. data/lib/fnordmetric/dashboard.rb +30 -0
  19. data/lib/fnordmetric/engine.rb +3 -0
  20. data/lib/fnordmetric/event.rb +28 -0
  21. data/lib/fnordmetric/funnel_widget.rb +2 -0
  22. data/lib/fnordmetric/metric.rb +80 -0
  23. data/lib/fnordmetric/metric_api.rb +37 -0
  24. data/lib/fnordmetric/numbers_widget.rb +18 -0
  25. data/lib/fnordmetric/report.rb +29 -0
  26. data/lib/fnordmetric/sum_metric.rb +13 -0
  27. data/lib/fnordmetric/timeline_widget.rb +17 -0
  28. data/lib/fnordmetric/widget.rb +75 -0
  29. data/pub/fnordmetric/fnordmetric.css +53 -0
  30. data/pub/fnordmetric/fnordmetric.js +44 -0
  31. data/pub/fnordmetric/widget_numbers.js +71 -0
  32. data/pub/fnordmetric/widget_timeline.css +0 -0
  33. data/pub/fnordmetric/widget_timeline.js +110 -0
  34. data/pub/highcharts/adapters/mootools-adapter.js +12 -0
  35. data/pub/highcharts/adapters/mootools-adapter.src.js +243 -0
  36. data/pub/highcharts/adapters/prototype-adapter.js +14 -0
  37. data/pub/highcharts/adapters/prototype-adapter.src.js +284 -0
  38. data/pub/highcharts/highcharts.js +170 -0
  39. data/pub/highcharts/highcharts.src.js +11103 -0
  40. data/pub/highcharts/modules/exporting.js +22 -0
  41. data/pub/highcharts/modules/exporting.src.js +703 -0
  42. data/pub/highcharts/themes/dark-blue.js +268 -0
  43. data/pub/highcharts/themes/dark-green.js +268 -0
  44. data/pub/highcharts/themes/gray.js +262 -0
  45. data/pub/highcharts/themes/grid.js +97 -0
  46. data/pub/jquery-1.6.1.min.js +18 -0
  47. data/pub/sprite.png +0 -0
  48. data/readme.rdoc +274 -0
  49. data/spec/app_spec.rb +178 -0
  50. data/spec/cache_spec.rb +53 -0
  51. data/spec/combine_metric_spec.rb +19 -0
  52. data/spec/core_spec.rb +50 -0
  53. data/spec/count_metric_spec.rb +32 -0
  54. data/spec/dashboard_spec.rb +67 -0
  55. data/spec/event_spec.rb +46 -0
  56. data/spec/metric_spec.rb +118 -0
  57. data/spec/report_spec.rb +87 -0
  58. data/spec/spec_helper.rb +13 -0
  59. data/spec/sum_metric_spec.rb +33 -0
  60. data/spec/widget_spec.rb +107 -0
  61. metadata +271 -0
@@ -0,0 +1,7 @@
1
+ class FnordMetric::AverageMetric < FnordMetric::Metric
2
+
3
+ def value_at(time_or_range)
4
+ 5.23 #fixme
5
+ end
6
+
7
+ end
@@ -0,0 +1,20 @@
1
+ class FnordMetric::Cache
2
+ include Mongoid::Document
3
+
4
+ self.collection_name = 'fnordmetric_cache'
5
+
6
+ field :cache_key, :type => String
7
+ field :data, :type => Hash
8
+
9
+ def self.store!(cache_key, data)
10
+ data = { :value => data } unless data.is_a?(Hash)
11
+ self.create(:cache_key => cache_key, :data => data)
12
+ end
13
+
14
+ def self.get(cache_key)
15
+ item = self.where(:cache_key => cache_key).last
16
+ return nil unless item
17
+ item.data.keys == ["value"] ? item.data["value"] : item.data
18
+ end
19
+
20
+ end
@@ -0,0 +1,7 @@
1
+ class FnordMetric::CombineMetric < FnordMetric::Metric
2
+
3
+ def value_at(time_or_range)
4
+ @options[:combine].call(time_or_range)
5
+ end
6
+
7
+ end
@@ -0,0 +1,66 @@
1
+ module FnordMetric
2
+
3
+ WIDGET_TYPES = %(timeline funnel numbers)
4
+
5
+ @@metrics = {}
6
+ @@widgets = {}
7
+ @@dashboards = Array.new
8
+
9
+ def self.define(metric_name, options)
10
+ warn "FnordMetric.metric is deprecated, please use FnordMetric.metric instead"
11
+ self.metric(metric_name, options)
12
+ end
13
+
14
+ def self.metric(metric_name, options)
15
+ options.merge!(:name => metric_name)
16
+ @@metrics[metric_name] = FnordMetric::Metric.from_options(options)
17
+ end
18
+
19
+ def self.widget(widget_name, options)
20
+ options.merge!(:widget_name => widget_name)
21
+ raise "missing option: :type" unless options[:type]
22
+ klass = if FnordMetric::WIDGET_TYPES.include?(options[:type].to_s)
23
+ "FnordMetric::#{options[:type].to_s.capitalize}Widget".constantize
24
+ else
25
+ raise "unknown widget type: #{options[:type]}"
26
+ end
27
+ [options[:metrics]].flatten.each do |m|
28
+ raise "unknown metric: #{m}" unless @@metrics[m]
29
+ end
30
+ @@widgets[widget_name] = klass.new(options)
31
+ end
32
+
33
+ def self.dashboard(title, options={}, &block)
34
+ options.merge!(:title => title)
35
+ @@dashboards << FnordMetric::Dashboard.new(options, block)
36
+ end
37
+
38
+ def self.track(event_name, event_data)
39
+ FnordMetric::Event.track!(event_name, event_data)
40
+ end
41
+
42
+ def self.report(options)
43
+ FnordMetric::Report.new(metrics, options)
44
+ end
45
+
46
+ def self.metrics
47
+ @@metrics
48
+ end
49
+
50
+ def self.widgets
51
+ @@widgets
52
+ end
53
+
54
+ def self.reset_metrics
55
+ @@metrics = {}
56
+ end
57
+
58
+ def self.reset_widgets
59
+ @@widgets = {}
60
+ end
61
+
62
+ def self.dashboards
63
+ @@dashboards
64
+ end
65
+
66
+ end
@@ -0,0 +1,13 @@
1
+ class FnordMetric::CountMetric < FnordMetric::Metric
2
+
3
+ private
4
+
5
+ def value_at(time_or_range)
6
+ # FIXME: value_at(my_time) is really slow, because it has to fetch all events that
7
+ # happened until that time (and this is not even cached, when time is in the future
8
+ # or just now), so we should redirect the call to something like:
9
+ # value_at(last_cache_since_my_time) + value_at(last_cache_since_my_time..my_time)
10
+ events_at(time_or_range).count
11
+ end
12
+
13
+ end
@@ -0,0 +1,30 @@
1
+ class FnordMetric::Dashboard
2
+
3
+ attr_accessor :widgets, :report
4
+
5
+ def initialize(options, _block=nil, &block)
6
+ @options = options.to_options
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]
11
+ end
12
+
13
+ def add_widget(w)
14
+ @widgets << (w.is_a?(FnordMetric::Widget) ? w : FnordMetric.widgets.fetch(w))
15
+ end
16
+
17
+ def title
18
+ @options[:title]
19
+ end
20
+
21
+ def token
22
+ title.to_s.gsub(/[\W]/, '')
23
+ end
24
+
25
+ def add_report(report)
26
+ @report = report
27
+ @widgets.each{ |w| w.add_report(report) }
28
+ end
29
+
30
+ end
@@ -0,0 +1,3 @@
1
+ class FnordMetric::Engine < Rails::Engine
2
+ endpoint FnordMetric::App
3
+ end
@@ -0,0 +1,28 @@
1
+ class FnordMetric::Event
2
+ include Mongoid::Document
3
+
4
+ self.collection_name = 'fnordmetric_event'
5
+
6
+ field :type, :type => String
7
+ field :client, :type => Integer
8
+ field :data, :type => Hash
9
+
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)
14
+ end
15
+
16
+ def time
17
+ read_attribute(:time).to_i
18
+ end
19
+
20
+ def [](key)
21
+ send(key)
22
+ end
23
+
24
+ def method_missing(method)
25
+ data[method.to_s]
26
+ end
27
+
28
+ end
@@ -0,0 +1,2 @@
1
+ class FnordMetric::FunnelWidget < FnordMetric::Widget
2
+ end
@@ -0,0 +1,80 @@
1
+ class FnordMetric::Metric
2
+
3
+ METRIC_TYPES = %w(count average sum combine)
4
+
5
+ def self.from_options(options)
6
+ if (klass_name = METRIC_TYPES.detect{ |n| !!options[n.intern] })
7
+ klass = "FnordMetric::#{klass_name.classify}Metric".constantize
8
+ return klass.new(options)
9
+ end
10
+ raise "please provide one of these options: average, sum, count, combine"
11
+ end
12
+
13
+ def initialize(options)
14
+ @options = options
15
+ end
16
+
17
+ def current
18
+ self.at(Time.now)
19
+ end
20
+
21
+ def at(time_or_range)
22
+ if cache_this?(time_or_range) && (_v=try_cache(time_or_range))
23
+ _v # cache hit
24
+ else # cache miss
25
+ value_at(time_or_range).tap do |_v|
26
+ store_cache(time_or_range, _v) if cache_this?(time_or_range)
27
+ end
28
+ end
29
+ end
30
+
31
+ def token
32
+ @options[:name]
33
+ end
34
+
35
+ def events
36
+ _events = FnordMetric::Event
37
+ if @options[:types]
38
+ _events = _events.where(:type.in => [@options[:types]].flatten)
39
+ end
40
+ _events
41
+ end
42
+
43
+ def events_at(time_or_range)
44
+ if time_or_range.is_a?(Range)
45
+ events.where(:time.lt => time_or_range.last.to_i).where(:time.gt => time_or_range.first.to_i)
46
+ else
47
+ events.where(:time.lt => time_or_range.to_i)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def value_at(time_or_range)
54
+ raise "implemented in subclass"
55
+ end
56
+
57
+ def cache_this?(time_or_range)
58
+ ((!time_or_range.is_a?(Range) && time_or_range.to_i < Time.now.to_i) ||
59
+ (time_or_range.is_a?(Range) && time_or_range.last.to_i < Time.now.to_i))
60
+ end
61
+
62
+ def try_cache(time_or_range)
63
+ FnordMetric::Cache.get(cache_key(time_or_range))
64
+ end
65
+
66
+ def store_cache(time_or_range, value)
67
+ FnordMetric::Cache.store!(cache_key(time_or_range), value)
68
+ end
69
+
70
+ def cache_key(time_or_range)
71
+ time_part = if time_or_range.is_a?(Range)
72
+ "r#{time_or_range.first.to_i}-#{time_or_range.last.to_i}"
73
+ else
74
+ "t#{time_or_range.to_i.to_s}"
75
+ end
76
+ [self.token, time_part].join("|")
77
+ end
78
+
79
+
80
+ end
@@ -0,0 +1,37 @@
1
+ class FnordMetric::MetricAPI
2
+
3
+ def initialize(params)
4
+ @params = params.to_options
5
+ @metric = FnordMetric.metrics.to_options[params[:name].to_sym]
6
+ end
7
+
8
+ def render
9
+ return {:error => "metric not found"}.to_json unless @metric
10
+ data = if @params[:at] && @params[:at] =~ /^[0-9]+$/
11
+ { :value => @metric.at(Time.at(@params[:at].to_i)) }
12
+ elsif @params[:at] && @params[:at] =~ /^([0-9]+)-([0-9]+)$/ && @params[:tick]
13
+ { :values => tick_data }
14
+ elsif @params[:at] && @params[:at] =~ /^([0-9]+)-([0-9]+)$/
15
+ { :value => range_data }
16
+ else
17
+ { :value => @metric.at(Time.now) }
18
+ end
19
+ data.to_json
20
+ end
21
+
22
+ private
23
+
24
+ def range_data
25
+ start_ts, end_ts = @params[:at].split("-").map(&:to_i)
26
+ @metric.at(start_ts..end_ts)
27
+ end
28
+
29
+ def tick_data(tick_length=@params[:tick].to_i)
30
+ start_ts, end_ts = @params[:at].split("-").map(&:to_i)
31
+ ticks = (start_ts..end_ts).step(tick_length)
32
+ ticks.map do |tick|
33
+ [tick, @metric.at(@params[:delta] ? (tick..tick+tick_length) : tick)]
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,18 @@
1
+ class FnordMetric::NumbersWidget < FnordMetric::Widget
2
+
3
+ def data
4
+ super.merge(
5
+ :autoupdate => !!@options[:autoupdate],
6
+ :round_to => @options[:round_to]||2,
7
+ :metrics => metrics.map{ |m| m.token },
8
+ :intervals => {
9
+ "Current" => "",
10
+ "Today" => "at=#{1.day.ago.to_i}-#{Time.now.to_i}",
11
+ "Month" => "at=#{30.days.ago.to_i}-#{Time.now.to_i}"
12
+ },
13
+ :widget_url => "/fnordmetric/widget_numbers.js?#{(rand*999).to_i}",
14
+ :widget_height => 100
15
+ )
16
+ end
17
+
18
+ end
@@ -0,0 +1,29 @@
1
+ class FnordMetric::Report
2
+
3
+ attr_accessor :events, :metrics
4
+
5
+ def initialize(_metrics, options)
6
+ @options = options
7
+ @metrics = Hash.new
8
+ @events = Array.new
9
+ _metrics.each{ |k,m| self.add_metric!(m) }
10
+ end
11
+
12
+ def add_metric!(metric)
13
+ @metrics[metric.token] = metric
14
+ add_helper_methods(metric)
15
+ end
16
+
17
+ def metaclass
18
+ class << self; self; end
19
+ end
20
+
21
+ private
22
+
23
+ def add_helper_methods(metric)
24
+ self.metaclass.send(:define_method, metric.token) do
25
+ @metrics[metric.token]
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,13 @@
1
+ class FnordMetric::SumMetric < FnordMetric::Metric
2
+
3
+ private
4
+
5
+ def value_at(time_or_range)
6
+ # FIXME: value_at(my_time) is really slow, because it has to fetch all events that
7
+ # happened until that time (and this is not even cached, when time is in the future
8
+ # or just now), so we should redirect the call to something like:
9
+ # value_at(last_cache_since_my_time) + value_at(last_cache_since_my_time..my_time)
10
+ events_at(time_or_range).sum(:"data.#{@options[:sum]}")
11
+ end
12
+
13
+ end
@@ -0,0 +1,17 @@
1
+ class FnordMetric::TimelineWidget < FnordMetric::Widget
2
+
3
+ def data
4
+ super.merge(
5
+ :start_timestamp => range_to_i.first,
6
+ :end_timestamp => range_to_i.last,
7
+ :tick => tick.to_i,
8
+ :delta => !!@options[:delta],
9
+ :autoupdate => !!@options[:autoupdate],
10
+ :metrics => metrics.map{ |m| m.token },
11
+ :widget_url => "/fnordmetric/widget_timeline.js?#{(rand*999).to_i}",
12
+ :chart_type => (@options[:chart] || "line"),
13
+ :widget_height => 320
14
+ )
15
+ end
16
+
17
+ end
@@ -0,0 +1,75 @@
1
+ class FnordMetric::Widget
2
+
3
+ attr_accessor :report
4
+
5
+ def initialize(options={})
6
+ @options = options
7
+ add_report(@options[:report]) if @options[:report]
8
+ end
9
+
10
+ def title
11
+ @options[:title]
12
+ end
13
+
14
+ def metrics
15
+ [@options[:metrics]].flatten.map{ |m|
16
+ m.is_a?(FnordMetric::Metric) ? m : FnordMetric.metrics.fetch(m)
17
+ }
18
+ end
19
+
20
+ def tick
21
+ @options[:tick] || 1.day
22
+ end
23
+
24
+ def range
25
+ @options[:range] || default_range
26
+ end
27
+
28
+ def range_to_i
29
+ (range.first.to_i..range.last.to_i)
30
+ end
31
+
32
+ def ticks
33
+ range_to_i.step(tick.to_i).map{ |ts|
34
+ (Time.at(ts)..Time.at(ts+tick.to_i))
35
+ }.compact
36
+ end
37
+
38
+ def default_range(now=Time.now)
39
+ if tick.to_i == 1.day.to_i
40
+ now = now+1.day
41
+ te = Time.utc(now.year, now.month, now.day)
42
+ (te-30.days)..(te-1.second)
43
+ elsif tick.to_i == 1.hour.to_i
44
+ te = Time.utc(now.year, now.month, now.day, now.hour)
45
+ (te-24.hours)..(te-1.second)
46
+ else
47
+ (now-(tick*30))..now
48
+ end
49
+ end
50
+
51
+ def add_report(report)
52
+ @report = report
53
+ end
54
+
55
+ def data
56
+ { :title => @options[:title] }
57
+ end
58
+
59
+ def data_json
60
+ data.to_json.gsub('"', '\'')
61
+ end
62
+
63
+ def include_current?
64
+ !(@options[:current] == false)
65
+ end
66
+
67
+ def render(elem_id)
68
+ %Q{
69
+ <script type='text/javascript'>
70
+ FnordMetric.render('#{elem_id}', #{data_json});
71
+ </script>
72
+ }
73
+ end
74
+
75
+ end