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 +5 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/bin/watcher +5 -0
- data/example/.watcher +26 -0
- data/heroku_mongo_watcher.gemspec +26 -0
- data/lib/heroku_mongo_watcher/cli.rb +309 -0
- data/lib/heroku_mongo_watcher/configuration.rb +39 -0
- data/lib/heroku_mongo_watcher/version.rb +3 -0
- data/lib/heroku_mongo_watcher.rb +5 -0
- metadata +90 -0
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/watcher
ADDED
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
|
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: []
|