log_replayer 0.0.4
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/.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: []
|