heroku_mongo_watcher 0.2.5.beta → 0.3.0

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/README.md CHANGED
@@ -17,14 +17,14 @@ It needed to accomplish the following:
17
17
 
18
18
  The output looks like the following ...
19
19
 
20
- |<---- heroku stats ----------------------------------------------------------------------->|<----mongo stats ------------------------------------------------------->|
21
- dyno reqs art max r_err w_err %err wait queue slowest | insert query update faults locked qr|qw netI/O time |
22
- 20 76 27 586 0 0 0.0% 0 0 /pxl/4fdbc97dc6b36c003000| 0 0 0 0 0 0|0 305b/257b 15:03:19
23
- 20 1592 62 1292 0 0 0.0% 0 0 /assets/companions/5009ab| 17 2 32 0 1.2 0|0 14k/26k 15:04:19
24
- [4] VAST Error
25
- [28] Timeout::Error
26
- [11] Cannot find impression when looking for asset
27
- 20 23935 190 7144 0 43 0.0% 0 0 /crossdomain.xml| 307 0 618 1 21.6 0|0 260k/221k 15:05:19
20
+ |<---- heroku stats ------------------------------------------------------------------->|<----mongo stats ------------------------------------------------>|
21
+ | dyno reqs art max r_err w_err %err wait queue slowest |insrt query updt flt lck lck:mrq qr|qw netI/O time |
22
+ 6 3096 57 870 0 2 0.06% 0 0 /assets/companions/50104c| 41 0 79 0 2.2% 0.71 0|0 36k/30k 15:59:29
23
+ 6 2705 80 3314 0 0 0.0% 0 0 /assets/companions/50104b| 34 0 67 0 2% 0.74 0|0 30k/25k 16:00:29
24
+ 6 2469 122 5708 0 0 0.0% 0 0 /ads/gw/50074348451823003| 30 0 57 0 1.7% 0.69 0|0 26k/22k 16:01:29
25
+ 6 2465 89 1347 0 0 0.0% 0 0 /assets/videos/501050b991| 30 0 59 0 1.8% 0.73 0|0 27k/22k 16:02:29
26
+ 6 2301 83 1912 0 4 0.17% 0 0 /assets/companions/501050| 28 0 57 0 1.7% 0.74 0|0 25k/21k 16:03:29
27
+ 6 1951 64 830 0 0 0.0% 0 0 /ads/gw/50074348451823003| 24 0 45 0 1.4% 0.72 0|0 21k/18k 16:04:29
28
28
 
29
29
  ### Legend
30
30
  <table>
@@ -42,6 +42,8 @@ The output looks like the following ...
42
42
  <tr><td>query</td><td>number of mongo queries</td></tr>
43
43
  <tr><td>update</td><td>number of mongo updates</td></tr>
44
44
  <tr><td>faults</td><td>number of mongo page faults</td></tr>
45
+ <tr><td>lck</td><td>mongo lock percentage</td></tr>
46
+ <tr><td>lck:mrq</td><td>ratio of lock% to 1000 requests</td></tr>
45
47
  <tr><td>qr|qw</td><td>number of mongo's queued read and writes</td></tr>
46
48
  <tr><td>netIO</td><td>size on mongo net in/ net out</td></tr>
47
49
  <tr><td>time</td><td>the time sampled</td></tr>
@@ -74,4 +76,16 @@ locally
74
76
  * `--print-errors` to print a summary of errors during each sample
75
77
  * `--print-requests` to print a summary of requests during each sample
76
78
 
