capistrano-ext 1.0.1
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/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:
|