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
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2011 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'ActiveMetric'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
Bundler::GemHelper.install_tasks
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
30
|
+
t.libs << 'lib'
|
31
|
+
t.libs << 'test'
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
33
|
+
t.verbose = false
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
task :default => :test
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'mongoid'
|
2
|
+
require 'active_support/test_case'
|
3
|
+
|
4
|
+
require 'active_metric/behavior/graph_calculation'
|
5
|
+
require 'active_metric/behavior/calculates_derivative'
|
6
|
+
require 'active_metric/calculators/standard_deviator'
|
7
|
+
|
8
|
+
require 'active_metric/subject'
|
9
|
+
require 'active_metric/measurement'
|
10
|
+
require 'active_metric/sample'
|
11
|
+
require 'active_metric/stat'
|
12
|
+
require 'active_metric/statistics/defaults'
|
13
|
+
require 'active_metric/statistics/standard_deviation'
|
14
|
+
require 'active_metric/report'
|
15
|
+
require 'active_metric/calculators/reservoir'
|
16
|
+
require 'active_metric/report_view_model'
|
17
|
+
require 'active_metric/graph_view_model'
|
18
|
+
require 'active_metric/series_data'
|
19
|
+
require 'active_metric/point_series_data'
|
20
|
+
require 'active_metric/axis'
|
21
|
+
require 'active_metric/stat_definition'
|
22
|
+
|
23
|
+
module ActiveMetric
|
24
|
+
CONFIG_PATH = File.join(File.dirname(__FILE__),"active_metric/config")
|
25
|
+
|
26
|
+
def self.logger
|
27
|
+
@logger ||= Logger.new(STDOUT)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.logger=(new_logger)
|
31
|
+
@logger = new_logger
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
Dir.glob("#{File.dirname(__FILE__)}/active_metric/config/initializers/*").each do |initializer|
|
37
|
+
require initializer
|
38
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
class Axis
|
3
|
+
include Mongoid::Document
|
4
|
+
|
5
|
+
embedded_in :graph_view_model, :class_name => "ActiveMetric::GraphViewModel"
|
6
|
+
|
7
|
+
field :label, default: "units", type: String
|
8
|
+
field :min
|
9
|
+
field :index, default: 0, type: Integer
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
module CalculatesDerivative
|
3
|
+
|
4
|
+
def derivative_from_seed_measurement(first, last)
|
5
|
+
duration_from_seed_measurement = calculable.duration_from_previous_sample_in_seconds
|
6
|
+
calculate_derivative(first, last, duration_from_seed_measurement)
|
7
|
+
end
|
8
|
+
|
9
|
+
def calculate_derivative(first, last, duration)
|
10
|
+
if duration > 0
|
11
|
+
(last - first).to_f / duration
|
12
|
+
else
|
13
|
+
0
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
module GraphCalculation
|
3
|
+
#REQUIRES SUMMARY AND INTERVAL SAMPLES AND SERIES DATA
|
4
|
+
MONGO_MAX_LIMIT = (1 << 31) - 1
|
5
|
+
|
6
|
+
def self.included(klass)
|
7
|
+
klass.has_one :graph_view_model,
|
8
|
+
:class_name => "ActiveMetric::GraphViewModel", :autosave => true
|
9
|
+
klass.after_initialize do
|
10
|
+
self.graph_view_model ||= initialize_graph_view_model
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def series
|
15
|
+
return nil unless attributes["series_data"]
|
16
|
+
attributes["series_data"].values
|
17
|
+
end
|
18
|
+
|
19
|
+
def has_graph_data
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def graph_view_model_starting_at(index)
|
24
|
+
GraphViewModel.where(subject_id: id).slice("series_data.data" => [index, MONGO_MAX_LIMIT]).first
|
25
|
+
end
|
26
|
+
|
27
|
+
def update_graph_model(interval_sample)
|
28
|
+
pop_necessary_samples(interval_sample)
|
29
|
+
interval_sample.stats.each do |stat|
|
30
|
+
series = graph_view_model.series_for(stat.access_name.to_s)
|
31
|
+
series.push_data([time(interval_sample.timestamp), stat.value]) if series && interval_sample.timestamp && start_time
|
32
|
+
end
|
33
|
+
|
34
|
+
self.save!
|
35
|
+
end
|
36
|
+
|
37
|
+
def pop_necessary_samples(interval_sample)
|
38
|
+
data_to_pop = calculate_data_to_pop(interval_sample)
|
39
|
+
pop_from_series(data_to_pop) if data_to_pop > 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def calculate_data_to_pop(interval_sample)
|
43
|
+
incoming_index = interval_sample.sample_index
|
44
|
+
next_index = graph_view_model.size
|
45
|
+
next_index - incoming_index
|
46
|
+
end
|
47
|
+
|
48
|
+
def pop_from_series(data_to_pop)
|
49
|
+
graph_view_model.series_data.each do |series|
|
50
|
+
series.pop_data(data_to_pop)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize_graph_view_model
|
55
|
+
if self.class.sample_type
|
56
|
+
axises_defined = self.class.sample_type.axises_defined
|
57
|
+
stats_defined = self.class.sample_type.stats_defined
|
58
|
+
else
|
59
|
+
axises_defined = []
|
60
|
+
stats_defined = []
|
61
|
+
end
|
62
|
+
GraphViewModel.create_from_meta_data(axises_defined, stats_defined, name: name)
|
63
|
+
end
|
64
|
+
|
65
|
+
def time(sample_time)
|
66
|
+
((sample_time - start_time)).to_i
|
67
|
+
end
|
68
|
+
|
69
|
+
def start_time
|
70
|
+
@start_time ||= summary.start_time
|
71
|
+
end
|
72
|
+
|
73
|
+
def debug(message)
|
74
|
+
ActiveMetric.logger.error "DEBUGAM #{self.name} #{message}"
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
class Reservoir
|
3
|
+
|
4
|
+
attr_reader :size
|
5
|
+
attr_accessor :measurements
|
6
|
+
|
7
|
+
def initialize(size)
|
8
|
+
@size = size
|
9
|
+
@measurements = []
|
10
|
+
@current_index = 0
|
11
|
+
@sorted_measurements = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def fill(measurement)
|
15
|
+
if reservoir_full?
|
16
|
+
update_reservoir_at_current_index(measurement) if should_replace_at_current_index?
|
17
|
+
else
|
18
|
+
update_reservoir_at_current_index(measurement)
|
19
|
+
end
|
20
|
+
update_index
|
21
|
+
end
|
22
|
+
|
23
|
+
def calculate_percentile(percentile, metric)
|
24
|
+
return 0 unless size_for_calculation > 0
|
25
|
+
measurements_for_metric = sorted_measurements_for(metric)
|
26
|
+
index = size_for_calculation * percentile
|
27
|
+
measurements_for_metric[index].send(metric.to_sym)
|
28
|
+
end
|
29
|
+
|
30
|
+
def calculate_standard_deviation(metric)
|
31
|
+
return 0 unless size_for_calculation > 0
|
32
|
+
measurements_for_metric = sorted_measurements_for(metric)
|
33
|
+
standard_deviation_on(measurements_for_metric, metric)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def standard_deviation_on(list, metric)
|
39
|
+
sd = StandardDeviator.new(metric)
|
40
|
+
list.each do |measurement|
|
41
|
+
sd.calculate measurement
|
42
|
+
end
|
43
|
+
sd.standard_deviation
|
44
|
+
end
|
45
|
+
|
46
|
+
def sorted_measurements_for(metric)
|
47
|
+
@sorted_measurements[metric.to_sym] ||= measurements.sort_by(&metric.to_sym)
|
48
|
+
end
|
49
|
+
|
50
|
+
def update_reservoir_at_current_index(measurement)
|
51
|
+
@measurements[@current_index] = measurement
|
52
|
+
@sorted_measurements.clear
|
53
|
+
end
|
54
|
+
|
55
|
+
def should_replace_at_current_index?
|
56
|
+
#chance = (@current_index.to_f / size_for_calculation.to_f) * 100
|
57
|
+
#replace = rand(chance) <= (100.0 / @current_round.to_f)
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
def simulate_chance(chance)
|
62
|
+
rand + (1-chance) >= 1
|
63
|
+
end
|
64
|
+
|
65
|
+
def update_index
|
66
|
+
if @current_index >= size - 1
|
67
|
+
@current_index = 0
|
68
|
+
else
|
69
|
+
@current_index+= 1
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def size_for_calculation
|
74
|
+
reservoir_full? ? size : measurements.size
|
75
|
+
end
|
76
|
+
|
77
|
+
def reservoir_full?
|
78
|
+
size == measurements.size
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
class StandardDeviator
|
3
|
+
attr_reader :count, :sum, :sum_of_squares, :property
|
4
|
+
|
5
|
+
def initialize(property)
|
6
|
+
@count = 0
|
7
|
+
@property = property
|
8
|
+
@sum = 0
|
9
|
+
@sum_of_squares = 0
|
10
|
+
@standard_deviation = 0.0
|
11
|
+
end
|
12
|
+
|
13
|
+
def calculate(measurement)
|
14
|
+
@standard_deviation = nil
|
15
|
+
@count +=1
|
16
|
+
value = measurement.send(property)
|
17
|
+
@sum += value
|
18
|
+
@sum_of_squares += value * value
|
19
|
+
end
|
20
|
+
|
21
|
+
def standard_deviation
|
22
|
+
@standard_deviation ||= calculate_standard_deviation
|
23
|
+
end
|
24
|
+
|
25
|
+
def calculate_standard_deviation
|
26
|
+
diff = mean_squares - (mean * mean)
|
27
|
+
Math.sqrt(diff)
|
28
|
+
end
|
29
|
+
|
30
|
+
def mean
|
31
|
+
sum.to_f / count
|
32
|
+
end
|
33
|
+
|
34
|
+
def mean_squares
|
35
|
+
sum_of_squares.to_f / count
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
|
3
|
+
class GraphViewModel
|
4
|
+
include Mongoid::Document
|
5
|
+
|
6
|
+
belongs_to :subject, :class_name => "ActiveMetric::Subject", :polymorphic => true
|
7
|
+
index({:subject_id => 1},{:background => true})
|
8
|
+
|
9
|
+
field :name
|
10
|
+
embeds_many :series_data, :class_name => "ActiveMetric::SeriesData"
|
11
|
+
embeds_many :y_axises, class_name: "ActiveMetric::Axis"
|
12
|
+
|
13
|
+
def ordered_y_axises
|
14
|
+
y_axises.asc(:index)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.create_from_meta_data(axises_defined,stats_defined, options = {})
|
18
|
+
graph = self.new(options)
|
19
|
+
graph.populate_axises(axises_defined)
|
20
|
+
graph.populate_series(stats_defined)
|
21
|
+
graph.save!
|
22
|
+
graph
|
23
|
+
end
|
24
|
+
|
25
|
+
def populate_axises(axises_defined)
|
26
|
+
axises_defined.each do |axis_options|
|
27
|
+
delete_axis_at_index(axis_options[:index])
|
28
|
+
y_axises << Axis.new(axis_options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def populate_series(stats_defined)
|
33
|
+
stats_defined.each do |stat_definition|
|
34
|
+
series_data << PointSeriesData.from_stat_definition(stat_definition) if stat_definition.graphable?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def series_for(label)
|
39
|
+
series_data.select{|series| series.label.eql? label}.first
|
40
|
+
end
|
41
|
+
|
42
|
+
def size
|
43
|
+
return 0 unless series_data.size > 0
|
44
|
+
series_data.first.size
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete_axis_at_index(index)
|
48
|
+
existing_axises = y_axises.select{|axis| axis.index == index}
|
49
|
+
if existing_axises.size > 0
|
50
|
+
existing_axises.each do |existing_axis|
|
51
|
+
y_axises.delete(existing_axis)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
|
3
|
+
class Measurement
|
4
|
+
include Mongoid::Document
|
5
|
+
|
6
|
+
#belongs_to :report, :class => "ActiveMetric::Report"
|
7
|
+
field :timestamp, :type => Integer, :default => -> {Time.now}
|
8
|
+
|
9
|
+
def time
|
10
|
+
Time.at((timestamp.to_i / 1000).to_i).to_datetime
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
class PointSeriesData < SeriesData
|
3
|
+
|
4
|
+
field :data, :type => Array, :default => []
|
5
|
+
|
6
|
+
delegate :size, :to => :data
|
7
|
+
|
8
|
+
def push_data(value)
|
9
|
+
push_all(:data, [value])
|
10
|
+
end
|
11
|
+
|
12
|
+
def pop_data(data_to_pop = 1)
|
13
|
+
pop(:data,data_to_pop)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ActiveMetric
|
2
|
+
class Report
|
3
|
+
include Mongoid::Document
|
4
|
+
|
5
|
+
before_save :save_display_name
|
6
|
+
|
7
|
+
field :display_name
|
8
|
+
|
9
|
+
has_many :subjects, :class_name => "ActiveMetric::Subject", :dependent => :destroy
|
10
|
+
|
11
|
+
def name
|
12
|
+
"report"
|
13
|
+
end
|
14
|
+
|
15
|
+
def save_display_name
|
16
|
+
self.display_name ||= set_display_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_display_name
|
20
|
+
"Report"
|
21
|
+
end
|
22
|
+
|
23
|
+
def series
|
24
|
+
subjects.map(&:series).flatten
|
25
|
+
end
|
26
|
+
|
27
|
+
def min
|
28
|
+
0
|
29
|
+
end
|
30
|
+
|
31
|
+
def view_model
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def subjects_by_type
|
36
|
+
subjects_type = {}
|
37
|
+
subjects.each do |subject|
|
38
|
+
type = subject.class.name.split("::").last.underscore
|
39
|
+
subjects_type[type] ||= []
|
40
|
+
subjects_type[type] << subject
|
41
|
+
end
|
42
|
+
subjects_type
|
43
|
+
end
|
44
|
+
|
45
|
+
def method_missing(method, *args)
|
46
|
+
subject_class = subject_class_for_report(method)
|
47
|
+
subject_class ? subject_class.where(report_id: id).all :
|
48
|
+
super(method, *args)
|
49
|
+
end
|
50
|
+
|
51
|
+
def subject_class_for_report(method)
|
52
|
+
return "#{self.class.parent}::#{method.to_s.classify}".constantize if method.to_s.match /subjects$/
|
53
|
+
rescue NameError
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|