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