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.
Files changed (45) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +5 -0
  3. data/Rakefile +37 -0
  4. data/lib/active_metric.rb +38 -0
  5. data/lib/active_metric/axis.rb +11 -0
  6. data/lib/active_metric/behavior/calculates_derivative.rb +18 -0
  7. data/lib/active_metric/behavior/graph_calculation.rb +78 -0
  8. data/lib/active_metric/calculators/reservoir.rb +81 -0
  9. data/lib/active_metric/calculators/standard_deviator.rb +39 -0
  10. data/lib/active_metric/config/initializers/inflections.rb +4 -0
  11. data/lib/active_metric/graph_view_model.rb +60 -0
  12. data/lib/active_metric/measurement.rb +13 -0
  13. data/lib/active_metric/point_series_data.rb +17 -0
  14. data/lib/active_metric/report.rb +57 -0
  15. data/lib/active_metric/report_view_model.rb +106 -0
  16. data/lib/active_metric/sample.rb +148 -0
  17. data/lib/active_metric/series_data.rb +26 -0
  18. data/lib/active_metric/stat.rb +72 -0
  19. data/lib/active_metric/stat_definition.rb +20 -0
  20. data/lib/active_metric/statistics/defaults.rb +157 -0
  21. data/lib/active_metric/statistics/standard_deviation.rb +43 -0
  22. data/lib/active_metric/subject.rb +117 -0
  23. data/lib/active_metric/version.rb +3 -0
  24. data/test/active_metric_test.rb +30 -0
  25. data/test/axis_test.rb +22 -0
  26. data/test/behavior_tests/calculates_derivative_test.rb +35 -0
  27. data/test/behavior_tests/graph_calculation_test.rb +68 -0
  28. data/test/config/mongoid.yml +13 -0
  29. data/test/dummy/db/test.sqlite3 +0 -0
  30. data/test/dummy/log/test.log +18597 -0
  31. data/test/graph_view_model_test.rb +92 -0
  32. data/test/integration_test.rb +149 -0
  33. data/test/measurement_test.rb +45 -0
  34. data/test/mongoid_test.rb +24 -0
  35. data/test/point_series_data_test.rb +27 -0
  36. data/test/report_test.rb +73 -0
  37. data/test/report_view_model_test.rb +94 -0
  38. data/test/reservoir_test.rb +67 -0
  39. data/test/sample_test.rb +142 -0
  40. data/test/series_data_test.rb +20 -0
  41. data/test/standard_deviator_test.rb +45 -0
  42. data/test/stat_test.rb +222 -0
  43. data/test/subject_test.rb +22 -0
  44. data/test/test_helper.rb +76 -0
  45. 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