log_replayer 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ *.gem # Don't check in built gems.
2
+ .yardoc # Yard metadata.
3
+ Gemfile.lock # Gems shouldn't lock gem versions. Apps that use them should.
4
+ _yardoc # More yard metadata.
5
+ doc/ # Don't check in yard-generated docs. See yard-doc.ooyala.com.
6
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in log_replayer.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1 @@
1
+ Copyright (c) 2012, Ooyala Inc
@@ -0,0 +1,3 @@
1
+ # LogReplayer
2
+
3
+ Replays log files, which can be used as load testing.
@@ -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: []