mouth 0.8.0
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/.gitignore +7 -0
- data/Capfile +26 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +138 -0
- data/Rakefile +19 -0
- data/TODO +32 -0
- data/bin/mouth +77 -0
- data/bin/mouth-console +18 -0
- data/bin/mouth-endoscope +18 -0
- data/lib/mouth.rb +61 -0
- data/lib/mouth/dashboard.rb +25 -0
- data/lib/mouth/endoscope.rb +120 -0
- data/lib/mouth/endoscope/public/222222_256x240_icons_icons.png +0 -0
- data/lib/mouth/endoscope/public/application.css +464 -0
- data/lib/mouth/endoscope/public/application.js +938 -0
- data/lib/mouth/endoscope/public/backbone.js +1158 -0
- data/lib/mouth/endoscope/public/d3.js +4707 -0
- data/lib/mouth/endoscope/public/d3.time.js +687 -0
- data/lib/mouth/endoscope/public/jquery-ui-1.8.16.custom.min.js +177 -0
- data/lib/mouth/endoscope/public/jquery.js +4 -0
- data/lib/mouth/endoscope/public/json2.js +480 -0
- data/lib/mouth/endoscope/public/keymaster.js +163 -0
- data/lib/mouth/endoscope/public/linen.js +46 -0
- data/lib/mouth/endoscope/public/seven.css +68 -0
- data/lib/mouth/endoscope/public/seven.js +291 -0
- data/lib/mouth/endoscope/public/underscore.js +931 -0
- data/lib/mouth/endoscope/views/dashboard.erb +67 -0
- data/lib/mouth/graph.rb +58 -0
- data/lib/mouth/instrument.rb +56 -0
- data/lib/mouth/record.rb +72 -0
- data/lib/mouth/runner.rb +89 -0
- data/lib/mouth/sequence.rb +284 -0
- data/lib/mouth/source.rb +76 -0
- data/lib/mouth/sucker.rb +235 -0
- data/lib/mouth/version.rb +3 -0
- data/mouth.gemspec +28 -0
- data/test/sequence_test.rb +163 -0
- data/test/sucker_test.rb +55 -0
- data/test/test_helper.rb +5 -0
- metadata +167 -0
data/lib/mouth/source.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Mouth
|
|
2
|
+
class Source
|
|
3
|
+
|
|
4
|
+
attr_accessor :all_attributes
|
|
5
|
+
|
|
6
|
+
def initialize(tuple)
|
|
7
|
+
self.all_attributes = tuple
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns an array of tuples:
|
|
11
|
+
# [{source: "auth.inline_logged_in", kind: "counter|timer"}, ...]
|
|
12
|
+
def self.all
|
|
13
|
+
col_names = Mouth.mongo.collections.collect(&:name) - %w(dashboards graphs system.indexes)
|
|
14
|
+
col_names.select! {|c| c =~ /^mouth_.+/ }
|
|
15
|
+
|
|
16
|
+
tuples = []
|
|
17
|
+
col_names.each do |col_name|
|
|
18
|
+
namespace = col_name.match(/mouth_(.+)/)[1]
|
|
19
|
+
counters, timers = all_for_collection(Mouth.collection(col_name))
|
|
20
|
+
counters.each {|s| tuples << {:source => "#{namespace}.#{s}", :kind => "counter"} }
|
|
21
|
+
timers.each {|s| tuples << {:source => "#{namespace}.#{s}", :kind => "timer"} }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
tuples.sort_by {|t| "#{t[:kind]}#{t[:source]}" }.collect {|t| new(t) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.all_for_collection(col, window = Time.now.to_i / 60 - 120)
|
|
28
|
+
map_function = <<-JS
|
|
29
|
+
function() {
|
|
30
|
+
var k, vh;
|
|
31
|
+
for (k in this.c) {
|
|
32
|
+
vh = {};
|
|
33
|
+
vh[k] = true;
|
|
34
|
+
emit("counters", {ks: vh});
|
|
35
|
+
}
|
|
36
|
+
for (k in this.m) {
|
|
37
|
+
vh = {};
|
|
38
|
+
vh[k] = true;
|
|
39
|
+
emit("timers", {ks: vh});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
JS
|
|
43
|
+
|
|
44
|
+
reduce_function = <<-JS
|
|
45
|
+
function(key, values) {
|
|
46
|
+
var ret = {ks: {}}, k;
|
|
47
|
+
|
|
48
|
+
values.forEach(function(value) {
|
|
49
|
+
for (k in value.ks) {
|
|
50
|
+
ret.ks[k] = true
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return ret;
|
|
54
|
+
}
|
|
55
|
+
JS
|
|
56
|
+
|
|
57
|
+
result = col.map_reduce(map_function, reduce_function, :out => {:inline => true}, :raw => true, :query => {"t" => {"$gte" => window}})
|
|
58
|
+
|
|
59
|
+
counters = []
|
|
60
|
+
timers = []
|
|
61
|
+
if result && result["results"].is_a?(Array)
|
|
62
|
+
result["results"].each do |r|
|
|
63
|
+
k, v = r["_id"], r["value"]
|
|
64
|
+
if k == "timers"
|
|
65
|
+
timers << v["ks"].keys
|
|
66
|
+
elsif k == "counters"
|
|
67
|
+
counters << v["ks"].keys
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
[counters.flatten.compact.uniq, timers.flatten.compact.uniq]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/mouth/sucker.rb
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
require 'em-mongo'
|
|
2
|
+
require 'eventmachine'
|
|
3
|
+
|
|
4
|
+
module Mouth
|
|
5
|
+
|
|
6
|
+
class SuckerConnection < EM::Connection
|
|
7
|
+
attr_accessor :sucker
|
|
8
|
+
|
|
9
|
+
def receive_data(data)
|
|
10
|
+
Mouth.logger.debug "UDP packet: '#{data}'"
|
|
11
|
+
|
|
12
|
+
sucker.store!(data)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Sucker
|
|
17
|
+
|
|
18
|
+
# Host/Port to suck UDP packets on
|
|
19
|
+
attr_accessor :host
|
|
20
|
+
attr_accessor :port
|
|
21
|
+
|
|
22
|
+
# Actual EM::Mongo connection
|
|
23
|
+
attr_accessor :mongo_db
|
|
24
|
+
|
|
25
|
+
# Info to connect to mongo
|
|
26
|
+
attr_accessor :mongo_db_name
|
|
27
|
+
attr_accessor :mongo_hosts
|
|
28
|
+
|
|
29
|
+
# Accumulators of our data
|
|
30
|
+
attr_accessor :counters
|
|
31
|
+
attr_accessor :timers
|
|
32
|
+
|
|
33
|
+
# Stats
|
|
34
|
+
attr_accessor :udp_packets_received
|
|
35
|
+
attr_accessor :mongo_flushes
|
|
36
|
+
|
|
37
|
+
def initialize(options = {})
|
|
38
|
+
self.host = options[:host] || "localhost"
|
|
39
|
+
self.port = options[:port] || 8889
|
|
40
|
+
self.mongo_db_name = options[:mongo_db_name] || "mouth"
|
|
41
|
+
self.mongo_hosts = options[:mongo_hosts] || ["localhost"]
|
|
42
|
+
|
|
43
|
+
self.udp_packets_received = 0
|
|
44
|
+
self.mongo_flushes = 0
|
|
45
|
+
|
|
46
|
+
self.counters = {}
|
|
47
|
+
self.timers = {}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def suck!
|
|
51
|
+
EM.run do
|
|
52
|
+
# Connect to mongo now
|
|
53
|
+
self.mongo_db
|
|
54
|
+
|
|
55
|
+
EM.open_datagram_socket host, port, SuckerConnection do |conn|
|
|
56
|
+
conn.sucker = self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
EM.add_periodic_timer(10) do
|
|
60
|
+
Mouth.logger.info "Counters: #{self.counters.inspect}"
|
|
61
|
+
Mouth.logger.info "Timers: #{self.timers.inspect}"
|
|
62
|
+
self.flush!
|
|
63
|
+
self.set_procline!
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
EM.next_tick do
|
|
67
|
+
Mouth.logger.info "Mouth reactor started..."
|
|
68
|
+
self.set_procline!
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# counter: gorets:1|c
|
|
74
|
+
# counter w/ sampling: gorets:1|c|@0.1
|
|
75
|
+
# timer: glork:320|ms
|
|
76
|
+
# (future) gauge: gaugor:333|g
|
|
77
|
+
def store!(data)
|
|
78
|
+
key_value, command_sampling = data.split("|", 2)
|
|
79
|
+
key, value = key_value.to_s.split(":")
|
|
80
|
+
command, sampling = command_sampling.split("|")
|
|
81
|
+
|
|
82
|
+
key = Mouth.parse_key(key).join(".")
|
|
83
|
+
value = value.to_f
|
|
84
|
+
|
|
85
|
+
ts = minute_timestamps
|
|
86
|
+
|
|
87
|
+
if command == "ms"
|
|
88
|
+
self.timers[ts] ||= {}
|
|
89
|
+
self.timers[ts][key] ||= []
|
|
90
|
+
self.timers[ts][key] << value
|
|
91
|
+
elsif command == "c"
|
|
92
|
+
factor = 1.0
|
|
93
|
+
if sampling
|
|
94
|
+
factor = sampling.sub("@", "").to_f
|
|
95
|
+
factor = (factor == 0.0 || factor > 1.0) ? 1.0 : 1.0 / factor
|
|
96
|
+
end
|
|
97
|
+
self.counters[ts] ||= {}
|
|
98
|
+
self.counters[ts][key] ||= 0.0
|
|
99
|
+
self.counters[ts][key] += value * factor
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
self.udp_packets_received += 1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def flush!
|
|
106
|
+
ts = minute_timestamps
|
|
107
|
+
limit_ts = ts - 1
|
|
108
|
+
mongo_docs = {}
|
|
109
|
+
|
|
110
|
+
# We're going to construct mongo_docs which look like this:
|
|
111
|
+
# "mycollections:234234": { # NOTE: this timpstamp will be popped into .t = 234234
|
|
112
|
+
# c: {
|
|
113
|
+
# happenings: 37,
|
|
114
|
+
# affairs: 3
|
|
115
|
+
# },
|
|
116
|
+
# m: {
|
|
117
|
+
# occasions: {...}
|
|
118
|
+
# }
|
|
119
|
+
# }
|
|
120
|
+
|
|
121
|
+
self.counters.each do |cur_ts, counters_to_save|
|
|
122
|
+
if cur_ts <= limit_ts
|
|
123
|
+
counters_to_save.each do |counter_key, value|
|
|
124
|
+
ns, sub_key = Mouth.parse_key(counter_key)
|
|
125
|
+
mongo_key = "#{ns}:#{ts}"
|
|
126
|
+
mongo_docs[mongo_key] ||= {}
|
|
127
|
+
|
|
128
|
+
cur_mongo_doc = mongo_docs[mongo_key]
|
|
129
|
+
cur_mongo_doc["c"] ||= {}
|
|
130
|
+
cur_mongo_doc["c"][sub_key] = value
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
self.counters.delete(cur_ts)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
self.timers.each do |cur_ts, timers_to_save|
|
|
138
|
+
if cur_ts <= limit_ts
|
|
139
|
+
timers_to_save.each do |timer_key, values|
|
|
140
|
+
ns, sub_key = Mouth.parse_key(timer_key)
|
|
141
|
+
mongo_key = "#{ns}:#{ts}"
|
|
142
|
+
mongo_docs[mongo_key] ||= {}
|
|
143
|
+
|
|
144
|
+
cur_mongo_doc = mongo_docs[mongo_key]
|
|
145
|
+
cur_mongo_doc["m"] ||= {}
|
|
146
|
+
cur_mongo_doc["m"][sub_key] = analyze_timer(values)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
self.timers.delete(cur_ts)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
save_documents!(mongo_docs)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def save_documents!(mongo_docs)
|
|
157
|
+
Mouth.logger.info "Saving Docs: #{mongo_docs.inspect}"
|
|
158
|
+
|
|
159
|
+
mongo_docs.each do |key, doc|
|
|
160
|
+
ns, ts = key.split(":")
|
|
161
|
+
collection_name = "mouth_#{ns}"
|
|
162
|
+
doc["t"] = ts.to_i
|
|
163
|
+
|
|
164
|
+
self.mongo_db.collection(collection_name).insert(doc)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
self.mongo_flushes += 1 if mongo_docs.any?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def mongo_db
|
|
171
|
+
@mongo_db ||= begin
|
|
172
|
+
if self.mongo_hosts.length == 1
|
|
173
|
+
EM::Mongo::Connection.new(self.mongo_hosts.first).db(self.mongo_db_name)
|
|
174
|
+
else
|
|
175
|
+
raise "TODO: ability to connect to a replica set."
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def set_procline!
|
|
181
|
+
$0 = "mouth [started] [UDP Recv: #{self.udp_packets_received}] [Mongo saves: #{self.mongo_flushes}]"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def minute_timestamps
|
|
187
|
+
Time.now.to_i / 60
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def analyze_timer(values)
|
|
191
|
+
values.sort!
|
|
192
|
+
|
|
193
|
+
count = values.length
|
|
194
|
+
min = values[0]
|
|
195
|
+
max = values[-1]
|
|
196
|
+
mean = nil
|
|
197
|
+
sum = 0.0
|
|
198
|
+
median = median_for(values)
|
|
199
|
+
stddev = 0.0
|
|
200
|
+
|
|
201
|
+
values.each {|v| sum += v }
|
|
202
|
+
mean = sum / count
|
|
203
|
+
|
|
204
|
+
values.each do |v|
|
|
205
|
+
devi = v - mean
|
|
206
|
+
stddev += (devi * devi)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
stddev = Math.sqrt(stddev / count)
|
|
210
|
+
|
|
211
|
+
{
|
|
212
|
+
"count" => count,
|
|
213
|
+
"min" => min,
|
|
214
|
+
"max" => max,
|
|
215
|
+
"mean" => mean,
|
|
216
|
+
"sum" => sum,
|
|
217
|
+
"median" => median,
|
|
218
|
+
"stddev" => stddev,
|
|
219
|
+
}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def median_for(values)
|
|
223
|
+
count = values.length
|
|
224
|
+
middle = count / 2
|
|
225
|
+
if count == 0
|
|
226
|
+
return 0
|
|
227
|
+
elsif count % 2 == 0
|
|
228
|
+
return (values[middle] + values[middle - 1]).to_f / 2
|
|
229
|
+
else
|
|
230
|
+
return values[middle]
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
end # class Sucker
|
|
235
|
+
end # module
|
data/mouth.gemspec
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require './lib/mouth/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = 'mouth'
|
|
5
|
+
s.version = Mouth::VERSION
|
|
6
|
+
s.author = 'Jonathan Novak'
|
|
7
|
+
s.email = 'jnovak@gmail.com'
|
|
8
|
+
s.homepage = 'http://github.com/cypriss/mouth'
|
|
9
|
+
s.summary = 'Collect and view stats in real time.'
|
|
10
|
+
s.description = 'Ruby Daemon collects statistics via UDP packets, stores them in Mongo, and then views them via a user friendly Sintra app.'
|
|
11
|
+
|
|
12
|
+
s.files = `git ls-files`.split("\n")
|
|
13
|
+
s.test_files = `git ls-files test`.split("\n")
|
|
14
|
+
s.require_path = 'lib'
|
|
15
|
+
s.bindir = 'bin'
|
|
16
|
+
s.executables << 'mouth' << 'mouth-endoscope'
|
|
17
|
+
|
|
18
|
+
s.add_runtime_dependency 'em-mongo', '~> 0.4.2' # For the EM collector
|
|
19
|
+
s.add_runtime_dependency 'mongo', '~> 1.6' # For the sinatra app
|
|
20
|
+
s.add_runtime_dependency 'bson_ext', '~> 1.6'
|
|
21
|
+
s.add_runtime_dependency 'eventmachine', '~> 0.12.10'
|
|
22
|
+
s.add_runtime_dependency 'vegas', '~> 0.1.8'
|
|
23
|
+
s.add_runtime_dependency 'sinatra', '~> 1.3.1'
|
|
24
|
+
s.add_runtime_dependency 'yajl-ruby', '~> 1.1.0'
|
|
25
|
+
|
|
26
|
+
s.required_ruby_version = '>= 1.9.2'
|
|
27
|
+
s.required_rubygems_version = '>= 1.3.4'
|
|
28
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
$LOAD_PATH.unshift 'test'
|
|
2
|
+
require 'test_helper'
|
|
3
|
+
|
|
4
|
+
require 'mouth/sequence'
|
|
5
|
+
|
|
6
|
+
class SequenceTest < Test::Unit::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
@start_time = Time.new(2012, 4, 1, 9, 30, 0, "-07:00")
|
|
9
|
+
@end_time = Time.new(2012, 4, 1, 11, 30, 0, "-07:00")
|
|
10
|
+
@namespace, @metric = Mouth.parse_key("test.test")
|
|
11
|
+
|
|
12
|
+
@start_timestamp = @start_time.to_i / 60
|
|
13
|
+
@end_timestamp = @end_time.to_i / 60
|
|
14
|
+
|
|
15
|
+
counter = 1
|
|
16
|
+
timer = {"count" => 5, "min" => 1, "max" => 10, "mean" => 5, "sum" => 20, "median" => 4, "stddev" => 2.3}
|
|
17
|
+
(@start_timestamp..@end_timestamp).each do |t|
|
|
18
|
+
# Insert the document into mongo
|
|
19
|
+
Mouth.collection(Mouth.mongo_collection_name(@namespace)).update({"t" => t}, {"$set" => {"c.#{@metric}" => counter, "m.#{@metric}" => timer}}, :upsert => true)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_minute_counter_sequences
|
|
24
|
+
seq = Mouth::Sequence.new("#{@namespace}.#{@metric}", :start_time => @start_time, :end_time => @end_time)
|
|
25
|
+
|
|
26
|
+
sequences = seq.sequences
|
|
27
|
+
|
|
28
|
+
assert_equal ["test"], sequences.keys
|
|
29
|
+
assert_equal 1, sequences.values.length
|
|
30
|
+
|
|
31
|
+
values = sequences.values.first
|
|
32
|
+
|
|
33
|
+
assert_equal 121, values.length
|
|
34
|
+
|
|
35
|
+
assert values.all? {|v| v == 1} # The 'test' sequence is all ones
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_15_minute_counter_sequences
|
|
39
|
+
seq = Mouth::Sequence.new("#{@namespace}.#{@metric}", :granularity_in_minutes => 15, :start_time => @start_time, :end_time => @end_time)
|
|
40
|
+
|
|
41
|
+
sequences = seq.sequences
|
|
42
|
+
|
|
43
|
+
assert_equal ["test"], sequences.keys
|
|
44
|
+
assert_equal 1, sequences.values.length
|
|
45
|
+
|
|
46
|
+
values = sequences.values.first
|
|
47
|
+
|
|
48
|
+
assert_equal 9, values.length
|
|
49
|
+
assert_equal [15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 1.0], values
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_15_minute_timer_sequences_basic
|
|
53
|
+
seq = Mouth::Sequence.new("#{@namespace}.#{@metric}", :kind => :timer, :granularity_in_minutes => 15, :start_time => @start_time, :end_time => @end_time)
|
|
54
|
+
|
|
55
|
+
sequences = seq.sequences
|
|
56
|
+
assert_equal ["test"], sequences.keys
|
|
57
|
+
assert_equal 1, sequences.values.length
|
|
58
|
+
|
|
59
|
+
values = sequences.values.first
|
|
60
|
+
|
|
61
|
+
assert_equal 9, values.length
|
|
62
|
+
|
|
63
|
+
timer = {"count"=>75,
|
|
64
|
+
"min"=>1,
|
|
65
|
+
"max"=>10,
|
|
66
|
+
"mean"=>4,
|
|
67
|
+
"sum"=>300,
|
|
68
|
+
"median"=>4,
|
|
69
|
+
"stddev"=>3.780211634287159}
|
|
70
|
+
assert_equal timer, values.first
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_15_minute_timer_sequences_one_grouping
|
|
74
|
+
@start_time = Time.new(2012, 4, 1, 9, 30, 0, "-07:00")
|
|
75
|
+
@end_time = Time.new(2012, 4, 1, 9, 44, 0, "-07:00")
|
|
76
|
+
|
|
77
|
+
@start_timestamp = @start_time.to_i / 60
|
|
78
|
+
@end_timestamp = @end_time.to_i / 60
|
|
79
|
+
|
|
80
|
+
col = Mouth.collection(Mouth.mongo_collection_name("test"))
|
|
81
|
+
col.remove
|
|
82
|
+
|
|
83
|
+
# NOTE: this data is totally fake, maybe we can get some real data
|
|
84
|
+
timers = [
|
|
85
|
+
{"count"=>3, "min"=>15, "max"=>30, "mean"=>100, "sum"=>300, "median"=>4, "stddev"=>1},
|
|
86
|
+
{"count"=>6, "min"=>14, "max"=>32, "mean"=>90, "sum"=>540, "median"=>5, "stddev"=>2},
|
|
87
|
+
{"count"=>9, "min"=>13, "max"=>30, "mean"=>85, "sum"=>765, "median"=>6, "stddev"=>3},
|
|
88
|
+
{"count"=>12, "min"=>12, "max"=>30, "mean"=>80, "sum"=>960, "median"=>7, "stddev"=>4},
|
|
89
|
+
{"count"=>15, "min"=>11, "max"=>30, "mean"=>70, "sum"=>1050, "median"=>8, "stddev"=>5},
|
|
90
|
+
{"count"=>18, "min"=>10, "max"=>30, "mean"=>65, "sum"=>1170, "median"=>9, "stddev"=>6},
|
|
91
|
+
{"count"=>21, "min"=>9, "max"=>30, "mean"=>60, "sum"=>1260, "median"=>10, "stddev"=>7},
|
|
92
|
+
{"count"=>24, "min"=>8, "max"=>30, "mean"=>55, "sum"=>1320, "median"=>11, "stddev"=>8},
|
|
93
|
+
{"count"=>27, "min"=>7, "max"=>30, "mean"=>50, "sum"=>1350, "median"=>12, "stddev"=>9},
|
|
94
|
+
{"count"=>30, "min"=>6, "max"=>30, "mean"=>40, "sum"=>1200, "median"=>13, "stddev"=>10},
|
|
95
|
+
{"count"=>33, "min"=>16, "max"=>30, "mean"=>30, "sum"=>990, "median"=>14, "stddev"=>11},
|
|
96
|
+
{"count"=>36, "min"=>17, "max"=>30, "mean"=>20, "sum"=>720, "median"=>15, "stddev"=>12},
|
|
97
|
+
{"count"=>39, "min"=>18, "max"=>30, "mean"=>10, "sum"=>390, "median"=>16, "stddev"=>13},
|
|
98
|
+
{"count"=>42, "min"=>19, "max"=>30, "mean"=>5, "sum"=>210, "median"=>17, "stddev"=>14},
|
|
99
|
+
{"count"=>45, "min"=>20, "max"=>30, "mean"=>1, "sum"=>45, "median"=>18, "stddev"=>15}
|
|
100
|
+
]
|
|
101
|
+
i = 0
|
|
102
|
+
(@start_timestamp..@end_timestamp).each do |t|
|
|
103
|
+
col.update({"t" => t}, {"$set" => {"m.test" => timers[i]}}, :upsert => true)
|
|
104
|
+
i += 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
seq = Mouth::Sequence.new("test.test", :kind => :timer, :granularity_in_minutes => 15, :start_time => @start_time, :end_time => @end_time)
|
|
108
|
+
values = seq.sequence
|
|
109
|
+
|
|
110
|
+
assert_equal 1, values.length
|
|
111
|
+
|
|
112
|
+
value = values.first
|
|
113
|
+
|
|
114
|
+
expected_count = timers.collect {|t| t["count"] }.inject(0) {|s,c| s + c }
|
|
115
|
+
expected_sum = timers.collect {|t| t["sum"] }.inject(0) {|s,c| s + c }
|
|
116
|
+
|
|
117
|
+
assert_equal expected_count, value["count"]
|
|
118
|
+
assert_equal expected_sum, value["sum"]
|
|
119
|
+
assert_equal timers.collect {|t| t["min"] }.min, value["min"]
|
|
120
|
+
assert_equal timers.collect {|t| t["max"] }.max, value["max"]
|
|
121
|
+
assert_equal expected_sum / expected_count, value["mean"].to_i
|
|
122
|
+
assert_equal 11, value["median"]
|
|
123
|
+
assert_equal 29, value["stddev"].to_i # NOTE: i don't actually know if this is correct
|
|
124
|
+
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_empty_timer
|
|
128
|
+
@start_time = Time.new(2012, 4, 1, 9, 30, 0, "-07:00")
|
|
129
|
+
@end_time = Time.new(2012, 4, 1, 9, 44, 0, "-07:00")
|
|
130
|
+
|
|
131
|
+
@start_timestamp = @start_time.to_i / 60
|
|
132
|
+
@end_timestamp = @end_time.to_i / 60
|
|
133
|
+
|
|
134
|
+
col = Mouth.collection(Mouth.mongo_collection_name("test"))
|
|
135
|
+
col.remove
|
|
136
|
+
seq = Mouth::Sequence.new("test.test", :kind => :timer, :granularity_in_minutes => 15, :start_time => @start_time, :end_time => @end_time)
|
|
137
|
+
values = seq.sequence
|
|
138
|
+
|
|
139
|
+
assert_equal [{"count"=>0,
|
|
140
|
+
"min"=>nil,
|
|
141
|
+
"max"=>nil,
|
|
142
|
+
"mean"=>nil,
|
|
143
|
+
"sum"=>0,
|
|
144
|
+
"median"=>nil,
|
|
145
|
+
"stddev"=>nil}], values
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def test_empty_sequence
|
|
149
|
+
@start_time = Time.new(2012, 4, 1, 9, 30, 0, "-07:00")
|
|
150
|
+
@end_time = Time.new(2012, 4, 1, 9, 44, 0, "-07:00")
|
|
151
|
+
|
|
152
|
+
@start_timestamp = @start_time.to_i / 60
|
|
153
|
+
@end_timestamp = @end_time.to_i / 60
|
|
154
|
+
|
|
155
|
+
col = Mouth.collection(Mouth.mongo_collection_name("test"))
|
|
156
|
+
col.remove
|
|
157
|
+
seq = Mouth::Sequence.new("test.test", :kind => :counter, :granularity_in_minutes => 15, :start_time => @start_time, :end_time => @end_time)
|
|
158
|
+
values = seq.sequence
|
|
159
|
+
|
|
160
|
+
assert_equal [0], values
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
end
|