switchtower-ext 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,29 @@
1
+ require 'thread'
2
+
3
+ def tail_lines(io)
4
+ io.each_line { |line| yield line }
5
+ if io.eof? then
6
+ sleep 0.25
7
+ io.pos = io.pos # reset eof?
8
+ retry
9
+ end
10
+ end
11
+
12
+ count = 0
13
+ mutex = Mutex.new
14
+
15
+ Thread.start do
16
+ loop do
17
+ sleep 1
18
+ mutex.synchronize do
19
+ puts count
20
+ count = 0
21
+ end
22
+ end
23
+ end
24
+
25
+ pattern = Regexp.new(ARGV.first)
26
+ tail_lines(STDIN) do |line|
27
+ next unless line =~ pattern
28
+ mutex.synchronize { count += 1 }
29
+ end
@@ -0,0 +1,239 @@
1
+ require 'switchtower'
2
+ require 'thread'
3
+
4
+ module MonitorServers
5
+ LONG_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
6
+ SHORT_TIME_FORMAT = "%H:%M:%S"
7
+
8
+ # A helper method for encapsulating the behavior of the date/time column
9
+ # in a report.
10
+ def date_column(operation, *args)
11
+ case operation
12
+ when :init
13
+ { :width => Time.now.strftime(LONG_TIME_FORMAT).length,
14
+ :last => nil,
15
+ :rows => 0 }
16
+ when :show
17
+ state = args.first
18
+ now = Time.now
19
+ date = now.strftime(
20
+ (state[:rows] % 10 == 0 || now.day != state[:last].day) ?
21
+ LONG_TIME_FORMAT : SHORT_TIME_FORMAT)
22
+ state[:last] = now
23
+ state[:rows] += 1
24
+ "%*s" % [state[:width], date]
25
+ else
26
+ raise "unknown operation #{operation.inspect}"
27
+ end
28
+ end
29
+
30
+ # A helper method for formatting table headers in a report.
31
+ def headers(*args)
32
+ 0.step(args.length-1, 2) do |n|
33
+ header = args[n]
34
+ size = args[n+1]
35
+ if header == "-" || header == " "
36
+ print header * size, " "
37
+ else
38
+ print header
39
+ padding = size - header.length - 1
40
+ print " ", "-" * padding if padding > 0
41
+ print " "
42
+ end
43
+ end
44
+ puts
45
+ end
46
+
47
+ # Monitor the load of the servers tied to the current task.
48
+ def load(options={})
49
+ servers = current_task.servers.sort
50
+ names = servers.map { |s| s.match(/^(\w+)/)[1] }
51
+ time = date_column(:init)
52
+ load_column_width = "0.00".length * 3 + 2
53
+
54
+ puts "connecting..."
55
+ connect!
56
+
57
+ parser = Proc.new { |text| text.match(/averages: (.*)$/)[1].split(/, /) }
58
+ delay = (options[:delay] || 30).to_i
59
+
60
+ running = true
61
+ trap("INT") { running = false; puts "[stopping]" }
62
+
63
+ # THE HEADER
64
+ header = Proc.new do
65
+ puts
66
+ headers("-", time[:width], *names.map { |n| [n, load_column_width] }.flatten)
67
+ end
68
+
69
+ while running
70
+ uptimes = {}
71
+ run "uptime" do |ch, stream, data|
72
+ raise "error: #{data}" if stream == :err
73
+ uptimes[ch[:host]] = parser[data.strip]
74
+ end
75
+
76
+ # redisplay the header every 40 rows
77
+ header.call if time[:rows] % 40 == 0
78
+
79
+ print(date_column(:show, time), " ")
80
+ servers.each { |server| print(uptimes[server].join("/"), " ") }
81
+ puts
82
+
83
+ # sleep this way, so that CTRL-C works immediately
84
+ delay.times { sleep 1; break unless running }
85
+ end
86
+ end
87
+
88
+ # Monitor the number of requests per second being logged on the various
89
+ # servers.
90
+ def requests_per_second(*logs)
91
+ # extract our configurable options from the arguments
92
+ options = logs.last.is_a?(Hash) ? logs.pop : {}
93
+ request_pattern = options[:request_pattern] || "Completed in [0-9]"
94
+ sample_size = options[:sample_size] || 5
95
+ stats_to_show = options[:stats] || [0, 1, 5, 15]
96
+ num_format = options[:format] || "%4.1f"
97
+
98
+ # set up the date column formatter, and get the list of servers
99
+ time = date_column(:init)
100
+ servers = current_task.servers.sort
101
+
102
+ # initialize various helper variables we'll be using
103
+ mutex = Mutex.new
104
+ count = Hash.new(0)
105
+ running = false
106
+ channels = {}
107
+
108
+ windows = Hash.new { |h,k|
109
+ h[k] = {
110
+ 1 => [], # last 1 minute
111
+ 5 => [], # last 5 minutes
112
+ 15 => [] # last 15 minutes
113
+ }
114
+ }
115
+
116
+ minute_1 = 60 / sample_size
117
+ minute_5 = 300 / sample_size
118
+ minute_15 = 900 / sample_size
119
+
120
+ # set up (but don't start) the runner thread, which accumulates request
121
+ # counts from the servers.
122
+ runner = Thread.new do Thread.stop
123
+ running = true
124
+ run("echo 0 && tail -F #{logs.join(" ")} | ruby /tmp/request-counter.rb '#{request_pattern}'") do |ch, stream, out|
125
+ channels[ch[:host]] ||= ch
126
+ puts "#{ch[:host]}: #{out}" and break if stream == :err
127
+ mutex.synchronize { count[ch[:host]] += out.to_i }
128
+ end
129
+ running = false
130
+ end
131
+
132
+ # store our helper script on the servers. This script reduces the amount
133
+ # of traffic caused by tailing busy logs across the network, and also reduces
134
+ # the amount of work the client has to do.
135
+ put_asset "request-counter.rb", "/tmp/request-counter.rb"
136
+
137
+ # let the runner thread get started
138
+ runner.wakeup
139
+ sleep 0.01 while !running
140
+
141
+ # trap interrupt for graceful shutdown
142
+ trap("INT") { puts "[stopping]"; channels.values.each { |ch| ch.close; ch[:status] = 0 } }
143
+
144
+ # compute the stuff we need to know for displaying the header
145
+ num_len = (num_format % 1).length
146
+ column_width = num_len * (servers.length + 1) + servers.length
147
+ abbvs = servers.map { |server| server.match(/^(\w+)/)[1][0,num_len] }
148
+ col_header = abbvs.map { |v| "%-*s" % [num_len, v] }.join("/")
149
+
150
+ # write both rows of the header
151
+ stat_columns = stats_to_show.map { |n|
152
+ case n
153
+ when 0 then "#{sample_size} sec"
154
+ when 1 then "1 min"
155
+ when 5 then "5 min"
156
+ when 15 then "15 min"
157
+ else raise "unknown statistic #{n.inspect}"
158
+ end
159
+ }
160
+
161
+ header = Proc.new do
162
+ puts
163
+ headers(" ", time[:width], *stat_columns.map { |v| [v, column_width] }.flatten)
164
+ headers("-", time[:width], *([col_header, column_width] * stats_to_show.length))
165
+ end
166
+
167
+ while running
168
+ # sleep for the specified sample size (5s by default)
169
+ (sample_size * 2).times { sleep(0.5); break unless running }
170
+ break unless running
171
+
172
+ # lock the counters and compute our stats at this point in time
173
+ mutex.synchronize do
174
+ totals = Hash.new { |h,k| h[k] = Hash.new(0) }
175
+
176
+ # for each server...
177
+ count.each do |k,c|
178
+ # push the latest sample onto the tracking queues
179
+ windows[k][1] = windows[k][1].push(count[k]).last(minute_1)
180
+ windows[k][5] = windows[k][5].push(count[k]).last(minute_5)
181
+ windows[k][15] = windows[k][15].push(count[k]).last(minute_15)
182
+
183
+ # compute the stats for this server (k)
184
+ totals[k][0] = count[k].to_f / sample_size
185
+ totals[k][1] = windows[k][1].inject(0) { |n,i| n + i } / (windows[k][1].length * sample_size).to_f
186
+ totals[k][5] = windows[k][5].inject(0) { |n,i| n + i } / (windows[k][5].length * sample_size).to_f
187
+ totals[k][15] = windows[k][15].inject(0) { |n,i| n + i } / (windows[k][15].length * sample_size).to_f
188
+
189
+ # add those stats to the totals per category
190
+ totals[:total][0] += totals[k][0]
191
+ totals[:total][1] += totals[k][1]
192
+ totals[:total][5] += totals[k][5]
193
+ totals[:total][15] += totals[k][15]
194
+ end
195
+
196
+ # redisplay the header every 40 rows
197
+ header.call if time[:rows] % 40 == 0
198
+
199
+ # show the stats
200
+ print(date_column(:show, time))
201
+ stats_to_show.each do |stat|
202
+ print " "
203
+ servers.each { |server| print "#{num_format}/" % totals[server][stat] }
204
+ print(num_format % totals[:total][stat])
205
+ end
206
+ puts
207
+
208
+ # reset the sample counter
209
+ count = Hash.new(0)
210
+ end
211
+ end
212
+ end
213
+
214
+ def put_asset(name, to)
215
+ put(File.read("#{File.dirname(__FILE__)}/assets/#{name}"), to)
216
+ end
217
+ end
218
+
219
+ SwitchTower.plugin :monitor, MonitorServers
220
+
221
+ SwitchTower.configuration(:must_exist).load do
222
+ desc <<-STR
223
+ Watch the load on the servers. Display is updated every 30 seconds by default,
224
+ though you can specify a DELAY environment variable to make it update more or
225
+ less frequently.
226
+ STR
227
+ task :watch_load do
228
+ monitor.load :delay => ENV["DELAY"]
229
+ end
230
+
231
+ desc <<-STR
232
+ Watch the number of requests/sec being logged on the application servers. By
233
+ default, the "production.log" is watched, but if your log is named something
234
+ else, you can specify it in the log_name variable.
235
+ STR
236
+ task :watch_requests, :roles => :app do
237
+ monitor.requests_per_second("#{shared_path}/log/#{self[:log_name] || "production.log"}")
238
+ end
239
+ end
@@ -0,0 +1,11 @@
1
+ module SwitchTower
2
+ module Ext
3
+ module Version #:nodoc:
4
+ MAJOR = 1
5
+ MINOR = 0
6
+ TINY = 0
7
+
8
+ STRING = [MAJOR, MINOR, TINY].join(".")
9
+ end
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: switchtower-ext
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2006-02-20 00:00:00 -07:00
8
+ summary: SwitchTower Extensions is a set of useful task libraries and methods that other developers may reference in their own recipe files.
9
+ require_paths:
10
+ - lib
11
+ email: jamis@37signals.com
12
+ homepage: http://www.rubyonrails.com
13
+ rubyforge_project:
14
+ description:
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ -
22
+ - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: ruby
27
+ signing_key:
28
+ cert_chain:
29
+ authors:
30
+ - Jamis Buck
31
+ files:
32
+ - lib/switchtower
33
+ - lib/switchtower/ext
34
+ - lib/switchtower/ext/assets
35
+ - lib/switchtower/ext/monitor.rb
36
+ - lib/switchtower/ext/version.rb
37
+ - lib/switchtower/ext/assets/request-counter.rb
38
+ test_files: []
39
+ rdoc_options: []
40
+ extra_rdoc_files: []
41
+ executables: []
42
+ extensions: []
43
+ requirements: []
44
+ dependencies:
45
+ - !ruby/object:Gem::Dependency
46
+ name: switchtower
47
+ version_requirement:
48
+ version_requirements: !ruby/object:Gem::Version::Requirement
49
+ requirements:
50
+ -
51
+ - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.0.0
54
+ version: