wvanbergen-request-log-analyzer 1.1.1 → 1.1.2
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/bin/request-log-analyzer +0 -11
- data/lib/request_log_analyzer/aggregator/summarizer.rb +7 -3
- data/lib/request_log_analyzer/controller.rb +15 -3
- data/lib/request_log_analyzer/file_format/merb.rb +5 -5
- data/lib/request_log_analyzer/file_format/rails.rb +35 -21
- data/lib/request_log_analyzer/file_format/rails_development.rb +27 -20
- data/lib/request_log_analyzer/file_format.rb +44 -16
- data/lib/request_log_analyzer/filter/{anonimize.rb → anonymize.rb} +3 -5
- data/lib/request_log_analyzer/filter/field.rb +1 -5
- data/lib/request_log_analyzer/filter/timespan.rb +0 -2
- data/lib/request_log_analyzer/line_definition.rb +12 -88
- data/lib/request_log_analyzer/log_processor.rb +5 -25
- data/lib/request_log_analyzer/request.rb +56 -4
- data/lib/request_log_analyzer/source/log_parser.rb +3 -4
- data/lib/request_log_analyzer/tracker/{category.rb → frequency.rb} +2 -2
- data/spec/controller_spec.rb +38 -19
- data/spec/file_format_spec.rb +2 -2
- data/spec/file_formats/spec_format.rb +12 -5
- data/spec/filter_spec.rb +3 -3
- data/spec/line_definition_spec.rb +18 -85
- data/spec/log_parser_spec.rb +2 -3
- data/spec/log_processor_spec.rb +0 -38
- data/spec/merb_format_spec.rb +1 -1
- data/spec/rails_format_spec.rb +6 -5
- data/spec/spec_helper.rb +16 -0
- metadata +4 -4
data/bin/request-log-analyzer
CHANGED
@@ -27,14 +27,6 @@ begin
|
|
27
27
|
strip.switch(:keep_junk_lines, :j)
|
28
28
|
end
|
29
29
|
|
30
|
-
command_line.command(:anonymize) do |anonymize|
|
31
|
-
anonymize.minimum_parameters = 1
|
32
|
-
anonymize.option(:format, :alias => :f, :default => 'rails')
|
33
|
-
anonymize.option(:output, :alias => :o)
|
34
|
-
anonymize.switch(:discard_teaser_lines, :t)
|
35
|
-
anonymize.switch(:keep_junk_lines, :j)
|
36
|
-
end
|
37
|
-
|
38
30
|
command_line.option(:format, :alias => :f, :default => 'rails')
|
39
31
|
command_line.option(:file, :alias => :e)
|
40
32
|
command_line.switch(:assume_correct_order)
|
@@ -111,9 +103,6 @@ when :install
|
|
111
103
|
when :strip
|
112
104
|
require File.dirname(__FILE__) + '/../lib/request_log_analyzer/log_processor'
|
113
105
|
RequestLogAnalyzer::LogProcessor.build(:strip, arguments).run!
|
114
|
-
when :anonymize
|
115
|
-
require File.dirname(__FILE__) + '/../lib/request_log_analyzer/log_processor'
|
116
|
-
RequestLogAnalyzer::LogProcessor.build(:anonymize, arguments).run!
|
117
106
|
else
|
118
107
|
puts "Request-log-analyzer, by Willem van Bergen and Bart ten Brinke - version #{RequestLogAnalyzer::VERSION}"
|
119
108
|
puts "Website: http://github.com/wvanbergen/request-log-analyzer"
|
@@ -12,15 +12,19 @@ module RequestLogAnalyzer::Aggregator
|
|
12
12
|
@trackers = []
|
13
13
|
end
|
14
14
|
|
15
|
+
def reset!
|
16
|
+
@trackers = []
|
17
|
+
end
|
18
|
+
|
15
19
|
def method_missing(tracker_method, *args)
|
16
20
|
track(tracker_method, args.first)
|
17
21
|
end
|
18
22
|
|
19
|
-
def
|
23
|
+
def frequency(category_field, options = {})
|
20
24
|
if category_field.kind_of?(Symbol)
|
21
|
-
track(:
|
25
|
+
track(:frequency, options.merge(:category => category_field))
|
22
26
|
elsif category_field.kind_of?(Hash)
|
23
|
-
track(:
|
27
|
+
track(:frequency, category_field.merge(options))
|
24
28
|
end
|
25
29
|
end
|
26
30
|
|
@@ -131,7 +131,7 @@ module RequestLogAnalyzer
|
|
131
131
|
def handle_progress(message, value = nil)
|
132
132
|
case message
|
133
133
|
when :started
|
134
|
-
@progress_bar = CommandLine::ProgressBar.new(File.basename(value), File.size(value))
|
134
|
+
@progress_bar = CommandLine::ProgressBar.new(File.basename(value), File.size(value), STDOUT)
|
135
135
|
when :finished
|
136
136
|
@progress_bar.finish
|
137
137
|
@progress_bar = nil
|
@@ -160,6 +160,18 @@ module RequestLogAnalyzer
|
|
160
160
|
@filters << filter.new(file_format, @options.merge(filter_options))
|
161
161
|
end
|
162
162
|
|
163
|
+
def filter_request(request)
|
164
|
+
@filters.each do |filter|
|
165
|
+
request = filter.filter(request)
|
166
|
+
return nil if request.nil?
|
167
|
+
end
|
168
|
+
return request
|
169
|
+
end
|
170
|
+
|
171
|
+
def aggregate_request(request)
|
172
|
+
@aggregators.each { |agg| agg.aggregate(request) }
|
173
|
+
end
|
174
|
+
|
163
175
|
# Runs RequestLogAnalyzer
|
164
176
|
# 1. Call prepare on every aggregator
|
165
177
|
# 2. Generate requests from source object
|
@@ -175,8 +187,8 @@ module RequestLogAnalyzer
|
|
175
187
|
|
176
188
|
begin
|
177
189
|
@source.each_request do |request|
|
178
|
-
|
179
|
-
|
190
|
+
request = filter_request(request)
|
191
|
+
aggregate_request(request) unless request.nil?
|
180
192
|
end
|
181
193
|
rescue Interrupt => e
|
182
194
|
handle_progress(:interrupted)
|
@@ -7,7 +7,7 @@ module RequestLogAnalyzer::FileFormat
|
|
7
7
|
line.header = true
|
8
8
|
line.teaser = /Started/
|
9
9
|
line.regexp = /Started request handling\:\ (.+)/
|
10
|
-
line.captures << { :name => :timestamp, :type => :timestamp
|
10
|
+
line.captures << { :name => :timestamp, :type => :timestamp }
|
11
11
|
end
|
12
12
|
|
13
13
|
# ~ Params: {"action"=>"create", "controller"=>"session"}
|
@@ -23,10 +23,10 @@ module RequestLogAnalyzer::FileFormat
|
|
23
23
|
line.footer = true
|
24
24
|
line.teaser = /\{:dispatch_time/
|
25
25
|
line.regexp = /\{\:dispatch_time=>(\d+\.\d+(?:e-?\d+)?), (?:\:after_filters_time=>(\d+\.\d+(?:e-?\d+)?), )?(?:\:before_filters_time=>(\d+\.\d+(?:e-?\d+)?), )?\:action_time=>(\d+\.\d+(?:e-?\d+)?)\}/
|
26
|
-
line.captures << { :name => :dispatch_time, :type => :
|
27
|
-
<< { :name => :after_filters_time, :type => :
|
28
|
-
<< { :name => :before_filters_time, :type => :
|
29
|
-
<< { :name => :action_time, :type => :
|
26
|
+
line.captures << { :name => :dispatch_time, :type => :duration } \
|
27
|
+
<< { :name => :after_filters_time, :type => :duration } \
|
28
|
+
<< { :name => :before_filters_time, :type => :duration } \
|
29
|
+
<< { :name => :action_time, :type => :duration }
|
30
30
|
end
|
31
31
|
|
32
32
|
|
@@ -9,9 +9,9 @@ module RequestLogAnalyzer::FileFormat
|
|
9
9
|
line.regexp = /Processing ((?:\w+::)?\w+)#(\w+)(?: to (\w+))? \(for (\d+\.\d+\.\d+\.\d+) at (\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)\) \[([A-Z]+)\]/
|
10
10
|
line.captures << { :name => :controller, :type => :string } \
|
11
11
|
<< { :name => :action, :type => :string } \
|
12
|
-
<< { :name => :format, :type => :
|
13
|
-
<< { :name => :ip, :type => :string
|
14
|
-
<< { :name => :timestamp, :type => :timestamp
|
12
|
+
<< { :name => :format, :type => :format } \
|
13
|
+
<< { :name => :ip, :type => :string } \
|
14
|
+
<< { :name => :timestamp, :type => :timestamp } \
|
15
15
|
<< { :name => :method, :type => :string }
|
16
16
|
end
|
17
17
|
|
@@ -28,7 +28,7 @@ module RequestLogAnalyzer::FileFormat
|
|
28
28
|
<< { :name => :message, :type => :string } \
|
29
29
|
<< { :name => :line, :type => :integer } \
|
30
30
|
<< { :name => :file, :type => :string } \
|
31
|
-
<< { :name => :stack_trace, :type => :string
|
31
|
+
<< { :name => :stack_trace, :type => :string }
|
32
32
|
end
|
33
33
|
|
34
34
|
|
@@ -49,42 +49,56 @@ module RequestLogAnalyzer::FileFormat
|
|
49
49
|
line.teaser = /Completed in /
|
50
50
|
line.regexp = Regexp.new("(?:#{RAILS_21_COMPLETED}|#{RAILS_22_COMPLETED})")
|
51
51
|
|
52
|
-
line.captures << { :name => :duration, :type => :
|
53
|
-
<< { :name => :view, :type => :
|
54
|
-
<< { :name => :db, :type => :
|
52
|
+
line.captures << { :name => :duration, :type => :duration, :unit => :sec } \
|
53
|
+
<< { :name => :view, :type => :duration, :unit => :sec } \
|
54
|
+
<< { :name => :db, :type => :duration, :unit => :sec } \
|
55
55
|
<< { :name => :status, :type => :integer } \
|
56
|
-
<< { :name => :url, :type => :string
|
56
|
+
<< { :name => :url, :type => :string } # Old variant
|
57
57
|
|
58
|
-
line.captures << { :name => :duration, :type => :
|
59
|
-
<< { :name => :view, :type => :
|
60
|
-
<< { :name => :db, :type => :
|
61
|
-
<< { :name => :status, :type => :integer} \
|
62
|
-
<< { :name => :url, :type => :string
|
58
|
+
line.captures << { :name => :duration, :type => :duration, :unit => :msec } \
|
59
|
+
<< { :name => :view, :type => :duration, :unit => :msec } \
|
60
|
+
<< { :name => :db, :type => :duration, :unit => :msec } \
|
61
|
+
<< { :name => :status, :type => :integer } \
|
62
|
+
<< { :name => :url, :type => :string } # 2.2 variant
|
63
63
|
end
|
64
64
|
|
65
65
|
|
66
66
|
|
67
67
|
REQUEST_CATEGORIZER = Proc.new do |request|
|
68
|
-
format
|
69
|
-
"#{request[:controller]}##{request[:action]}.#{format} [#{request[:method]}]"
|
68
|
+
"#{request[:controller]}##{request[:action]}.#{request[:format]} [#{request[:method]}]"
|
70
69
|
end
|
71
70
|
|
72
71
|
report do |analyze|
|
73
72
|
analyze.timespan :line_type => :processing
|
74
|
-
analyze.
|
75
|
-
analyze.
|
76
|
-
analyze.
|
77
|
-
analyze.
|
73
|
+
analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Top 20 hits', :amount => 20, :line_type => :processing
|
74
|
+
analyze.frequency :method, :title => 'HTTP methods'
|
75
|
+
analyze.frequency :status, :title => 'HTTP statuses returned'
|
76
|
+
analyze.frequency :category => lambda { |request| request =~ :cache_hit ? 'Cache hit' : 'No hit' }, :title => 'Rails action cache hits'
|
78
77
|
|
79
78
|
analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
|
80
79
|
analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
|
81
80
|
analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
|
82
81
|
|
83
|
-
analyze.
|
82
|
+
analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
|
84
83
|
:if => lambda { |request| request[:duration] && request[:duration] > 1.0 }, :amount => 20
|
85
84
|
|
86
85
|
analyze.hourly_spread :line_type => :processing
|
87
|
-
analyze.
|
86
|
+
analyze.frequency :error, :title => 'Failed requests', :line_type => :failed, :amount => 20
|
87
|
+
end
|
88
|
+
|
89
|
+
# Define a custom Request class for the Rails file format to speed up timestamp handling
|
90
|
+
# and to ensure that a format is always set.
|
91
|
+
class Request < RequestLogAnalyzer::Request
|
92
|
+
|
93
|
+
# Do not use DateTime.parse
|
94
|
+
def convert_timestamp(value, definition)
|
95
|
+
value.gsub(/[^0-9]/, '')[0...14].to_i unless value.nil?
|
96
|
+
end
|
97
|
+
|
98
|
+
# Set 'html' as default format for a request
|
99
|
+
def convert_format(value, definition)
|
100
|
+
value || 'html'
|
101
|
+
end
|
88
102
|
end
|
89
103
|
|
90
104
|
end
|
@@ -10,8 +10,8 @@ module RequestLogAnalyzer::FileFormat
|
|
10
10
|
line.captures << { :name => :controller, :type => :string } \
|
11
11
|
<< { :name => :action, :type => :string } \
|
12
12
|
<< { :name => :format, :type => :string } \
|
13
|
-
<< { :name => :ip, :type => :string
|
14
|
-
<< { :name => :timestamp, :type => :timestamp
|
13
|
+
<< { :name => :ip, :type => :string } \
|
14
|
+
<< { :name => :timestamp, :type => :timestamp } \
|
15
15
|
<< { :name => :method, :type => :string }
|
16
16
|
end
|
17
17
|
|
@@ -25,14 +25,14 @@ module RequestLogAnalyzer::FileFormat
|
|
25
25
|
line.teaser = /Rendered /
|
26
26
|
line.regexp = /Rendered (\w+(?:\/\w+)+) \((\d+\.\d+)ms\)/
|
27
27
|
line.captures << { :name => :render_file, :type => :string } \
|
28
|
-
<< { :name => :render_duration, :type => :msec }
|
28
|
+
<< { :name => :render_duration, :type => :duration, :unit => :msec }
|
29
29
|
end
|
30
30
|
|
31
31
|
# [4;36;1mUser Load (0.4ms)[0m [0;1mSELECT * FROM `users` WHERE (`users`.`id` = 18205844) [0m
|
32
32
|
line_definition :query_executed do |line|
|
33
33
|
line.regexp = /\s+(?:\e\[4;36;1m)?((?:\w+::)*\w+) Load \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0;1m)?(.+) (?:\e\[0m)?/
|
34
34
|
line.captures << { :name => :query_class, :type => :string } \
|
35
|
-
<< { :name => :query_duration, :type => :msec } \
|
35
|
+
<< { :name => :query_duration, :type => :duration, :unit => :msec } \
|
36
36
|
<< { :name => :query_sql, :type => :string }
|
37
37
|
end
|
38
38
|
|
@@ -40,7 +40,7 @@ module RequestLogAnalyzer::FileFormat
|
|
40
40
|
line_definition :query_cached do |line|
|
41
41
|
line.teaser = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?(.+) (?:\e\[0m)?/
|
42
42
|
line.regexp = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?(.+) (?:\e\[0m)?/
|
43
|
-
line.captures << { :name => :cached_duration, :type => :msec } \
|
43
|
+
line.captures << { :name => :cached_duration, :type => :duration, :unit => :msec } \
|
44
44
|
<< { :name => :cached_sql, :type => :string }
|
45
45
|
end
|
46
46
|
|
@@ -52,7 +52,7 @@ module RequestLogAnalyzer::FileFormat
|
|
52
52
|
<< { :name => :message, :type => :string } \
|
53
53
|
<< { :name => :line, :type => :integer } \
|
54
54
|
<< { :name => :file, :type => :string } \
|
55
|
-
<< { :name => :stack_trace, :type => :string
|
55
|
+
<< { :name => :stack_trace, :type => :string }
|
56
56
|
end
|
57
57
|
|
58
58
|
|
@@ -73,17 +73,17 @@ module RequestLogAnalyzer::FileFormat
|
|
73
73
|
line.teaser = /Completed in /
|
74
74
|
line.regexp = Regexp.new("(?:#{RAILS_21_COMPLETED}|#{RAILS_22_COMPLETED})")
|
75
75
|
|
76
|
-
line.captures << { :name => :duration, :type => :
|
77
|
-
<< { :name => :view, :type => :
|
78
|
-
<< { :name => :db, :type => :
|
76
|
+
line.captures << { :name => :duration, :type => :duration } \
|
77
|
+
<< { :name => :view, :type => :duration } \
|
78
|
+
<< { :name => :db, :type => :duration } \
|
79
79
|
<< { :name => :status, :type => :integer } \
|
80
|
-
<< { :name => :url, :type => :string
|
80
|
+
<< { :name => :url, :type => :string } # Old variant
|
81
81
|
|
82
|
-
line.captures << { :name => :duration, :type => :
|
83
|
-
<< { :name => :view, :type => :
|
84
|
-
<< { :name => :db, :type => :
|
82
|
+
line.captures << { :name => :duration, :type => :duration, :unit => :msec } \
|
83
|
+
<< { :name => :view, :type => :duration, :unit => :msec } \
|
84
|
+
<< { :name => :db, :type => :duration, :unit => :msec } \
|
85
85
|
<< { :name => :status, :type => :integer} \
|
86
|
-
<< { :name => :url, :type => :string
|
86
|
+
<< { :name => :url, :type => :string } # 2.2 variant
|
87
87
|
end
|
88
88
|
|
89
89
|
REQUEST_CATEGORIZER = Proc.new do |request|
|
@@ -93,20 +93,27 @@ module RequestLogAnalyzer::FileFormat
|
|
93
93
|
|
94
94
|
report do |analyze|
|
95
95
|
analyze.timespan :line_type => :processing
|
96
|
-
analyze.
|
97
|
-
analyze.
|
98
|
-
analyze.
|
99
|
-
analyze.
|
96
|
+
analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Top 20 hits', :amount => 20, :line_type => :processing
|
97
|
+
analyze.frequency :method, :title => 'HTTP methods'
|
98
|
+
analyze.frequency :status, :title => 'HTTP statuses returned'
|
99
|
+
analyze.frequency :category => lambda { |request| request =~ :cache_hit ? 'Cache hit' : 'No hit' }, :title => 'Rails action cache hits'
|
100
100
|
|
101
101
|
analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
|
102
102
|
analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
|
103
103
|
analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
|
104
104
|
|
105
|
-
analyze.
|
105
|
+
analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
|
106
106
|
:if => lambda { |request| request[:duration] && request[:duration] > 1.0 }, :amount => 20
|
107
107
|
|
108
108
|
analyze.hourly_spread :line_type => :processing
|
109
|
-
analyze.
|
109
|
+
analyze.frequency :error, :title => 'Failed requests', :line_type => :failed, :amount => 20
|
110
110
|
end
|
111
|
+
|
112
|
+
class Request < RequestLogAnalyzer::Request
|
113
|
+
|
114
|
+
def convert_timestamp(value, definition)
|
115
|
+
value.gsub(/[^0-9]/)[0...14].to_i unless value.nil?
|
116
|
+
end
|
117
|
+
end
|
111
118
|
end
|
112
119
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module RequestLogAnalyzer::FileFormat
|
2
2
|
|
3
|
-
|
4
3
|
def self.const_missing(const)
|
5
4
|
RequestLogAnalyzer::load_default_class_file(self, const)
|
6
5
|
end
|
@@ -24,14 +23,24 @@ module RequestLogAnalyzer::FileFormat
|
|
24
23
|
elsif file_format.kind_of?(String) && File.exist?(file_format)
|
25
24
|
# load a format from a ruby file
|
26
25
|
require file_format
|
27
|
-
|
26
|
+
const = RequestLogAnalyzer::to_camelcase(File.basename(file_format, '.rb'))
|
27
|
+
if RequestLogAnalyzer::FileFormat.const_defined?(const)
|
28
|
+
klass = RequestLogAnalyzer::FileFormat.const_get(const)
|
29
|
+
elsif Object.const_defined?(const)
|
30
|
+
klass = Object.const_get(const)
|
31
|
+
else
|
32
|
+
raise "Cannot load class #{const} from #{file_format}!"
|
33
|
+
end
|
28
34
|
|
29
35
|
else
|
30
36
|
# load a provided file format
|
31
37
|
klass = RequestLogAnalyzer::FileFormat.const_get(RequestLogAnalyzer::to_camelcase(file_format))
|
32
38
|
end
|
33
39
|
|
34
|
-
|
40
|
+
# check the returned klass to see if it can be used
|
41
|
+
raise "Could not load a file format from #{file_format.inspect}" if klass.nil?
|
42
|
+
raise "Invalid FileFormat class" unless klass.kind_of?(Class) && klass.ancestors.include?(RequestLogAnalyzer::FileFormat::Base)
|
43
|
+
|
35
44
|
@current_file_format = klass.new # return an instance of the class
|
36
45
|
end
|
37
46
|
|
@@ -56,13 +65,29 @@ module RequestLogAnalyzer::FileFormat
|
|
56
65
|
|
57
66
|
# Registers the line definer instance for a subclass.
|
58
67
|
def self.inherited(subclass)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
68
|
+
if subclass.superclass == RequestLogAnalyzer::FileFormat::Base
|
69
|
+
|
70
|
+
# Create aline and report definer for this class
|
71
|
+
subclass.class_eval do
|
72
|
+
instance_variable_set(:@line_definer, RequestLogAnalyzer::LineDefinition::Definer.new)
|
73
|
+
instance_variable_set(:@report_definer, RequestLogAnalyzer::Aggregator::Summarizer::Definer.new)
|
74
|
+
class << self; attr_accessor :line_definer, :report_definer; end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create a custom Request class for this file format
|
78
|
+
subclass.const_set('Request', Class.new(RequestLogAnalyzer::Request)) unless subclass.const_defined?('Request')
|
79
|
+
else
|
80
|
+
|
81
|
+
# Copy the line and report definer from the parent class.
|
82
|
+
subclass.class_eval do
|
83
|
+
instance_variable_set(:@line_definer, superclass.line_definer)
|
84
|
+
instance_variable_set(:@report_definer, superclass.report_definer)
|
85
|
+
class << self; attr_accessor :line_definer, :report_definer; end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Create a custom Request class based on the superclass's Request class
|
89
|
+
subclass.const_set('Request', Class.new(subclass.superclass::Request)) unless subclass.const_defined?('Request')
|
90
|
+
end
|
66
91
|
end
|
67
92
|
|
68
93
|
# Specifies a single line defintions.
|
@@ -77,16 +102,19 @@ module RequestLogAnalyzer::FileFormat
|
|
77
102
|
# Specifies multiple line definitions at once using a block
|
78
103
|
def self.format_definition(&block)
|
79
104
|
if block_given?
|
80
|
-
yield
|
105
|
+
yield self.line_definer
|
81
106
|
else
|
82
|
-
return
|
107
|
+
return self.line_definer
|
83
108
|
end
|
84
109
|
end
|
85
110
|
|
86
111
|
# Specifies the summary report using a block.
|
87
|
-
def self.report(&block)
|
88
|
-
|
89
|
-
|
112
|
+
def self.report(mode = :append, &block)
|
113
|
+
if mode == :overwrite
|
114
|
+
self.report_definer.reset!
|
115
|
+
end
|
116
|
+
|
117
|
+
yield(self.report_definer)
|
90
118
|
end
|
91
119
|
|
92
120
|
# Returns all line definitions
|
@@ -96,7 +124,7 @@ module RequestLogAnalyzer::FileFormat
|
|
96
124
|
|
97
125
|
# Returns all the defined trackers for the summary report.
|
98
126
|
def report_trackers
|
99
|
-
self.class.
|
127
|
+
self.class.report_definer.trackers# => rescue []
|
100
128
|
end
|
101
129
|
|
102
130
|
# Checks whether the line definitions form a valid language.
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module RequestLogAnalyzer::Filter
|
2
2
|
|
3
|
-
# Filter to
|
3
|
+
# Filter to anonymize parsed values
|
4
4
|
# Options
|
5
5
|
# * <tt>:mode</tt> :reject or :accept.
|
6
6
|
# * <tt>:field</tt> Specific field to accept or reject.
|
7
7
|
# * <tt>:value</tt> Value that the field should match to be accepted or rejected.
|
8
|
-
class
|
8
|
+
class Anonymize < Base
|
9
9
|
|
10
10
|
def prepare
|
11
11
|
end
|
@@ -22,9 +22,7 @@ module RequestLogAnalyzer::Filter
|
|
22
22
|
value * ((75 + rand(50)) / 100.0)
|
23
23
|
end
|
24
24
|
|
25
|
-
def filter(request)
|
26
|
-
return nil unless request
|
27
|
-
|
25
|
+
def filter(request)
|
28
26
|
request.attributes.each do |key, value|
|
29
27
|
if key == :ip
|
30
28
|
request.attributes[key] = generate_random_ip
|
@@ -27,13 +27,9 @@ module RequestLogAnalyzer::Filter
|
|
27
27
|
# Returns nil otherwise.
|
28
28
|
# <tt>request</tt> Request Object
|
29
29
|
def filter(request)
|
30
|
-
|
31
|
-
|
32
|
-
found_field = request.every(@field).any? { |value| @value === value }
|
33
|
-
|
30
|
+
found_field = request.every(@field).any? { |value| @value === value.to_s }
|
34
31
|
return nil if !found_field && @mode == :select
|
35
32
|
return nil if found_field && @mode == :reject
|
36
|
-
|
37
33
|
return request
|
38
34
|
end
|
39
35
|
end
|
@@ -22,8 +22,6 @@ module RequestLogAnalyzer::Filter
|
|
22
22
|
# Returns nil otherwise
|
23
23
|
# <tt>request</tt> Request object.
|
24
24
|
def filter(request)
|
25
|
-
return nil unless request
|
26
|
-
|
27
25
|
if @after && @before && request.timestamp <= @before && @after <= request.timestamp
|
28
26
|
return request
|
29
27
|
elsif @after && @before.nil? && @after <= request.timestamp
|
@@ -1,22 +1,10 @@
|
|
1
1
|
module RequestLogAnalyzer
|
2
2
|
|
3
|
-
module Anonymizers
|
4
|
-
def anonymizer_for_ip(value, capture_definition)
|
5
|
-
'127.0.0.1'
|
6
|
-
end
|
7
|
-
|
8
|
-
def anonymizer_for_url(value, capture_definition)
|
9
|
-
value.sub(/^https?\:\/\/[A-Za-z0-9\.-]+\//, "http://example.com/")
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
3
|
# The line definition class is used to specify what lines should be parsed from the log file.
|
14
4
|
# It contains functionality to match a line against the definition and parse the information
|
15
5
|
# from this line. This is used by the LogParser class when parsing a log file..
|
16
6
|
class LineDefinition
|
17
7
|
|
18
|
-
include RequestLogAnalyzer::Anonymizers
|
19
|
-
|
20
8
|
class Definer
|
21
9
|
|
22
10
|
attr_accessor :line_definitions
|
@@ -52,20 +40,6 @@ module RequestLogAnalyzer
|
|
52
40
|
return definition
|
53
41
|
end
|
54
42
|
|
55
|
-
# Converts a parsed value (String) to the desired value using some heuristics.
|
56
|
-
def convert_value(value, type)
|
57
|
-
case type
|
58
|
-
when :integer; value.to_i
|
59
|
-
when :float; value.to_f
|
60
|
-
when :decimal; value.to_f
|
61
|
-
when :symbol; value.to_sym
|
62
|
-
when :sec; value.to_f
|
63
|
-
when :msec; value.to_f / 1000
|
64
|
-
when :timestamp; value.gsub(/[^0-9]/,'')[0..13].to_i # Retrieve with: DateTime.parse(value, '%Y%m%d%H%M%S')
|
65
|
-
else value
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
43
|
# Checks whether a given line matches this definition.
|
70
44
|
# It will return false if a line does not match. If the line matches, a hash is returned
|
71
45
|
# with all the fields parsed from that line as content.
|
@@ -74,17 +48,7 @@ module RequestLogAnalyzer
|
|
74
48
|
def matches(line, lineno = nil, parser = nil)
|
75
49
|
if @teaser.nil? || @teaser =~ line
|
76
50
|
if match_data = line.match(@regexp)
|
77
|
-
|
78
|
-
|
79
|
-
captures.each_with_index do |capture, index|
|
80
|
-
next if capture == :ignore
|
81
|
-
|
82
|
-
if match_data.captures[index]
|
83
|
-
request_info[capture[:name]] = convert_value(match_data.captures[index], capture[:type])
|
84
|
-
end
|
85
|
-
|
86
|
-
end
|
87
|
-
return request_info
|
51
|
+
return { :line_definition => self, :lineno => lineno, :captures => match_data.captures}
|
88
52
|
else
|
89
53
|
if @teaser && parser
|
90
54
|
parser.warn(:teaser_check_failed, "Teaser matched for #{name.inspect}, but full line did not:\n#{line.inspect}")
|
@@ -97,63 +61,23 @@ module RequestLogAnalyzer
|
|
97
61
|
end
|
98
62
|
|
99
63
|
alias :=~ :matches
|
100
|
-
|
101
|
-
def
|
102
|
-
if
|
103
|
-
|
64
|
+
|
65
|
+
def match_for(line, request, lineno = nil, parser = nil)
|
66
|
+
if match_info = matches(line, lineno, parser)
|
67
|
+
convert_captured_values(match_info[:captures], request)
|
104
68
|
else
|
105
|
-
|
106
|
-
when nil; value
|
107
|
-
when false; value
|
108
|
-
when true; '***'
|
109
|
-
when :slightly; anonymize_slightly(value, capture_definition)
|
110
|
-
else
|
111
|
-
method_name = "anonymizer_for_#{capture_definition[:anonymize]}".to_sym
|
112
|
-
self.respond_to?(method_name) ? self.send(method_name, value, capture_definition) : '***'
|
113
|
-
end
|
69
|
+
false
|
114
70
|
end
|
115
71
|
end
|
116
|
-
|
117
|
-
def anonymize_slightly(value, capture_definition)
|
118
|
-
case capture_definition[:type]
|
119
|
-
when :integer
|
120
|
-
(value.to_i * (0.8 + rand * 0.4)).to_i
|
121
|
-
when :double
|
122
|
-
(value.to_f * (0.8 + rand * 0.4)).to_f
|
123
|
-
when :msec
|
124
|
-
(value.to_i * (0.8 + rand * 0.4)).to_i
|
125
|
-
when :sec
|
126
|
-
(value.to_f * (0.8 + rand * 0.4)).to_f
|
127
|
-
when :timestamp
|
128
|
-
(DateTime.parse(value) + (rand(100) - 50)).to_s
|
129
|
-
else
|
130
|
-
puts "Cannot anonymize #{capture_definition[:type].inspect} slightly, using ***"
|
131
|
-
'***'
|
132
|
-
end
|
133
|
-
end
|
134
72
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
pos_adjustment = 0
|
140
|
-
captures.each_with_index do |capture, index|
|
141
|
-
unless $~[index + 1].nil?
|
142
|
-
anonymized_value = anonymize_value($~[index + 1], capture).to_s
|
143
|
-
line[($~.begin(index + 1) + pos_adjustment)...($~.end(index + 1) + pos_adjustment)] = anonymized_value
|
144
|
-
pos_adjustment += anonymized_value.length - $~[index + 1].length
|
145
|
-
end
|
146
|
-
end
|
147
|
-
line
|
148
|
-
elsif self.teaser.nil?
|
149
|
-
nil
|
150
|
-
else
|
151
|
-
options[:discard_teaser_lines] ? "" : line
|
152
|
-
end
|
153
|
-
else
|
154
|
-
nil
|
73
|
+
def convert_captured_values(values, request)
|
74
|
+
value_hash = {}
|
75
|
+
captures.each_with_index do |capture, index|
|
76
|
+
value_hash[capture[:name]] ||= request.convert_value(values[index], capture)
|
155
77
|
end
|
78
|
+
return value_hash
|
156
79
|
end
|
80
|
+
|
157
81
|
end
|
158
82
|
|
159
83
|
end
|
@@ -6,12 +6,9 @@ module RequestLogAnalyzer
|
|
6
6
|
# contents of the line, remain it intact or remove it altogether, based on the current
|
7
7
|
# file format
|
8
8
|
#
|
9
|
-
# Currently,
|
9
|
+
# Currently, one processors is supported:
|
10
10
|
# * :strip will remove all irrelevent lines (according to the file format) from the
|
11
11
|
# sources. A compact, information packed log will remain/.
|
12
|
-
# * :anonymize will anonymize sensitive information from the lines according to the
|
13
|
-
# anonymization rules in the file format. The result can be passed to third parties
|
14
|
-
# without privacy concerns.
|
15
12
|
#
|
16
13
|
class LogProcessor
|
17
14
|
|
@@ -21,8 +18,8 @@ module RequestLogAnalyzer
|
|
21
18
|
attr_accessor :output_file
|
22
19
|
|
23
20
|
# Builds a logprocessor instance from the arguments given on the command line
|
24
|
-
# <tt>command</tt> The command hat was used to start the log processor. This
|
25
|
-
#
|
21
|
+
# <tt>command</tt> The command hat was used to start the log processor. This will set the
|
22
|
+
# processing mode. Currently, only :strip is supported.
|
26
23
|
# <tt>arguments</tt> The parsed command line arguments (a CommandLine::Arguments instance)
|
27
24
|
def self.build(command, arguments)
|
28
25
|
|
@@ -43,7 +40,7 @@ module RequestLogAnalyzer
|
|
43
40
|
|
44
41
|
# Initializes a new LogProcessor instance.
|
45
42
|
# <tt>format</tt> The file format to use (e.g. :rails).
|
46
|
-
# <tt>mode</tt> The processing mode
|
43
|
+
# <tt>mode</tt> The processing mode
|
47
44
|
# <tt>options</tt> A hash with options to take into account
|
48
45
|
def initialize(format, mode, options = {})
|
49
46
|
@options = options
|
@@ -61,12 +58,11 @@ module RequestLogAnalyzer
|
|
61
58
|
end
|
62
59
|
|
63
60
|
# Processes an input stream by iteration over each line and processing it according to
|
64
|
-
# the current operation mode
|
61
|
+
# the current operation mode
|
65
62
|
# <tt>io</tt> The IO instance to process.
|
66
63
|
def process_io(io)
|
67
64
|
case mode
|
68
65
|
when :strip; io.each_line { |line| @output << strip_line(line) }
|
69
|
-
when :anonymize; io.each_line { |line| @output << anonymize_line(line) }
|
70
66
|
end
|
71
67
|
end
|
72
68
|
|
@@ -77,22 +73,6 @@ module RequestLogAnalyzer
|
|
77
73
|
file_format.line_definitions.any? { |name, definition| definition =~ line } ? line : ""
|
78
74
|
end
|
79
75
|
|
80
|
-
# Returns an anonymized version of the provided line. This can be a copy of the line it self,
|
81
|
-
# an empty string or a string in which some substrings are substituted for anonymized values.
|
82
|
-
# <tt>line</tt> The line to anonymize
|
83
|
-
def anonymize_line(line)
|
84
|
-
anonymized_line = nil
|
85
|
-
file_format.line_definitions.detect { |name, definition| anonymized_line = definition.anonymize(line, options) }
|
86
|
-
|
87
|
-
if anonymized_line
|
88
|
-
return anonymized_line
|
89
|
-
elsif options[:keep_junk_lines]
|
90
|
-
return line
|
91
|
-
else
|
92
|
-
return ""
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
76
|
# Runs the log processing by setting up the output stream and iterating over all the
|
97
77
|
# input sources. Input sources can either be filenames (String instances) or IO streams
|
98
78
|
# (IO instances). The strings "-" and "STDIN" will be substituted for the $stdin variable.
|
@@ -9,7 +9,45 @@ module RequestLogAnalyzer
|
|
9
9
|
# Request#every(field_name) returns all values corresponding to the given field name as array.
|
10
10
|
class Request
|
11
11
|
|
12
|
+
module Converters
|
13
|
+
|
14
|
+
def convert_value(value, capture_definition)
|
15
|
+
custom_converter_method = "convert_#{capture_definition[:type]}".to_sym
|
16
|
+
if respond_to?(custom_converter_method)
|
17
|
+
send(custom_converter_method, value, capture_definition)
|
18
|
+
elsif !value.nil?
|
19
|
+
case capture_definition[:type]
|
20
|
+
when :decimal; value.to_f
|
21
|
+
when :float; value.to_f
|
22
|
+
when :double; value.to_f
|
23
|
+
when :integer; value.to_i
|
24
|
+
when :int; value.to_i
|
25
|
+
when :symbol; value.to_sym
|
26
|
+
else; value.to_s
|
27
|
+
end
|
28
|
+
else
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Slow default method to parse timestamps
|
34
|
+
def convert_timestamp(value, capture_definition)
|
35
|
+
DateTime.parse(value).strftime('%Y%m%d%H%M%S').to_i unless value.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
def convert_duration(value, capture_definition)
|
39
|
+
if value.nil?
|
40
|
+
nil
|
41
|
+
elsif capture_definition[:unit] == :msec
|
42
|
+
value.to_f / 1000.0
|
43
|
+
else
|
44
|
+
value.to_f
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
12
49
|
include RequestLogAnalyzer::FileFormat::Awareness
|
50
|
+
include Converters
|
13
51
|
|
14
52
|
attr_reader :lines
|
15
53
|
attr_reader :attributes
|
@@ -32,12 +70,22 @@ module RequestLogAnalyzer
|
|
32
70
|
|
33
71
|
# Adds another line to the request.
|
34
72
|
# The line should be provides as a hash of the fields parsed from the line.
|
35
|
-
def add_parsed_line (
|
36
|
-
|
37
|
-
|
73
|
+
def add_parsed_line (parsed_line)
|
74
|
+
value_hash = parsed_line[:line_definition].convert_captured_values(parsed_line[:captures], self)
|
75
|
+
value_hash[:line_type] = parsed_line[:line_definition].name
|
76
|
+
value_hash[:lineno] = parsed_line[:lineno]
|
77
|
+
add_line_hash(value_hash)
|
78
|
+
end
|
79
|
+
|
80
|
+
def add_line_hash(value_hash)
|
81
|
+
@lines << value_hash
|
82
|
+
@attributes = value_hash.merge(@attributes)
|
38
83
|
end
|
39
84
|
|
40
|
-
|
85
|
+
|
86
|
+
def <<(hash)
|
87
|
+
hash[:line_definition] ? add_parsed_line(hash) : add_line_hash(hash)
|
88
|
+
end
|
41
89
|
|
42
90
|
# Checks whether the given line type was parsed from the log file for this request
|
43
91
|
def has_line_type?(line_type)
|
@@ -81,6 +129,10 @@ module RequestLogAnalyzer
|
|
81
129
|
header_found && footer_found
|
82
130
|
end
|
83
131
|
|
132
|
+
# This function is called before a Requests is yielded.
|
133
|
+
def validate
|
134
|
+
end
|
135
|
+
|
84
136
|
# Returns the first timestamp encountered in a request.
|
85
137
|
def timestamp
|
86
138
|
first(:timestamp)
|
@@ -131,7 +131,6 @@ module RequestLogAnalyzer::Source
|
|
131
131
|
if header_line?(request_data)
|
132
132
|
unless @current_request.nil?
|
133
133
|
if options[:assume_correct_order]
|
134
|
-
@parsed_requests += 1
|
135
134
|
handle_request(@current_request, &block) #yield @current_request
|
136
135
|
@current_request = @file_format.create_request(request_data)
|
137
136
|
else
|
@@ -146,7 +145,6 @@ module RequestLogAnalyzer::Source
|
|
146
145
|
unless @current_request.nil?
|
147
146
|
@current_request << request_data
|
148
147
|
if footer_line?(request_data)
|
149
|
-
@parsed_requests += 1
|
150
148
|
handle_request(@current_request, &block) # yield @current_request
|
151
149
|
@current_request = nil
|
152
150
|
end
|
@@ -161,18 +159,19 @@ module RequestLogAnalyzer::Source
|
|
161
159
|
# The default controller will send the request to every running aggegator.
|
162
160
|
def handle_request(request, &block)
|
163
161
|
@parsed_requests += 1
|
162
|
+
request.validate
|
164
163
|
accepted = block_given? ? yield(request) : true
|
165
164
|
@skipped_requests += 1 if not accepted
|
166
165
|
end
|
167
166
|
|
168
167
|
# Checks whether a given line hash is a header line.
|
169
168
|
def header_line?(hash)
|
170
|
-
|
169
|
+
hash[:line_definition].header
|
171
170
|
end
|
172
171
|
|
173
172
|
# Checks whether a given line hash is a footer line.
|
174
173
|
def footer_line?(hash)
|
175
|
-
|
174
|
+
hash[:line_definition].footer
|
176
175
|
end
|
177
176
|
end
|
178
177
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module RequestLogAnalyzer::Tracker
|
2
2
|
|
3
|
-
# Catagorize requests.
|
3
|
+
# Catagorize requests by frequency.
|
4
4
|
# Count and analyze requests for a specific attribute
|
5
5
|
#
|
6
6
|
# Accepts the following options:
|
@@ -19,7 +19,7 @@ module RequestLogAnalyzer::Tracker
|
|
19
19
|
# PUT | 13685 hits (28.4%) |░░░░░░░░░░░
|
20
20
|
# POST | 11662 hits (24.2%) |░░░░░░░░░
|
21
21
|
# DELETE | 512 hits (1.1%) |
|
22
|
-
class
|
22
|
+
class Frequency < Base
|
23
23
|
|
24
24
|
attr_reader :categories
|
25
25
|
|
data/spec/controller_spec.rb
CHANGED
@@ -4,42 +4,61 @@ describe RequestLogAnalyzer::Controller do
|
|
4
4
|
|
5
5
|
include RequestLogAnalyzerSpecHelper
|
6
6
|
|
7
|
-
|
8
|
-
# controller = RequestLogAnalyzer::Controller.new(:rails)
|
9
|
-
# (class << controller; self; end).ancestors.include?(RequestLogAnalyzer::FileFormat::Rails)
|
10
|
-
# end
|
11
|
-
|
12
|
-
it "should call the aggregators when run" do
|
7
|
+
it "should use a custom output generator correctly" do
|
13
8
|
|
14
|
-
mock_output = mock('
|
15
|
-
mock_output.stub!(:io).and_return(
|
9
|
+
mock_output = mock('RequestLogAnalyzer::Output::Base')
|
10
|
+
mock_output.stub!(:io).and_return(mock_io)
|
16
11
|
mock_output.should_receive(:header)
|
17
12
|
mock_output.should_receive(:footer)
|
13
|
+
|
14
|
+
file_format = RequestLogAnalyzer::FileFormat.load(:rails)
|
15
|
+
source = RequestLogAnalyzer::Source::LogParser.new(file_format, :source_files => log_fixture(:rails_1x))
|
16
|
+
controller = RequestLogAnalyzer::Controller.new(source, :output => mock_output)
|
17
|
+
|
18
|
+
controller.run!
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should call aggregators correctly when run" do
|
18
22
|
|
19
23
|
file_format = RequestLogAnalyzer::FileFormat.load(:rails)
|
20
24
|
source = RequestLogAnalyzer::Source::LogParser.new(file_format, :source_files => log_fixture(:rails_1x))
|
21
25
|
controller = RequestLogAnalyzer::Controller.new(source, :output => mock_output)
|
22
26
|
|
23
|
-
mock_aggregator = mock('
|
27
|
+
mock_aggregator = mock('RequestLogAnalyzer::Aggregator::Base')
|
24
28
|
mock_aggregator.should_receive(:prepare).once.ordered
|
25
29
|
mock_aggregator.should_receive(:aggregate).with(an_instance_of(file_format.class::Request)).at_least(:twice).ordered
|
26
30
|
mock_aggregator.should_receive(:finalize).once.ordered
|
27
31
|
mock_aggregator.should_receive(:report).once.ordered
|
32
|
+
|
33
|
+
controller.aggregators << mock_aggregator
|
34
|
+
controller.run!
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should call filters when run" do
|
38
|
+
file_format = RequestLogAnalyzer::FileFormat.load(:rails)
|
39
|
+
source = RequestLogAnalyzer::Source::LogParser.new(file_format, :source_files => log_fixture(:rails_1x))
|
40
|
+
controller = RequestLogAnalyzer::Controller.new(source, :output => mock_output)
|
28
41
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
controller.
|
42
|
+
mock_filter = mock('RequestLogAnalyzer::Filter::Base')
|
43
|
+
mock_filter.should_receive(:prepare).once.ordered
|
44
|
+
mock_filter.should_receive(:filter).at_least(:twice)
|
45
|
+
|
46
|
+
controller.should_not_receive(:aggregate_request)
|
47
|
+
|
48
|
+
controller.filters << mock_filter
|
36
49
|
controller.run!
|
37
50
|
end
|
38
51
|
|
39
|
-
it "should run well from the command line" do
|
40
|
-
|
41
|
-
|
52
|
+
it "should run well from the command line with the most important features" do
|
53
|
+
|
54
|
+
temp_file = "#{File.dirname(__FILE__)}/fixtures/report.txt"
|
55
|
+
temp_db = "#{File.dirname(__FILE__)}/fixtures/output.db"
|
56
|
+
binary = "#{File.dirname(__FILE__)}/../bin/request-log-analyzer"
|
57
|
+
|
58
|
+
system("#{binary} #{log_fixture(:rails_1x)} --database #{temp_db} --select Controller PeopleController --file #{temp_file} > /dev/null").should be_true
|
59
|
+
|
42
60
|
File.unlink(temp_file)
|
61
|
+
File.unlink(temp_db)
|
43
62
|
end
|
44
63
|
|
45
64
|
end
|
data/spec/file_format_spec.rb
CHANGED
@@ -50,13 +50,13 @@ describe RequestLogAnalyzer::FileFormat, :format_definition do
|
|
50
50
|
line.first_test :regexp => /test/, :captures => []
|
51
51
|
end
|
52
52
|
|
53
|
+
|
53
54
|
@second_file_format.format_definition do |line|
|
54
55
|
line.second_test :regexp => /test/, :captures => []
|
55
56
|
end
|
56
57
|
|
58
|
+
@first_file_format.line_definer.should_not eql(@second_file_format.line_definer)
|
57
59
|
@first_file_format.new.should have(1).line_definitions
|
58
|
-
@first_file_format.new.line_definitions[:first_test].should_not be_nil
|
59
|
-
@second_file_format.new.should have(1).line_definitions
|
60
60
|
@second_file_format.new.line_definitions[:second_test].should_not be_nil
|
61
61
|
end
|
62
62
|
end
|
@@ -4,24 +4,31 @@ class SpecFormat < RequestLogAnalyzer::FileFormat::Base
|
|
4
4
|
line.header = true
|
5
5
|
line.teaser = /processing /
|
6
6
|
line.regexp = /processing request (\d+)/
|
7
|
-
line.captures = [{ :name => :request_no, :type => :integer
|
7
|
+
line.captures = [{ :name => :request_no, :type => :integer }]
|
8
8
|
end
|
9
9
|
|
10
10
|
format_definition.test do |line|
|
11
11
|
line.teaser = /testing /
|
12
|
-
line.regexp = /testing is (\w+)
|
13
|
-
line.captures = [{ :name => :test_capture, :type => :
|
12
|
+
line.regexp = /testing is (\w+)(?: in (\d+\.\d+)ms)?/
|
13
|
+
line.captures = [{ :name => :test_capture, :type => :test_type },
|
14
|
+
{ :name => :duration, :type => :duration, :unit => :msec }]
|
14
15
|
end
|
15
16
|
|
16
17
|
format_definition.last do |line|
|
17
18
|
line.footer = true
|
18
19
|
line.teaser = /finishing /
|
19
20
|
line.regexp = /finishing request (\d+)/
|
20
|
-
line.captures = [{ :name => :request_no, :type => :integer}]
|
21
|
+
line.captures = [{ :name => :request_no, :type => :integer }]
|
21
22
|
end
|
22
23
|
|
23
24
|
report do |analyze|
|
24
|
-
analyze.
|
25
|
+
analyze.frequency :test_capture, :title => 'What is testing exactly?'
|
26
|
+
end
|
27
|
+
|
28
|
+
class Request < RequestLogAnalyzer::Request
|
29
|
+
def convert_test_type(value, definition)
|
30
|
+
"Testing is #{value}"
|
31
|
+
end
|
25
32
|
end
|
26
33
|
|
27
34
|
end
|
data/spec/filter_spec.rb
CHANGED
@@ -134,11 +134,11 @@ describe RequestLogAnalyzer::Filter::Field, 'regexp in accept mode' do
|
|
134
134
|
end
|
135
135
|
end
|
136
136
|
|
137
|
-
describe RequestLogAnalyzer::Filter::
|
137
|
+
describe RequestLogAnalyzer::Filter::Anonymize, 'anonymize request' do
|
138
138
|
include RequestLogAnalyzerSpecHelper
|
139
139
|
|
140
140
|
before(:each) do
|
141
|
-
@filter = RequestLogAnalyzer::Filter::
|
141
|
+
@filter = RequestLogAnalyzer::Filter::Anonymize.new(spec_format)
|
142
142
|
@filter.prepare
|
143
143
|
end
|
144
144
|
|
@@ -150,7 +150,7 @@ describe RequestLogAnalyzer::Filter::Anonimize, 'anonimize request' do
|
|
150
150
|
@filter.filter(request(:url => 'https://test.mysite.com/employees'))[:url].should eql('http://example.com/employees')
|
151
151
|
end
|
152
152
|
|
153
|
-
it "should
|
153
|
+
it "should fuzz durations" do
|
154
154
|
@filter.filter(request(:duration => 100))[:duration].should_not eql(100)
|
155
155
|
end
|
156
156
|
|
@@ -22,103 +22,36 @@ describe RequestLogAnalyzer::LineDefinition, :parsing do
|
|
22
22
|
(@line_definition =~ "Testing LineDefinition, tries: 123").should be_kind_of(Hash)
|
23
23
|
end
|
24
24
|
|
25
|
-
it "should return a hash with
|
25
|
+
it "should return a hash with :captures set to an array" do
|
26
26
|
hash = @line_definition.matches("Testing LineDefinition, tries: 123")
|
27
|
-
hash[:
|
28
|
-
hash[:
|
27
|
+
hash[:captures][0].should == "LineDefinition"
|
28
|
+
hash[:captures][1].should == "123"
|
29
29
|
end
|
30
30
|
|
31
|
-
it "should return a hash with :
|
32
|
-
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
describe RequestLogAnalyzer::LineDefinition, :anonymizing_basics do
|
37
|
-
before(:each) do
|
38
|
-
@line_definition = RequestLogAnalyzer::LineDefinition.new(:test, {
|
39
|
-
:teaser => /Anonymize /,
|
40
|
-
:regexp => /Anonymize (\w+)!/,
|
41
|
-
:captures => [{ :name => :what, :type => :string }]
|
42
|
-
})
|
43
|
-
end
|
44
|
-
|
45
|
-
it "should return nil if the teaser does not match" do
|
46
|
-
@line_definition.anonymize("Nonsense").should be_nil
|
47
|
-
end
|
48
|
-
|
49
|
-
it "should return nil if no teaser exists and the regexp doesn't match" do
|
50
|
-
line_definition = RequestLogAnalyzer::LineDefinition.new(:test, {
|
51
|
-
:regexp => /Anonymize!/, :captures => []})
|
52
|
-
|
53
|
-
line_definition.anonymize('nonsense').should be_nil
|
54
|
-
end
|
55
|
-
|
56
|
-
it "should return itself if only the teaser matches" do
|
57
|
-
@line_definition.anonymize("Anonymize 456").should == "Anonymize 456"
|
58
|
-
end
|
59
|
-
|
60
|
-
it "should return an empty string if the teaser matches and discard_teaser_lines is set" do
|
61
|
-
@line_definition.anonymize("Anonymize 456", :discard_teaser_lines => true).should == ""
|
31
|
+
it "should return a hash with :line_definition set" do
|
32
|
+
@line_definition.matches("Testing LineDefinition, tries: 123")[:line_definition].should == @line_definition
|
62
33
|
end
|
63
|
-
|
64
|
-
it "should return a string if the line matches" do
|
65
|
-
@line_definition.anonymize("Anonymize anonymizing!").should be_kind_of(String)
|
66
|
-
end
|
67
|
-
|
68
|
-
it "should not anonymize :what" do
|
69
|
-
@line_definition.anonymize("Anonymize anonymizing!").should == "Anonymize anonymizing!"
|
70
|
-
end
|
71
34
|
end
|
72
35
|
|
73
|
-
describe RequestLogAnalyzer::LineDefinition, :
|
74
|
-
|
75
|
-
it "should anonymize completely if anonymize is true" do
|
76
|
-
@line_definition = RequestLogAnalyzer::LineDefinition.new(:test, {
|
77
|
-
:regexp => /Anonymize (.+)!/, :captures => [{ :name => :what, :type => :string, :anonymize => true }]})
|
36
|
+
describe RequestLogAnalyzer::LineDefinition, :converting do
|
78
37
|
|
79
|
-
|
80
|
-
end
|
81
|
-
|
82
|
-
it "should anonymize a URL" do
|
83
|
-
@line_definition = RequestLogAnalyzer::LineDefinition.new(:test, {
|
84
|
-
:regexp => /Anonymize (.+)!/, :captures => [{ :name => :what, :type => :string, :anonymize => :url }]})
|
85
|
-
|
86
|
-
@line_definition.anonymize("Anonymize https://www.not-anonymous.com/path/to/file.html!").should == "Anonymize http://example.com/path/to/file.html!"
|
87
|
-
end
|
88
|
-
|
89
|
-
it "should anonymize an IP address" do
|
90
|
-
@line_definition = RequestLogAnalyzer::LineDefinition.new(:test, {
|
91
|
-
:regexp => /Anonymize (.+)!/, :captures => [{ :name => :what, :type => :string, :anonymize => :ip }]})
|
92
|
-
|
93
|
-
@line_definition.anonymize("Anonymize 1.2.3.4!").should == "Anonymize 127.0.0.1!"
|
94
|
-
end
|
38
|
+
include RequestLogAnalyzerSpecHelper
|
95
39
|
|
96
|
-
|
97
|
-
@
|
98
|
-
|
99
|
-
|
100
|
-
@line_definition.anonymize("Anonymize 1.2.3.4!").should == "Anonymize ***!"
|
40
|
+
before(:each) do
|
41
|
+
@file_format = spec_format
|
42
|
+
@request = @file_format.create_request
|
101
43
|
end
|
102
44
|
|
103
|
-
it "should
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
@line_definition.anonymize("Anonymize 1234!").should =~ /Anonymize \d{3,4}\!/
|
45
|
+
it "should convert captures to a hash of converted values" do
|
46
|
+
hash = @file_format.line_definitions[:first].convert_captured_values(["456"], @request)
|
47
|
+
hash[:request_no].should == 456
|
108
48
|
end
|
109
|
-
|
110
|
-
it "should anonymize an integer slightly" do
|
111
|
-
@line_definition = RequestLogAnalyzer::LineDefinition.new(:test, {
|
112
|
-
:regexp => /Anonymize (.+)!/, :captures => [{ :name => :what, :type => :integer, :anonymize => :slightly }]})
|
113
49
|
|
114
|
-
|
50
|
+
it "should convert captures to a hash" do
|
51
|
+
hash = @file_format.line_definitions[:test].convert_captured_values(["willem", nil], @request)
|
52
|
+
hash[:test_capture].should == 'Testing is willem'
|
53
|
+
hash[:duration].should be_nil
|
115
54
|
end
|
116
|
-
|
117
|
-
it "should anonymize an double slightly" do
|
118
|
-
@line_definition = RequestLogAnalyzer::LineDefinition.new(:test, {
|
119
|
-
:regexp => /Anonymize (.+)!/, :captures => [{ :name => :what, :type => :double, :anonymize => :slightly }]})
|
120
55
|
|
121
|
-
|
122
|
-
end
|
123
|
-
|
56
|
+
|
124
57
|
end
|
data/spec/log_parser_spec.rb
CHANGED
@@ -28,10 +28,9 @@ describe RequestLogAnalyzer::Source::LogParser, :requests do
|
|
28
28
|
|
29
29
|
it "should parse all request values when spanned over multiple files" do
|
30
30
|
@log_parser.parse_files([log_fixture(:multiple_files_1), log_fixture(:multiple_files_2)]) do |request|
|
31
|
-
request.lines.should have(4).items
|
32
|
-
|
31
|
+
request.lines.should have(4).items
|
33
32
|
request[:request_no].should == 1
|
34
|
-
request[:test_capture].should == "amazing"
|
33
|
+
request[:test_capture].should == "Testing is amazing" # Note the custom converter
|
35
34
|
end
|
36
35
|
end
|
37
36
|
|
data/spec/log_processor_spec.rb
CHANGED
@@ -1,44 +1,6 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper'
|
2
2
|
require 'request_log_analyzer/log_processor'
|
3
3
|
|
4
|
-
describe RequestLogAnalyzer::LogProcessor, 'anonymization' do
|
5
|
-
|
6
|
-
include RequestLogAnalyzerSpecHelper
|
7
|
-
|
8
|
-
before(:each) do
|
9
|
-
@log_anonymizer = RequestLogAnalyzer::LogProcessor.new(spec_format, :anonymize, {})
|
10
|
-
@alternate_log_anonymizer = RequestLogAnalyzer::LogProcessor.new(spec_format, :anonymize, {:keep_junk_lines => true, :discard_teaser_lines => true})
|
11
|
-
end
|
12
|
-
|
13
|
-
it "should keep a junk line if :keep_junk_lines is true" do
|
14
|
-
@alternate_log_anonymizer.anonymize_line("junk line\n").should == "junk line\n"
|
15
|
-
end
|
16
|
-
|
17
|
-
it "should remove a junk line" do
|
18
|
-
@log_anonymizer.anonymize_line("junk line\n").should be_empty
|
19
|
-
end
|
20
|
-
|
21
|
-
it "should keep a teaser line intact" do
|
22
|
-
@log_anonymizer.anonymize_line("processing 1234\n").should == "processing 1234\n"
|
23
|
-
end
|
24
|
-
|
25
|
-
it "should discard a teaser line if discard_teaser_line is true" do
|
26
|
-
@alternate_log_anonymizer.anonymize_line("processing 1234\n").should be_empty
|
27
|
-
end
|
28
|
-
|
29
|
-
it "should keep a matching line intact if no anonymizing is declared" do
|
30
|
-
@alternate_log_anonymizer.anonymize_line("finishing request 130\n").should == "finishing request 130\n"
|
31
|
-
end
|
32
|
-
|
33
|
-
it "should anonymize values completely if requested" do
|
34
|
-
@alternate_log_anonymizer.anonymize_line("testing is great\n").should == "testing is ***\n"
|
35
|
-
end
|
36
|
-
|
37
|
-
it "should anonymize values slightly if requested" do
|
38
|
-
@alternate_log_anonymizer.anonymize_line("finishing request 130\n").should =~ /^finishing request 1\d\d\n$/
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
4
|
describe RequestLogAnalyzer::LogProcessor, 'stripping log files' do
|
43
5
|
|
44
6
|
include RequestLogAnalyzerSpecHelper
|
data/spec/merb_format_spec.rb
CHANGED
@@ -29,7 +29,7 @@ describe RequestLogAnalyzer::Source::LogParser, :merb do
|
|
29
29
|
@log_parser.parse_file(log_fixture(:merb)) { |found_request| request ||= found_request }
|
30
30
|
|
31
31
|
request.should be_completed
|
32
|
-
|
32
|
+
request[:timestamp].should == 20080829111023 # 'Fri Aug 29 11:10:23 +0200 2008'
|
33
33
|
request[:dispatch_time].should == 0.243424
|
34
34
|
request[:after_filters_time].should == 6.9e-05
|
35
35
|
request[:before_filters_time].should == 0.213213
|
data/spec/rails_format_spec.rb
CHANGED
@@ -80,6 +80,7 @@ describe "RequestLogAnalyzer::FileFormat::RailsDevelopment - Rails with developm
|
|
80
80
|
|
81
81
|
before(:each) do
|
82
82
|
@file_format = RequestLogAnalyzer::FileFormat.load(:rails_development)
|
83
|
+
@request = @file_format.create_request
|
83
84
|
end
|
84
85
|
|
85
86
|
it "should have a valid language definitions" do
|
@@ -87,33 +88,33 @@ describe "RequestLogAnalyzer::FileFormat::RailsDevelopment - Rails with developm
|
|
87
88
|
end
|
88
89
|
|
89
90
|
it "should parse a rendered line" do
|
90
|
-
info = @file_format.line_definitions[:rendered].
|
91
|
+
info = @file_format.line_definitions[:rendered].match_for("Rendered layouts/_footer (2.9ms)", @request)
|
91
92
|
info[:render_file].should == 'layouts/_footer'
|
92
93
|
info[:render_duration].should == 0.0029
|
93
94
|
end
|
94
95
|
|
95
96
|
it "should parse a query executed line with colors" do
|
96
|
-
info = @file_format.line_definitions[:query_executed].
|
97
|
+
info = @file_format.line_definitions[:query_executed].match_for(" [4;36;1mUser Load (0.4ms)[0m [0;1mSELECT * FROM `users` WHERE (`users`.`id` = 18205844) [0m", @request)
|
97
98
|
info[:query_class].should == 'User'
|
98
99
|
info[:query_duration].should == 0.0004
|
99
100
|
info[:query_sql].should == 'SELECT * FROM `users` WHERE (`users`.`id` = 18205844)'
|
100
101
|
end
|
101
102
|
|
102
103
|
it "should parse a query executed line without colors" do
|
103
|
-
info = @file_format.line_definitions[:query_executed].
|
104
|
+
info = @file_format.line_definitions[:query_executed].match_for(" User Load (0.4ms) SELECT * FROM `users` WHERE (`users`.`id` = 18205844) ", @request)
|
104
105
|
info[:query_class].should == 'User'
|
105
106
|
info[:query_duration].should == 0.0004
|
106
107
|
info[:query_sql].should == 'SELECT * FROM `users` WHERE (`users`.`id` = 18205844)'
|
107
108
|
end
|
108
109
|
|
109
110
|
it "should parse a cached query line with colors" do
|
110
|
-
info = @file_format.line_definitions[:query_cached].
|
111
|
+
info = @file_format.line_definitions[:query_cached].match_for(' [4;35;1mCACHE (0.0ms)[0m [0mSELECT * FROM `users` WHERE (`users`.`id` = 0) [0m', @request)
|
111
112
|
info[:cached_duration].should == 0.0
|
112
113
|
info[:cached_sql].should == 'SELECT * FROM `users` WHERE (`users`.`id` = 0)'
|
113
114
|
end
|
114
115
|
|
115
116
|
it "should parse a cached query line without colors" do
|
116
|
-
info = @file_format.line_definitions[:query_cached].
|
117
|
+
info = @file_format.line_definitions[:query_cached].match_for(' CACHE (0.0ms) SELECT * FROM `users` WHERE (`users`.`id` = 0) ', @request)
|
117
118
|
info[:cached_duration].should == 0.0
|
118
119
|
info[:cached_sql].should == 'SELECT * FROM `users` WHERE (`users`.`id` = 0)'
|
119
120
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -28,6 +28,22 @@ module RequestLogAnalyzerSpecHelper
|
|
28
28
|
format.create_request(fields)
|
29
29
|
end
|
30
30
|
end
|
31
|
+
|
32
|
+
def mock_io
|
33
|
+
mio = mock('IO')
|
34
|
+
mio.stub!(:print)
|
35
|
+
mio.stub!(:puts)
|
36
|
+
mio.stub!(:write)
|
37
|
+
return mio
|
38
|
+
end
|
39
|
+
|
40
|
+
def mock_output
|
41
|
+
output = mock('RequestLogAnalyzer::Output')
|
42
|
+
output.stub!(:header)
|
43
|
+
output.stub!(:footer)
|
44
|
+
output.stub!(:io).and_return(mock_io)
|
45
|
+
return output
|
46
|
+
end
|
31
47
|
|
32
48
|
end
|
33
49
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wvanbergen-request-log-analyzer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Willem van Bergen
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2009-01-
|
13
|
+
date: 2009-01-24 00:00:00 -08:00
|
14
14
|
default_executable: request-log-analyzer
|
15
15
|
dependencies: []
|
16
16
|
|
@@ -49,7 +49,7 @@ files:
|
|
49
49
|
- lib/request_log_analyzer/file_format/rails_development.rb
|
50
50
|
- lib/request_log_analyzer/filter
|
51
51
|
- lib/request_log_analyzer/filter.rb
|
52
|
-
- lib/request_log_analyzer/filter/
|
52
|
+
- lib/request_log_analyzer/filter/anonymize.rb
|
53
53
|
- lib/request_log_analyzer/filter/field.rb
|
54
54
|
- lib/request_log_analyzer/filter/timespan.rb
|
55
55
|
- lib/request_log_analyzer/line_definition.rb
|
@@ -64,8 +64,8 @@ files:
|
|
64
64
|
- lib/request_log_analyzer/source/log_parser.rb
|
65
65
|
- lib/request_log_analyzer/tracker
|
66
66
|
- lib/request_log_analyzer/tracker.rb
|
67
|
-
- lib/request_log_analyzer/tracker/category.rb
|
68
67
|
- lib/request_log_analyzer/tracker/duration.rb
|
68
|
+
- lib/request_log_analyzer/tracker/frequency.rb
|
69
69
|
- lib/request_log_analyzer/tracker/hourly_spread.rb
|
70
70
|
- lib/request_log_analyzer/tracker/timespan.rb
|
71
71
|
- spec
|