79
+ ##Experimental
80
+
81
+ ( you probably
82
+
83
+ * `--autoscale` to autoscale dynos
84
+ * `--min-dynos=n` to set the min default 6
85
+ * `--max-dynos=n` to set the min default 50
86
+
77
87
  note: you can set these defaults in your .watcher file
88
+
89
+ ## Contact
90
+
91
+ jtushman@pipewave.com
data/example/.watcher CHANGED
@@ -21,9 +21,8 @@ mongo_password: 'skret'
21
21
  # note you already need to have the heroku gem installed
22
22
  # with creditails already set up for this directory
23
23
  heroku_appname: 'my-heroku-app-name'
24
-
25
- #if you have multiple accounts
26
- heroku_account: 'my-heroku-account'
24
+ heroku_username: 'ninja'
25
+ heroku_password: 'skret'
27
26
 
28
27
  # If you want to display the list of errors in the logs
29
28
  print_errors: true
@@ -0,0 +1,86 @@
1
+ require 'heroku_mongo_watcher'
2
+ require 'heroku_mongo_watcher/data_row'
3
+ require 'heroku_mongo_watcher/configuration'
4
+ require 'heroku'
5
+ class HerokuMongoWatcher::Autoscaler
6
+ include Singleton
7
+
8
+ attr_reader :last_scaled, :options
9
+
10
+ def initialize()
11
+ @last_scaled = Time.now - 60
12
+ @options = default_options.merge(config)
13
+ end
14
+
15
+ def default_options
16
+ {
17
+ min_dynos: 6,
18
+ max_dynos: 50,
19
+ step: 5,
20
+ requests_per_dyno: 1000,
21
+ min_frequency: 60 # seconds
22
+ }
23
+ end
24
+
25
+ def config
26
+ HerokuMongoWatcher::Configuration.instance.config
27
+ end
28
+
29
+ # should play it safe and add config[:step] to the calculated result
30
+ # if rpm > 10,000 scale to 15
31
+ # if rpm > 15,000 scale to 20
32
+ # if rpm > 20,000 scale to 25
33
+ # if rpm then drops to 14,000 drop to 15
34
+ # if rpm then drops to 300 drop to 6 (minimum)
35
+ #
36
+ # also do not scale down for 5 minutes
37
+ # always allow to scale up
38
+ def scale(data_row)
39
+
40
+ rpm = data_row.total_requests
41
+ current_dynos = data_row.dynos
42
+
43
+ # 32,012 rpm => 32 dynos
44
+ ideal_dynos = (rpm / options[:requests_per_dyno]).round
45
+
46
+ # return if the delta is less than 5 dynos | don't think I need to do this ...
47
+ #return if (ideal_dynos - current_dynos).abs < options[:step]
48
+
49
+ #this turns 32 into 30
50
+ stepped_dynos = (ideal_dynos/options[:step]).round * options[:step]
51
+
52
+ #this turns 30 into 35
53
+ stepped_dynos += options[:step]
54
+
55
+ #this makes sure that it stays within the min max bounds
56
+ stepped_dynos = options[:min_dynos] if stepped_dynos < options[:min_dynos]
57
+ stepped_dynos = options[:max_dynos] if stepped_dynos > options[:max_dynos]
58
+
59
+ # Don't allow downscaling until 5 minutes
60
+ if stepped_dynos < current_dynos && (Time.now - last_scaled) < (60 * 60) # 1 hour
61
+ #puts ">> Current: [#{current_dynos}], Ideal: [#{stepped_dynos}] | will not downscale within an hour"
62
+ nil
63
+ elsif stepped_dynos != current_dynos
64
+ set_dynos(stepped_dynos)
65
+ stepped_dynos
66
+ else
67
+ nil
68
+ end
69
+
70
+ end
71
+
72
+ def heroku
73
+ @heroku ||= Heroku::Client.new(config[:heroku_username], config[:heroku_password])
74
+ end
75
+
76
+ def set_dynos(count)
77
+ @last_scaled = Time.now
78
+ t = Thread.new('Setting Dynos') do
79
+ i = heroku.ps_scale(config[:heroku_appname], :type => 'web', :qty => count)
80
+ end
81
+ t.join
82
+
83
+ end
84
+
85
+
86
+ end
@@ -1,20 +1,9 @@
1
+ require 'heroku'
1
2
  require 'heroku_mongo_watcher'
2
3
  require 'heroku_mongo_watcher/configuration'
4
+ require 'heroku_mongo_watcher/autoscaler'
5
+ require 'heroku_mongo_watcher/mailer'
3
6
  require 'heroku_mongo_watcher/data_row'
4
- require 'trollop'
5
-
6
- #http://stackoverflow.com/a/9117903/192791
7
- require 'net/smtp'
8
- Net.instance_eval {remove_const :SMTPSession} if defined?(Net::SMTPSession)
9
-
10
- require 'net/pop'
11
- Net::POP.instance_eval {remove_const :Revision} if defined?(Net::POP::Revision)
12
- Net.instance_eval {remove_const :POP} if defined?(Net::POP)
13
- Net.instance_eval {remove_const :POPSession} if defined?(Net::POPSession)
14
- Net.instance_eval {remove_const :POP3Session} if defined?(Net::POP3Session)
15
- Net.instance_eval {remove_const :APOPSession} if defined?(Net::APOPSession)
16
-
17
- require 'tlsmail'
18
7
 
19
8
  class HerokuMongoWatcher::CLI
20
9
 
@@ -22,9 +11,20 @@ class HerokuMongoWatcher::CLI
22
11
  HerokuMongoWatcher::Configuration.instance.config
23
12
  end
24
13
 
14
+ def self.mailer
15
+ HerokuMongoWatcher::Mailer.instance
16
+ end
17
+
18
+ def self.autoscaler
19
+ HerokuMongoWatcher::Autoscaler.instance
20
+ end
21
+
22
+ def self.heroku
23
+ @heroku || Heroku::Client.new(config[:heroku_username], config[:heroku_password])
24
+ end
25
+
25
26
  def self.watch
26
27
  Thread.abort_on_exception = true
27
- notify("Mongo Watcher enabled!")
28
28
 
29
29
  # lock warnings flags
30
30
  @lock_critical_notified = false
@@ -39,37 +39,14 @@ class HerokuMongoWatcher::CLI
39
39
 
40
40
  @mutex = Mutex.new
41
41
 
42
- # Call Tails heroku logs updates counts
43
- heroku_watcher = Thread.new('heroku_logs') do
42
+ # Let people know that its on, and confirm emails are being received
43
+ mailer.notify(@current_row, "Mongo Watcher enabled!")
44
44
 
45
- cmd_string = "heroku logs --tail --app #{config[:heroku_appname]}"
46
- cmd_string = cmd_string + " --account #{config[:heroku_account]}" if config[:heroku_account] && config[:heroku_account].length > 0
47
- IO.popen(cmd_string) do |f|
48
- while line = f.gets
49
- @mutex.synchronize do
50
- @current_row.process_heroku_router_line(line) if line.include? 'heroku[router]'
51
- @current_row.process_heroku_web_line(line) if line.include? 'app[web'
52
- end
53
- end
54
- end
55
- end
45
+ # Call Tails heroku logs updates counts
46
+ heroku_watcher = Thread.new('heroku_logs') { tail_and_process_heroku }
56
47
 
57
48
  # Call Heroku PS to check the number of up dynos
58
- heroku_ps = Thread.new('heroku_ps') do
59
- while true do
60
- dynos = 0
61
- cmd_string = "heroku ps --app #{config[:heroku_appname]}"
62
- cmd_string = cmd_string + " --account #{config[:heroku_account]}" if config[:heroku_account] && config[:heroku_account].length > 0
63
- IO.popen(cmd_string) do |p|
64
- while line = p.gets
65
- dynos += 1 if line =~ /^web/ && line.split(' ')[1] == 'up'
66
- end
67
- end
68
- @mutex.synchronize { @current_row.dynos = dynos }
69
- sleep(30)
70
- end
71
- end
72
-
49
+ heroku_ps = Thread.new('heroku_ps') { periodicly_capture_heroku_ps }
73
50
 
74
51
  HerokuMongoWatcher::DataRow.print_header
75
52
 
@@ -83,6 +60,8 @@ class HerokuMongoWatcher::CLI
83
60
 
84
61
  check_and_notify
85
62
 
63
+ autoscale if config[:autoscale]
64
+
86
65
  @last_row = @current_row
87
66
  @current_row = HerokuMongoWatcher::DataRow.new
88
67
  @current_row.dynos = @last_row.dynos
@@ -94,6 +73,40 @@ class HerokuMongoWatcher::CLI
94
73
  heroku_watcher.join
95
74
  heroku_ps.join
96
75
 
76
+ rescue Interrupt
77
+ puts "\nexiting ..."
78
+ end
79
+
80
+ def self.autoscale
81
+ new_dyno_level = autoscaler.scale(@current_row)
82
+ if new_dyno_level
83
+ puts "! Scaling -> #{new_dyno_level}"
84
+ mailer.notify(@current_row, "Scaling to #{new_dyno_level}")
85
+ end
86
+ end
87
+
88
+ def self.periodicly_capture_heroku_ps
89
+ while true do
90
+ results = heroku.ps(config[:heroku_appname])
91
+ dynos = results.select { |ps| ps['process'] =~ /^web./ && ps['state'] == 'up' }.count
92
+
93
+ @mutex.synchronize { @current_row.dynos = dynos }
94
+ sleep(30)
95
+ end
96
+ end
97
+
98
+ def self.tail_and_process_heroku
99
+ heroku.read_logs(config[:heroku_appname], ['tail=1']) do |chunk|
100
+ unless chunk.empty?
101
+ chunk.split("\n").each do |line|
102
+ @mutex.synchronize do
103
+ @current_row.process_heroku_router_line(line) if line.include? 'heroku[router]'
104
+ @current_row.process_heroku_web_line(line) if line.include? 'app[web'
105
+ end
106
+ end
107
+ end
108
+
109
+ end
97
110
  end
98
111
 
99
112
  def self.check_and_notify
@@ -104,14 +117,14 @@ class HerokuMongoWatcher::CLI
104
117
  protected
105
118
 
106
119
  def self.check_and_notify_locks
107
- l = Float(@current_row.lock)
120
+ l = Float(@current_row.locked)
108
121
  if l > 90
109
- notify '[CRITICAL] Locks above 90%' unless @lock_critical_notified
122
+ mailer.notify(@current_row, '[CRITICAL] Locks above 90%') unless @lock_critical_notified
110
123
  elsif l > 70
111
- notify '[WARNING] Locks above 70%' unless @lock_warning_notified
124
+ mailer.notify(@current_row, '[WARNING] Locks above 70%') unless @lock_warning_notified
112
125
  elsif l < 50
113
126
  if @lock_warning_notified || @lock_critical_notified
114
- notify '[Resolved] locks below 50%'
127
+ mailer.notify(@current_row, '[Resolved] locks below 50%')
115
128
  @lock_warning_notified = false
116
129
  @lock_critical_notified = false
117
130
  end
@@ -122,55 +135,18 @@ class HerokuMongoWatcher::CLI
122
135
  def self.check_and_notify_response_time
123
136
  return unless @current_row.total_requests > 200
124
137
  if @current_row.average_response_time > 10_000 || @current_row.error_rate > 4
125
- notify "[SEVERE WARNING] Application not healthy | [#{@current_row.total_requests} rpm,#{@current_row.average_response_time} art]" unless @art_critical_notified
138
+ mailer.notify @current_row, "[SEVERE WARNING] Application not healthy | [#{@current_row.total_requests} rpm,#{@current_row.average_response_time} art]" unless @art_critical_notified
126
139
  @art_critical_notified = true
127
140
  elsif @current_row.average_response_time > 500 || @current_row.error_rate > 1 || @current_row.total_requests > 30_000
128
- notify "[WARNING] Application heating up | [#{@current_row.total_requests} rpm,#{@current_row.average_response_time} art]" unless @art_warning_notified
141
+ mailer.notify @current_row, "[WARNING] Application heating up | [#{@current_row.total_requests} rpm,#{@current_row.average_response_time} art]" unless @art_warning_notified
129
142
  @art_warning_notified = true
130
143
  elsif @current_row.average_response_time < 300 && @current_row.total_requests < 25_000
131
144
  if @art_warning_notified || @art_critical_notified
132
- notify "[RESOLVED] | [#{@current_row.total_requests} rpm,#{@current_row.average_response_time} art]"
145
+ mailer.notify @current_row, "[RESOLVED] | [#{@current_row.total_requests} rpm,#{@current_row.average_response_time} art]"
133
146
  @art_warning_notified = false
134
147
  @art_critical_notified = false
135
148
  end
136
149
  end
137
150
  end
138
151
 
139
- def self.notify(msg)
140
- Thread.new('notify_admins') do
141
- subscribers = config[:notify] || []
142
- subscribers.each { |user_email| send_email(user_email, msg) }
143
- end
144
- end
145
-
146
- def self.send_email(to, msg)
147
- return unless config[:gmail_username] && config[:gmail_password]
148
- content = [
149
- "From: Mongo Watcher <#{config[:gmail_username]}>",
150
- "To: #{to}",
151
- "Subject: #{msg}",
152
- "",
153
- "RPM: #{@last_row.total_requests}",
154
- "Average Reponse Time: #{@last_row.average_response_time}",
155
- "Application Errors: #{@last_row.total_web_errors}",
156
- "Router Errors (timeouts): #{@last_row.total_router_errors}",
157
- "Error Rate: #{@last_row.error_rate}%",
158
- "Dynos: #{@last_row.dynos}",
159
- "",
160
- "Locks: #{@last_row.lock}",
161
- "Queries: #{@last_row.queries}",
162
- "Inserts: #{@last_row.inserts}",
163
- "Updates: #{@last_row.updates}",
164
- "Faults: #{@last_row.faults}",
165
- "NetI/O: #{@last_row.net_in}/#{@last_row.net_out}",
166
- ]
167
- content = content + @last_row.error_content_for_email + @last_row.request_content_for_email
168
-
169
- content = content.join("\r\n")
170
- Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
171
- Net::SMTP.start('smtp.gmail.com', 587, 'gmail.com', config[:gmail_username], config[:gmail_password], :login) do |smtp|
172
- smtp.send_message(content, config[:gmail_username], to)
173
- end
174
- end
175
-
176
152
  end
@@ -14,8 +14,9 @@ class HerokuMongoWatcher::Configuration
14
14
  mongo_host: '',
15
15
  mongo_username: '',
16
16
  mongo_password: '',
17
+ heroku_username: '',
18
+ heroku_password: '',
17
19
  heroku_appname: '',
18
- heroku_account: '',
19
20
  print_errors: false,
20
21
  print_requests: false
21
22
  }
