active_metric 2.5.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/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
|