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 CHANGED
@@ -5,3 +5,4 @@ log/*.log
5
5
  log/*.pid
6
6
  Gemfile.lock
7
7
  *.gem
8
+ Capfile
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Jonathan Novak - http://github.com/cypriss
1
+ Copyright (c) 2012 Jonathan Novak - http://github.com/cypriss
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Mouth
2
2
 
3
- Mouth is a Ruby daemon that collects stats 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/).
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
- * how many occurances of something happen per minute?
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.occurances", 3.3) # This occurance took 3.3 ms to occur
28
- * Standard usage 2: Mouth.measure("myapp.occurances") { do_occurance() }
29
- * Gives you a time series of occurance timings by minute: {"myapp.happenings" => [1, 2, 5, 20, ...]}
30
- * **Gauges**: (Coming soon)
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.server = "0.0.0.0:8889"
80
+ Mouth.daemon_hostport = "0.0.0.0:8889"
77
81
  Mouth.increment('hello.world')
78
- Mouth.measure('hello.happening', 42.9)
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.server = "0.0.0.0:8889"
93
+ Mouth.daemon_hostport = "0.0.0.0:8889"
89
94
  Mouth.increment('hello.world')
90
- Mouth.measure('hello.happening', 42.9)
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
- require 'mouth'
101
- require 'mouth/sequence'
102
-
103
- Mouth.host = "0.0.0.0:8889"
104
- Sequence.new("exceptions.app", :kind => :counter).sequence
105
- # => [4, 9, 0, ...]
106
-
107
- Sequence.new("app.requests", :kind => :timer, :granularity_in_minutes => 15, :start_time => Time.now - 86400, :end_time => Time.now).sequence
108
- # => [{:count => 3, :min => 1, :max => 30, :mean => 17.0, :sum => 51.0, :median => 20, :stddev => 12.02}, ...]
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 infrastucture. By putting everything in Ruby, node + python aren't needed.
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*. Here are the basic steps:
138
+ You're interested in contributing to Mouth? *AWESOME*. Both bug reports and pull requests are welcome!
126
139
 
127
- fork Mouth from here: http://github.com/cypriss/mouth
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 |mongo_host|
64
- options[:mongo_hosts] ||= []
65
- options[:mongo_hosts] << mongo_host
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
@@ -8,7 +8,7 @@ require 'irb/completion'
8
8
  $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
9
9
 
10
10
  require 'mouth'
11
- require 'mouth/sequence'
11
+ require 'mouth/sequence_query'
12
12
  require 'mouth/record'
13
13
  require 'mouth/graph'
14
14
  require 'mouth/dashboard'
@@ -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 HOST", "STORE SUCKINGS IN THIS MONGO HOST") do |mongo_host|
16
- Mouth.mongo_host = mongo_host
17
- end
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
@@ -4,14 +4,35 @@ require 'mouth/version'
4
4
  module Mouth
5
5
  class << self
6
6
  attr_accessor :logger
7
- attr_accessor :mongo_host
8
- attr_accessor :mongo_port
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
- Mongo::Connection.new(self.mongo_host || "localhost", self.mongo_port || 27017, :pool_size => 5, :pool_timeout => 20).db("mouth")
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
@@ -2,7 +2,7 @@ require 'sinatra/base'
2
2
  require 'erb'
3
3
  require 'yajl'
4
4
 
5
- require 'mouth/sequence'
5
+ require 'mouth/sequence_query'
6
6
  require 'mouth/record'
7
7
  require 'mouth/graph'
8
8
  require 'mouth/dashboard'
@@ -41,7 +41,7 @@ var Dashboard = Backbone.Model.extend({
41
41
 
42
42
  var Graph = Backbone.Model.extend({
43
43
  defaults: {
44
- kind: 'counters'
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 timers are unchecked
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
 
@@ -8,7 +8,7 @@ module Mouth
8
8
  # :height => 7,
9
9
  # :width => 20
10
10
  # },
11
- # :kind => 'counters' || 'timer',
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 = Sequence.new(sources, seq_opts)
30
+ sequence = SequenceQuery.new(sources, seq_opts)
31
31
  seqs = sequence.sequences
32
32
  seqs.map do |seq|
33
33
  {
@@ -4,20 +4,20 @@ require 'benchmark'
4
4
  module Mouth
5
5
  unless self.respond_to?(:measure)
6
6
  class << self
7
- attr_accessor :host, :port, :disabled
7
+ attr_accessor :daemon_host, :daemon_port, :disabled
8
8
 
9
9
  # Mouth.server = 'localhost:1234'
10
- def server=(conn)
11
- self.host, port = conn.split(':')
12
- self.port = port.to_i
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 host
16
- @host || "localhost"
15
+ def daemon_host
16
+ @daemon_host || "localhost"
17
17
  end
18
18
 
19
- def port
20
- @port || 8889
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.host, self.port)
56
+ socket.send(command, 0, self.daemon_host, self.daemon_port)
53
57
  end
54
58
  end
55
59
  end
@@ -1,5 +1,6 @@
1
1
  module Mouth
2
-
2
+
3
+ # Simple base class to serve as an ORM for the mongo driver, without importing a large ORM like mongomapper.
3
4
  class Record
4
5
 
5
6
  # Keys are symbols
@@ -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