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/.gitignore
CHANGED
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# Mouth
|
2
2
|
|
3
|
-
Mouth is a Ruby daemon that collects
|
3
|
+
Mouth is a Ruby daemon that collects metrics via UDP and stores them in Mongo. It comes with a modern UI that allows you to view graphs and create dashboards of these statistics. Mouth is very similar to [StatsD](https://github.com/etsy/statsd) + [Graphite](http://graphite.wikidot.com/) + [Graphene](http://jondot.github.com/graphene/).
|
4
4
|
|
5
|
+

|
5
6
|
|
6
7
|
## Why
|
7
8
|
|
8
9
|
Why duplicate effort of the excellent StatsD / Graphite packages? I wanted a graphing and monitoring tool that offered:
|
9
10
|
|
10
|
-
|
11
11
|
* **Accessible data**. Because the metrics are stored in Mongo, they are very accessible to any scripts you want to write to read and act on your data.
|
12
12
|
* **Easy installation**. Mouth depends on just Ruby and Mongo, both incredibly easy to install.
|
13
13
|
* **Modern, friendly UI**. Mouth graphs are beautiful and easy to work with. Exploring data and building dashboards are a joy.
|
@@ -17,20 +17,24 @@ Why duplicate effort of the excellent StatsD / Graphite packages? I wanted a gr
|
|
17
17
|
There are two kinds of metrics currently: counters and timers. Both are stored in a time series with minute granularity.
|
18
18
|
|
19
19
|
* **Counters**:
|
20
|
-
*
|
20
|
+
* How many occurrences of something happen per minute?
|
21
21
|
* Can be sampled
|
22
22
|
* Standard usage: Mouth.increment("myapp.happenings")
|
23
23
|
* Advanced usage: Mouth.increment("myapp.happenings", 1, 0.1) # 1/10 sample rate. UDP packets are sent 1 in 10 times, but count for 1 * 10 each.
|
24
24
|
* Gives you a time series of happenings by minute: {"myapp.happenings" => [1, 2, 5, 20, ...]}
|
25
25
|
* **Timers**:
|
26
26
|
* How long does something take?
|
27
|
-
* Standard usage: Mouth.measure("myapp.
|
28
|
-
* Standard usage 2: Mouth.measure("myapp.
|
29
|
-
* Gives you a time series of
|
30
|
-
* **Gauges**:
|
27
|
+
* Standard usage: Mouth.measure("myapp.occurrences", 3.3) # This occurrence took 3.3 ms to occur
|
28
|
+
* Standard usage 2: Mouth.measure("myapp.occurrences") { do_occurance() }
|
29
|
+
* Gives you a time series of occurrence timings by minute: {"myapp.happenings" => [1, 2, 5, 20, ...]}
|
30
|
+
* **Gauges**:
|
31
|
+
* Record the value of something at a specific time
|
32
|
+
* Standard usage: Mouth.gauge("myapp.subscriber_count", 3000) # At this time, you have 3000 subscribers.
|
31
33
|
|
32
34
|
## Installation on OSX
|
33
35
|
|
36
|
+
Make sure you're using Ruby 1.9.2+ or compatible.
|
37
|
+
|
34
38
|
Install mouth:
|
35
39
|
|
36
40
|
gem install mouth
|
@@ -39,7 +43,7 @@ Install MongoDB if you haven't:
|
|
39
43
|
|
40
44
|
brew install mongodb
|
41
45
|
|
42
|
-
Start collector:
|
46
|
+
Start collector daemon:
|
43
47
|
|
44
48
|
mouth
|
45
49
|
|
@@ -73,21 +77,23 @@ Mouth comes with a built-in facility to instrument your apps:
|
|
73
77
|
require 'mouth'
|
74
78
|
require 'mouth/instrument'
|
75
79
|
|
76
|
-
Mouth.
|
80
|
+
Mouth.daemon_hostport = "0.0.0.0:8889"
|
77
81
|
Mouth.increment('hello.world')
|
78
|
-
Mouth.measure('hello.happening'
|
82
|
+
Mouth.measure('hello.happening') { happen! }
|
83
|
+
Mouth.gauge('hello.level', 1000)
|
79
84
|
|
80
85
|
### Using mouth-instrument
|
81
86
|
|
82
|
-
mouth-instrument is a lightweight gem that doesn't have the baggage of the various gems that come with mouth. Its usage is nearly identical:
|
87
|
+
[mouth-instrument](http://www.github.com/cypriss/mouth-instrument) is a lightweight gem that doesn't have the baggage of the various gems that come with mouth. Its usage is nearly identical:
|
83
88
|
|
84
89
|
gem install mouth-instrument
|
85
90
|
|
86
91
|
require 'mouth-instrument'
|
87
92
|
|
88
|
-
Mouth.
|
93
|
+
Mouth.daemon_hostport = "0.0.0.0:8889"
|
89
94
|
Mouth.increment('hello.world')
|
90
|
-
Mouth.measure('hello.happening'
|
95
|
+
Mouth.measure('hello.happening') { happen! }
|
96
|
+
Mouth.gauge('hello.level', 1000)
|
91
97
|
|
92
98
|
### Using any StatsD instrumentation
|
93
99
|
|
@@ -97,19 +103,27 @@ Mouth is StatsD compatible -- if you've instrumented your application to record
|
|
97
103
|
|
98
104
|
You can access and act on your metrics quite easily.
|
99
105
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
require 'mouth'
|
107
|
+
require 'mouth/sequence_query'
|
108
|
+
|
109
|
+
Mouth::SequenceQuery.new("exceptions.app", :kind => :counter).sequence
|
110
|
+
# => [4, 9, 0, ...]
|
111
|
+
|
112
|
+
Mouth::SequenceQuery.new("app.requests", :kind => :timer, :granularity_in_minutes => 15, :start_time => Time.now - 86400, :end_time => Time.now).sequence
|
113
|
+
# => [{:count => 3, :min => 1, :max => 30, :mean => 17.0, :sum => 51.0, :median => 20, :stddev => 12.02}, ...]
|
114
|
+
|
115
|
+
Additionally, you can insert metrics directly into the Mongo store, without sending UDP packets. You might want to do this if you need guarantees UDP can't provide.
|
116
|
+
|
117
|
+
require 'mouth'
|
118
|
+
require 'mouth/recorder'
|
119
|
+
|
120
|
+
Mouth::Recorder.increment("app.happening")
|
121
|
+
Mouth::Recorder.gauge("app.level", 10)
|
122
|
+
# Mouth::Recorder.measure("app.occurrence") { occur! } # Currently unsupported
|
109
123
|
|
110
124
|
## Tech
|
111
125
|
|
112
|
-
* **Ruby** - 1.9.2+ is required. Ruby was chosen because many Ruby shops already have it deployed as part of their
|
126
|
+
* **Ruby** - 1.9.2+ is required. Ruby was chosen because many Ruby shops already have it deployed as part of their infrastructure. By putting everything in Ruby, node + python aren't needed.
|
113
127
|
* **MongoDB** - Mouth stores metrics in Mongo. Mongo was chosen for 3 reasons:
|
114
128
|
* It's very easy to install and get going
|
115
129
|
* It has drivers for everything, so getting at your data is super easy
|
@@ -119,12 +133,11 @@ You can access and act on your metrics quite easily.
|
|
119
133
|
* **Backbone.js** - The web UI is powered by Backbone.js
|
120
134
|
* **D3.js** - The graphs are powered by D3.js
|
121
135
|
|
122
|
-
|
123
136
|
## Contributing
|
124
137
|
|
125
|
-
You're interested in contributing to Mouth? *AWESOME*.
|
138
|
+
You're interested in contributing to Mouth? *AWESOME*. Both bug reports and pull requests are welcome!
|
126
139
|
|
127
|
-
|
140
|
+
Fork Mouth from here: http://github.com/cypriss/mouth
|
128
141
|
|
129
142
|
1. Clone your fork
|
130
143
|
2. Hack away
|
@@ -135,4 +148,4 @@ fork Mouth from here: http://github.com/cypriss/mouth
|
|
135
148
|
|
136
149
|
## Thanks
|
137
150
|
|
138
|
-
Thanks to UserVoice.com for sponsoring this project. Thanks to the [StatsD](https://github.com/etsy/statsd) project for massive inspiration. Other contributors: https://github.com/cypriss/mouth/graphs/contributors
|
151
|
+
Thanks to [UserVoice.com](http://developer.uservoice.com) for sponsoring this project. Thanks to the [StatsD](https://github.com/etsy/statsd) project for massive inspiration. Other contributors: https://github.com/cypriss/mouth/graphs/contributors
|
data/Rakefile
CHANGED
@@ -7,13 +7,13 @@ rescue Bundler::BundlerError => e
|
|
7
7
|
$stderr.puts "Run `bundle install` to install missing gems"
|
8
8
|
exit e.status_code
|
9
9
|
end
|
10
|
-
require 'rake'
|
11
10
|
|
12
11
|
require 'rake/testtask'
|
13
12
|
Rake::TestTask.new(:test) do |test|
|
13
|
+
test.ruby_opts << '-rubygems'
|
14
14
|
test.libs << 'lib' << 'test'
|
15
15
|
test.pattern = 'test/*_test.rb'
|
16
16
|
test.verbose = true
|
17
17
|
end
|
18
18
|
|
19
|
-
task :default => :test
|
19
|
+
task :default => :test
|
data/TODO
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
Now
|
2
|
-
- Build a caching system so that ticks don't pull in old data
|
3
|
-
|
4
1
|
Next
|
2
|
+
- Logging before/after daemonization
|
3
|
+
- Build a caching system so that ticks don't pull in old data
|
5
4
|
- Cookie-ize the timespan settings
|
6
5
|
- Re implement graph autoplacement
|
7
6
|
- Better backend impl of source list
|
@@ -10,6 +9,8 @@ Next
|
|
10
9
|
- Build in some mechanisms to diagnose mongo connection issues
|
11
10
|
- Auto refreshing source list (every minute?)
|
12
11
|
- More Tests
|
12
|
+
- sequence: generate sample
|
13
|
+
- source
|
13
14
|
- Figure out time zones
|
14
15
|
- replicaset connection for endoscope
|
15
16
|
|
@@ -29,7 +30,6 @@ Someday
|
|
29
30
|
|
30
31
|
Bugs:
|
31
32
|
- When picker is open, changing dashboards should hide picker
|
32
|
-
- tooltip spawns multiple copies on large datasets
|
33
33
|
|
34
34
|
Refactorings:
|
35
35
|
- reconcile naming of 'sources' sometimes being array of strings and sometimes array of objects
|
data/bin/mouth
CHANGED
@@ -60,9 +60,9 @@ parser = OptionParser.new do |op|
|
|
60
60
|
end
|
61
61
|
|
62
62
|
# NOTE: this option can be given multiple times for a replica set
|
63
|
-
op.on("--mongohost HOSTPORT", "STORE SUCKINGS IN THIS MONGO") do |
|
64
|
-
options[:
|
65
|
-
options[:
|
63
|
+
op.on("--mongohost HOSTPORT", "STORE SUCKINGS IN THIS MONGO [eg, localhost or localhost:27017]") do |mongo_hostport|
|
64
|
+
options[:mongo_hostports] ||= []
|
65
|
+
options[:mongo_hostports] << mongo_hostport
|
66
66
|
end
|
67
67
|
|
68
68
|
op.on("-h", "--help", "I WANT MOAR HALP") do
|
data/bin/mouth-console
CHANGED
data/bin/mouth-endoscope
CHANGED
@@ -12,11 +12,8 @@ rescue LoadError
|
|
12
12
|
end
|
13
13
|
|
14
14
|
Vegas::Runner.new(Mouth::Endoscope, 'mouth-endoscope') do |runner, opts, app|
|
15
|
-
opts.on("--mongohost
|
16
|
-
Mouth.
|
17
|
-
|
18
|
-
|
19
|
-
opts.on("--mongoport PORT", "STORE SUCKINGS IN THIS MONGO PORT") do |mongo_port|
|
20
|
-
Mouth.mongo_port = mongo_port
|
15
|
+
opts.on("--mongohost HOSTPORT", "READ FROM THIS MONGO [eg, localhost or localhost:27017]") do |mongo_hostport|
|
16
|
+
Mouth.mongo_hostports ||= []
|
17
|
+
Mouth.mongo_hostports << mongo_hostport
|
21
18
|
end
|
22
19
|
end
|
data/lib/mouth.rb
CHANGED
@@ -4,14 +4,35 @@ require 'mouth/version'
|
|
4
4
|
module Mouth
|
5
5
|
class << self
|
6
6
|
attr_accessor :logger
|
7
|
-
|
8
|
-
|
7
|
+
|
8
|
+
# Mongo connection
|
9
|
+
attr_accessor :mongo
|
10
|
+
|
11
|
+
# Info to connect to mongo
|
12
|
+
attr_accessor :mongo_db_name
|
13
|
+
attr_accessor :mongo_hostports
|
9
14
|
|
10
15
|
# Returns a mongo connection (NOT an em-mongo connection)
|
11
16
|
def mongo
|
12
17
|
@mongo ||= begin
|
13
18
|
require 'mongo' # require mongo here, as opposed to the top, because we don't want mongo included in the reactor (use em-mongo for that)
|
14
|
-
|
19
|
+
|
20
|
+
hostports = self.mongo_hostports || [["localhost", Mongo::Connection::DEFAULT_PORT]]
|
21
|
+
self.mongo_hostports = hostports.collect do |hp|
|
22
|
+
if hp.is_a?(String)
|
23
|
+
host, port = hp.split(":")
|
24
|
+
[host, port || Mongo::Connection::DEFAULT_PORT]
|
25
|
+
else
|
26
|
+
hp
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if self.mongo_hostports.length == 1
|
31
|
+
hostport = self.mongo_hostports.first
|
32
|
+
Mongo::Connection.new(hostport[0], hostport[1], :pool_size => 5, :pool_timeout => 20).db(self.mongo_db_name || "mouth")
|
33
|
+
else
|
34
|
+
raise "repls set con not impl"
|
35
|
+
end
|
15
36
|
end
|
16
37
|
end
|
17
38
|
|
@@ -28,6 +49,10 @@ module Mouth
|
|
28
49
|
"mouth_#{namespace}"
|
29
50
|
end
|
30
51
|
|
52
|
+
def collection_for(namespace)
|
53
|
+
collection(mongo_collection_name(namespace))
|
54
|
+
end
|
55
|
+
|
31
56
|
def sanitize_namespace(key)
|
32
57
|
key.gsub(/\s+/, '_').gsub(/\//, '-').gsub(/[^a-zA-Z_\-0-9]/, '')
|
33
58
|
end
|
@@ -55,8 +80,15 @@ module Mouth
|
|
55
80
|
end
|
56
81
|
|
57
82
|
[sanitize_namespace(namespace), sanitize_metric(metric)]
|
58
|
-
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def current_timestamp
|
86
|
+
timestamp_for(Time.now)
|
87
|
+
end
|
59
88
|
|
89
|
+
def timestamp_for(time)
|
90
|
+
time.to_i / 60
|
91
|
+
end
|
60
92
|
|
61
|
-
end
|
93
|
+
end
|
62
94
|
end
|
data/lib/mouth/endoscope.rb
CHANGED
@@ -41,7 +41,7 @@ var Dashboard = Backbone.Model.extend({
|
|
41
41
|
|
42
42
|
var Graph = Backbone.Model.extend({
|
43
43
|
defaults: {
|
44
|
-
kind: '
|
44
|
+
kind: 'counter'
|
45
45
|
},
|
46
46
|
|
47
47
|
name: function() {
|
@@ -538,6 +538,7 @@ var GraphView = Backbone.View.extend({
|
|
538
538
|
params.end_time = Math.floor(+range.endDate / 1000);
|
539
539
|
|
540
540
|
$.getJSON('/graphs/' + self.model.get('id') + '/data', params, function(data) {
|
541
|
+
$('#' + self.graphId).empty().unbind();
|
541
542
|
var seven = null;
|
542
543
|
_.each(data, function(d) {
|
543
544
|
if (!seven) {
|
@@ -665,7 +666,7 @@ var SourceList = Backbone.View.extend({
|
|
665
666
|
|
666
667
|
html = [
|
667
668
|
'<div class="head">',
|
668
|
-
'<h2>Choose multiple counters or one timer:</h2>',
|
669
|
+
'<h2>Choose multiple counters, multiple gaugues, or one timer:</h2>',
|
669
670
|
// '<input type="text" class="idea" />',
|
670
671
|
'<a href="#" class="cancel">X</a>',
|
671
672
|
'</div>',
|
@@ -774,9 +775,14 @@ var SourceList = Backbone.View.extend({
|
|
774
775
|
target.prop('checked', true);
|
775
776
|
}
|
776
777
|
|
777
|
-
// If you check a counter, all
|
778
|
+
// If you check a counter, all non-counters are unchecked
|
778
779
|
if (source.kind === 'counter') {
|
779
|
-
el.find('input:checkbox.timer').prop('checked', false);
|
780
|
+
el.find('input:checkbox.timer, input:checkbox.gauge').prop('checked', false);
|
781
|
+
}
|
782
|
+
|
783
|
+
// If you check a gauge, all non-gauges are unchecked
|
784
|
+
if (source.kind === 'gauge') {
|
785
|
+
el.find('input:checkbox.timer, input:checkbox.counter').prop('checked', false);
|
780
786
|
}
|
781
787
|
}
|
782
788
|
|
@@ -31,6 +31,7 @@ window.Seven = function(selector, opts) {
|
|
31
31
|
this.points = opts.points;
|
32
32
|
this.endDate = dateFor(this.startDate, this.granularityInMinutes, this.points);
|
33
33
|
this.kind = opts.kind || 'counter';
|
34
|
+
if (this.kind == 'gauge') this.kind = 'counter'; // Gauges and counters and graphed the same
|
34
35
|
this.dateFormat = d3.time.format("%Y-%m-%d %H:%M:%S");
|
35
36
|
}
|
36
37
|
|
data/lib/mouth/graph.rb
CHANGED
@@ -8,7 +8,7 @@ module Mouth
|
|
8
8
|
# :height => 7,
|
9
9
|
# :width => 20
|
10
10
|
# },
|
11
|
-
# :kind => '
|
11
|
+
# :kind => 'counter' || 'timer' || 'gauge',
|
12
12
|
# :sources => ["auth.authentications"]
|
13
13
|
# }
|
14
14
|
class Graph < Record
|
@@ -27,7 +27,7 @@ module Mouth
|
|
27
27
|
def data(opts = {})
|
28
28
|
sources = self.attributes[:sources] || []
|
29
29
|
seq_opts = {:kind => self.attributes[:kind].to_sym}.merge(opts)
|
30
|
-
sequence =
|
30
|
+
sequence = SequenceQuery.new(sources, seq_opts)
|
31
31
|
seqs = sequence.sequences
|
32
32
|
seqs.map do |seq|
|
33
33
|
{
|
data/lib/mouth/instrument.rb
CHANGED
@@ -4,20 +4,20 @@ require 'benchmark'
|
|
4
4
|
module Mouth
|
5
5
|
unless self.respond_to?(:measure)
|
6
6
|
class << self
|
7
|
-
attr_accessor :
|
7
|
+
attr_accessor :daemon_host, :daemon_port, :disabled
|
8
8
|
|
9
9
|
# Mouth.server = 'localhost:1234'
|
10
|
-
def
|
11
|
-
self.
|
12
|
-
self.
|
10
|
+
def daemon_hostport=(hostport)
|
11
|
+
self.daemon_host, port = hostport.split(':')
|
12
|
+
self.daemon_port = port.to_i
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
16
|
-
@
|
15
|
+
def daemon_host
|
16
|
+
@daemon_host || "localhost"
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
20
|
-
@
|
19
|
+
def daemon_port
|
20
|
+
@daemon_port || 8889
|
21
21
|
end
|
22
22
|
|
23
23
|
def measure(key, milli = nil)
|
@@ -32,6 +32,10 @@ module Mouth
|
|
32
32
|
def increment(key, delta = 1, sample_rate = nil)
|
33
33
|
write(key, delta, :c, sample_rate)
|
34
34
|
end
|
35
|
+
|
36
|
+
def gauge(key, value)
|
37
|
+
write(key, value, :g)
|
38
|
+
end
|
35
39
|
|
36
40
|
protected
|
37
41
|
|
@@ -49,7 +53,7 @@ module Mouth
|
|
49
53
|
command = "#{k}:#{v}|#{op}"
|
50
54
|
command << "|@#{sample_rate}" if sample_rate
|
51
55
|
|
52
|
-
socket.send(command, 0, self.
|
56
|
+
socket.send(command, 0, self.daemon_host, self.daemon_port)
|
53
57
|
end
|
54
58
|
end
|
55
59
|
end
|
data/lib/mouth/record.rb
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Mouth
|
2
|
+
|
3
|
+
# If you don't want to use the Mouth daemon to collect UDP packets and write digests, you can use this to write metrics directly into mongo.
|
4
|
+
# IMPORTANT: You can't use this + the daemon simultaneously for a given metric namespace.
|
5
|
+
module Recorder
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def increment(key, delta = 1, sample_rate = nil)
|
9
|
+
factor = 1.0
|
10
|
+
if sample_rate
|
11
|
+
sample_rate = 1 if sample_rate > 1
|
12
|
+
return if rand > sample_rate
|
13
|
+
factor = (sample_rate <= 0.0 || sample_rate > 1.0) ? 1.0 : 1.0 / sample_rate
|
14
|
+
end
|
15
|
+
|
16
|
+
ns, metric = Mouth.parse_key(key)
|
17
|
+
collection = Mouth.collection_for(ns)
|
18
|
+
t = Mouth.current_timestamp
|
19
|
+
|
20
|
+
|
21
|
+
collection.update({"t" => t}, {"$inc" => {"c.#{metric}" => delta * factor}}, :upsert => true)
|
22
|
+
end
|
23
|
+
|
24
|
+
def gauge(key, value)
|
25
|
+
ns, metric = Mouth.parse_key(key)
|
26
|
+
collection = Mouth.collection_for(ns)
|
27
|
+
t = Mouth.current_timestamp
|
28
|
+
|
29
|
+
|
30
|
+
collection.update({"t" => t}, {"$set" => {"g.#{metric}" => value}}, :upsert => true)
|
31
|
+
end
|
32
|
+
|
33
|
+
def measure(key, milli = nil)
|
34
|
+
raise "Not implemented"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|