@@ -30,6 +31,9 @@ class HerokuMongoWatcher::Configuration
30
31
  opt :print_errors, "show aggregate error summaries", default: false
31
32
  opt :print_requests, "show aggregate requests summaries", default: false
32
33
  opt :interval, "frequency in seconds to poll mongostat and print out results", default: 60
34
+ opt :autoscale, "autoscale dynos -- WARNING ONLY DO THIS IF YOU KNOW WHAT YOUR ARE DOING", default: false
35
+ opt :min_dynos, "For autoscaling, the minimum dynos to use", default: 6
36
+ opt :max_dynos, "For autoscaling, the max dynos to use", default: 50
33
37
  end
34
38
 
35
39
  @@config.merge!(opts)
@@ -10,7 +10,7 @@ class HerokuMongoWatcher::DataRow
10
10
 
11
11
  # Mongo Attributes
12
12
  @@attributes.concat [:inserts, :queries, :ops, :updates, :deletes,
13
- :faults, :lock, :qrw, :net_in, :net_out]
13
+ :faults, :locked, :qrw, :net_in, :net_out]
14
14
 
15
15
  @@attributes.each { |attr| attr_accessor attr }
16
16
 
@@ -41,13 +41,53 @@ class HerokuMongoWatcher::DataRow
41
41
  total_requests > 0 ? ((((total_web_errors + total_router_errors)*(1.0)) / total_requests)* 100).round(2) : 'N/A'
