Skip to content

Instantly share code, notes, and snippets.

@nroose
Last active November 9, 2023 04:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nroose/c3d7697d680c36b306c567535b211764 to your computer and use it in GitHub Desktop.
Save nroose/c3d7697d680c36b306c567535b211764 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
require 'benchmark'
require 'cgi'
require 'net/http'
require 'resolv'
require 'resolv-replace'
host = ''
period = 10
count = 3
verbose = false
user_agent = $PROGRAM_NAME
def sanitize_host(host)
return nil if host.nil? || host.strip == ''
protocol = host.sub(%r{^(https?://)?.*$}, '\1')
protocol = 'https://' if protocol.strip == ''
name = "#{host.sub(%r{^(?:https?://)?(.*[^/])/?$}, '\1')}/"
host && protocol + name
end
def resolve_host(host)
ip = Resolv.getaddress(host)
custom_hosts = '/tmp/custom_hosts'
File.open(custom_hosts, 'w') do |file|
file.write("#{ip} #{host}\n")
end
hosts_resolver = Resolv::Hosts.new(custom_hosts)
dns_resolver = Resolv::DNS.new
Resolv::DefaultResolver.replace_resolvers([hosts_resolver, dns_resolver])
end
while ARGV[0] =~ /^-/
if ARGV[0] == '-h'
ARGV.shift
host = ARGV.shift
host = sanitize_host(host)
end
if ARGV[0] == '-H'
ARGV.shift
header = ARGV.shift
value = ARGV.shift
end
if ARGV[0] == '-p'
ARGV.shift
period = ARGV.shift.to_i
end
if ARGV[0] == '-c'
ARGV.shift
count = ARGV.shift.to_i
end
if ARGV[0] == '-r'
ARGV.shift
ramp = ARGV.shift.to_i
end
if ARGV[0] == '-l'
ARGV.shift
log_header = ARGV.shift
end
if ARGV[0] == '-v'
verbose = true
ARGV.shift
end
if ARGV[0] == '-t'
terse = true
ARGV.shift
end
if ARGV[0] == '-w'
wait = true
ARGV.shift
end
if ARGV[0] == '-P'
ARGV.shift
pause = ARGV.shift.to_i
end
if ARGV[0] == '-u'
ARGV.shift
user_agent = ARGV.shift
end
if ARGV[0] == '-s'
ARGV.shift
strong = true
end
end
if ARGV.empty?
puts "#{$PROGRAM_NAME} [-v] [-p <period>] [-c <count>] [-l <log-header>] [-H <requewst header> <value>] " \
'[-h <host>] [-r <ramp seconds>] [-P <seconds pause>] [-u <user agent>] [-s] url/path ...'
puts 'Send repeated get requests in a thead for each url (which are paths if you specify the host parameter). ' \
'For period seconds. ' \
'With count threads for each url. ' \
'Log the log header. ' \
'Verbose (-v) shows result of each request instead of running status.' \
'Wait to start each additional thread for ramp seconds. ' \
'Pause for pause seconds between each successive hits in each thread. ' \
'Use the user_agent. ' \
'If you need to use basic auth, set the env variables HTTP_USER and HTTP_PASS. ' \
'Use strong (-s) to keeep going on excptions.'
exit
end
urls = {}
ARGV.each do |url|
urls[url] = {}
end
puts "Period: #{period}"
puts "Count: #{count}"
puts "Host: #{host}" if host
puts "Log Header: #{log_header}" if log_header
puts "Ramp: #{ramp}" if ramp
puts "Pause: #{pause}" if pause
puts "Agent: #{user_agent}" if user_agent
puts "Urls/Paths: #{urls.keys.inspect}"
recents = [nil] * count * urls.count
seconds = 0
all = {}
all[:durations] = []
all[:codes] = {}
all[log_header] = {} if log_header
start = Time.now
threads = 0
urls.each_key do |url|
url_with_host = host + url.sub(%r{^/?(.*)}, '\1')
count.times do
break unless Time.now - start < period
urls[url][:thread] ||= []
urls[url][:thread] << Thread.new do
cookies = nil
threads += 1
while Time.now - start < period
begin
url_start = Time.now
uri = URI.parse(CGI.unescapeHTML(url_with_host))
resolve_host(uri.hostname)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.instance_of? URI::HTTPS
request = Net::HTTP::Get.new(uri.request_uri, { 'User-Agent' => user_agent })
request['Accept'] = '*/*'
request[header] = value if header
request.basic_auth(ENV['HTTP_USER'], ENV['HTTP_PASS']) if ENV['HTTP_USER']
request['Cookie'] = cookies if cookies
res = nil
dur = Benchmark.measure do
res = http.request(request)
end
cookies = res.get_fields('set-cookie')
next unless res && dur
all[:durations] << dur.real
all[:codes][res.code.to_s] ||= 0
all[:codes][res.code.to_s] += 1
if log_header && res[log_header]
all[log_header][res[log_header]] ||= 0
all[log_header][res[log_header]] += 1
end
urls[url][:durations] ||= []
urls[url][:durations] << dur.real
urls[url][:codes] ||= {}
urls[url][:codes][res.code.to_s] ||= 0
urls[url][:codes][res.code.to_s] += 1
if log_header && res[log_header]
urls[url][log_header] ||= {}
urls[url][log_header][res[log_header]] ||= 0
urls[url][log_header][res[log_header]] += 1
end
recents.shift
recents << { code: res.code.to_s, duration: dur.real }
recent_durations = recents.compact.map { |recent| recent[:duration] }
recent_duration = recent_durations.compact.sum / recent_durations.compact.count
recent_codes = {}
recents.compact.each do |recent|
recent_codes[recent[:code]] ||= 0
recent_codes[recent[:code]] += 1
end
if verbose
puts "#{(Time.now - start).round}/#{period}s #{threads} #{res.code} #{dur.real.round(3)} #{host}#{url} #{res[log_header] if log_header}"
elsif terse
unless seconds == (Time.now - start).round
seconds = (Time.now - start).round
avg_duration = (all[:durations].sum / all[:durations].count)
puts "#{(Time.now - start).round}/#{period}s #{threads} threads " \
"All: #{"%.3f" % avg_duration} #{all[:codes].inspect} " \
"#{all[log_header].inspect if log_header} - " \
"Recent: #{"%.3f" % recent_duration} #{recent_codes.inspect}"
end
else
avg_duration = (all[:durations].sum / all[:durations].count)
print "\r\033[0K#{(Time.now - start).round}/#{period}s #{threads} threads " \
"All: #{"%.3f" % avg_duration} #{all[:codes].inspect} " \
"#{all[log_header].inspect if log_header} - " \
"Recent: #{"%.3f" % recent_duration} #{recent_codes.inspect}"
end
sleep(pause) if pause
sleep(avg_duration) if wait && res.code == 503
rescue => e
all[:codes]['EXC'] ||= 0
all[:codes]['EXC'] += 1
urls[url][:codes] ||= {}
urls[url][:codes]['EXC'] ||= 0
urls[url][:codes]['EXC'] += 1
recents.shift
recents << { code: 'EXC', duration: nil }
puts "rescue on #{url_with_host} #{e} after #{Time.now - url_start}"
raise unless strong
end
end
ensure
threads -= 1
end
sleep(ramp) if ramp
end
end
urls.each_key do |url|
urls[url][:thread]&.each(&:join)
rescue Exception
urls[url][:thread]&.each(&:exit)
break
end
all_avg_duration = (all[:durations].sum / all[:durations].count).round(3)
if File.directory?('tmp')
File.open('tmp/all_avg_duration', 'w') do |file|
file.write("All Avg Duration: #{all_avg_duration * 1000}\n")
end
end
puts "\r\033[0KAll: #{(Time.now - start).round}/#{period}s #{all_avg_duration} #{all[:codes].inspect}"
puts "#{log_header}: #{all[log_header].inspect}" if log_header
urls.each do |k, v|
puts "#{k}: #{(v[:durations].sum / v[:durations].count).round(3)} #{v[:codes].inspect}" if v[:durations]
end
exit all[:codes]['500'].to_i + all[:codes]['EXC'].to_i
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment