heroku_mongo_watcher 0.0.1.alpha

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/.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: []