capistrano-ext 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/capistrano/ext/assets/request-counter.rb +29 -0
- data/lib/capistrano/ext/monitor.rb +250 -0
- data/lib/capistrano/ext/version.rb +11 -0
- metadata +54 -0
@@ -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,250 @@
|
|
1
|
+
require 'capistrano'
|
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
|
+
# Get a value from the remote environment
|
48
|
+
def remote_env(value)
|
49
|
+
result = ""
|
50
|
+
run("echo $#{value}", :once => true) do |ch, stream, data|
|
51
|
+
raise "could not get environment variable #{value}: #{data}" if stream == :err
|
52
|
+
result << data
|
53
|
+
end
|
54
|
+
result.chomp
|
55
|
+
end
|
56
|
+
|
57
|
+
# Monitor the load of the servers tied to the current task.
|
58
|
+
def load(options={})
|
59
|
+
servers = current_task.servers.sort
|
60
|
+
names = servers.map { |s| s.match(/^(\w+)/)[1] }
|
61
|
+
time = date_column(:init)
|
62
|
+
load_column_width = "0.00".length * 3 + 2
|
63
|
+
|
64
|
+
puts "connecting..."
|
65
|
+
connect!
|
66
|
+
|
67
|
+
parser = Proc.new { |text| text.match(/averages: (.*)$/)[1].split(/, /) }
|
68
|
+
delay = (options[:delay] || 30).to_i
|
69
|
+
|
70
|
+
running = true
|
71
|
+
trap("INT") { running = false; puts "[stopping]" }
|
72
|
+
|
73
|
+
# THE HEADER
|
74
|
+
header = Proc.new do
|
75
|
+
puts
|
76
|
+
headers("-", time[:width], *names.map { |n| [n, load_column_width] }.flatten)
|
77
|
+
end
|
78
|
+
|
79
|
+
while running
|
80
|
+
uptimes = {}
|
81
|
+
run "uptime" do |ch, stream, data|
|
82
|
+
raise "error: #{data}" if stream == :err
|
83
|
+
uptimes[ch[:host]] = parser[data.strip]
|
84
|
+
end
|
85
|
+
|
86
|
+
# redisplay the header every 40 rows
|
87
|
+
header.call if time[:rows] % 40 == 0
|
88
|
+
|
89
|
+
print(date_column(:show, time), " ")
|
90
|
+
servers.each { |server| print(uptimes[server].join("/"), " ") }
|
91
|
+
puts
|
92
|
+
|
93
|
+
# sleep this way, so that CTRL-C works immediately
|
94
|
+
delay.times { sleep 1; break unless running }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Monitor the number of requests per second being logged on the various
|
99
|
+
# servers.
|
100
|
+
def requests_per_second(*logs)
|
101
|
+
# extract our configurable options from the arguments
|
102
|
+
options = logs.last.is_a?(Hash) ? logs.pop : {}
|
103
|
+
request_pattern = options[:request_pattern] || "Completed in [0-9]"
|
104
|
+
sample_size = options[:sample_size] || 5
|
105
|
+
stats_to_show = options[:stats] || [0, 1, 5, 15]
|
106
|
+
num_format = options[:format] || "%4.1f"
|
107
|
+
|
108
|
+
# set up the date column formatter, and get the list of servers
|
109
|
+
time = date_column(:init)
|
110
|
+
servers = current_task.servers.sort
|
111
|
+
|
112
|
+
# initialize various helper variables we'll be using
|
113
|
+
mutex = Mutex.new
|
114
|
+
count = Hash.new(0)
|
115
|
+
running = false
|
116
|
+
channels = {}
|
117
|
+
|
118
|
+
windows = Hash.new { |h,k|
|
119
|
+
h[k] = {
|
120
|
+
1 => [], # last 1 minute
|
121
|
+
5 => [], # last 5 minutes
|
122
|
+
15 => [] # last 15 minutes
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
minute_1 = 60 / sample_size
|
127
|
+
minute_5 = 300 / sample_size
|
128
|
+
minute_15 = 900 / sample_size
|
129
|
+
|
130
|
+
# store our helper script on the servers. This script reduces the amount
|
131
|
+
# of traffic caused by tailing busy logs across the network, and also reduces
|
132
|
+
# the amount of work the client has to do.
|
133
|
+
script = "#{remote_env("HOME")}/x-request-counter.rb"
|
134
|
+
put_asset "request-counter.rb", script
|
135
|
+
|
136
|
+
# set up (but don't start) the runner thread, which accumulates request
|
137
|
+
# counts from the servers.
|
138
|
+
runner = Thread.new do Thread.stop
|
139
|
+
running = true
|
140
|
+
run("echo 0 && tail -F #{logs.join(" ")} | ruby #{script} '#{request_pattern}'") do |ch, stream, out|
|
141
|
+
channels[ch[:host]] ||= ch
|
142
|
+
puts "#{ch[:host]}: #{out}" and break if stream == :err
|
143
|
+
mutex.synchronize { count[ch[:host]] += out.to_i }
|
144
|
+
end
|
145
|
+
running = false
|
146
|
+
end
|
147
|
+
|
148
|
+
# let the runner thread get started
|
149
|
+
runner.wakeup
|
150
|
+
sleep 0.01 while !running
|
151
|
+
|
152
|
+
# trap interrupt for graceful shutdown
|
153
|
+
trap("INT") { puts "[stopping]"; channels.values.each { |ch| ch.close; ch[:status] = 0 } }
|
154
|
+
|
155
|
+
# compute the stuff we need to know for displaying the header
|
156
|
+
num_len = (num_format % 1).length
|
157
|
+
column_width = num_len * (servers.length + 1) + servers.length
|
158
|
+
abbvs = servers.map { |server| server.match(/^(\w+)/)[1][0,num_len] }
|
159
|
+
col_header = abbvs.map { |v| "%-*s" % [num_len, v] }.join("/")
|
160
|
+
|
161
|
+
# write both rows of the header
|
162
|
+
stat_columns = stats_to_show.map { |n|
|
163
|
+
case n
|
164
|
+
when 0 then "#{sample_size} sec"
|
165
|
+
when 1 then "1 min"
|
166
|
+
when 5 then "5 min"
|
167
|
+
when 15 then "15 min"
|
168
|
+
else raise "unknown statistic #{n.inspect}"
|
169
|
+
end
|
170
|
+
}
|
171
|
+
|
172
|
+
header = Proc.new do
|
173
|
+
puts
|
174
|
+
headers(" ", time[:width], *stat_columns.map { |v| [v, column_width] }.flatten)
|
175
|
+
headers("-", time[:width], *([col_header, column_width] * stats_to_show.length))
|
176
|
+
end
|
177
|
+
|
178
|
+
while running
|
179
|
+
# sleep for the specified sample size (5s by default)
|
180
|
+
(sample_size * 2).times { sleep(0.5); break unless running }
|
181
|
+
break unless running
|
182
|
+
|
183
|
+
# lock the counters and compute our stats at this point in time
|
184
|
+
mutex.synchronize do
|
185
|
+
totals = Hash.new { |h,k| h[k] = Hash.new(0) }
|
186
|
+
|
187
|
+
# for each server...
|
188
|
+
count.each do |k,c|
|
189
|
+
# push the latest sample onto the tracking queues
|
190
|
+
windows[k][1] = windows[k][1].push(count[k]).last(minute_1)
|
191
|
+
windows[k][5] = windows[k][5].push(count[k]).last(minute_5)
|
192
|
+
windows[k][15] = windows[k][15].push(count[k]).last(minute_15)
|
193
|
+
|
194
|
+
# compute the stats for this server (k)
|
195
|
+
totals[k][0] = count[k].to_f / sample_size
|
196
|
+
totals[k][1] = windows[k][1].inject(0) { |n,i| n + i } / (windows[k][1].length * sample_size).to_f
|
197
|
+
totals[k][5] = windows[k][5].inject(0) { |n,i| n + i } / (windows[k][5].length * sample_size).to_f
|
198
|
+
totals[k][15] = windows[k][15].inject(0) { |n,i| n + i } / (windows[k][15].length * sample_size).to_f
|
199
|
+
|
200
|
+
# add those stats to the totals per category
|
201
|
+
totals[:total][0] += totals[k][0]
|
202
|
+
totals[:total][1] += totals[k][1]
|
203
|
+
totals[:total][5] += totals[k][5]
|
204
|
+
totals[:total][15] += totals[k][15]
|
205
|
+
end
|
206
|
+
|
207
|
+
# redisplay the header every 40 rows
|
208
|
+
header.call if time[:rows] % 40 == 0
|
209
|
+
|
210
|
+
# show the stats
|
211
|
+
print(date_column(:show, time))
|
212
|
+
stats_to_show.each do |stat|
|
213
|
+
print " "
|
214
|
+
servers.each { |server| print "#{num_format}/" % totals[server][stat] }
|
215
|
+
print(num_format % totals[:total][stat])
|
216
|
+
end
|
217
|
+
puts
|
218
|
+
|
219
|
+
# reset the sample counter
|
220
|
+
count = Hash.new(0)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def put_asset(name, to)
|
226
|
+
put(File.read("#{File.dirname(__FILE__)}/assets/#{name}"), to)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
Capistrano.plugin :monitor, MonitorServers
|
231
|
+
|
232
|
+
Capistrano.configuration(:must_exist).load do
|
233
|
+
desc <<-STR
|
234
|
+
Watch the load on the servers. Display is updated every 30 seconds by default,
|
235
|
+
though you can specify a DELAY environment variable to make it update more or
|
236
|
+
less frequently.
|
237
|
+
STR
|
238
|
+
task :watch_load do
|
239
|
+
monitor.load :delay => ENV["DELAY"]
|
240
|
+
end
|
241
|
+
|
242
|
+
desc <<-STR
|
243
|
+
Watch the number of requests/sec being logged on the application servers. By
|
244
|
+
default, the "production.log" is watched, but if your log is named something
|
245
|
+
else, you can specify it in the log_name variable.
|
246
|
+
STR
|
247
|
+
task :watch_requests, :roles => :app do
|
248
|
+
monitor.requests_per_second("#{shared_path}/log/#{self[:log_name] || "production.log"}")
|
249
|
+
end
|
250
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: capistrano-ext
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 1.0.1
|
7
|
+
date: 2006-03-06 00:00:00 -07:00
|
8
|
+
summary: Capistrano 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/capistrano
|
33
|
+
- lib/capistrano/ext
|
34
|
+
- lib/capistrano/ext/assets
|
35
|
+
- lib/capistrano/ext/monitor.rb
|
36
|
+
- lib/capistrano/ext/version.rb
|
37
|
+
- lib/capistrano/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: capistrano
|
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:
|