log_replayer 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/LICENSE +1 -0
- data/README.md +3 -0
- data/lib/log_replayer.rb +169 -0
- data/log_replayer.gemspec +18 -0
- metadata +73 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Copyright (c) 2012, Ooyala Inc
|
data/README.md
ADDED
data/lib/log_replayer.rb
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Load test SAS by replaying requests in a multiple of real-time.
|
4
|
+
#
|
5
|
+
# TODO:
|
6
|
+
# - Add configurability. Lots of it.
|
7
|
+
# - Instead of spewing lots of data, set thresholds and log requests that go over them.
|
8
|
+
# Or log lots of data in a gnuplot/csv friendly format for comparing graphs
|
9
|
+
# Or give back useful average/percentile metrics
|
10
|
+
# - Find the "maximum stable multiplier"; replay requests at increasing speed until we start seeing delays
|
11
|
+
# - Replay at maximum speed (effectively an infinite multiplier)
|
12
|
+
# - Automatically pull down useful graphs from Ganglia or machine metrics from Hastur
|
13
|
+
# - Figure out why we don't need to sign requests, fix that, and then resign them
|
14
|
+
|
15
|
+
|
16
|
+
require "em-synchrony"
|
17
|
+
require "em-synchrony/em-http"
|
18
|
+
require "em-synchrony/fiber_iterator"
|
19
|
+
require "time"
|
20
|
+
require "trollop"
|
21
|
+
|
22
|
+
module LogReplayer
|
23
|
+
class Replayer
|
24
|
+
IP_REGEX = "[^-]*" # Can include multiple comma (with optional spaces) ips or "unknown"
|
25
|
+
DATE_REGEX = "[0-9]{1,2}/[A-Z][a-z]{2}/[0-9]{4} *[0-9]{1,2}:[0-9]{2}:[0-9]{2}"
|
26
|
+
LOG_LINE_REGEX = /^#{IP_REGEX} - - \[(#{DATE_REGEX})\] "GET ([^"]*) HTTP\/1.[01]"/
|
27
|
+
|
28
|
+
EXCLUDE_ROUTES = %w{/health_check}.map { |regex| /^#{regex}/ }
|
29
|
+
|
30
|
+
def initialize(options)
|
31
|
+
@host = options[:host]
|
32
|
+
@concurrency = options[:concurrency]
|
33
|
+
@speed = options[:speed]
|
34
|
+
@csv_output = options[:csv_output]
|
35
|
+
@summary_output = options[:summary_output]
|
36
|
+
@profiler = options[:profiler]
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_request_routes(file_name)
|
40
|
+
@requests = []
|
41
|
+
File.open(file_name) do |log_file|
|
42
|
+
log_file.each_line do |log_line|
|
43
|
+
@requests << parse_requests(log_line)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
@requests.compact!
|
47
|
+
puts @requests.length
|
48
|
+
end
|
49
|
+
|
50
|
+
# returns hash of { :time, :route, :action, :request_options }
|
51
|
+
# override this in subclass to work support different log formats
|
52
|
+
def parse_requests(log_line)
|
53
|
+
match = log_line.match(LOG_LINE_REGEX)
|
54
|
+
return nil unless match
|
55
|
+
date, route = match[1..2]
|
56
|
+
return nil if EXCLUDE_ROUTES.any? { |exclude| route.match(exclude) }
|
57
|
+
{ :time => Time.parse(date), :route => route, :action => :get, :request_options => {} }
|
58
|
+
end
|
59
|
+
|
60
|
+
# returns string with additional string for the summary, override for different statusz implementations
|
61
|
+
def additional_summary
|
62
|
+
statusz = EM::HttpRequest.new("http://#{@host}/statusz").get
|
63
|
+
[/Branch.*/, /Ruby Version.*/].map {|x| x.match(statusz.response)}.compact.join(" -- ")
|
64
|
+
end
|
65
|
+
|
66
|
+
# Replay logs in (a multiple of) real time
|
67
|
+
def replay_requests
|
68
|
+
log_start_time = @requests[0][:time]
|
69
|
+
replay_start_time = Time.now
|
70
|
+
count = 0
|
71
|
+
csv_file = File.open(@csv_output, "w") if @csv_output
|
72
|
+
csv_file.puts "id,status,processing time,start delay" if @csv_output
|
73
|
+
|
74
|
+
error_count = 0
|
75
|
+
status_counts = Hash.new(0)
|
76
|
+
total_time = 0
|
77
|
+
max_request_processing_time = 0
|
78
|
+
min_request_processing_time = nil
|
79
|
+
app_build = nil
|
80
|
+
ruby_version = nil
|
81
|
+
summary_header = ''
|
82
|
+
EM.synchrony do
|
83
|
+
# __start__ and __stop__ are for rack-perftools_profiler.
|
84
|
+
EventMachine::HttpRequest.new("http://#{@host}/__start__").get if @profiler
|
85
|
+
EM::Synchrony::FiberIterator.new(@requests, @concurrency).each do |request|
|
86
|
+
next unless [:get, :post, :patch, :put, :delete].include? request[:action]
|
87
|
+
# Track which request this is. Note that count is shared between fibers, so will likely be changed by
|
88
|
+
# another fiber before it's used. So store it in the block-local mycount.
|
89
|
+
count += 1
|
90
|
+
mycount = count
|
91
|
+
|
92
|
+
# Figure out when the request should start, and sleep until then
|
93
|
+
request_offset = request[:time] - log_start_time
|
94
|
+
replay_time = replay_start_time + request_offset / @speed
|
95
|
+
sleep_time = replay_time - Time.now
|
96
|
+
EM::Synchrony.sleep(sleep_time) if sleep_time > 0
|
97
|
+
|
98
|
+
# Send the request, and track how long it takes to complete
|
99
|
+
url = "http://#{@host}#{request[:route]}"
|
100
|
+
request_start_time = Time.now
|
101
|
+
client = EventMachine::HttpRequest.new(url, :connect_timeout => 15, :inactivity_timeout => 60)
|
102
|
+
response = client.public_send(request[:action], request[:request_options])
|
103
|
+
|
104
|
+
request_processing_time = Time.now - request_start_time
|
105
|
+
if (request_processing_time > max_request_processing_time)
|
106
|
+
max_request_processing_time = request_processing_time
|
107
|
+
end
|
108
|
+
if min_request_processing_time.nil? || min_request_processing_time > request_processing_time
|
109
|
+
min_request_processing_time = request_processing_time
|
110
|
+
end
|
111
|
+
status = response.response_header.http_status
|
112
|
+
|
113
|
+
if csv_file
|
114
|
+
csv_file.puts "#{mycount},#{status},#{request_processing_time},#{request_start_time - replay_time}"
|
115
|
+
end
|
116
|
+
total_time += request_processing_time
|
117
|
+
status_counts[status] += 1
|
118
|
+
error_count += 1 if response.error
|
119
|
+
STDERR.puts "#{mycount}: Error: #{response.error}" if response.error
|
120
|
+
unless status == 200
|
121
|
+
STDERR.puts "#{mycount}: received status code: #{status}, body: #{response.response}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
EventMachine::HttpRequest.new("http://#{@host}/__stop__").get if @profiler
|
125
|
+
summary_header = additional_summary
|
126
|
+
EventMachine.stop
|
127
|
+
end
|
128
|
+
csv_file.close if csv_file
|
129
|
+
if @summary_output
|
130
|
+
File.open(@summary_output, "w") do |summary|
|
131
|
+
summary.puts summary_header
|
132
|
+
summary.puts "Log File: #{ARGV[0]}"
|
133
|
+
summary.puts "Speed: #{@speed}"
|
134
|
+
summary.puts "Concurrency: #{@concurrency}"
|
135
|
+
summary.puts "Average request time,#{total_time/count}"
|
136
|
+
summary.puts "Max request time,#{max_request_processing_time}"
|
137
|
+
summary.puts "Min request time,#{min_request_processing_time}"
|
138
|
+
summary.puts "Errors,#{error_count}"
|
139
|
+
status_counts.each { |status, count| summary.puts "Status #{status},#{count}" }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.execute
|
145
|
+
options = Trollop::options do
|
146
|
+
banner "Usage: #{$0} [options] sas_log_file"
|
147
|
+
opt :host, "Host to run load test against", :type => :string, :default => "localhost:4567"
|
148
|
+
opt :concurrency, "Client-side concurrency; maximum number of simultaneous requests", :type => :int,
|
149
|
+
:default => 40
|
150
|
+
opt :speed, "Multiple of real-time to play back requests", :type => :float, :default => 1.0
|
151
|
+
opt :csv_output, "File to output CSV data to", :type => :string
|
152
|
+
opt :summary_output, "File to output summary data to", :type => :string
|
153
|
+
opt :force, "Overwrite existing output file"
|
154
|
+
opt :profiler, "trigger ruby prof"
|
155
|
+
end
|
156
|
+
if options[:csv_output] && File.exists?(options[:csv_output]) && !options[:force]
|
157
|
+
STDERR.puts "Cowardly refusing to overwrite existing file '#{options[:csv_output]}'. " +
|
158
|
+
"Use --force to provide extra bravery."
|
159
|
+
exit 1
|
160
|
+
end
|
161
|
+
Trollop::die "missing log file" unless ARGV[0]
|
162
|
+
load_tester = self.new(options)
|
163
|
+
load_tester.load_request_routes(ARGV[0])
|
164
|
+
load_tester.replay_requests
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Bo Chen"]
|
5
|
+
gem.email = ["bochen@ooyala.com"]
|
6
|
+
gem.description = %q{Reads access logs and replays them}
|
7
|
+
gem.summary = %q{Use to replay load test by replaying logs at different speeds}
|
8
|
+
gem.homepage = ""
|
9
|
+
|
10
|
+
gem.version = "0.0.4"
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "log_replayer"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.add_dependency "em-synchrony"
|
17
|
+
gem.add_dependency "em-http-request"
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: log_replayer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Bo Chen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-15 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: em-synchrony
|
16
|
+
requirement: &70096980187540 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70096980187540
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: em-http-request
|
27
|
+
requirement: &70096980187060 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70096980187060
|
36
|
+
description: Reads access logs and replays them
|
37
|
+
email:
|
38
|
+
- bochen@ooyala.com
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- .gitignore
|
44
|
+
- Gemfile
|
45
|
+
- LICENSE
|
46
|
+
- README.md
|
47
|
+
- lib/log_replayer.rb
|
48
|
+
- log_replayer.gemspec
|
49
|
+
homepage: ''
|
50
|
+
licenses: []
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.8.10
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Use to replay load test by replaying logs at different speeds
|
73
|
+
test_files: []
|