42
42
  end
43
43
 
44
+ def lock_request_ratio
45
+ total_requests > 0 ? ((((Float(locked) * 1.0)/ total_requests)) * 1_000).round(2) : 'N/A'
46
+ end
47
+
48
+ def self.print_header
49
+ puts
50
+ puts "|<---- heroku stats ------------------------------------------------------------------->|<----mongo stats ------------------------------------------------>|"
51
+ puts "| dyno reqs art max r_err w_err %err wait queue slowest |insrt query updt flt lck lck:mrq qr|qw netI/O time |"
52
+ end
53
+
54
+ def error_content_for_email
55
+ content = []
56
+ if @errors && @errors.keys && @errors.keys.length > 0
57
+ content << ""
58
+ content << "Errors"
59
+ @errors.each do |error,count|
60
+ content << "\t\t[#{count}] #{error}"
61
+ end
62
+ end
63
+ content
64
+ end
65
+
66
+ def request_content_for_email
67
+ content = []
68
+ if @requests && @requests.keys && @requests.keys.length > 0
69
+ content << ""
70
+ content << "Requests"
71
+ @requests.sort_by{|req,count| -count}.first(10).each do |row|
72
+ content << "\t\t[#{row.last}] #{row.first}"
73
+ end
74
+ end
75
+ content
76
+ end
77
+
44
78
  def process_heroku_router_line(line)
