active_metric 2.5.2

Sign up to get free protection for your applications and to get access to all the features.
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