switchtower-ext 1.0.0

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.
@@ -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: