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
@@ -0,0 +1,67 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Mouth Endoscope</title>
|
5
|
+
|
6
|
+
<link rel="stylesheet" href="/application.css" type="text/css" media="all">
|
7
|
+
<link rel="stylesheet" href="/seven.css" type="text/css" media="all">
|
8
|
+
<script src="/jquery.js" type="text/javascript"></script>
|
9
|
+
<script src="/jquery-ui-1.8.16.custom.min.js" type="text/javascript"></script>
|
10
|
+
<script src="/json2.js" type="text/javascript"></script>
|
11
|
+
<script src="/underscore.js" type="text/javascript"></script>
|
12
|
+
<script src="/backbone.js" type="text/javascript"></script>
|
13
|
+
<script src="/d3.js" type="text/javascript"></script>
|
14
|
+
<script src="/d3.time.js" type="text/javascript"></script>
|
15
|
+
<script src="/seven.js" type="text/javascript"></script>
|
16
|
+
<script src="/keymaster.js" type="text/javascript"></script>
|
17
|
+
<script src="/application.js" type="text/javascript"></script>
|
18
|
+
<script src="/linen.js" type="text/javascript"></script>
|
19
|
+
</head>
|
20
|
+
<body>
|
21
|
+
|
22
|
+
<section id="mouth-canvas">
|
23
|
+
<header id="mouth-header">
|
24
|
+
<h1>Mouth</h1>
|
25
|
+
|
26
|
+
<nav id="dashboards" data-dashboard>
|
27
|
+
<ul></ul>
|
28
|
+
|
29
|
+
<a href="#" class="add-dashboard">+</a>
|
30
|
+
</nav>
|
31
|
+
</header>
|
32
|
+
|
33
|
+
<section id="info-panel">
|
34
|
+
<div id="date-range-picker" class="current">
|
35
|
+
<h2>Time Span</h2>
|
36
|
+
<ul>
|
37
|
+
<li><label><input type="radio" name="time_span" value="2_hours" checked> 2 Hours</label></li>
|
38
|
+
<li><label><input type="radio" name="time_span" value="24_hours"> 24 Hours</label></li>
|
39
|
+
<li><label><input type="radio" name="time_span" value="7_days"> 7 Days</label></li>
|
40
|
+
</ul>
|
41
|
+
|
42
|
+
<h2>Starting At</h2>
|
43
|
+
<div class="starting-at">
|
44
|
+
<div class="if-current">
|
45
|
+
<a href="#" class="custom-date">2 hours ago</a>
|
46
|
+
</div>
|
47
|
+
<div class="unless-current">
|
48
|
+
<input type="text" name="starting_at" class="date-starting-at">
|
49
|
+
<div class="example">(Format: 2012-04-26 11:22)</div>
|
50
|
+
<div><a href="#" class="date-reset">Reset</a></div>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
|
54
|
+
<h2>Ending At</h2>
|
55
|
+
<div class="ending-at">
|
56
|
+
<span>Now</span>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
</section>
|
60
|
+
<section id="current-dashboard">
|
61
|
+
<ul id="grid"></ul>
|
62
|
+
<a href="#" class="add-graph">Add Graph</a>
|
63
|
+
</section>
|
64
|
+
</section>
|
65
|
+
|
66
|
+
</body>
|
67
|
+
</html>
|
data/lib/mouth/graph.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Mouth
|
2
|
+
|
3
|
+
# {
|
4
|
+
# :dashboard_id => BSON::ObjectId,
|
5
|
+
# :position => {
|
6
|
+
# :top => 0,
|
7
|
+
# :left => 0,
|
8
|
+
# :height => 7,
|
9
|
+
# :width => 20
|
10
|
+
# },
|
11
|
+
# :kind => 'counters' || 'timer',
|
12
|
+
# :sources => ["auth.authentications"]
|
13
|
+
# }
|
14
|
+
class Graph < Record
|
15
|
+
|
16
|
+
#
|
17
|
+
def save
|
18
|
+
bson_object_id_ize(:dashboard_id) do
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Options:
|
24
|
+
# - :start_time (Time object)
|
25
|
+
# - :end_time (Time object)
|
26
|
+
# - :granularity_in_minutes
|
27
|
+
def data(opts = {})
|
28
|
+
sources = self.attributes[:sources] || []
|
29
|
+
seq_opts = {:kind => self.attributes[:kind].to_sym}.merge(opts)
|
30
|
+
sequence = Sequence.new(sources, seq_opts)
|
31
|
+
seqs = sequence.sequences
|
32
|
+
seqs.map do |seq|
|
33
|
+
{
|
34
|
+
:data => seq[1],
|
35
|
+
:start_time => sequence.start_time_epoch,
|
36
|
+
:source => seq[0]
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
def bson_object_id_ize(*args)
|
43
|
+
orig_attrs = self.attributes.dup
|
44
|
+
args.each do |a|
|
45
|
+
self.attributes[a] = BSON::ObjectId(self.attributes[a])
|
46
|
+
end
|
47
|
+
res = yield
|
48
|
+
orig_attrs[:id] = self.attributes[:id]
|
49
|
+
self.attributes = orig_attrs
|
50
|
+
|
51
|
+
res
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.for_dashboard(dashboard_id)
|
55
|
+
collection.find({:dashboard_id => BSON::ObjectId(dashboard_id.to_s)}).to_a.collect {|g| new(g) }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
module Mouth
|
5
|
+
unless self.respond_to?(:measure)
|
6
|
+
class << self
|
7
|
+
attr_accessor :host, :port, :disabled
|
8
|
+
|
9
|
+
# Mouth.server = 'localhost:1234'
|
10
|
+
def server=(conn)
|
11
|
+
self.host, port = conn.split(':')
|
12
|
+
self.port = port.to_i
|
13
|
+
end
|
14
|
+
|
15
|
+
def host
|
16
|
+
@host || "localhost"
|
17
|
+
end
|
18
|
+
|
19
|
+
def port
|
20
|
+
@port || 8889
|
21
|
+
end
|
22
|
+
|
23
|
+
def measure(key, milli = nil)
|
24
|
+
result = nil
|
25
|
+
ms = milli || (Benchmark.realtime { result = yield } * 1000).to_i
|
26
|
+
|
27
|
+
write(key, ms, :ms)
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
def increment(key, delta = 1, sample_rate = nil)
|
33
|
+
write(key, delta, :c, sample_rate)
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def socket
|
39
|
+
@socket ||= UDPSocket.new
|
40
|
+
end
|
41
|
+
|
42
|
+
def write(k, v, op, sample_rate = nil)
|
43
|
+
return if self.disabled
|
44
|
+
if sample_rate
|
45
|
+
sample_rate = 1 if sample_rate > 1
|
46
|
+
return if rand > sample_rate
|
47
|
+
end
|
48
|
+
|
49
|
+
command = "#{k}:#{v}|#{op}"
|
50
|
+
command << "|@#{sample_rate}" if sample_rate
|
51
|
+
|
52
|
+
socket.send(command, 0, self.host, self.port)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/mouth/record.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
module Mouth
|
2
|
+
|
3
|
+
class Record
|
4
|
+
|
5
|
+
# Keys are symbols
|
6
|
+
# id is :id, not _id
|
7
|
+
attr_accessor :attributes
|
8
|
+
|
9
|
+
def initialize(attrs = {})
|
10
|
+
self.attributes = normalize_attributes(attrs)
|
11
|
+
end
|
12
|
+
|
13
|
+
def all_attributes
|
14
|
+
self.attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
def save
|
18
|
+
if self.attributes[:id]
|
19
|
+
attrs = self.attributes.dup
|
20
|
+
the_id = attrs.delete(:id).to_s
|
21
|
+
doc = self.class.collection.update({"_id" => BSON::ObjectId(the_id)}, attrs)
|
22
|
+
else
|
23
|
+
self.class.collection.insert(self.attributes)
|
24
|
+
self.attributes[:id] = self.attributes.delete(:_id).to_s
|
25
|
+
end
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def update(new_attrs)
|
30
|
+
self.attributes = normalize_attributes(new_attrs)
|
31
|
+
self.save
|
32
|
+
end
|
33
|
+
|
34
|
+
def destroy
|
35
|
+
self.class.collection.remove({"_id" => BSON::ObjectId(self.attributes[:id])})
|
36
|
+
end
|
37
|
+
|
38
|
+
def normalize_attributes(attrs)
|
39
|
+
normalize = lambda do |h|
|
40
|
+
hd = {}
|
41
|
+
h.each_pair do |key, val|
|
42
|
+
val = normalize.call(val) if val.is_a?(Hash)
|
43
|
+
val = val.to_s if val.is_a?(BSON::ObjectId)
|
44
|
+
# TODO: arrays :(
|
45
|
+
hd[key.to_s == "_id" ? :id : key.to_sym] = val
|
46
|
+
end
|
47
|
+
hd
|
48
|
+
end
|
49
|
+
normalize.call attrs
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.collection
|
53
|
+
demodularized = self.to_s.match(/(.+::)?(.+)$/)[2] || "record"
|
54
|
+
tableized = demodularized.downcase + "s" # (: lol :)
|
55
|
+
@collection ||= Mouth.mongo.collection(tableized)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.find(id)
|
59
|
+
collection.find({"_id" => BSON::ObjectId(id)}).to_a.collect {|d| new(d) }.first
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.create(attributes)
|
63
|
+
r = new(attributes)
|
64
|
+
r.save
|
65
|
+
r
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.all
|
69
|
+
collection.find.to_a.collect {|d| new(d) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/mouth/runner.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'logger'
|
3
|
+
require 'mouth/sucker'
|
4
|
+
|
5
|
+
module Mouth
|
6
|
+
class Runner
|
7
|
+
|
8
|
+
attr_accessor :log_file
|
9
|
+
attr_accessor :pid_file
|
10
|
+
attr_accessor :logger
|
11
|
+
attr_accessor :verbosity # 0: Only errors/warnings 1: informational 2: debug/all incomding UDP packets
|
12
|
+
attr_accessor :options
|
13
|
+
|
14
|
+
def initialize(opts={})
|
15
|
+
puts "Starting Mouth..."
|
16
|
+
|
17
|
+
self.log_file = opts[:log_file]
|
18
|
+
self.pid_file = opts[:pid_file]
|
19
|
+
self.verbosity = opts[:verbosity]
|
20
|
+
self.options = opts
|
21
|
+
end
|
22
|
+
|
23
|
+
def run!
|
24
|
+
kill! if self.options[:kill]
|
25
|
+
|
26
|
+
daemonize!
|
27
|
+
save_pid!
|
28
|
+
setup_logging!
|
29
|
+
|
30
|
+
# Start the reactor!
|
31
|
+
sucker = Mouth::Sucker.new(self.options)
|
32
|
+
sucker.suck!
|
33
|
+
end
|
34
|
+
|
35
|
+
def kill!
|
36
|
+
if @pid_file
|
37
|
+
pid = File.read(@pid_file)
|
38
|
+
#logger.warn "Sending #{kill_command} to #{pid.to_i}"
|
39
|
+
Process.kill(:INT, pid.to_i)
|
40
|
+
else
|
41
|
+
#logger.warn "No pid_file specified"
|
42
|
+
end
|
43
|
+
ensure
|
44
|
+
exit(0)
|
45
|
+
end
|
46
|
+
|
47
|
+
def daemonize!
|
48
|
+
# Fork and continue in forked process
|
49
|
+
# Also calls setsid
|
50
|
+
# Also redirects all output to /dev/null
|
51
|
+
Process.daemon(true)
|
52
|
+
|
53
|
+
# Reset umask
|
54
|
+
File.umask(0000)
|
55
|
+
|
56
|
+
# Set the procline
|
57
|
+
$0 = "mouth [initializing]"
|
58
|
+
end
|
59
|
+
|
60
|
+
def save_pid!
|
61
|
+
if @pid_file
|
62
|
+
pid = Process.pid
|
63
|
+
FileUtils.mkdir_p(File.dirname(@pid_file))
|
64
|
+
File.open(@pid_file, 'w') { |f| f.write(pid) }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def setup_logging!
|
69
|
+
if @log_file
|
70
|
+
STDERR.reopen(@log_file, 'a')
|
71
|
+
|
72
|
+
# Open a logger
|
73
|
+
self.logger = Logger.new(@log_file)
|
74
|
+
self.logger.level = case self.verbosity
|
75
|
+
when 0
|
76
|
+
Logger::WARN
|
77
|
+
when 1
|
78
|
+
Logger::INFO
|
79
|
+
else
|
80
|
+
Logger::DEBUG
|
81
|
+
end
|
82
|
+
Mouth.logger = self.logger
|
83
|
+
|
84
|
+
self.logger.info "Mouth Initialized..."
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end # class Runner
|
89
|
+
end # module Mouth
|
@@ -0,0 +1,284 @@
|
|
1
|
+
module Mouth
|
2
|
+
|
3
|
+
# Usage:
|
4
|
+
# Sequence.new(["namespace.foobar_occurances"]).sequences
|
5
|
+
# # => {"foobar_occurances" => [4, 9, 0, ...]}
|
6
|
+
#
|
7
|
+
# Sequence.new(["namespace.foobar_occurances", "namespace.baz"], :kind => :timer).sequences
|
8
|
+
# # => {"foobar_occurances" => [{:count => 3, :min => 1, ...}, ...], "baz" => [...]}
|
9
|
+
#
|
10
|
+
# s = Sequence.new(...)
|
11
|
+
# s.time_sequence
|
12
|
+
# # => [Time.new(first datapoint), Time.new(second datapoint), ..., Time.new(last datapoint)]
|
13
|
+
class Sequence
|
14
|
+
|
15
|
+
attr_accessor :keys
|
16
|
+
attr_accessor :kind
|
17
|
+
attr_accessor :granularity_in_minutes
|
18
|
+
attr_accessor :start_time
|
19
|
+
attr_accessor :end_time
|
20
|
+
attr_accessor :namespace
|
21
|
+
attr_accessor :metrics
|
22
|
+
|
23
|
+
def initialize(keys, opts = {})
|
24
|
+
opts = {
|
25
|
+
:kind => :counter,
|
26
|
+
:granularity_in_minutes => 1,
|
27
|
+
:start_time => Time.now - (119 * 60),
|
28
|
+
:end_time => Time.now,
|
29
|
+
}.merge(opts)
|
30
|
+
|
31
|
+
self.keys = Array(keys)
|
32
|
+
self.kind = opts[:kind]
|
33
|
+
self.granularity_in_minutes = opts[:granularity_in_minutes]
|
34
|
+
self.start_time = opts[:start_time]
|
35
|
+
self.end_time = opts[:end_time]
|
36
|
+
|
37
|
+
self.metrics = []
|
38
|
+
namespaces = []
|
39
|
+
self.keys.each do |k|
|
40
|
+
namespace, metric = Mouth.parse_key(k)
|
41
|
+
namespaces << namespace
|
42
|
+
self.metrics << metric
|
43
|
+
end
|
44
|
+
raise StandardError.new("Batch calculation must come from the same namespace") if namespaces.uniq.length > 1
|
45
|
+
self.namespace = namespaces.first
|
46
|
+
end
|
47
|
+
|
48
|
+
def sequence
|
49
|
+
sequences.values.first
|
50
|
+
end
|
51
|
+
|
52
|
+
def sequences
|
53
|
+
return sequences_for_minute if self.granularity_in_minutes == 1
|
54
|
+
sequences_for_x_minutes(self.granularity_in_minutes)
|
55
|
+
end
|
56
|
+
|
57
|
+
def start_time_epoch
|
58
|
+
if self.granularity_in_minutes == 1
|
59
|
+
(self.start_time.to_i / 60) * 60
|
60
|
+
else
|
61
|
+
timestamp_to_nearest(self.start_time, self.granularity_in_minutes, :down) * 60
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def time_sequence
|
66
|
+
end
|
67
|
+
|
68
|
+
# Epoch in seconds
|
69
|
+
def epoch_sequence
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
def sequences_for_x_minutes(minutes)
|
75
|
+
start_timestamp = timestamp_to_nearest(self.start_time, minutes, :down)
|
76
|
+
end_timestamp = timestamp_to_nearest(self.end_time, minutes, :up)
|
77
|
+
# Then, for a timestamp xyz, it's in bucket (xyz - start_timestamp) / minutes
|
78
|
+
|
79
|
+
###
|
80
|
+
map_function = <<-JS
|
81
|
+
function() {
|
82
|
+
emit(Math.floor((this.t - #{start_timestamp}) / #{minutes}), this);
|
83
|
+
}
|
84
|
+
JS
|
85
|
+
|
86
|
+
reduce_function = <<-JS
|
87
|
+
function(key, values) {
|
88
|
+
var result = {c: {}, m: {}}
|
89
|
+
, k
|
90
|
+
, existing
|
91
|
+
, current
|
92
|
+
;
|
93
|
+
|
94
|
+
values.forEach(function(value) {
|
95
|
+
if (value.c) {
|
96
|
+
for (k in value.c) {
|
97
|
+
existing = result.c[k] || 0;
|
98
|
+
result.c[k] = existing + value.c[k]
|
99
|
+
}
|
100
|
+
}
|
101
|
+
if (value.m) {
|
102
|
+
for (k in value.m) {
|
103
|
+
current = value.m[k]
|
104
|
+
existing = result.m[k];
|
105
|
+
if (!existing) {
|
106
|
+
current.median = [current.median];
|
107
|
+
current.stddev = [current.stddev];
|
108
|
+
current.mean = [current.mean];
|
109
|
+
current.count = [current.count];
|
110
|
+
|
111
|
+
result.m[k] = current;
|
112
|
+
} else {
|
113
|
+
// {"count" => 0, "min" => nil, "max" => nil, "mean" => nil, "sum" => 0, "median" => nil, "stddev" => nil}
|
114
|
+
// Ok here existing is a non null one of these ^, and so is current. We just need to merge them.
|
115
|
+
existing.min = existing.min < current.min ? existing.min : current.min;
|
116
|
+
existing.max = existing.max > current.max ? existing.max : current.max;
|
117
|
+
existing.sum = existing.sum + current.sum;
|
118
|
+
|
119
|
+
// Save the individual stuff for later. Need it later for proper merge.
|
120
|
+
existing.median.push(current.median);
|
121
|
+
existing.stddev.push(current.stddev);
|
122
|
+
existing.mean.push(current.mean);
|
123
|
+
existing.count.push(current.count);
|
124
|
+
}
|
125
|
+
}
|
126
|
+
}
|
127
|
+
});
|
128
|
+
|
129
|
+
for (k in result.m) {
|
130
|
+
existing = result.m[k];
|
131
|
+
|
132
|
+
var count = existing.median.length
|
133
|
+
, middle = Math.floor(count / 2)
|
134
|
+
, overallCount = 0
|
135
|
+
, secondMoment = 0
|
136
|
+
, mean
|
137
|
+
, i
|
138
|
+
;
|
139
|
+
|
140
|
+
// Total datapoints
|
141
|
+
for (i = 0; i < count; i += 1) {
|
142
|
+
overallCount = overallCount + existing.count[i];
|
143
|
+
}
|
144
|
+
|
145
|
+
// Mean
|
146
|
+
mean = existing.sum / overallCount;
|
147
|
+
|
148
|
+
// Median: approximate and take the median median.
|
149
|
+
if (count % 2 == 0) {
|
150
|
+
existing.median = (existing.median[middle] + existing.median[middle - 1]) / 2;
|
151
|
+
} else {
|
152
|
+
existing.median = existing.median[middle];
|
153
|
+
}
|
154
|
+
|
155
|
+
// Stddev
|
156
|
+
// weighted average of "second moments": M2 += count(i)/overallCount * (stddev(i)^2 + mean(i)^2)
|
157
|
+
// Then stddev = sqrt(M2 - mean^2)
|
158
|
+
for (i = 0; i < count; i += 1) {
|
159
|
+
var stddev_i = existing.stddev[i]
|
160
|
+
, mean_i = existing.mean[i]
|
161
|
+
;
|
162
|
+
|
163
|
+
secondMoment = secondMoment + (existing.count[i] / overallCount) * (stddev_i * stddev_i + mean_i * mean_i)
|
164
|
+
}
|
165
|
+
existing.stddev = Math.sqrt(secondMoment - mean * mean);
|
166
|
+
|
167
|
+
// Mean:
|
168
|
+
existing.mean = mean;
|
169
|
+
|
170
|
+
// Count
|
171
|
+
existing.count = overallCount;
|
172
|
+
}
|
173
|
+
|
174
|
+
return result;
|
175
|
+
}
|
176
|
+
JS
|
177
|
+
|
178
|
+
result = collection.map_reduce(map_function, reduce_function, :out => {:inline => true}, :raw => true, :query => {"t" => {"$gte" => start_timestamp, "$lte" => end_timestamp}})
|
179
|
+
|
180
|
+
docs = result["results"].collect do |r|
|
181
|
+
ordinal = r["_id"]
|
182
|
+
doc = r["value"]
|
183
|
+
doc["t"] = (start_timestamp + ordinal * minutes).to_i
|
184
|
+
doc
|
185
|
+
end
|
186
|
+
|
187
|
+
sequences_from_documents(docs, start_timestamp, end_timestamp - 1, minutes)
|
188
|
+
end
|
189
|
+
|
190
|
+
def sequences_for_minute
|
191
|
+
start_timestamp = self.start_time.to_i / 60
|
192
|
+
end_timestamp = self.end_time.to_i / 60
|
193
|
+
|
194
|
+
docs = self.collection.find({"t" => {"$gte" => start_timestamp, "$lte" => end_timestamp}}, :fields => self.fields).to_a
|
195
|
+
|
196
|
+
sequences_from_documents(docs, start_timestamp, end_timestamp, 1)
|
197
|
+
end
|
198
|
+
|
199
|
+
def sequences_from_documents(docs, start_timestamp, end_timestamp, minutes)
|
200
|
+
timestamp_to_metrics = docs.inject({}) do |h, e|
|
201
|
+
h[e["t"]] = e[self.kind_letter]
|
202
|
+
h
|
203
|
+
end
|
204
|
+
|
205
|
+
default = self.kind == :counter ? 0 : {"count" => 0, "min" => nil, "max" => nil, "mean" => nil, "sum" => 0, "median" => nil, "stddev" => nil}
|
206
|
+
|
207
|
+
seqs = {}
|
208
|
+
self.metrics.each do |m|
|
209
|
+
seq = []
|
210
|
+
(start_timestamp..end_timestamp).step(minutes) do |t|
|
211
|
+
mets = timestamp_to_metrics[t]
|
212
|
+
seq << ((mets && mets[m]) || default)
|
213
|
+
end
|
214
|
+
seqs[m] = seq
|
215
|
+
end
|
216
|
+
|
217
|
+
seqs
|
218
|
+
end
|
219
|
+
|
220
|
+
def collection
|
221
|
+
@collection ||= Mouth.collection(Mouth.mongo_collection_name(self.namespace))
|
222
|
+
end
|
223
|
+
|
224
|
+
def kind_letter
|
225
|
+
@kind_letter ||= self.kind == :counter ? "c" : "m"
|
226
|
+
end
|
227
|
+
|
228
|
+
def fields
|
229
|
+
@fields ||= ["t"].concat(self.metrics.map {|m| "#{kind_letter}.#{m}" })
|
230
|
+
end
|
231
|
+
|
232
|
+
# timestamp_to_nearest(Time.now, 15, :down)
|
233
|
+
# => t = 22122825 such that t * 60 is a second-epoch time on a 15-minute boundary, eg, 2012-01-23 17:45:00
|
234
|
+
def timestamp_to_nearest(time, minute, rounded = :down)
|
235
|
+
start_timestamp = time.to_i / 60 # This is minute granularity
|
236
|
+
if rounded == :down
|
237
|
+
start_timestamp -= time.min % minute
|
238
|
+
else
|
239
|
+
start_timestamp += minute - time.min % minute
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
public
|
244
|
+
|
245
|
+
# Generates a sample sequence of both counter and timing
|
246
|
+
def self.generate_sample(opts = {})
|
247
|
+
opts = {
|
248
|
+
:namespace => "sample",
|
249
|
+
:metric => "sample",
|
250
|
+
:start_time => (Time.now.to_i / 60 - 300),
|
251
|
+
:end_time => (Time.now.to_i / 60),
|
252
|
+
}.merge(opts)
|
253
|
+
|
254
|
+
collection_name = Mouth.mongo_collection_name(opts[:namespace])
|
255
|
+
|
256
|
+
counter = 99
|
257
|
+
(opts[:start_time]..opts[:end_time]).each do |t|
|
258
|
+
|
259
|
+
# Generate garbage data for the sample
|
260
|
+
# NOTE: candidate for improvement
|
261
|
+
m_count = rand(20) + 1
|
262
|
+
m_mean = rand(20) + 40
|
263
|
+
m_doc = {
|
264
|
+
"count" => m_count,
|
265
|
+
"min" => rand(20),
|
266
|
+
"max" => rand(20) + 80,
|
267
|
+
"mean" => m_mean,
|
268
|
+
"sum" => m_mean * m_count,
|
269
|
+
"median" => m_mean + rand(5),
|
270
|
+
"stddev" => rand(10)
|
271
|
+
}
|
272
|
+
|
273
|
+
# Insert the document into mongo
|
274
|
+
Mouth.collection(collection_name).update({"t" => t}, {"$set" => {"c.#{opts[:metric]}" => counter, "m.#{opts[:metric]}" => m_doc}}, :upsert => true)
|
275
|
+
|
276
|
+
# Update counter randomly
|
277
|
+
counter += rand(10) - 5
|
278
|
+
counter = 0 if counter < 0
|
279
|
+
end
|
280
|
+
|
281
|
+
true
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|