mouth 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +7 -0
  2. data/Capfile +26 -0
  3. data/Gemfile +3 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +138 -0
  6. data/Rakefile +19 -0
  7. data/TODO +32 -0
  8. data/bin/mouth +77 -0
  9. data/bin/mouth-console +18 -0
  10. data/bin/mouth-endoscope +18 -0
  11. data/lib/mouth.rb +61 -0
  12. data/lib/mouth/dashboard.rb +25 -0
  13. data/lib/mouth/endoscope.rb +120 -0
  14. data/lib/mouth/endoscope/public/222222_256x240_icons_icons.png +0 -0
  15. data/lib/mouth/endoscope/public/application.css +464 -0
  16. data/lib/mouth/endoscope/public/application.js +938 -0
  17. data/lib/mouth/endoscope/public/backbone.js +1158 -0
  18. data/lib/mouth/endoscope/public/d3.js +4707 -0
  19. data/lib/mouth/endoscope/public/d3.time.js +687 -0
  20. data/lib/mouth/endoscope/public/jquery-ui-1.8.16.custom.min.js +177 -0
  21. data/lib/mouth/endoscope/public/jquery.js +4 -0
  22. data/lib/mouth/endoscope/public/json2.js +480 -0
  23. data/lib/mouth/endoscope/public/keymaster.js +163 -0
  24. data/lib/mouth/endoscope/public/linen.js +46 -0
  25. data/lib/mouth/endoscope/public/seven.css +68 -0
  26. data/lib/mouth/endoscope/public/seven.js +291 -0
  27. data/lib/mouth/endoscope/public/underscore.js +931 -0
  28. data/lib/mouth/endoscope/views/dashboard.erb +67 -0
  29. data/lib/mouth/graph.rb +58 -0
  30. data/lib/mouth/instrument.rb +56 -0
  31. data/lib/mouth/record.rb +72 -0
  32. data/lib/mouth/runner.rb +89 -0
  33. data/lib/mouth/sequence.rb +284 -0
  34. data/lib/mouth/source.rb +76 -0
  35. data/lib/mouth/sucker.rb +235 -0
  36. data/lib/mouth/version.rb +3 -0
  37. data/mouth.gemspec +28 -0
  38. data/test/sequence_test.rb +163 -0
  39. data/test/sucker_test.rb +55 -0
  40. data/test/test_helper.rb +5 -0
  41. metadata +167 -0
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .rvmrc
2
+ *.log
3
+ *.pid
4
+ log/*.log
5
+ log/*.pid
6
+ Gemfile.lock
7
+
data/Capfile ADDED
@@ -0,0 +1,26 @@
1
+ load 'deploy'
2
+
3
+ # Simply way to deploy the app if you're developing it
4
+ # I will probably remove this file later.
5
+ set :application, "mouth"
6
+ default_run_options[:pty] = true
7
+
8
+ set :scm, :none
9
+ role :app, "sugar"
10
+
11
+ task :omg do
12
+ res = `gem build mouth.gemspec`
13
+ if res =~ /Success/
14
+ home = "#{capture('pwd').strip}/mouth"
15
+ file = res.match(/File: (.+)$/)[1]
16
+ run "mkdir -p ~/mouth"
17
+ loc = "#{home}/#{file}"
18
+ upload file, loc
19
+ sudo "gem install #{loc}"
20
+ run "mouth-endoscope -K"
21
+ run "mouth --pidfile #{home}/mouth.pid -K"
22
+ run "sleep 5"
23
+ run "mouth-endoscope --mongohost 10.36.103.70"
24
+ run "mouth --pidfile #{home}/mouth.pid --logfile #{home}/mouth.log -H 10.18.23.135 -P 8889 --mongohost 10.36.103.70"
25
+ end
26
+ end
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Jonathan Novak - http://github.com/cypriss
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # Mouth
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/).
4
+
5
+
6
+ ## Why
7
+
8
+ Why duplicate effort of the excellent StatsD / Graphite packages? I wanted a graphing and monitoring tool that offered:
9
+
10
+
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
+ * **Easy installation**. Mouth depends on just Ruby and Mongo, both incredibly easy to install.
13
+ * **Modern, friendly UI**. Mouth graphs are beautiful and easy to work with. Exploring data and building dashboards are a joy.
14
+
15
+ ## Kinds of Metrics
16
+
17
+ There are two kinds of metrics currently: counters and timers. Both are stored in a time series with minute granularity.
18
+
19
+ * **Counters**:
20
+ * how many occurances of something happen per minute?
21
+ * Can be sampled
22
+ * Standard usage: Mouth.increment("myapp.happenings")
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
+ * Gives you a time series of happenings by minute: {"myapp.happenings" => [1, 2, 5, 20, ...]}
25
+ * **Timers**:
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)
31
+
32
+ ## Installation on OSX
33
+
34
+ Install mouth:
35
+
36
+ gem install mouth
37
+
38
+ Install MongoDB if you haven't:
39
+
40
+ brew install mongodb
41
+
42
+ Start collector:
43
+
44
+ mouth
45
+
46
+ Start web UI:
47
+
48
+ mouth-endoscope
49
+
50
+ Record a metric:
51
+
52
+ ruby -e 'require "mouth"; require "mouth/instrument"; Mouth.increment("gorets")'
53
+
54
+ To load the web UI, go to http://0.0.0.0:5678/ (or whatever port got chosen -- see the Terminal). Click 'Add Graph' in the lower right-hand corner.
55
+
56
+ ## Installation in Production
57
+
58
+ You'll want to follow the general gist of what you did for OSX, but make sure to specify your hosts, ports, and log locations.
59
+ NOTE: there is no config file -- all options are via command-line.
60
+
61
+ sudo gem install mouth
62
+ mouth --pidfile /path/to/log/mouth.pid --logfile /path/to/log/mouth.log -H x.x.x.x -P 8889 --mongohost y.y.y.y --verbosity 1
63
+ mouth-endoscope --mongohost x.x.x.x
64
+
65
+ ## Instrumenting Your Application to Record Metrics
66
+
67
+ There are many ways to instrument your application:
68
+
69
+ ### Using the mouth gem
70
+
71
+ Mouth comes with a built-in facility to instrument your apps:
72
+
73
+ require 'mouth'
74
+ require 'mouth/instrument'
75
+
76
+ Mouth.server = "0.0.0.0:8889"
77
+ Mouth.increment('hello.world')
78
+ Mouth.measure('hello.happening', 42.9)
79
+
80
+ ### Using mouth-instrument
81
+
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:
83
+
84
+ gem install mouth-instrument
85
+
86
+ require 'mouth-instrument'
87
+
88
+ Mouth.server = "0.0.0.0:8889"
89
+ Mouth.increment('hello.world')
90
+ Mouth.measure('hello.happening', 42.9)
91
+
92
+ ### Using any StatsD instrumentation
93
+
94
+ Mouth is StatsD compatible -- if you've instrumented your application to record StatsD metrics, it should work on Mouth. Just replace your StatsD server with a mouth process.
95
+
96
+ ## Accessing Your Data Via Scripts
97
+
98
+ You can access and act on your metrics quite easily.
99
+
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}, ...]
109
+
110
+ ## Tech
111
+
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.
113
+ * **MongoDB** - Mouth stores metrics in Mongo. Mongo was chosen for 3 reasons:
114
+ * It's very easy to install and get going
115
+ * It has drivers for everything, so getting at your data is super easy
116
+ * It's schemaless design is fairly good for storing time series metrics.
117
+ * **EventMachine** - Mouth is powered by EM, the Ruby way of doing nonblocking IO.
118
+ * **Sinatra** - The web UI is served with a simple Sinatra app.
119
+ * **Backbone.js** - The web UI is powered by Backbone.js
120
+ * **D3.js** - The graphs are powered by D3.js
121
+
122
+
123
+ ## Contributing
124
+
125
+ You're interested in contributing to Mouth? *AWESOME*. Here are the basic steps:
126
+
127
+ fork Mouth from here: http://github.com/cypriss/mouth
128
+
129
+ 1. Clone your fork
130
+ 2. Hack away
131
+ 3. If you are adding new functionality, document it in the README
132
+ 4. If necessary, rebase your commits into logical chunks, without errors
133
+ 5. Push the branch up to GitHub
134
+ 6. Send a pull request to the cypriss/mouth project.
135
+
136
+ ## Thanks
137
+
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
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'rake/testtask'
13
+ Rake::TestTask.new(:test) do |test|
14
+ test.libs << 'lib' << 'test'
15
+ test.pattern = 'test/*_test.rb'
16
+ test.verbose = true
17
+ end
18
+
19
+ task :default => :test
data/TODO ADDED
@@ -0,0 +1,32 @@
1
+ Next
2
+ - Cookie-ize the timespan settings
3
+ - Re implement graph autoplacement
4
+ - Better backend impl of source list
5
+ - Figure out how dashboards can scroll
6
+ - Show period averages / rates
7
+ - Full keyboard navigation
8
+ - Build in some mechanisms to diagnose mongo connection issues
9
+ - Auto refreshing source list (every minute?)
10
+ - More Tests
11
+ - Figure out time zones
12
+
13
+ Someday
14
+ - Picker:
15
+ - When you re-pick sources, it should scroll to the existing picked items
16
+ - Selected rows should also be highlighted in addition to checked
17
+ - should show a little sparkline or some sense of how old the data is
18
+ - Source picker textbox filter
19
+ - Manual refresh source list icon
20
+ - Graphing
21
+ - Restructure charting code to not remove everything when updating with new data
22
+ - Plugin system
23
+ - 'Dashboards' should be a module
24
+ - 'Explorer' module
25
+ - 'Monitor' module
26
+
27
+ Bugs:
28
+ - When picker is open, changing dashboards should hide picker
29
+ - tooltip spawns multiple copies on large datasets
30
+
31
+ Refactorings:
32
+ - reconcile naming of 'sources' sometimes being array of strings and sometimes array of objects
data/bin/mouth ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'optparse'
6
+ require 'mouth'
7
+ require 'mouth/runner'
8
+ require 'mouth/sucker'
9
+
10
+ options = {
11
+ :verbosity => 0,
12
+ :host => "localhost",
13
+ :port => 8889
14
+ }
15
+
16
+ parser = OptionParser.new do |op|
17
+ op.banner = "Usage: mouth [options]"
18
+
19
+ op.separator ""
20
+ op.separator "Options:"
21
+
22
+ ##
23
+ ## Daemonization / Logging options
24
+ ##
25
+
26
+ op.on("--pidfile PATH", "DO YOU WANT ME TO WRITE A PIDFILE SOMEWHERE FOR U?") do |pid_file|
27
+ options[:pid_file] = pid_file
28
+ end
29
+
30
+ op.on("--logfile PATH", "I'LL POOP OUT LOGS HERE FOR U") do |log_file|
31
+ options[:log_file] = log_file
32
+ end
33
+
34
+ op.on("-v", "--verbosity LEVEL", "HOW MUCH POOP DO U WANT IN UR LOGS? [LEVEL=0:errors,1:some,2:lots of poop]") do |verbosity|
35
+ options[:verbosity] = verbosity.to_i
36
+ end
37
+
38
+ op.on("-K", "--kill", "SHUT THE MOUTH") do
39
+ options[:kill] = true
40
+ end
41
+
42
+ ##
43
+ ## Socket to suck from
44
+ ##
45
+
46
+ op.on("-H", "--host HOST", "I SUCK ON THIS NETWORK INTERFACE") do |host|
47
+ options[:host] = host
48
+ end
49
+
50
+ op.on("-P", "--port PORT", "I SUCK FROM THIS HOLE") do |port|
51
+ options[:port] = port.to_i
52
+ end
53
+
54
+ ##
55
+ ## Mongo
56
+ ##
57
+ op.on("--mongodb DATABASE", "STORE SUCKINGS IN THIS DB") do |mongo_db|
58
+ options[:mongo_db] = mongo_db
59
+ end
60
+
61
+ # NOTE: this option can be given multiple times for a replica set
62
+ op.on("--mongohost HOSTPORT", "STORE SUCKINGS IN THIS MONGO") do |mongo_host|
63
+ options[:mongo_hosts] ||= []
64
+ options[:mongo_hosts] << mongo_host
65
+ end
66
+
67
+ op.on("-h", "--help", "I WANT MOAR HALP") do
68
+ puts op
69
+ exit
70
+ end
71
+
72
+ op.separator ""
73
+ end
74
+
75
+ parser.parse!
76
+
77
+ Mouth::Runner.new(options).run!
data/bin/mouth-console ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Running bin/mouth-console will put you in an IRB session with the mouth files loaded. Convenient for development and experimentation.
4
+
5
+ require 'irb'
6
+ require 'irb/completion'
7
+
8
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
9
+
10
+ require 'mouth'
11
+ require 'mouth/sequence'
12
+ require 'mouth/record'
13
+ require 'mouth/graph'
14
+ require 'mouth/dashboard'
15
+ require 'mouth/source'
16
+ require 'mouth/instrument'
17
+
18
+ IRB.start
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'vegas'
6
+ require 'mouth'
7
+ require 'mouth/endoscope'
8
+
9
+ begin
10
+ require 'thin'
11
+ rescue LoadError
12
+ end
13
+
14
+ Vegas::Runner.new(Mouth::Endoscope, 'mouth-endoscope') do |runner, opts, app|
15
+ opts.on("--mongohost HOSTPORT", "STORE SUCKINGS IN THIS MONGO") do |mongo_host|
16
+ Mouth.mongo_host = mongo_host
17
+ end
18
+ end
data/lib/mouth.rb ADDED
@@ -0,0 +1,61 @@
1
+ # Since the gem can be used in many ways (sucker daemon, sinatra app, instrument, ad-hoc, don't include anything)
2
+ require 'mouth/version'
3
+
4
+ module Mouth
5
+ class << self
6
+ attr_accessor :logger
7
+ attr_accessor :mongo_host
8
+
9
+ # Returns a mongo connection (NOT an em-mongo connection)
10
+ def mongo
11
+ @mongo ||= begin
12
+ 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)
13
+ Mongo::Connection.new(self.mongo_host || "localhost").db("mouth")
14
+ end
15
+ end
16
+
17
+ def collection(collection_name)
18
+ @collections ||= {}
19
+ @collections[collection_name] ||= begin
20
+ c = mongo.collection(collection_name)
21
+ c.ensure_index([["t", 1]], {:background => true, :unique => true})
22
+ c
23
+ end
24
+ end
25
+
26
+ def mongo_collection_name(namespace)
27
+ "mouth_#{namespace}"
28
+ end
29
+
30
+ def sanitize_namespace(key)
31
+ key.gsub(/\s+/, '_').gsub(/\//, '-').gsub(/[^a-zA-Z_\-0-9]/, '')
32
+ end
33
+
34
+ def sanitize_metric(key)
35
+ key.gsub(/\s+/, '_').gsub(/[^a-zA-Z0-9\-_\/]/, '')
36
+ end
37
+
38
+ # Parses a key into two parts: namespace, and metric. Also sanitizes each field
39
+ # Returns an array of values: [namespace, metric]
40
+ # eg,
41
+ # parse_key("Ticket.process_new_ticket") # => ["Ticket", "process_new_ticket"]
42
+ # parse_key("Forum List.other! stuff.ok") # => ["Forum_List", "other_stuff-ok"]
43
+ # parse_key("no_namespace") # => ["default", "no_namespace"]
44
+ def parse_key(key)
45
+ parts = key.split(".")
46
+ namespace = nil
47
+ metric = nil
48
+ if parts.length > 1
49
+ namespace = parts.shift
50
+ metric = parts.join("-")
51
+ else
52
+ namespace = "default"
53
+ metric = parts.shift
54
+ end
55
+
56
+ [sanitize_namespace(namespace), sanitize_metric(metric)]
57
+ end
58
+
59
+
60
+ end
61
+ end
@@ -0,0 +1,25 @@
1
+ module Mouth
2
+
3
+ # A dashboard has these fields:
4
+ # id
5
+ # name
6
+ # todo: order
7
+ class Dashboard < Record
8
+
9
+ def all_attributes
10
+ self.attributes.tap {|attrs| attrs[:graphs] = graphs.collect(&:all_attributes) }
11
+ end
12
+
13
+ def destroy
14
+ self.graphs.each(&:destroy)
15
+ super
16
+ end
17
+
18
+ # An array of graphs
19
+ def graphs
20
+ @graphs ||= begin
21
+ Graph.for_dashboard(self.attributes[:id])
22
+ end
23
+ end
24
+ end
25
+ end