45
79
  items = line.split
46
80
 
81
+ self.total_requests +=1
82
+
47
83
  if line =~ /Error/
48
84
  # Note: The lion share of these are timeouts
49
85
  #Full list here: https://devcenter.heroku.com/articles/error-codes
50
86
  self.total_router_errors += 1
87
+ time = items[0]
88
+ process = items[1]
89
+ clean_line = line.sub(time, '').sub(process, '').strip
90
+ add_error(clean_line)
51
91
  else
52
92
  time = items[0]
53
93
  process = items[1]
@@ -63,7 +103,6 @@ class HerokuMongoWatcher::DataRow
63
103
  path = extract_path(url)
64
104
 
65
105
  if is_number?(service) && is_number?(wait) && is_number?(queue)
66
- self.total_requests +=1
67
106
  self.total_service += Integer(service) if service
68
107
  self.total_wait += Integer(wait) if wait
69
108
  self.total_queue += Integer(queue) if queue
@@ -83,12 +122,6 @@ class HerokuMongoWatcher::DataRow
83
122
  end
84
123
  end
85
124
 
86
- def extract_path(url)
87
- URI('http://' + url).path
88
- rescue
89
- return url
90
- end
91
-
92
125
  def process_heroku_web_line(line)
93
126
  # Only care about errors
