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.
- data/.document +5 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +69 -0
- data/Procfile +2 -0
- data/Rakefile +28 -0
- data/VERSION +1 -0
- data/doc/example_server.rb +56 -0
- data/fnordmetric.gemspec +145 -0
- data/haml/app.haml +48 -0
- data/haml/widget.haml +9 -0
- data/lib/fnordmetric.rb +21 -0
- data/lib/fnordmetric/app.rb +70 -0
- data/lib/fnordmetric/average_metric.rb +7 -0
- data/lib/fnordmetric/cache.rb +20 -0
- data/lib/fnordmetric/combine_metric.rb +7 -0
- data/lib/fnordmetric/core.rb +66 -0
- data/lib/fnordmetric/count_metric.rb +13 -0
- data/lib/fnordmetric/dashboard.rb +30 -0
- data/lib/fnordmetric/engine.rb +3 -0
- data/lib/fnordmetric/event.rb +28 -0
- data/lib/fnordmetric/funnel_widget.rb +2 -0
- data/lib/fnordmetric/metric.rb +80 -0
- data/lib/fnordmetric/metric_api.rb +37 -0
- data/lib/fnordmetric/numbers_widget.rb +18 -0
- data/lib/fnordmetric/report.rb +29 -0
- data/lib/fnordmetric/sum_metric.rb +13 -0
- data/lib/fnordmetric/timeline_widget.rb +17 -0
- data/lib/fnordmetric/widget.rb +75 -0
- data/pub/fnordmetric/fnordmetric.css +53 -0
- data/pub/fnordmetric/fnordmetric.js +44 -0
- data/pub/fnordmetric/widget_numbers.js +71 -0
- data/pub/fnordmetric/widget_timeline.css +0 -0
- data/pub/fnordmetric/widget_timeline.js +110 -0
- data/pub/highcharts/adapters/mootools-adapter.js +12 -0
- data/pub/highcharts/adapters/mootools-adapter.src.js +243 -0
- data/pub/highcharts/adapters/prototype-adapter.js +14 -0
- data/pub/highcharts/adapters/prototype-adapter.src.js +284 -0
- data/pub/highcharts/highcharts.js +170 -0
- data/pub/highcharts/highcharts.src.js +11103 -0
- data/pub/highcharts/modules/exporting.js +22 -0
- data/pub/highcharts/modules/exporting.src.js +703 -0
- data/pub/highcharts/themes/dark-blue.js +268 -0
- data/pub/highcharts/themes/dark-green.js +268 -0
- data/pub/highcharts/themes/gray.js +262 -0
- data/pub/highcharts/themes/grid.js +97 -0
- data/pub/jquery-1.6.1.min.js +18 -0
- data/pub/sprite.png +0 -0
- data/readme.rdoc +274 -0
- data/spec/app_spec.rb +178 -0
- data/spec/cache_spec.rb +53 -0
- data/spec/combine_metric_spec.rb +19 -0
- data/spec/core_spec.rb +50 -0
- data/spec/count_metric_spec.rb +32 -0
- data/spec/dashboard_spec.rb +67 -0
- data/spec/event_spec.rb +46 -0
- data/spec/metric_spec.rb +118 -0
- data/spec/report_spec.rb +87 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/sum_metric_spec.rb +33 -0
- data/spec/widget_spec.rb +107 -0
- metadata +271 -0
@@ -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,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,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,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
|