heroku_mongo_watcher 0.0.1.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .idea/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in heroku_mongo_watcher.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/watcher ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require "heroku_mongo_watcher/cli"
5
+ HerokuMongoWatcher::CLI.watch(*ARGV)
data/example/.watcher ADDED
@@ -0,0 +1,26 @@
1
+ # text to single a error in your app -- will uptick w_err
2
+ error_messages: [Error, Exception]
3
+
4
+ # list of email addresses to notify upon an error
5
+ notify: [bill@example.com, fred@example.com]
6
+
7
+ # Currently we only support sending out emails if you have a gmail account
8
+ # if either of these is blank, we will not send out an email
9
+ gmail_username: bill@example.com
10
+ gmail_password: sekret
11
+
12
+ # interval in seconds to ping mongostat and to output and summarize data
13
+ interval: 60
14
+
15
+ # to connect to mongostat
16
+ mongo_host: 'my-dbawesome-db.member0.mongolayer.com'
17
+ mongo_username: 'ninja'
18
+ mongo_password: 'skret'
19
+
20
+ # to connect to heroku app
21
+ # note you already need to have the heroku gem installed
22
+ # with creditails already set up for this directory
23
+ heroku_appname: 'my-heroku-app-name'
24
+
25
+ #if you have multiple accounts
26
+ heroku_account: 'my-heroku-account'
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "heroku_mongo_watcher/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "heroku_mongo_watcher"
7
+ s.version = HerokuMongoWatcher::VERSION
8
+ s.authors = ["Jonathan Tushman"]
9
+ s.email = ["jtushman@gmail.com"]
10
+ s.homepage = "http://www.tushman.com"
11
+ s.summary = %q{Watches Mongostat and heroku logs to give you a pulse of your application}
12
+ s.description = %q{Also notifies you when certain thresholds are hit. I have found this much more accurate than New Relic}
13
+
14
+ s.rubyforge_project = "heroku_mongo_watcher"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ # s.add_development_dependency "rspec"
23
+ s.add_runtime_dependency "term-ansicolor"
24
+ s.add_runtime_dependency "tlsmail"
25
+ s.add_runtime_dependency "heroku"
26
+ end
@@ -0,0 +1,309 @@
1
+ require 'heroku_mongo_watcher/configuration'
2
+ require 'term/ansicolor'
3
+ require 'tlsmail'
4
+
5
+ class HerokuMongoWatcher::CLI
6
+
7
+ extend HerokuMongoWatcher::Configuration
8
+
9
+ def self.watch(*args)
10
+ f = File.join(File.expand_path('~'),'.watcher')
11
+ configure_with(f)
12
+
13
+ # lock warnings flags
14
+ @lock_critical_notified = false
15
+ @lock_warning_notified = false
16
+
17
+ # average response time warning flags
18
+ @art_critical_notified = false
19
+ @art_warning_notified = false
20
+
21
+ # memos
22
+ @total_lines = 0
23
+ @total_service = 0
24
+ @total_wait = 0
25
+ @total_queue = 0
26
+ @total_router_errors = 0
27
+ @total_web_errors = 0
28
+ @max_service = 0
29
+ @dynos = 0
30
+ @slowest_request = nil
31
+ @errors = {}
32
+
33
+ @mutex = Mutex.new
34
+
35
+ # Call Tails heroku logs updates counts
36
+ heroku_watcher = Thread.new('heroku_logs') do
37
+ IO.popen("heroku logs --tail --app #{config[:heroku_appname]} --account #{config[:heroku_account]}") do |f|
38
+ while line = f.gets
39
+ process_heroku_router_line(line) if line.include? 'heroku[router]'
40
+ process_heroku_web_line(line) if line.include? 'app[web'
41
+ end
42
+ end
43
+ end
44
+
45
+ # Call Heroku PS to check the number of up dynos
46
+ heroku_ps = Thread.new('heroku_ps') do
47
+ while true do
48
+ dynos = 0
49
+ IO.popen("heroku ps --app #{config[:heroku_appname]} --account #{config[:heroku_account]}") do |p|
50
+ while line = p.gets
51
+ dynos += 1 if line =~ /^web/ && line.split(' ')[1] == 'up'
52
+ end
53
+ end
54
+ @mutex.synchronize { @dynos = dynos }
55
+ sleep(30)
56
+ end
57
+ end
58
+
59
+ puts
60
+ puts "|<---- heroku stats ------------------------------------------------------------>|<----mongo stats ------------------------------------------------------->|"
61
+ puts "dyno reqs art max r_err w_err wait queue slowest | insert query update faults locked qr|qw netIn netOut time |"
62
+
63
+ IO.popen("mongostat --rowcount 0 #{config[:interval]} --host #{config[:mongo_host]} --username #{config[:mongo_username]} --password #{config[:mongo_password]} --noheaders") do |f|
64
+ while line = f.gets
65
+ process_mongo_line(line) if line =~ /^/ && !(line =~ /^connected/)
66
+ end
67
+ end
68
+
69
+ heroku_watcher.join
70
+ heroku_ps.join
71
+
72
+ end
73
+
74
+ protected
75
+
76
+ def self.process_mongo_line(line)
77
+ items = line.split
78
+
79
+ inserts = items[0]
80
+ query = items[1]
81
+ update = items[2]
82
+ delete = items[3]
83
+ getmore = items[4]
84
+ command = items[5]
85
+ flushes = items[6]
86
+ mapped = items[7]
87
+ vsize = items[8]
88
+ res = items[9]
89
+ faults = items[10]
90
+ locked = items[11]
91
+ idx_miss = items[12]
92
+ qrw = items[13]
93
+ arw = items[14]
94
+ netIn = items[15]
95
+ netOut = items[16]
96
+ conn = items[17]
97
+ set = items[18]
98
+ repl = items[19]
99
+ time = items[20]
100
+
101
+ @mutex.synchronize do
102
+ art = average_response_time
103
+ err = @total_router_errors
104
+ average_wait = @total_lines > 0 ? @total_wait / @total_lines : 'N/A'
105
+ average_queue = @total_lines > 0 ? @total_queue / @total_lines : 'N/A'
106
+
107
+ print_errors
108
+
109
+ color_print @dynos, length: 4
110
+ color_print @total_lines
111
+ color_print art, warning: 1000, critical: 10_000, bold: true
112
+ color_print @max_service, warning: 10_000, critical: 20_000
113
+ color_print err, warning: 1, critical: 10
114
+ color_print @total_web_errors, warning: 1, critical: 10
115
+ color_print average_wait, warning: 10, critical: 100
116
+ color_print average_queue, warning: 10, critical: 100
117
+ color_print @slowest_request.slice(0,25), length: 28
118
+ print '|'
119
+ color_print inserts
120
+ color_print query
121
+ color_print update
122
+ color_print faults
123
+ color_print locked, bold: true, warning: 70, critical: 90
124
+ color_print qrw
125
+ color_print netIn
126
+ color_print netOut
127
+ color_print time, length: 10
128
+ printf "\n"
129
+
130
+ check_and_notify_locks(locked)
131
+ check_and_notify_response_time(art, @total_lines)
132
+
133
+ reset_memos
134
+
135
+ end
136
+
137
+
138
+ end
139
+
140
+ def self.average_response_time
141
+ @total_lines > 0 ? @total_service / @total_lines : 'N/A'
142
+ end
143
+
144
+ def self.print_errors
145
+ if @errors && @errors.keys && @errors.keys.length > 0
146
+ @errors.each do |error,count|
147
+ puts "\t\t[#{count}] #{error}"
148
+ end
149
+ end
150
+ end
151
+
152
+ def self.process_heroku_web_line(line)
153
+ # Only care about errors
154
+ error_messages = config[:error_messages]
155
+
156
+ if error_messages.any? { |mes| line.include? mes }
157
+ items = line.split
158
+ time = items[0]
159
+ process = items[1]
160
+ clean_line = line.sub(time,'').sub(process,'').strip
161
+ @mutex.synchronize do
162
+ @total_web_errors += 1
163
+ if @errors.has_key? clean_line
164
+ @errors[clean_line] = @errors[clean_line] + 1
165
+ else
166
+ @errors[clean_line] = 1
167
+ end
168
+ end
169
+ end
170
+
171
+
172
+
173
+ end
174
+
175
+ def self.process_heroku_router_line(line)
176
+ # 2012-07-05T20:24:10+00:00 heroku[router]: GET myapp.com/pxl/4fdbc97dc6b36c0030001160?value=1 dyno=web.14 queue=0 wait=0ms service=8ms status=200 bytes=35
177
+
178
+ # or if error
179
+
180
+ #2012-07-05T20:17:12+00:00 heroku[router]: Error H12 (Request timeout) -> GET myapp.com/crossdomain.xml dyno=web.4 queue= wait= service=30000ms status=503 bytes=0
181
+
182
+ items = line.split
183
+
184
+ if line =~ /Error/
185
+ @mutex.synchronize { @total_router_errors += 1 }
186
+ else
187
+
188
+
189
+ time = items[0]
190
+ process = items[1]
191
+ http_type = items[2]
192
+ url = items[3]
193
+ dyno = items[4].split('=').last if items[4]
194
+ queue = items[5].split('=').last.sub('ms', '') if items[5]
195
+ wait = items[6].split('=').last.sub('ms', '') if items[6]
196
+ service = items[7].split('=').last.sub('ms', '') if items[7]
197
+ status = items[8].split('=').last if items[8]
198
+ bytes = items[9].split('=').last if items[9]
199
+
200
+ if is_number?(service) && is_number?(wait) && is_number?(queue)
201
+ @mutex.synchronize do
202
+ @total_lines +=1
203
+ @total_service += Integer(service) if service
204
+ @total_wait += Integer(wait) if wait
205
+ @total_queue += Integer(queue) if queue
206
+ if Integer(service) > @max_service
207
+ @max_service = Integer(service)
208
+ @slowest_request = URI('http://' + url).path
209
+ end
210
+ end
211
+ end
212
+
213
+ end
214
+
215
+ end
216
+
217
+ def self.reset_memos
218
+ @total_service = @total_lines = @total_wait = @total_queue = @total_router_errors = @total_web_errors = @max_service = 0
219
+ @errors = {}
220
+ end
221
+
222
+ def self.check_and_notify_locks(locked)
223
+ l = Float(locked)
224
+ if l > 90
225
+ notify '[CRITICAL] Locks above 90%' unless @lock_critical_notified
226
+ elsif l > 70
227
+ notify '[WARNING] Locks above 70%' unless @lock_warning_notified
228
+ elsif l < 50
229
+ if @lock_warning_notified || @lock_critical_notified
230
+ notify '[Resolved] locks below 50%'
231
+ @lock_warning_notified = false
232
+ @lock_critical_notified = false
233
+ end
234
+
235
+ end
236
+ end
237
+
238
+ def self.check_and_notify_response_time(avt, requests)
239
+ return unless requests > 200
240
+ if avt > 10_000 || @total_router_errors > 100
241
+ notify "[SEVERE WARNING] Application not healthy | [#{@total_lines} rpm,#{avt} art]" unless @art_critical_notified
242
+ # @art_critical_notified = true
243
+ elsif avt > 500 || @total_router_errors > 10
244
+ notify "[WARNING] Application heating up | [#{@total_lines} rpm,#{avt} art]" unless @art_warning_notified
245
+ # @art_warning_notified = true
246
+ elsif avt < 300
247
+ if @art_warning_notified || @art_critical_notified
248
+ @art_warning_notified = false
249
+ @art_critical_notified = false
250
+ end
251
+ end
252
+ end
253
+
254
+ def self.notify(msg)
255
+ Thread.new('notify_admins') do
256
+ notify.each { |user_email| send_email(user_email, msg) } unless notify.empty?
257
+ end
258
+ end
259
+
260
+ def self.is_number?(string)
261
+ _is_number = true
262
+ begin
263
+ num = Integer(string)
264
+ rescue
265
+ _is_number = false
266
+ end
267
+ _is_number
268
+ end
269
+
270
+ def self.send_email(to, msg)
271
+
272
+ return unless config[:gmail_username] && config[:gmail_password]
273
+
274
+ content = [
275
+ "From: Mongo Watcher <#{config[:gmail_username]}>",
276
+ "To: #{to}",
277
+ "Subject: #{msg}",
278
+ "",
279
+ "RPM: #{@total_lines}",
280
+ "Average Reponse Time: #{average_response_time}",
281
+ "Application Errors: #{@total_web_errors}",
282
+ "Router Errors (timeouts): #{@total_router_errors}",
283
+ "Dynos: #{@dynos}"
284
+ ].join("\r\n")
285
+
286
+ Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
287
+ Net::SMTP.start('smtp.gmail.com', 587, 'gmail.com', config[:gmail_username], config[:gmail_password], :login) do |smtp|
288
+ smtp.send_message(content, config[:gmail_username], to)
289
+ end
290
+
291
+
292
+ end
293
+
294
+ def self.color_print(field, options ={})
295
+ options[:length] = 7 unless options[:length]
296
+ print Term::ANSIColor.bold if options[:bold] == true
297
+ if options[:critical] && is_number?(field) && Integer(field) > options[:critical]
298
+ print "\a" #beep
299
+ print Term::ANSIColor.red
300
+ print Term::ANSIColor.bold
301
+ elsif options[:warning] && is_number?(field) && Integer(field) > options[:warning]
302
+ print Term::ANSIColor.yellow
303
+ end
304
+ printf "%#{options[:length]}s", field
305
+ print Term::ANSIColor.clear
306
+ end
307
+
308
+
309
+ end
@@ -0,0 +1,39 @@
1
+ module HerokuMongoWatcher::Configuration
2
+
3
+ @@config = {
4
+ error_messages: ['Error', 'Exception', 'Cannot find impression', 'Timed out running'],
5
+ notify: [],
6
+ gmail_username: '',
7
+ gmail_password: '',
8
+ interval: 60,
9
+ mongo_host: '',
10
+ mongo_username: '',
11
+ mongo_password: '',
12
+ heroku_appname: '',
13
+ heroku_account: ''
14
+ }
15
+
16
+ @@valid_config_keys = @@config.keys
17
+
18
+ def config
19
+ @@config
20
+ end
21
+
22
+ # Configure through yaml file
23
+ def configure_with(path_to_yaml_file)
24
+ begin
25
+ y_config = YAML::load(IO.read(path_to_yaml_file))
26
+ rescue Errno::ENOENT
27
+ log(:warning, "YAML configuration file couldn't be found. Using defaults."); return
28
+ rescue Psych::SyntaxError
29
+ log(:warning, "YAML configuration file contains invalid syntax. Using defaults."); return
30
+ end
31
+
32
+ configure(y_config)
33
+ end
34
+
35
+ # Configure through hash
36
+ def configure(opts = {})
37
+ opts.each { |k, v| config[k.to_sym] = v if @@valid_config_keys.include? k.to_sym }
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module HerokuMongoWatcher
2
+ VERSION = "0.0.1.alpha"
3
+ end
@@ -0,0 +1,5 @@
1
+ require "heroku_mongo_watcher/version"
2
+
3
+ module HerokuMongoWatcher
4
+
5
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: heroku_mongo_watcher
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.alpha
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Jonathan Tushman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-20 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: term-ansicolor
16
+ requirement: &2157145900 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2157145900
25
+ - !ruby/object:Gem::Dependency
26
+ name: tlsmail
27
+ requirement: &2157145160 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2157145160
36
+ - !ruby/object:Gem::Dependency
37
+ name: heroku
38
+ requirement: &2157144040 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *2157144040
47
+ description: Also notifies you when certain thresholds are hit. I have found this
48
+ much more accurate than New Relic
49
+ email:
50
+ - jtushman@gmail.com
51
+ executables:
52
+ - watcher
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - .gitignore
57
+ - Gemfile
58
+ - Rakefile
59
+ - bin/watcher
60
+ - example/.watcher
61
+ - heroku_mongo_watcher.gemspec
62
+ - lib/heroku_mongo_watcher.rb
63
+ - lib/heroku_mongo_watcher/cli.rb
64
+ - lib/heroku_mongo_watcher/configuration.rb
65
+ - lib/heroku_mongo_watcher/version.rb
66
+ homepage: http://www.tushman.com
67
+ licenses: []
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>'
82
+ - !ruby/object:Gem::Version
83
+ version: 1.3.1
84
+ requirements: []
85
+ rubyforge_project: heroku_mongo_watcher
86
+ rubygems_version: 1.8.17
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: Watches Mongostat and heroku logs to give you a pulse of your application
90
+ test_files: []