94
127
  error_messages = config[:error_messages]
@@ -100,15 +133,19 @@ class HerokuMongoWatcher::DataRow
100
133
  clean_line = line.sub(time, '').sub(process, '').strip
101
134
 
102
135
  self.total_web_errors += 1
103
- if self.errors.has_key? clean_line
104
- self.errors[clean_line] = self.errors[clean_line] + 1
105
- else
106
- self.errors[clean_line] = 1
107
- end
136
+ add_error(clean_line)
108
137
  end
109
138
 
110
139
  end
111
140
 
141
+ def add_error(error_line)
142
+ if self.errors.has_key? error_line
143
+ self.errors[error_line] = self.errors[error_line] + 1
144
+ else
145
+ self.errors[error_line] = 1
146
+ end
147
+ end
148
+
112
149
  def process_mongo_line(line)
113
150
  items = line.split
114
151
 
@@ -136,12 +173,6 @@ class HerokuMongoWatcher::DataRow
136
173
 
137
174
  end
138
175
 
139
- def self.print_header
140
- puts
141
- puts "|<---- heroku stats ------------------------------------------------------------------->|<----mongo stats ------------------------------------------------>|"
142
- puts "| dyno reqs art max r_err w_err %err wait queue slowest | insert query update faults locked qr|qw netI/O time |"
143
- end
144
-
145
176
  def print_row
146
177
  print_hash(@errors) if config[:print_errors]
147
178
  print_hash(@requests) if config[:print_requests]
@@ -153,21 +184,24 @@ class HerokuMongoWatcher::DataRow
153
184
  color_print @total_router_errors, warning: 1
154
185
  color_print @total_web_errors, warning: 1
155
186
  color_print error_rate, warning: 1, critical: 3, percent: true
156
- color_print average_wait, warning: 10, critical: 100
157
- color_print average_queue, warning: 10, critical: 100
187
+ color_print @total_wait #, warning: 10, critical: 100
188
+ color_print @total_queue #, warning: 10, critical: 100
158
189
  color_print @slowest_request, length: 28, slice: 25
159
190
  print '|'
160
- color_print @inserts
161
- color_print @queries
162
- color_print @updates
163
- color_print @faults
164
- color_print @locked, bold: true, warning: 40, critical: 70
191
+ color_print @inserts, length: 5
192
+ color_print @queries, length: 6
193
+ color_print @updates, length: 5
194
+ color_print @faults, length: 5
195
+ color_print @locked, bold: true, warning: 40, critical: 70, length: 6, percent: true
196
+ color_print lock_request_ratio
165
197
  color_print @qrw
