mouth 0.8.1 → 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/MIT-LICENSE +1 -1
- data/README.md +40 -27
- data/Rakefile +2 -2
- data/TODO +4 -4
- data/bin/mouth +3 -3
- data/bin/mouth-console +1 -1
- data/bin/mouth-endoscope +3 -6
- data/lib/mouth.rb +37 -5
- data/lib/mouth/endoscope.rb +1 -1
- data/lib/mouth/endoscope/public/application.js +10 -4
- data/lib/mouth/endoscope/public/seven.js +1 -0
- data/lib/mouth/graph.rb +2 -2
- data/lib/mouth/instrument.rb +13 -9
- data/lib/mouth/record.rb +2 -1
- data/lib/mouth/recorder.rb +39 -0
- data/lib/mouth/runner.rb +2 -2
- data/lib/mouth/{sequence.rb → sequence_query.rb} +71 -16
- data/lib/mouth/source.rb +12 -3
- data/lib/mouth/sucker.rb +46 -19
- data/lib/mouth/version.rb +1 -1
- data/test/instrument_test.rb +79 -0
- data/test/recorder_test.rb +53 -0
- data/test/{sequence_test.rb → sequence_query_test.rb} +46 -8
- metadata +25 -21
- data/Capfile +0 -27
data/lib/mouth/runner.rb
CHANGED
@@ -12,8 +12,6 @@ module Mouth
|
|
12
12
|
attr_accessor :options
|
13
13
|
|
14
14
|
def initialize(opts={})
|
15
|
-
puts "Starting Mouth..."
|
16
|
-
|
17
15
|
self.log_file = opts[:log_file]
|
18
16
|
self.pid_file = opts[:pid_file]
|
19
17
|
self.verbosity = opts[:verbosity]
|
@@ -23,6 +21,8 @@ module Mouth
|
|
23
21
|
def run!
|
24
22
|
kill! if self.options[:kill]
|
25
23
|
|
24
|
+
puts "Starting Mouth..."
|
25
|
+
|
26
26
|
daemonize!
|
27
27
|
save_pid!
|
28
28
|
setup_logging!
|
@@ -1,16 +1,16 @@
|
|
1
1
|
module Mouth
|
2
2
|
|
3
3
|
# Usage:
|
4
|
-
#
|
4
|
+
# SequenceQuery.new(["namespace.foobar_occurances"]).sequences
|
5
5
|
# # => {"foobar_occurances" => [4, 9, 0, ...]}
|
6
6
|
#
|
7
|
-
#
|
7
|
+
# SequenceQuery.new(["namespace.foobar_occurances", "namespace.baz"], :kind => :timer).sequences
|
8
8
|
# # => {"foobar_occurances" => [{:count => 3, :min => 1, ...}, ...], "baz" => [...]}
|
9
9
|
#
|
10
|
-
# s =
|
10
|
+
# s = SequenceQuery.new(...)
|
11
11
|
# s.time_sequence
|
12
12
|
# # => [Time.new(first datapoint), Time.new(second datapoint), ..., Time.new(last datapoint)]
|
13
|
-
class
|
13
|
+
class SequenceQuery
|
14
14
|
|
15
15
|
attr_accessor :keys
|
16
16
|
attr_accessor :kind
|
@@ -63,10 +63,24 @@ module Mouth
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def time_sequence
|
66
|
+
if self.granularity_in_minutes == 1
|
67
|
+
start_timestamp = self.start_time.to_i / 60
|
68
|
+
end_timestamp = self.end_time.to_i / 60
|
69
|
+
else
|
70
|
+
start_timestamp = timestamp_to_nearest(self.start_time, self.granularity_in_minutes, :down)
|
71
|
+
end_timestamp = timestamp_to_nearest(self.end_time, self.granularity_in_minutes, :up) - 1
|
72
|
+
end
|
73
|
+
|
74
|
+
seq = []
|
75
|
+
(start_timestamp..end_timestamp).step(self.granularity_in_minutes) do |ts|
|
76
|
+
seq << Time.at(ts * 60)
|
77
|
+
end
|
78
|
+
seq
|
66
79
|
end
|
67
80
|
|
68
81
|
# Epoch in seconds
|
69
82
|
def epoch_sequence
|
83
|
+
time_sequence.collect(&:to_i)
|
70
84
|
end
|
71
85
|
|
72
86
|
protected
|
@@ -79,8 +93,8 @@ module Mouth
|
|
79
93
|
###
|
80
94
|
map_function = <<-JS
|
81
95
|
function() {
|
82
|
-
var doc = {t: this.t, m: {}, c: {}}
|
83
|
-
, thisMetrics = this.#{self.kind_letter.to_s}
|
96
|
+
var doc = {t: this.t, m: {}, c: {}, g: {}}
|
97
|
+
, thisMetrics = this.#{self.kind_letter.to_s} || {}
|
84
98
|
, docMetrics = doc.#{self.kind_letter.to_s}
|
85
99
|
, fields = #{self.metrics.to_s}
|
86
100
|
, i, k, val
|
@@ -99,7 +113,7 @@ module Mouth
|
|
99
113
|
|
100
114
|
reduce_function = <<-JS
|
101
115
|
function(key, values) {
|
102
|
-
var result = {c: {}, m: {}}
|
116
|
+
var result = {c: {}, m: {}, g: {}}
|
103
117
|
, k
|
104
118
|
, existing
|
105
119
|
, current
|
@@ -109,12 +123,20 @@ module Mouth
|
|
109
123
|
if (value.c) {
|
110
124
|
for (k in value.c) {
|
111
125
|
existing = result.c[k] || 0;
|
112
|
-
result.c[k] = existing + value.c[k]
|
126
|
+
result.c[k] = existing + value.c[k];
|
127
|
+
}
|
128
|
+
}
|
129
|
+
if (value.g) {
|
130
|
+
for (k in value.g) {
|
131
|
+
existing = result.g[k] || [0, -1];
|
132
|
+
if (value.t > existing[1]) {
|
133
|
+
result.g[k] = [value.g[k], value.t];
|
134
|
+
}
|
113
135
|
}
|
114
136
|
}
|
115
137
|
if (value.m) {
|
116
138
|
for (k in value.m) {
|
117
|
-
current = value.m[k]
|
139
|
+
current = value.m[k];
|
118
140
|
existing = result.m[k];
|
119
141
|
if (!existing) {
|
120
142
|
current.median = [current.median];
|
@@ -140,6 +162,10 @@ module Mouth
|
|
140
162
|
}
|
141
163
|
});
|
142
164
|
|
165
|
+
for (k in result.g) {
|
166
|
+
result.g[k] = result.g[k][0];
|
167
|
+
}
|
168
|
+
|
143
169
|
for (k in result.m) {
|
144
170
|
existing = result.m[k];
|
145
171
|
|
@@ -216,7 +242,14 @@ module Mouth
|
|
216
242
|
h
|
217
243
|
end
|
218
244
|
|
219
|
-
default = self.kind
|
245
|
+
default = case self.kind
|
246
|
+
when :counter
|
247
|
+
0
|
248
|
+
when :timer
|
249
|
+
{"count" => 0, "min" => nil, "max" => nil, "mean" => nil, "sum" => 0, "median" => nil, "stddev" => nil}
|
250
|
+
when :gauge
|
251
|
+
nil
|
252
|
+
end
|
220
253
|
|
221
254
|
seqs = {}
|
222
255
|
self.metrics.each do |m|
|
@@ -228,15 +261,29 @@ module Mouth
|
|
228
261
|
seqs[m] = seq
|
229
262
|
end
|
230
263
|
|
264
|
+
if self.kind == :gauge
|
265
|
+
seqs.each_pair do |met, seq|
|
266
|
+
cur_val = nil
|
267
|
+
seq.each_with_index do |v, i|
|
268
|
+
seq[i] = cur_val || 0 unless v # Note: In the future, we can remove || 0 and have leading nils. Then, fill those in with values from a previous time period
|
269
|
+
cur_val = v || cur_val
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
231
274
|
seqs
|
232
275
|
end
|
233
276
|
|
234
277
|
def collection
|
235
|
-
@collection ||= Mouth.
|
278
|
+
@collection ||= Mouth.collection_for(self.namespace)
|
236
279
|
end
|
237
280
|
|
238
281
|
def kind_letter
|
239
|
-
@kind_letter ||= self.kind
|
282
|
+
@kind_letter ||= case self.kind
|
283
|
+
when :counter then "c"
|
284
|
+
when :timer then "m"
|
285
|
+
when :gauge then "g"
|
286
|
+
end
|
240
287
|
end
|
241
288
|
|
242
289
|
def fields
|
@@ -261,13 +308,14 @@ module Mouth
|
|
261
308
|
opts = {
|
262
309
|
:namespace => "sample",
|
263
310
|
:metric => "sample",
|
264
|
-
:start_time => (
|
265
|
-
:end_time => (
|
311
|
+
:start_time => (Mouth.current_timestamp - 300),
|
312
|
+
:end_time => (Mouth.current_timestamp / 60),
|
266
313
|
}.merge(opts)
|
267
314
|
|
268
|
-
|
315
|
+
collection = Mouth.collection_for(opts[:namespace])
|
269
316
|
|
270
317
|
counter = 99
|
318
|
+
gauge = 50
|
271
319
|
(opts[:start_time]..opts[:end_time]).each do |t|
|
272
320
|
|
273
321
|
# Generate garbage data for the sample
|
@@ -284,12 +332,19 @@ module Mouth
|
|
284
332
|
"stddev" => rand(10)
|
285
333
|
}
|
286
334
|
|
335
|
+
set = {"c.#{opts[:metric]}" => counter, "m.#{opts[:metric]}" => m_doc}
|
336
|
+
if rand(4) == 0
|
337
|
+
set["g.#{opts[:metric]}"] = gauge
|
338
|
+
end
|
339
|
+
|
287
340
|
# Insert the document into mongo
|
288
|
-
|
341
|
+
collection.update({"t" => t}, {"$set" => set}, :upsert => true)
|
289
342
|
|
290
343
|
# Update counter randomly
|
291
344
|
counter += rand(10) - 5
|
345
|
+
gauge += rand(10) - 5
|
292
346
|
counter = 0 if counter < 0
|
347
|
+
gauge = 0 if gauge < 0
|
293
348
|
end
|
294
349
|
|
295
350
|
true
|
data/lib/mouth/source.rb
CHANGED
@@ -16,15 +16,16 @@ module Mouth
|
|
16
16
|
tuples = []
|
17
17
|
col_names.each do |col_name|
|
18
18
|
namespace = col_name.match(/mouth_(.+)/)[1]
|
19
|
-
counters, timers = all_for_collection(Mouth.collection(col_name))
|
19
|
+
counters, timers, gauges = all_for_collection(Mouth.collection(col_name))
|
20
20
|
counters.each {|s| tuples << {:source => "#{namespace}.#{s}", :kind => "counter"} }
|
21
21
|
timers.each {|s| tuples << {:source => "#{namespace}.#{s}", :kind => "timer"} }
|
22
|
+
gauges.each {|s| tuples << {:source => "#{namespace}.#{s}", :kind => "gauge"} }
|
22
23
|
end
|
23
24
|
|
24
25
|
tuples.sort_by {|t| "#{t[:kind]}#{t[:source]}" }.collect {|t| new(t) }
|
25
26
|
end
|
26
27
|
|
27
|
-
def self.all_for_collection(col, window =
|
28
|
+
def self.all_for_collection(col, window = Mouth.current_timestamp - 120)
|
28
29
|
map_function = <<-JS
|
29
30
|
function() {
|
30
31
|
var k, vh;
|
@@ -38,6 +39,11 @@ module Mouth
|
|
38
39
|
vh[k] = true;
|
39
40
|
emit("timers", {ks: vh});
|
40
41
|
}
|
42
|
+
for (k in this.g) {
|
43
|
+
vh = {};
|
44
|
+
vh[k] = true;
|
45
|
+
emit("gauges", {ks: vh});
|
46
|
+
}
|
41
47
|
}
|
42
48
|
JS
|
43
49
|
|
@@ -58,6 +64,7 @@ module Mouth
|
|
58
64
|
|
59
65
|
counters = []
|
60
66
|
timers = []
|
67
|
+
gauges = []
|
61
68
|
if result && result["results"].is_a?(Array)
|
62
69
|
result["results"].each do |r|
|
63
70
|
k, v = r["_id"], r["value"]
|
@@ -65,11 +72,13 @@ module Mouth
|
|
65
72
|
timers << v["ks"].keys
|
66
73
|
elsif k == "counters"
|
67
74
|
counters << v["ks"].keys
|
75
|
+
elsif k == "gauges"
|
76
|
+
gauges << v["ks"].keys
|
68
77
|
end
|
69
78
|
end
|
70
79
|
end
|
71
80
|
|
72
|
-
[counters.flatten.compact.uniq, timers.flatten.compact.uniq]
|
81
|
+
[counters.flatten.compact.uniq, timers.flatten.compact.uniq, gauges.flatten.compact.uniq]
|
73
82
|
end
|
74
83
|
|
75
84
|
end
|
data/lib/mouth/sucker.rb
CHANGED
@@ -20,15 +20,16 @@ module Mouth
|
|
20
20
|
attr_accessor :port
|
21
21
|
|
22
22
|
# Actual EM::Mongo connection
|
23
|
-
attr_accessor :
|
23
|
+
attr_accessor :mongo
|
24
24
|
|
25
25
|
# Info to connect to mongo
|
26
26
|
attr_accessor :mongo_db_name
|
27
|
-
attr_accessor :
|
27
|
+
attr_accessor :mongo_hostports
|
28
28
|
|
29
29
|
# Accumulators of our data
|
30
30
|
attr_accessor :counters
|
31
31
|
attr_accessor :timers
|
32
|
+
attr_accessor :gauges
|
32
33
|
|
33
34
|
# Stats
|
34
35
|
attr_accessor :udp_packets_received
|
@@ -38,19 +39,28 @@ module Mouth
|
|
38
39
|
self.host = options[:host] || "localhost"
|
39
40
|
self.port = options[:port] || 8889
|
40
41
|
self.mongo_db_name = options[:mongo_db_name] || "mouth"
|
41
|
-
|
42
|
+
hostports = options[:mongo_hostports] || [["localhost", EM::Mongo::DEFAULT_PORT]]
|
43
|
+
self.mongo_hostports = hostports.collect do |hp|
|
44
|
+
if hp.is_a?(String)
|
45
|
+
host, port = hp.split(":")
|
46
|
+
[host, port || EM::Mongo::DEFAULT_PORT]
|
47
|
+
else
|
48
|
+
hp
|
49
|
+
end
|
50
|
+
end
|
42
51
|
|
43
52
|
self.udp_packets_received = 0
|
44
53
|
self.mongo_flushes = 0
|
45
54
|
|
46
55
|
self.counters = {}
|
47
56
|
self.timers = {}
|
57
|
+
self.gauges = {}
|
48
58
|
end
|
49
59
|
|
50
60
|
def suck!
|
51
61
|
EM.run do
|
52
62
|
# Connect to mongo now
|
53
|
-
self.
|
63
|
+
self.mongo
|
54
64
|
|
55
65
|
EM.open_datagram_socket host, port, SuckerConnection do |conn|
|
56
66
|
conn.sucker = self
|
@@ -59,6 +69,7 @@ module Mouth
|
|
59
69
|
EM.add_periodic_timer(10) do
|
60
70
|
Mouth.logger.info "Counters: #{self.counters.inspect}"
|
61
71
|
Mouth.logger.info "Timers: #{self.timers.inspect}"
|
72
|
+
Mouth.logger.info "Gauges: #{self.gauges.inspect}"
|
62
73
|
self.flush!
|
63
74
|
self.set_procline!
|
64
75
|
end
|
@@ -73,7 +84,7 @@ module Mouth
|
|
73
84
|
# counter: gorets:1|c
|
74
85
|
# counter w/ sampling: gorets:1|c|@0.1
|
75
86
|
# timer: glork:320|ms
|
76
|
-
#
|
87
|
+
# gauge: gaugor:333|g
|
77
88
|
def store!(data)
|
78
89
|
key_value, command_sampling = data.to_s.split("|", 2)
|
79
90
|
key, value = key_value.to_s.split(":")
|
@@ -84,7 +95,7 @@ module Mouth
|
|
84
95
|
key = Mouth.parse_key(key).join(".")
|
85
96
|
value = value.to_f
|
86
97
|
|
87
|
-
ts =
|
98
|
+
ts = Mouth.current_timestamp
|
88
99
|
|
89
100
|
if command == "ms"
|
90
101
|
self.timers[ts] ||= {}
|
@@ -99,13 +110,16 @@ module Mouth
|
|
99
110
|
self.counters[ts] ||= {}
|
100
111
|
self.counters[ts][key] ||= 0.0
|
101
112
|
self.counters[ts][key] += value * factor
|
113
|
+
elsif command == "g"
|
114
|
+
self.gauges[ts] ||= {}
|
115
|
+
self.gauges[ts][key] = value
|
102
116
|
end
|
103
117
|
|
104
118
|
self.udp_packets_received += 1
|
105
119
|
end
|
106
120
|
|
107
121
|
def flush!
|
108
|
-
ts =
|
122
|
+
ts = Mouth.current_timestamp
|
109
123
|
limit_ts = ts - 1
|
110
124
|
mongo_docs = {}
|
111
125
|
|
@@ -117,7 +131,8 @@ module Mouth
|
|
117
131
|
# },
|
118
132
|
# m: {
|
119
133
|
# occasions: {...}
|
120
|
-
# }
|
134
|
+
# },
|
135
|
+
# g: {things: 3}
|
121
136
|
# }
|
122
137
|
|
123
138
|
self.counters.each do |cur_ts, counters_to_save|
|
@@ -136,6 +151,22 @@ module Mouth
|
|
136
151
|
end
|
137
152
|
end
|
138
153
|
|
154
|
+
self.gauges.each do |cur_ts, gauges_to_save|
|
155
|
+
if cur_ts <= limit_ts
|
156
|
+
gauges_to_save.each do |gauge_key, value|
|
157
|
+
ns, sub_key = Mouth.parse_key(gauge_key)
|
158
|
+
mongo_key = "#{ns}:#{ts}"
|
159
|
+
mongo_docs[mongo_key] ||= {}
|
160
|
+
|
161
|
+
cur_mongo_doc = mongo_docs[mongo_key]
|
162
|
+
cur_mongo_doc["g"] ||= {}
|
163
|
+
cur_mongo_doc["g"][sub_key] = value
|
164
|
+
end
|
165
|
+
|
166
|
+
self.gauges.delete(cur_ts)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
139
170
|
self.timers.each do |cur_ts, timers_to_save|
|
140
171
|
if cur_ts <= limit_ts
|
141
172
|
timers_to_save.each do |timer_key, values|
|
@@ -160,21 +191,21 @@ module Mouth
|
|
160
191
|
|
161
192
|
mongo_docs.each do |key, doc|
|
162
193
|
ns, ts = key.split(":")
|
163
|
-
collection_name =
|
194
|
+
collection_name = Mouth.mongo_collection_name(ns)
|
164
195
|
doc["t"] = ts.to_i
|
165
196
|
|
166
|
-
self.
|
197
|
+
self.mongo.collection(collection_name).insert(doc)
|
167
198
|
end
|
168
199
|
|
169
200
|
self.mongo_flushes += 1 if mongo_docs.any?
|
170
201
|
end
|
171
202
|
|
172
|
-
def
|
173
|
-
@
|
174
|
-
if self.
|
175
|
-
EM::Mongo::Connection.new(self.
|
203
|
+
def mongo
|
204
|
+
@mongo ||= begin
|
205
|
+
if self.mongo_hostports.length == 1
|
206
|
+
EM::Mongo::Connection.new(*self.mongo_hostports.first).db(self.mongo_db_name)
|
176
207
|
else
|
177
|
-
raise "
|
208
|
+
raise "Ability to connect to a replica set not implemented."
|
178
209
|
end
|
179
210
|
end
|
180
211
|
end
|
@@ -185,10 +216,6 @@ module Mouth
|
|
185
216
|
|
186
217
|
private
|
187
218
|
|
188
|
-
def minute_timestamps
|
189
|
-
Time.now.to_i / 60
|
190
|
-
end
|
191
|
-
|
192
219
|
def analyze_timer(values)
|
193
220
|
values.sort!
|
194
221
|
|
data/lib/mouth/version.rb
CHANGED
@@ -0,0 +1,79 @@
|
|
1
|
+
$LOAD_PATH.unshift 'test'
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
require 'mouth/instrument'
|
5
|
+
|
6
|
+
class MouthInstrumentTest < Test::Unit::TestCase
|
7
|
+
def setup
|
8
|
+
@@udp ||= UDPSocket.new.tap do |u|
|
9
|
+
u.bind("127.0.0.1", 32123)
|
10
|
+
end
|
11
|
+
@udp = @@udp
|
12
|
+
100.times { @udp.recvfrom_nonblock(1024) } rescue nil
|
13
|
+
Mouth.daemon_hostport = "127.0.0.1:32123"
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_increment
|
17
|
+
Mouth.increment("bob")
|
18
|
+
|
19
|
+
res = @udp.recvfrom(1024)
|
20
|
+
assert_equal "bob:1|c", res[0]
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_increment_delta
|
24
|
+
Mouth.increment("bob.rob", 123)
|
25
|
+
|
26
|
+
res = @udp.recvfrom(1024)
|
27
|
+
assert_equal "bob.rob:123|c", res[0]
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_increment_sample
|
31
|
+
pack = nil
|
32
|
+
nothing = false
|
33
|
+
|
34
|
+
100.times do
|
35
|
+
Mouth.increment("bob.rob/blah-haha", 123, 0.1)
|
36
|
+
|
37
|
+
begin
|
38
|
+
pack ||= @udp.recvfrom_nonblock(1024)
|
39
|
+
break if nothing
|
40
|
+
rescue IO::WaitReadable
|
41
|
+
nothing = true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
assert nothing
|
46
|
+
assert_equal "bob.rob/blah-haha:123|c|@0.1", pack[0]
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_gauge
|
50
|
+
Mouth.gauge("bob", 77)
|
51
|
+
|
52
|
+
res = @udp.recvfrom(1024)
|
53
|
+
assert_equal "bob:77|g", res[0]
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_measure
|
57
|
+
Mouth.measure("bob", 1.2)
|
58
|
+
|
59
|
+
res = @udp.recvfrom(1024)
|
60
|
+
assert_equal "bob:1.2|ms", res[0]
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_measure_block
|
64
|
+
result = Mouth.measure("bob") do
|
65
|
+
sleep 0.2
|
66
|
+
9876
|
67
|
+
end
|
68
|
+
|
69
|
+
assert_equal 9876, result
|
70
|
+
|
71
|
+
res = @udp.recvfrom(1024)[0]
|
72
|
+
m = res.match(/bob:(\d+)\|ms/)
|
73
|
+
assert m
|
74
|
+
|
75
|
+
# Make sure the number is within a resonable range
|
76
|
+
assert m[1].to_i > 180
|
77
|
+
assert m[1].to_i < 220
|
78
|
+
end
|
79
|
+
end
|