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
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
|