166
198
  color_print "#{@net_in}/#{@net_out}", length: 10
167
- color_print @mongo_time, length: 9
199
+ color_print @mongo_time, length: 13
168
200
  printf "\n"
169
201
  end
170
202
 
203
+ private
204
+
171
205
  def print_hash(hash)
172
206
  if hash && hash.keys && hash.keys.length > 0
173
207
  hash.sort_by{|key,count| -count}.first(10).each do |row|
@@ -176,32 +210,12 @@ class HerokuMongoWatcher::DataRow
176
210
  end
177
211
  end
178
212
 
179
- def error_content_for_email
180
- content = []
181
- if @errors && @errors.keys && @errors.keys.length > 0
182
- content << ""
183
- content << "Errors"
184
- @errors.each do |error,count|
185
- content << "\t\t[#{count}] #{error}"
186
- end
187
- end
188
- content
189
- end
190
-
191
- def request_content_for_email
192
- content = []
193
- if @requests && @requests.keys && @requests.keys.length > 0
194
- content << ""
195
- content << "Requests"
196
- @requests.sort_by{|req,count| -count}.first(10).each do |row|
197
- content << "\t\t[#{row.last}] #{row.first}"
198
- end
199
- end
200
- content
213
+ def extract_path(url)
214
+ URI('http://' + url).path
215
+ rescue
216
+ return url
201
217
  end
202
218
 
203
- private
204
-
205
219
  def is_number?(string)
206
220
  _is_number = true
207
221
  begin
@@ -235,5 +249,4 @@ class HerokuMongoWatcher::DataRow
235
249
  print "\a"
236
250
  end
237
251
 
238
-
239
252
  end
@@ -0,0 +1,63 @@
1
+ require 'heroku_mongo_watcher'
2
+ require 'heroku_mongo_watcher/configuration'
3
+
4
+ #http://stackoverflow.com/a/9117903/192791
5
+ require 'net/smtp'
6
+ Net.instance_eval {remove_const :SMTPSession} if defined?(Net::SMTPSession)
7
+
8
+ require 'net/pop'
9
+ Net::POP.instance_eval {remove_const :Revision} if defined?(Net::POP::Revision)
10
+ Net.instance_eval {remove_const :POP} if defined?(Net::POP)
11
+ Net.instance_eval {remove_const :POPSession} if defined?(Net::POPSession)
12
+ Net.instance_eval {remove_const :POP3Session} if defined?(Net::POP3Session)
13
+ Net.instance_eval {remove_const :APOPSession} if defined?(Net::APOPSession)
14
+
15
+ require 'tlsmail'
16
+
17
+ class HerokuMongoWatcher::Mailer
18
+ include Singleton
19
+
20
+ def config
21
+ HerokuMongoWatcher::Configuration.instance.config
22
+ end
23
+
24
+ def notify(data_row,msg)
25
+ Thread.new('notify_admins') do
26
+ subscribers = config[:notify] || []
27
+ subscribers.each { |user_email| send_email(user_email, msg, data_row) }
28
+ end
29
+ end
30
+
31
+ protected
32
+
33
+ def send_email(to, msg, data_row)
34
+ return unless config[:gmail_username] && config[:gmail_password]
35
+ content = [
36
+ "From: Mongo Watcher <#{config[:gmail_username]}>",
37
+ "To: #{to}",
38
+ "Subject: #{msg}",
39
+ "",
40
+ "RPM: #{data_row.total_requests}",
41
+ "Average Reponse Time: #{data_row.average_response_time}",
42
+ "Application Errors: #{data_row.total_web_errors}",
43
+ "Router Errors (timeouts): #{data_row.total_router_errors}",
44
+ "Error Rate: #{data_row.error_rate}%",
45
+ "Dynos: #{data_row.dynos}",
46
+ "",
47
+ "Locks: #{data_row.locked}",
48
+ "Queries: #{data_row.queries}",
49
+ "Inserts: #{data_row.inserts}",
50
+ "Updates: #{data_row.updates}",
51
+ "Faults: #{data_row.faults}",
52
+ "NetI/O: #{data_row.net_in}/#{data_row.net_out}",
53
+ ]
54
+ content = content + data_row.error_content_for_email + data_row.request_content_for_email
55
+
56
+ content = content.join("\r\n")
57
+ Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
58
+ Net::SMTP.start('smtp.gmail.com', 587, 'gmail.com', config[:gmail_username], config[:gmail_password], :login) do |smtp|
59
+ smtp.send_message(content, config[:gmail_username], to)
60
+ end
61
+ end
62
+
63
+ end
@@ -1,3 +1,3 @@
1
1
  module HerokuMongoWatcher
