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