mouth 0.8.1 → 0.8.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/.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
|