request-log-analyzer 1.1.1 → 1.2.0
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/DESIGN +24 -10
- data/README.rdoc +1 -1
- data/Rakefile +1 -2
- data/bin/request-log-analyzer +4 -38
- data/lib/cli/progressbar.rb +2 -19
- data/lib/cli/tools.rb +46 -0
- data/lib/request_log_analyzer/aggregator/database.rb +33 -8
- data/lib/request_log_analyzer/aggregator/echo.rb +1 -0
- data/lib/request_log_analyzer/aggregator/summarizer.rb +22 -16
- data/lib/request_log_analyzer/controller.rb +21 -5
- data/lib/request_log_analyzer/file_format/merb.rb +22 -9
- data/lib/request_log_analyzer/file_format/rails.rb +37 -23
- data/lib/request_log_analyzer/file_format/rails_development.rb +33 -88
- data/lib/request_log_analyzer/file_format.rb +50 -18
- data/lib/request_log_analyzer/filter/{anonimize.rb → anonymize.rb} +3 -8
- data/lib/request_log_analyzer/filter/field.rb +7 -6
- data/lib/request_log_analyzer/filter/timespan.rb +7 -3
- data/lib/request_log_analyzer/filter.rb +0 -4
- data/lib/request_log_analyzer/line_definition.rb +22 -88
- data/lib/request_log_analyzer/log_processor.rb +5 -25
- data/lib/request_log_analyzer/output/fixed_width.rb +14 -3
- data/lib/request_log_analyzer/output/html.rb +1 -0
- data/lib/request_log_analyzer/request.rb +62 -4
- data/lib/request_log_analyzer/source/database.rb +76 -0
- data/lib/request_log_analyzer/source/log_parser.rb +104 -52
- data/lib/request_log_analyzer/source.rb +28 -6
- data/lib/request_log_analyzer/tracker/duration.rb +90 -15
- data/lib/request_log_analyzer/tracker/{category.rb → frequency.rb} +24 -12
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +20 -9
- data/lib/request_log_analyzer/tracker/timespan.rb +15 -8
- data/lib/request_log_analyzer.rb +29 -16
- data/spec/integration/command_line_usage_spec.rb +71 -0
- data/spec/lib/helper.rb +33 -0
- data/spec/lib/mocks.rb +47 -0
- data/spec/lib/testing_format.rb +39 -0
- data/spec/spec_helper.rb +5 -25
- data/spec/{database_inserter_spec.rb → unit/aggregator/database_inserter_spec.rb} +40 -37
- data/spec/unit/aggregator/summarizer_spec.rb +28 -0
- data/spec/unit/controller/controller_spec.rb +43 -0
- data/spec/unit/controller/log_processor_spec.rb +20 -0
- data/spec/{file_format_spec.rb → unit/file_format/file_format_api_spec.rb} +18 -6
- data/spec/unit/file_format/line_definition_spec.rb +64 -0
- data/spec/{merb_format_spec.rb → unit/file_format/merb_format_spec.rb} +3 -3
- data/spec/{rails_format_spec.rb → unit/file_format/rails_format_spec.rb} +24 -15
- data/spec/unit/filter/anonymize_filter_spec.rb +22 -0
- data/spec/unit/filter/field_filter_spec.rb +69 -0
- data/spec/unit/filter/timespan_filter_spec.rb +61 -0
- data/spec/{log_parser_spec.rb → unit/source/log_parser_spec.rb} +9 -10
- data/spec/{request_spec.rb → unit/source/request_spec.rb} +5 -5
- data/spec/unit/tracker/duration_tracker_spec.rb +99 -0
- data/spec/unit/tracker/frequency_tracker_spec.rb +83 -0
- data/spec/unit/tracker/hourly_spread_spec.rb +75 -0
- data/spec/unit/tracker/timespan_tracker_spec.rb +65 -0
- data/spec/unit/tracker/tracker_api_test.rb +45 -0
- data/tasks/rspec.rake +12 -0
- metadata +56 -28
- data/spec/controller_spec.rb +0 -45
- data/spec/file_formats/spec_format.rb +0 -27
- data/spec/filter_spec.rb +0 -157
- data/spec/line_definition_spec.rb +0 -124
- data/spec/log_processor_spec.rb +0 -57
- data/spec/summarizer_spec.rb +0 -9
|
@@ -1,112 +1,57 @@
|
|
|
1
1
|
module RequestLogAnalyzer::FileFormat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# The RailsDevelopment FileFormat is an extention to the default Rails file format. It includes
|
|
4
|
+
# all lines of the normal Rails file format, but parses SQL queries and partial rendering lines
|
|
5
|
+
# as well.
|
|
6
|
+
class RailsDevelopment < Rails
|
|
4
7
|
|
|
5
|
-
#
|
|
6
|
-
line_definition :
|
|
7
|
-
line.
|
|
8
|
-
line.
|
|
9
|
-
line.
|
|
10
|
-
line.captures << { :name => :controller, :type => :string } \
|
|
11
|
-
<< { :name => :action, :type => :string } \
|
|
12
|
-
<< { :name => :format, :type => :string } \
|
|
13
|
-
<< { :name => :ip, :type => :string, :anonymize => :ip } \
|
|
14
|
-
<< { :name => :timestamp, :type => :timestamp, :anonymize => :slightly } \
|
|
15
|
-
<< { :name => :method, :type => :string }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Filter chain halted as [#<ActionController::Caching::Actions::ActionCacheFilter:0x2a999ad620 @check=nil, @options={:store_options=>{}, :layout=>nil, :cache_path=>#<Proc:0x0000002a999b8890@/app/controllers/cached_controller.rb:8>}>] rendered_or_redirected.
|
|
19
|
-
line_definition :cache_hit do |line|
|
|
20
|
-
line.regexp = /Filter chain halted as \[\#<ActionController::Caching::Actions::ActionCacheFilter:.+>\] rendered_or_redirected/
|
|
8
|
+
# Parameters: {"action"=>"demo", "controller"=>"page"}
|
|
9
|
+
line_definition :parameters do |line|
|
|
10
|
+
line.teaser = /Parameters/
|
|
11
|
+
line.regexp = /\s+Parameters:\s+(\{.*\})/
|
|
12
|
+
line.captures << { :name => :params, :type => :eval }
|
|
21
13
|
end
|
|
22
14
|
|
|
23
15
|
# Rendered layouts/_footer (2.9ms)
|
|
24
16
|
line_definition :rendered do |line|
|
|
25
|
-
line.teaser = /Rendered /
|
|
26
17
|
line.regexp = /Rendered (\w+(?:\/\w+)+) \((\d+\.\d+)ms\)/
|
|
27
18
|
line.captures << { :name => :render_file, :type => :string } \
|
|
28
|
-
<< { :name => :render_duration, :type => :msec }
|
|
19
|
+
<< { :name => :render_duration, :type => :duration, :unit => :msec }
|
|
29
20
|
end
|
|
30
21
|
|
|
31
22
|
# [4;36;1mUser Load (0.4ms)[0m [0;1mSELECT * FROM `users` WHERE (`users`.`id` = 18205844) [0m
|
|
32
23
|
line_definition :query_executed do |line|
|
|
33
|
-
line.regexp = /\s+(?:\e\[4;36;1m)?((?:\w+::)*\w+) Load \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0;1m)?(
|
|
24
|
+
line.regexp = /\s+(?:\e\[4;36;1m)?((?:\w+::)*\w+) Load \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0;1m)?([^\e]+) ?(?:\e\[0m)?/
|
|
34
25
|
line.captures << { :name => :query_class, :type => :string } \
|
|
35
|
-
<< { :name => :query_duration, :type => :msec } \
|
|
36
|
-
<< { :name => :query_sql, :type => :
|
|
26
|
+
<< { :name => :query_duration, :type => :duration, :unit => :msec } \
|
|
27
|
+
<< { :name => :query_sql, :type => :sql }
|
|
37
28
|
end
|
|
38
29
|
|
|
39
30
|
# [4;35;1mCACHE (0.0ms)[0m [0mSELECT * FROM `users` WHERE (`users`.`id` = 0) [0m
|
|
40
31
|
line_definition :query_cached do |line|
|
|
41
|
-
line.
|
|
42
|
-
line.
|
|
43
|
-
|
|
44
|
-
<< { :name => :cached_sql, :type => :string }
|
|
32
|
+
line.regexp = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?([^\e]+) ?(?:\e\[0m)?/
|
|
33
|
+
line.captures << { :name => :cached_duration, :type => :duration, :unit => :msec } \
|
|
34
|
+
<< { :name => :cached_sql, :type => :sql }
|
|
45
35
|
end
|
|
46
|
-
|
|
47
|
-
# RuntimeError (Cannot destroy employee): /app/models/employee.rb:198:in `before_destroy'
|
|
48
|
-
line_definition :failed do |line|
|
|
49
|
-
line.footer = true
|
|
50
|
-
line.regexp = /((?:[A-Z]\w+\:\:)*[A-Z]\w+) \((.*)\)(?: on line #(\d+) of .+)?\:(.*)/
|
|
51
|
-
line.captures << { :name => :error, :type => :string } \
|
|
52
|
-
<< { :name => :message, :type => :string } \
|
|
53
|
-
<< { :name => :line, :type => :integer } \
|
|
54
|
-
<< { :name => :file, :type => :string } \
|
|
55
|
-
<< { :name => :stack_trace, :type => :string, :anonymize => true }
|
|
56
|
-
end
|
|
57
36
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# are both parsed by the same regular expression. The format in Rails 2.2 was slightly changed,
|
|
69
|
-
# but the line contains exactly the same information.
|
|
70
|
-
line_definition :completed do |line|
|
|
71
|
-
|
|
72
|
-
line.footer = true
|
|
73
|
-
line.teaser = /Completed in /
|
|
74
|
-
line.regexp = Regexp.new("(?:#{RAILS_21_COMPLETED}|#{RAILS_22_COMPLETED})")
|
|
75
|
-
|
|
76
|
-
line.captures << { :name => :duration, :type => :sec, :anonymize => :slightly } \
|
|
77
|
-
<< { :name => :view, :type => :sec, :anonymize => :slightly } \
|
|
78
|
-
<< { :name => :db, :type => :sec, :anonymize => :slightly } \
|
|
79
|
-
<< { :name => :status, :type => :integer } \
|
|
80
|
-
<< { :name => :url, :type => :string, :anonymize => :url } # Old variant
|
|
81
|
-
|
|
82
|
-
line.captures << { :name => :duration, :type => :msec, :anonymize => :slightly } \
|
|
83
|
-
<< { :name => :view, :type => :msec, :anonymize => :slightly } \
|
|
84
|
-
<< { :name => :db, :type => :msec, :anonymize => :slightly } \
|
|
85
|
-
<< { :name => :status, :type => :integer} \
|
|
86
|
-
<< { :name => :url, :type => :string, :anonymize => :url } # 2.2 variant
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
REQUEST_CATEGORIZER = Proc.new do |request|
|
|
90
|
-
format = request[:format] || 'html'
|
|
91
|
-
"#{request[:controller]}##{request[:action]}.#{format} [#{request[:method]}]"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
report do |analyze|
|
|
95
|
-
analyze.timespan :line_type => :processing
|
|
96
|
-
analyze.category :category => REQUEST_CATEGORIZER, :title => 'Top 20 hits', :amount => 20, :line_type => :processing
|
|
97
|
-
analyze.category :method, :title => 'HTTP methods'
|
|
98
|
-
analyze.category :status, :title => 'HTTP statuses returned'
|
|
99
|
-
analyze.category :category => lambda { |request| request =~ :cache_hit ? 'Cache hit' : 'No hit' }, :title => 'Rails action cache hits'
|
|
100
|
-
|
|
101
|
-
analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
|
|
102
|
-
analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
|
|
103
|
-
analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
|
|
37
|
+
# Define the reporting for the additional parsed lines
|
|
38
|
+
report(:append) do |analyze|
|
|
39
|
+
|
|
40
|
+
analyze.duration :render_duration, :category => :render_file, :multiple_per_request => true,
|
|
41
|
+
:amount => 20, :title => 'Partial rendering duration'
|
|
42
|
+
|
|
43
|
+
analyze.duration :query_duration, :category => :query_sql, :multiple_per_request => true,
|
|
44
|
+
:amount => 20, :title => 'Query duration'
|
|
45
|
+
|
|
46
|
+
end
|
|
104
47
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
48
|
+
# Add a converter method for the SQL fields the the Rails request class
|
|
49
|
+
class Request < Rails::Request
|
|
50
|
+
|
|
51
|
+
# Sanitizes SQL queries so that they can be grouped
|
|
52
|
+
def convert_sql(sql, definition)
|
|
53
|
+
sql.gsub(/\b\d+\b/, ':int').gsub(/`([^`]+)`/, '\1').gsub(/'[^']*'/, ':string').rstrip
|
|
54
|
+
end
|
|
110
55
|
end
|
|
111
56
|
end
|
|
112
57
|
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.clone)
|
|
84
|
+
instance_variable_set(:@report_definer, superclass.report_definer.clone)
|
|
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.
|
|
@@ -70,23 +95,30 @@ module RequestLogAnalyzer::FileFormat
|
|
|
70
95
|
@line_definer.send(name, &block)
|
|
71
96
|
end
|
|
72
97
|
|
|
73
|
-
def
|
|
74
|
-
self.class::Request
|
|
98
|
+
def request_class
|
|
99
|
+
self.class::Request
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def request(*hashes)
|
|
103
|
+
request_class.create(self, *hashes)
|
|
75
104
|
end
|
|
76
105
|
|
|
77
106
|
# Specifies multiple line definitions at once using a block
|
|
78
107
|
def self.format_definition(&block)
|
|
79
108
|
if block_given?
|
|
80
|
-
yield
|
|
109
|
+
yield self.line_definer
|
|
81
110
|
else
|
|
82
|
-
return
|
|
111
|
+
return self.line_definer
|
|
83
112
|
end
|
|
84
113
|
end
|
|
85
114
|
|
|
86
115
|
# Specifies the summary report using a block.
|
|
87
|
-
def self.report(&block)
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
def self.report(mode = :append, &block)
|
|
117
|
+
if mode == :overwrite
|
|
118
|
+
self.report_definer.reset!
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
yield(self.report_definer)
|
|
90
122
|
end
|
|
91
123
|
|
|
92
124
|
# Returns all line definitions
|
|
@@ -96,7 +128,7 @@ module RequestLogAnalyzer::FileFormat
|
|
|
96
128
|
|
|
97
129
|
# Returns all the defined trackers for the summary report.
|
|
98
130
|
def report_trackers
|
|
99
|
-
self.class.
|
|
131
|
+
self.class.report_definer.trackers# => rescue []
|
|
100
132
|
end
|
|
101
133
|
|
|
102
134
|
# Checks whether the line definitions form a valid language.
|
|
@@ -1,15 +1,12 @@
|
|
|
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
|
-
def prepare
|
|
11
|
-
end
|
|
12
|
-
|
|
13
10
|
def generate_random_ip
|
|
14
11
|
"#{rand(256)}.#{rand(256)}.#{rand(256)}.#{rand(256)}"
|
|
15
12
|
end
|
|
@@ -22,9 +19,7 @@ module RequestLogAnalyzer::Filter
|
|
|
22
19
|
value * ((75 + rand(50)) / 100.0)
|
|
23
20
|
end
|
|
24
21
|
|
|
25
|
-
def filter(request)
|
|
26
|
-
return nil unless request
|
|
27
|
-
|
|
22
|
+
def filter(request)
|
|
28
23
|
request.attributes.each do |key, value|
|
|
29
24
|
if key == :ip
|
|
30
25
|
request.attributes[key] = generate_random_ip
|
|
@@ -9,8 +9,13 @@ module RequestLogAnalyzer::Filter
|
|
|
9
9
|
|
|
10
10
|
attr_reader :field, :value, :mode
|
|
11
11
|
|
|
12
|
+
def initialize(file_format, options = {})
|
|
13
|
+
super(file_format, options)
|
|
14
|
+
setup_filter
|
|
15
|
+
end
|
|
16
|
+
|
|
12
17
|
# Setup mode, field and value.
|
|
13
|
-
def
|
|
18
|
+
def setup_filter
|
|
14
19
|
@mode = (@options[:mode] || :accept).to_sym
|
|
15
20
|
@field = @options[:field].to_sym
|
|
16
21
|
|
|
@@ -27,13 +32,9 @@ module RequestLogAnalyzer::Filter
|
|
|
27
32
|
# Returns nil otherwise.
|
|
28
33
|
# <tt>request</tt> Request Object
|
|
29
34
|
def filter(request)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
found_field = request.every(@field).any? { |value| @value === value }
|
|
33
|
-
|
|
35
|
+
found_field = request.every(@field).any? { |value| @value === value.to_s }
|
|
34
36
|
return nil if !found_field && @mode == :select
|
|
35
37
|
return nil if found_field && @mode == :reject
|
|
36
|
-
|
|
37
38
|
return request
|
|
38
39
|
end
|
|
39
40
|
end
|
|
@@ -8,9 +8,15 @@ module RequestLogAnalyzer::Filter
|
|
|
8
8
|
|
|
9
9
|
attr_reader :before, :after
|
|
10
10
|
|
|
11
|
+
def initialize(file_format, options = {})
|
|
12
|
+
super(file_format, options)
|
|
13
|
+
setup_filter
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
11
17
|
# Convert the timestamp to the correct formats for quick timestamp comparisons.
|
|
12
18
|
# These are stored in the before and after attr_reader fields.
|
|
13
|
-
def
|
|
19
|
+
def setup_filter
|
|
14
20
|
@after = @options[:after].strftime('%Y%m%d%H%M%S').to_i if options[:after]
|
|
15
21
|
@before = @options[:before].strftime('%Y%m%d%H%M%S').to_i if options[:before]
|
|
16
22
|
end
|
|
@@ -22,8 +28,6 @@ module RequestLogAnalyzer::Filter
|
|
|
22
28
|
# Returns nil otherwise
|
|
23
29
|
# <tt>request</tt> Request object.
|
|
24
30
|
def filter(request)
|
|
25
|
-
return nil unless request
|
|
26
|
-
|
|
27
31
|
if @after && @before && request.timestamp <= @before && @after <= request.timestamp
|
|
28
32
|
return request
|
|
29
33
|
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
|
|
@@ -24,6 +12,10 @@ module RequestLogAnalyzer
|
|
|
24
12
|
def initialize
|
|
25
13
|
@line_definitions = {}
|
|
26
14
|
end
|
|
15
|
+
|
|
16
|
+
def initialize_copy(other)
|
|
17
|
+
@line_definitions = other.line_definitions.dup
|
|
18
|
+
end
|
|
27
19
|
|
|
28
20
|
def method_missing(name, *args, &block)
|
|
29
21
|
if block_given?
|
|
@@ -45,27 +37,13 @@ module RequestLogAnalyzer
|
|
|
45
37
|
@captures = []
|
|
46
38
|
definition.each { |key, value| self.send("#{key.to_s}=".to_sym, value) }
|
|
47
39
|
end
|
|
48
|
-
|
|
40
|
+
|
|
49
41
|
def self.define(name, &block)
|
|
50
42
|
definition = self.new(name)
|
|
51
43
|
yield(definition) if block_given?
|
|
52
44
|
return definition
|
|
53
45
|
end
|
|
54
46
|
|
|
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
47
|
# Checks whether a given line matches this definition.
|
|
70
48
|
# It will return false if a line does not match. If the line matches, a hash is returned
|
|
71
49
|
# with all the fields parsed from that line as content.
|
|
@@ -74,17 +52,7 @@ module RequestLogAnalyzer
|
|
|
74
52
|
def matches(line, lineno = nil, parser = nil)
|
|
75
53
|
if @teaser.nil? || @teaser =~ line
|
|
76
54
|
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
|
|
55
|
+
return { :line_definition => self, :lineno => lineno, :captures => match_data.captures}
|
|
88
56
|
else
|
|
89
57
|
if @teaser && parser
|
|
90
58
|
parser.warn(:teaser_check_failed, "Teaser matched for #{name.inspect}, but full line did not:\n#{line.inspect}")
|
|
@@ -97,63 +65,29 @@ module RequestLogAnalyzer
|
|
|
97
65
|
end
|
|
98
66
|
|
|
99
67
|
alias :=~ :matches
|
|
100
|
-
|
|
101
|
-
def
|
|
102
|
-
if
|
|
103
|
-
|
|
68
|
+
|
|
69
|
+
def match_for(line, request, lineno = nil, parser = nil)
|
|
70
|
+
if match_info = matches(line, lineno, parser)
|
|
71
|
+
convert_captured_values(match_info[:captures], request)
|
|
104
72
|
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
|
|
73
|
+
false
|
|
114
74
|
end
|
|
115
75
|
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
76
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
77
|
+
def convert_captured_values(values, request)
|
|
78
|
+
value_hash = {}
|
|
79
|
+
captures.each_with_index do |capture, index|
|
|
80
|
+
converted = request.convert_value(values[index], capture)
|
|
81
|
+
value_hash[capture[:name]] ||= converted
|
|
82
|
+
if converted.kind_of?(Hash) && capture[:provides].kind_of?(Hash)
|
|
83
|
+
capture[:provides].each do |name, type|
|
|
84
|
+
value_hash[name] ||= request.convert_value(converted[name], { :type => type })
|
|
85
|
+
end
|
|
152
86
|
end
|
|
153
|
-
else
|
|
154
|
-
nil
|
|
155
87
|
end
|
|
88
|
+
return value_hash
|
|
156
89
|
end
|
|
90
|
+
|
|
157
91
|
end
|
|
158
92
|
|
|
159
93
|
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.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
# coding: utf-8
|
|
1
2
|
module RequestLogAnalyzer::Output
|
|
2
3
|
|
|
4
|
+
# Fixed Width output class.
|
|
5
|
+
# Outputs a fixed width ASCII or UF8 report.
|
|
3
6
|
class FixedWidth < Base
|
|
4
7
|
|
|
5
8
|
module Monochrome
|
|
@@ -126,8 +129,12 @@ module RequestLogAnalyzer::Output
|
|
|
126
129
|
end
|
|
127
130
|
|
|
128
131
|
if column_widths.include?(nil)
|
|
132
|
+
fill_column = columns[column_widths.index(nil)]
|
|
129
133
|
width_left = options[:width] - ((columns.length - 1) * (style[:cell_separator] ? 3 : 1)) - column_widths.compact.inject(0) { |sum, col| sum + col}
|
|
130
|
-
column_widths[column_widths.index(nil)] =
|
|
134
|
+
column_widths[column_widths.index(nil)] = case fill_column[:type]
|
|
135
|
+
when :ratio; width_left # max out
|
|
136
|
+
else; [width_left, fill_column[:actual_width]].min
|
|
137
|
+
end
|
|
131
138
|
end
|
|
132
139
|
|
|
133
140
|
# Print table header
|
|
@@ -156,14 +163,18 @@ module RequestLogAnalyzer::Output
|
|
|
156
163
|
bar << colorize(characters[:block] * (width.to_f * (row[index].to_f - column[:treshold])).round, :red)
|
|
157
164
|
row_values.push(bar)
|
|
158
165
|
else
|
|
166
|
+
# Create a bar by combining block characters
|
|
159
167
|
row_values.push(characters[:block] * (width.to_f * row[index].to_f).round)
|
|
160
168
|
end
|
|
161
169
|
else
|
|
170
|
+
# Too few characters for a ratio bar. Display nothing
|
|
162
171
|
row_values.push('')
|
|
163
172
|
end
|
|
164
173
|
else
|
|
165
|
-
alignment = (columns[index][:align] == :right ? '' : '-')
|
|
166
|
-
|
|
174
|
+
alignment = (columns[index][:align] == :right ? '' : '-')
|
|
175
|
+
cell_value = "%#{alignment}#{width}s" % row[index].to_s[0...width]
|
|
176
|
+
cell_value = colorize(cell_value, :bold, :brown) if columns[index][:highlight]
|
|
177
|
+
row_values.push(cell_value)
|
|
167
178
|
end
|
|
168
179
|
end
|
|
169
180
|
puts row_values.join(style[:cell_separator] ? " #{characters[:vertical_line]} " : ' ')
|