mouth 0.8.1 → 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Mouth Screenshot](http://cypriss.github.com/mouth/images/ss/mouth-screenshot1.png)
|
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
|