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