active_metric 2.5.2
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +5 -0
- data/Rakefile +37 -0
- data/lib/active_metric.rb +38 -0
- data/lib/active_metric/axis.rb +11 -0
- data/lib/active_metric/behavior/calculates_derivative.rb +18 -0
- data/lib/active_metric/behavior/graph_calculation.rb +78 -0
- data/lib/active_metric/calculators/reservoir.rb +81 -0
- data/lib/active_metric/calculators/standard_deviator.rb +39 -0
- data/lib/active_metric/config/initializers/inflections.rb +4 -0
- data/lib/active_metric/graph_view_model.rb +60 -0
- data/lib/active_metric/measurement.rb +13 -0
- data/lib/active_metric/point_series_data.rb +17 -0
- data/lib/active_metric/report.rb +57 -0
- data/lib/active_metric/report_view_model.rb +106 -0
- data/lib/active_metric/sample.rb +148 -0
- data/lib/active_metric/series_data.rb +26 -0
- data/lib/active_metric/stat.rb +72 -0
- data/lib/active_metric/stat_definition.rb +20 -0
- data/lib/active_metric/statistics/defaults.rb +157 -0
- data/lib/active_metric/statistics/standard_deviation.rb +43 -0
- data/lib/active_metric/subject.rb +117 -0
- data/lib/active_metric/version.rb +3 -0
- data/test/active_metric_test.rb +30 -0
- data/test/axis_test.rb +22 -0
- data/test/behavior_tests/calculates_derivative_test.rb +35 -0
- data/test/behavior_tests/graph_calculation_test.rb +68 -0
- data/test/config/mongoid.yml +13 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +18597 -0
- data/test/graph_view_model_test.rb +92 -0
- data/test/integration_test.rb +149 -0
- data/test/measurement_test.rb +45 -0
- data/test/mongoid_test.rb +24 -0
- data/test/point_series_data_test.rb +27 -0
- data/test/report_test.rb +73 -0
- data/test/report_view_model_test.rb +94 -0
- data/test/reservoir_test.rb +67 -0
- data/test/sample_test.rb +142 -0
- data/test/series_data_test.rb +20 -0
- data/test/standard_deviator_test.rb +45 -0
- data/test/stat_test.rb +222 -0
- data/test/subject_test.rb +22 -0
- data/test/test_helper.rb +76 -0
- metadata +123 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
class ReportViewModel
|
3
|
+
attr_reader :tables
|
4
|
+
|
5
|
+
class TableDoesNotExist < Exception; end
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@tables = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.table(table_name)
|
12
|
+
table_templates[table_name] = @current_template = TableTemplate.new(table_name)
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.table_templates
|
17
|
+
@table_templates ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.column(header, field, format_options = {})
|
21
|
+
@current_template.add_column(header,field,format_options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_table(table_name, table_data, options = {})
|
25
|
+
template = self.class.table_templates[table_name]
|
26
|
+
raise TableDoesNotExist unless template
|
27
|
+
tables << TableViewModel.new(template, table_data, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
class TableTemplate
|
31
|
+
attr_reader :name
|
32
|
+
attr_reader :columns
|
33
|
+
|
34
|
+
def initialize(name)
|
35
|
+
@name = name
|
36
|
+
@columns = []
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_column(header,field, format_options)
|
40
|
+
@columns << ColumnTemplate.new(header, field, format_options)
|
41
|
+
end
|
42
|
+
|
43
|
+
def headers
|
44
|
+
columns.map(&:header)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class ColumnTemplate
|
50
|
+
attr_reader :header
|
51
|
+
attr_reader :field
|
52
|
+
attr_reader :format_options
|
53
|
+
|
54
|
+
def initialize(header, field, format_options)
|
55
|
+
@header = header
|
56
|
+
@field = field
|
57
|
+
@format_options = format_options
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
class TableViewModel
|
64
|
+
attr_reader :title
|
65
|
+
attr_reader :rows
|
66
|
+
attr_reader :headers
|
67
|
+
|
68
|
+
def initialize(template, table_data, options)
|
69
|
+
@title = options[:title]
|
70
|
+
@rows = []
|
71
|
+
@headers = template.headers
|
72
|
+
table_data.each do |row_data|
|
73
|
+
@rows << RowViewModel.new(row_data, template.columns)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class RowViewModel
|
79
|
+
attr_reader :cells
|
80
|
+
attr_reader :row_id
|
81
|
+
attr_reader :has_series
|
82
|
+
|
83
|
+
def initialize(row_data, columns)
|
84
|
+
@cells = []
|
85
|
+
@row_id = row_data.to_param
|
86
|
+
@has_series = row_data.has_graph_data
|
87
|
+
columns.each do |col|
|
88
|
+
value = row_data.send(col.field)
|
89
|
+
cells << CellViewModel.new(value, col.format_options)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
class CellViewModel
|
96
|
+
attr_reader :value
|
97
|
+
attr_reader :format_options
|
98
|
+
|
99
|
+
def initialize(value, format_options)
|
100
|
+
@value = value
|
101
|
+
@format_options = format_options
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
|
3
|
+
class Sample
|
4
|
+
include Mongoid::Document
|
5
|
+
|
6
|
+
belongs_to :samplable, :polymorphic => true, index: true
|
7
|
+
|
8
|
+
embeds_many :stats, :as => :calculable
|
9
|
+
|
10
|
+
attr_accessor :seed_measurement, :latest_measurement
|
11
|
+
|
12
|
+
field :interval, :type => Integer, :default => nil
|
13
|
+
field :start_time, :type => Integer
|
14
|
+
field :end_time, :type => Integer
|
15
|
+
field :timestamp, :type => Integer
|
16
|
+
field :measurement_count, :type => Integer, :default => 0
|
17
|
+
field :sum, :type => Integer, :default => 0
|
18
|
+
field :sample_index, :type => Integer
|
19
|
+
|
20
|
+
def initialize(attr = nil, options = nil, measurement = nil, index = 0)
|
21
|
+
@seed_measurement = measurement
|
22
|
+
@latest_measurement = nil
|
23
|
+
super(attr, options)
|
24
|
+
self.sample_index = index
|
25
|
+
if stats.empty?
|
26
|
+
self.stats = self.class.stats_defined.map(&:create_stat)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(method, *args)
|
31
|
+
self.class.send(:define_method, method.to_sym) { get_stat_by_name(method) }
|
32
|
+
get_stat_by_name(method)
|
33
|
+
end
|
34
|
+
|
35
|
+
def calculate(measurement)
|
36
|
+
set_start_time(measurement)
|
37
|
+
@latest_measurement = measurement
|
38
|
+
update_time(measurement)
|
39
|
+
update_stats(measurement)
|
40
|
+
end
|
41
|
+
|
42
|
+
def complete
|
43
|
+
return false if measurement_count < 1
|
44
|
+
self.stats.each do |statistic|
|
45
|
+
statistic.complete
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def duration_in_seconds
|
50
|
+
return end_time - start_time if end_time && start_time
|
51
|
+
return 0
|
52
|
+
end
|
53
|
+
|
54
|
+
def duration_from_previous_sample_in_seconds
|
55
|
+
return duration_in_seconds unless seed_measurement
|
56
|
+
end_time - seed_measurement.timestamp
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_stat_by_name(name_of_stat)
|
60
|
+
stats_by_name[name_of_stat] || raw_stat
|
61
|
+
end
|
62
|
+
|
63
|
+
def stats_by_name
|
64
|
+
@stats_by_name ||= generate_stats_by_name
|
65
|
+
end
|
66
|
+
|
67
|
+
def is_summary?
|
68
|
+
!interval
|
69
|
+
end
|
70
|
+
|
71
|
+
def within_interval?(measurement)
|
72
|
+
return true if is_summary?
|
73
|
+
return true unless self.start_time
|
74
|
+
(measurement.timestamp - self.start_time) < self.interval
|
75
|
+
end
|
76
|
+
|
77
|
+
def new_sample
|
78
|
+
self.class.new({:samplable => self.samplable, :interval => interval}, {}, @latest_measurement, sample_index + 1)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def set_start_time(measurement)
|
84
|
+
unless start_time
|
85
|
+
self.start_time = measurement.timestamp
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def generate_stats_by_name
|
90
|
+
stat_name_hash = {}
|
91
|
+
stats.each do |stat|
|
92
|
+
stat_name_hash[stat.access_name] = stat
|
93
|
+
end
|
94
|
+
stat_name_hash
|
95
|
+
end
|
96
|
+
|
97
|
+
def update_time(measurement)
|
98
|
+
self.sum += measurement.timestamp
|
99
|
+
self.measurement_count += 1
|
100
|
+
self.end_time = measurement.timestamp
|
101
|
+
self.timestamp = (self.sum / self.measurement_count).to_i
|
102
|
+
end
|
103
|
+
|
104
|
+
def update_stats(measurement)
|
105
|
+
self.stats.each do |stat|
|
106
|
+
stat.calculate(measurement)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.stats_defined
|
111
|
+
@stats_defined ||= []
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.custom_stats_defined
|
115
|
+
@custom_stats_defined ||= []
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.axises_defined
|
119
|
+
@axises_defined ||= []
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.stat(property, stats_to_define = [:min, :mean, :max, :eightieth, :ninety_eighth], options = {})
|
123
|
+
stats_to_define.each do |stat|
|
124
|
+
klass = Stat.class_for(stat)
|
125
|
+
access_name = klass.access_name(property)
|
126
|
+
self.stats_defined << StatDefinition.new(property, klass, access_name, options)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.custom_stat(name_of_stat, value_type, default = nil, axis = -1, &block)
|
131
|
+
custom_stat_klass = Stat.create_custom_stat(name_of_stat,
|
132
|
+
value_type,
|
133
|
+
default,
|
134
|
+
block)
|
135
|
+
access_name = custom_stat_klass.access_name
|
136
|
+
options = {axis: axis}
|
137
|
+
self.stats_defined << StatDefinition.new(name_of_stat, custom_stat_klass, access_name, options)
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.axis(options)
|
141
|
+
axises_defined << options
|
142
|
+
end
|
143
|
+
|
144
|
+
def raw_stat
|
145
|
+
Stat.new(:value)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
class SeriesData
|
3
|
+
include Mongoid::Document
|
4
|
+
|
5
|
+
embedded_in :graph_view_model, :class_name => "ActiveMetric::GraphViewModel"
|
6
|
+
|
7
|
+
field :label
|
8
|
+
field :y_axis, :default => 0
|
9
|
+
field :x_axis, :default => 0
|
10
|
+
field :approximation, :default => "high"
|
11
|
+
field :visible, :default => true
|
12
|
+
|
13
|
+
def self.from_stat_definition(stat_definition)
|
14
|
+
series = self.new(label: stat_definition.access_name.to_s)
|
15
|
+
options = stat_definition.options
|
16
|
+
|
17
|
+
series.x_axis = options[:x_axis] if options[:x_axis]
|
18
|
+
series.y_axis = options[:axis] if options[:axis]
|
19
|
+
series.approximation = options[:approximation] if options[:approximation]
|
20
|
+
series.visible = options[:visible] unless options[:visible].nil?
|
21
|
+
|
22
|
+
series
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
class CannotInstantiateBaseStat < Exception;
|
3
|
+
end
|
4
|
+
|
5
|
+
class Stat
|
6
|
+
include Mongoid::Document
|
7
|
+
|
8
|
+
embedded_in :calculable, :polymorphic => true
|
9
|
+
field :value, :type => Float, :default => 0
|
10
|
+
field :property, :type => String
|
11
|
+
field :axis, :type => Integer, :default => 0
|
12
|
+
|
13
|
+
def initialize(property, *args)
|
14
|
+
super(*args)
|
15
|
+
self.property = property
|
16
|
+
end
|
17
|
+
|
18
|
+
def access_name
|
19
|
+
self.class.access_name(property)
|
20
|
+
end
|
21
|
+
|
22
|
+
def calculate(measurement)
|
23
|
+
raise CannotInstantiateBaseStat
|
24
|
+
end
|
25
|
+
|
26
|
+
def complete
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.class_for(stat)
|
30
|
+
eval(stat.to_s.classify)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.access_name(property = nil)
|
34
|
+
title = name.split("::").last.underscore
|
35
|
+
title += "_#{property}" if property
|
36
|
+
title.to_sym
|
37
|
+
end
|
38
|
+
|
39
|
+
#TODO Make custom classes namespaced to where they are being defined
|
40
|
+
def self.create_custom_stat(name_of_stat, value_type, default, calculate_block)
|
41
|
+
class_name = name_of_stat.to_s.camelcase
|
42
|
+
if ActiveMetric.const_defined?(class_name)
|
43
|
+
ActiveMetric.logger.warn "#{class_name} is already defined. It won't be defined again."
|
44
|
+
return ActiveMetric.const_get(class_name)
|
45
|
+
end
|
46
|
+
klass = Class.new(Custom) do
|
47
|
+
define_method(:calculate, calculate_block)
|
48
|
+
end
|
49
|
+
klass.send(:field, :value, :type => value_type, :default => default)
|
50
|
+
ActiveMetric.const_set(class_name, klass)
|
51
|
+
return klass
|
52
|
+
end
|
53
|
+
|
54
|
+
def subject
|
55
|
+
self.calculable.samplable
|
56
|
+
end
|
57
|
+
|
58
|
+
def property_from(measurement)
|
59
|
+
return nil unless measurement
|
60
|
+
measurement.send(self.property)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
class Custom < Stat
|
66
|
+
|
67
|
+
def access_name
|
68
|
+
self.class.access_name
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
class StatDefinition
|
3
|
+
|
4
|
+
attr_reader :name_of_stat, :klass, :access_name, :options
|
5
|
+
|
6
|
+
def initialize(name_of_stat, klass, access_name, options)
|
7
|
+
@name_of_stat, @klass, @access_name, @options = name_of_stat, klass, access_name, options
|
8
|
+
@options[:axis] ||= 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_stat
|
12
|
+
klass.new(name_of_stat)
|
13
|
+
end
|
14
|
+
|
15
|
+
def graphable?
|
16
|
+
options[:axis] >= 0
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
class Min < Stat
|
3
|
+
field :value, :type => Float, :default => (1 << 62)
|
4
|
+
|
5
|
+
def calculate(measurement)
|
6
|
+
self.value = [self.value, measurement.send(self.property)].min
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Max < Stat
|
11
|
+
def calculate(measurement)
|
12
|
+
self.value = [self.value, measurement.send(self.property)].max
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Mean < Stat
|
17
|
+
field :sum, :type => Float, :default => 0.0
|
18
|
+
field :count, :type => Integer, :default => 0
|
19
|
+
|
20
|
+
def calculate(measurement)
|
21
|
+
self.count += 1
|
22
|
+
self.sum += measurement.send(self.property)
|
23
|
+
end
|
24
|
+
|
25
|
+
def complete
|
26
|
+
self.value = (self.sum.to_f / self.count)
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Derivative < Stat
|
32
|
+
include CalculatesDerivative
|
33
|
+
field :first
|
34
|
+
field :last
|
35
|
+
|
36
|
+
def calculate(measurement)
|
37
|
+
self.last = property_from(measurement)
|
38
|
+
self.first ||= (property_from(calculable.seed_measurement) || self.last)
|
39
|
+
self.value = derivative_from_seed_measurement(first, last)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
class Speed < Stat
|
45
|
+
include CalculatesDerivative
|
46
|
+
field :count, :type => Integer, :default => 0
|
47
|
+
|
48
|
+
def calculate(measurement)
|
49
|
+
self.count +=1
|
50
|
+
self.value = derivative_from_seed_measurement(0, count)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Bucket < Stat
|
55
|
+
field :value, :type => Hash, :default => {}
|
56
|
+
|
57
|
+
MONGO_UNSAFE = /\.|\$/
|
58
|
+
|
59
|
+
def calculate(measurement)
|
60
|
+
key = property_from(measurement).to_s.gsub(MONGO_UNSAFE, "_")
|
61
|
+
self.value[key] ||= 0
|
62
|
+
self.value[key] += 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class LastDerivative < Derivative
|
67
|
+
field :previous_timestamp
|
68
|
+
|
69
|
+
def calculate(measurement)
|
70
|
+
self.first = (self.last || property_from(calculable.seed_measurement) || property_from(measurement))
|
71
|
+
|
72
|
+
duration = measurement.timestamp - (self.previous_timestamp || measurement.timestamp)
|
73
|
+
self.previous_timestamp = measurement.timestamp
|
74
|
+
|
75
|
+
self.last = property_from(measurement)
|
76
|
+
self.value = calculate_derivative(first, last, duration)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
class Delta < Stat
|
82
|
+
field :first
|
83
|
+
|
84
|
+
def calculate(measurement)
|
85
|
+
|
86
|
+
seed_value = property_from(calculable.seed_measurement)
|
87
|
+
current_value = property_from(measurement)
|
88
|
+
|
89
|
+
self.first ||= (seed_value || current_value)
|
90
|
+
|
91
|
+
self.value = current_value - first
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class Sum < Stat
|
96
|
+
def calculate(measurement)
|
97
|
+
self.value += measurement.send(self.property)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Eightieth < Stat
|
102
|
+
def calculate(measurement)
|
103
|
+
end
|
104
|
+
|
105
|
+
def complete
|
106
|
+
self.value = subject.reservoir.calculate_percentile(0.8, self.property)
|
107
|
+
super
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class NinetyEighth < Stat
|
112
|
+
def calculate(measurement)
|
113
|
+
end
|
114
|
+
|
115
|
+
def complete
|
116
|
+
self.value = subject.reservoir.calculate_percentile(0.98, self.property)
|
117
|
+
super
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Last < Stat
|
122
|
+
def calculate(measurement)
|
123
|
+
self.value = measurement.send(self.property)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class Count < Stat
|
128
|
+
def calculate(measurement)
|
129
|
+
self.value += 1
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class TrueCount < Stat
|
134
|
+
def calculate(measurement)
|
135
|
+
self.value +=1 if measurement.send(self.property)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class FalseCount < Stat
|
140
|
+
def calculate(measurement)
|
141
|
+
self.value +=1 unless measurement.send(self.property)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class PercentFalse < Stat
|
146
|
+
field :failures, :type => Float, default: 0.0
|
147
|
+
field :total, :type => Float, default: 0.0
|
148
|
+
|
149
|
+
def calculate(measurement)
|
150
|
+
boolean = !! measurement.send(self.property)
|
151
|
+
self.total += 1
|
152
|
+
self.failures += 1 unless boolean
|
153
|
+
self.value = (failures / total) * 100
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|