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 +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: []
|