heroku_mongo_watcher 0.2.5.beta → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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