mouth 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- # Sequence.new(["namespace.foobar_occurances"]).sequences
4
+ # SequenceQuery.new(["namespace.foobar_occurances"]).sequences
5
5
  # # => {"foobar_occurances" => [4, 9, 0, ...]}
6
6
  #
7
- # Sequence.new(["namespace.foobar_occurances", "namespace.baz"], :kind => :timer).sequences
7
+ # SequenceQuery.new(["namespace.foobar_occurances", "namespace.baz"], :kind => :timer).sequences
8
8
  # # => {"foobar_occurances" => [{:count => 3, :min => 1, ...}, ...], "baz" => [...]}
9
9
  #
10
- # s = Sequence.new(...)
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 Sequence
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 == :counter ? 0 : {"count" => 0, "min" => nil, "max" => nil, "mean" => nil, "sum" => 0, "median" => nil, "stddev" => nil}
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.collection(Mouth.mongo_collection_name(self.namespace))
278
+ @collection ||= Mouth.collection_for(self.namespace)
236
279
  end
237
280
 
238
281
  def kind_letter
239
- @kind_letter ||= self.kind == :counter ? "c" : "m"
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 => (Time.now.to_i / 60 - 300),
265
- :end_time => (Time.now.to_i / 60),
311
+ :start_time => (Mouth.current_timestamp - 300),
312
+ :end_time => (Mouth.current_timestamp / 60),
266
313
  }.merge(opts)
267
314
 
268
- collection_name = Mouth.mongo_collection_name(opts[:namespace])
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
- Mouth.collection(collection_name).update({"t" => t}, {"$set" => {"c.#{opts[:metric]}" => counter, "m.#{opts[:metric]}" => m_doc}}, :upsert => true)
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
@@ -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 = Time.now.to_i / 60 - 120)
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
@@ -20,15 +20,16 @@ module Mouth
20
20
  attr_accessor :port
21
21
 
22
22
  # Actual EM::Mongo connection
23
- attr_accessor :mongo_db
23
+ attr_accessor :mongo
24
24
 
25
25
  # Info to connect to mongo
26
26
  attr_accessor :mongo_db_name
27
- attr_accessor :mongo_hosts
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
- self.mongo_hosts = options[:mongo_hosts] || ["localhost"]
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.mongo_db
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
- # (future) gauge: gaugor:333|g
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 = minute_timestamps
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 = minute_timestamps
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 = "mouth_#{ns}"
194
+ collection_name = Mouth.mongo_collection_name(ns)
164
195
  doc["t"] = ts.to_i
165
196
 
166
- self.mongo_db.collection(collection_name).insert(doc)
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 mongo_db
173
- @mongo_db ||= begin
174
- if self.mongo_hosts.length == 1
175
- EM::Mongo::Connection.new(self.mongo_hosts.first).db(self.mongo_db_name)
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 "TODO: ability to connect to a replica set."
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
 
@@ -1,3 +1,3 @@
1
1
  module Mouth
2
- VERSION = "0.8.1"
2
+ VERSION = "0.8.2"
3
3
  end
@@ -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