2
- VERSION = "0.2.5.beta"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,43 @@
1
+ require 'eventmachine'
2
+ require 'heroku_mongo_watcher'
3
+ require 'heroku_mongo_watcher/configuration'
4
+
5
+ module MongoStatProcessor
6
+ def post_init
7
+ puts "started to read mongostat"
8
+ end
9
+
10
+ def receive_data data
11
+ puts "ruby sent me: #{data}"
12
+ end
13
+
14
+ def unbind
15
+ puts "ruby died with exit status: #{get_status.exitstatus}"
16
+ end
17
+ end
18
+
19
+
20
+ class HerokuMongoWatcher::Watcher
21
+
22
+ def self.config
23
+ HerokuMongoWatcher::Configuration.instance.config
24
+ end
25
+
26
+ def self.watch
27
+
28
+ EM::run {
29
+
30
+
31
+ cmd = "mongostat --rowcount 0 10 --host #{config[:mongo_host]} \
32
+ --username #{config[:mongo_username]} --password #{config[:mongo_password]} --noheaders"
33
+
34
+ EM.popen(cmd, MongoStatProcessor)
35
+
36
+
37
+ }
38
+
39
+
40
+ end
41
+
42
+
43
+ end
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heroku_mongo_watcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5.beta
5
- prerelease: 6
4
+ version: 0.3.0
5
+ prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Jonathan Tushman
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-24 00:00:00.000000000Z
12
+ date: 2012-08-01 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: term-ansicolor
16
- requirement: &2160762040 !ruby/object:Gem::Requirement
16
+ requirement: &2157847200 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2160762040
24
+ version_requirements: *2157847200
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: tlsmail
27
- requirement: &2160761240 !ruby/object:Gem::Requirement
27
+ requirement: &2157846540 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *2160761240
35
+ version_requirements: *2157846540
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: heroku
38
- requirement: &2160760460 !ruby/object:Gem::Requirement
38
+ requirement: &2157845780 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *2160760460
46
+ version_requirements: *2157845780
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: trollop
49
- requirement: &2160759800 !ruby/object:Gem::Requirement
49
+ requirement: &2157845020 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,7 @@ dependencies:
54
54
  version: '0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *2160759800
57
+ version_requirements: *2157845020
58
58
  description: Also notifies you when certain thresholds are hit. I have found this
59
59
  much more accurate than New Relic
60
60
  email:
@@ -72,10 +72,13 @@ files:
72
72
  - example/.watcher
73
73
  - heroku_mongo_watcher.gemspec
74
74
  - lib/heroku_mongo_watcher.rb
75
+ - lib/heroku_mongo_watcher/autoscaler.rb
75
76
  - lib/heroku_mongo_watcher/cli.rb
76
77
  - lib/heroku_mongo_watcher/configuration.rb
77
78
  - lib/heroku_mongo_watcher/data_row.rb
79
+ - lib/heroku_mongo_watcher/mailer.rb
78
80
  - lib/heroku_mongo_watcher/version.rb
81
+ - lib/heroku_mongo_watcher/watcher.rb
79
82
  homepage: https://github.com/jtushman/heroku_mongo_watcher
80
83
  licenses: []
81
84
  post_install_message:
@@ -91,9 +94,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
91
94
  required_rubygems_version: !ruby/object:Gem::Requirement
92
95
  none: false
93
96
  requirements:
94
- - - ! '>'
97
+ - - ! '>='
95
98
  - !ruby/object:Gem::Version
96
- version: 1.3.1
99
+ version: '0'
97
100
  requirements: []
98
101
  rubyforge_project: heroku_mongo_watcher
99
102
  rubygems_version: 1.8.17