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.
Files changed (62) hide show
  1. data/DESIGN +24 -10
  2. data/README.rdoc +1 -1
  3. data/Rakefile +1 -2
  4. data/bin/request-log-analyzer +4 -38
  5. data/lib/cli/progressbar.rb +2 -19
  6. data/lib/cli/tools.rb +46 -0
  7. data/lib/request_log_analyzer/aggregator/database.rb +33 -8
  8. data/lib/request_log_analyzer/aggregator/echo.rb +1 -0
  9. data/lib/request_log_analyzer/aggregator/summarizer.rb +22 -16
  10. data/lib/request_log_analyzer/controller.rb +21 -5
  11. data/lib/request_log_analyzer/file_format/merb.rb +22 -9
  12. data/lib/request_log_analyzer/file_format/rails.rb +37 -23
  13. data/lib/request_log_analyzer/file_format/rails_development.rb +33 -88
  14. data/lib/request_log_analyzer/file_format.rb +50 -18
  15. data/lib/request_log_analyzer/filter/{anonimize.rb → anonymize.rb} +3 -8
  16. data/lib/request_log_analyzer/filter/field.rb +7 -6
  17. data/lib/request_log_analyzer/filter/timespan.rb +7 -3
  18. data/lib/request_log_analyzer/filter.rb +0 -4
  19. data/lib/request_log_analyzer/line_definition.rb +22 -88
  20. data/lib/request_log_analyzer/log_processor.rb +5 -25
  21. data/lib/request_log_analyzer/output/fixed_width.rb +14 -3
  22. data/lib/request_log_analyzer/output/html.rb +1 -0
  23. data/lib/request_log_analyzer/request.rb +62 -4
  24. data/lib/request_log_analyzer/source/database.rb +76 -0
  25. data/lib/request_log_analyzer/source/log_parser.rb +104 -52
  26. data/lib/request_log_analyzer/source.rb +28 -6
  27. data/lib/request_log_analyzer/tracker/duration.rb +90 -15
  28. data/lib/request_log_analyzer/tracker/{category.rb → frequency.rb} +24 -12
  29. data/lib/request_log_analyzer/tracker/hourly_spread.rb +20 -9
  30. data/lib/request_log_analyzer/tracker/timespan.rb +15 -8
  31. data/lib/request_log_analyzer.rb +29 -16
  32. data/spec/integration/command_line_usage_spec.rb +71 -0
  33. data/spec/lib/helper.rb +33 -0
  34. data/spec/lib/mocks.rb +47 -0
  35. data/spec/lib/testing_format.rb +39 -0
  36. data/spec/spec_helper.rb +5 -25
  37. data/spec/{database_inserter_spec.rb → unit/aggregator/database_inserter_spec.rb} +40 -37
  38. data/spec/unit/aggregator/summarizer_spec.rb +28 -0
  39. data/spec/unit/controller/controller_spec.rb +43 -0
  40. data/spec/unit/controller/log_processor_spec.rb +20 -0
  41. data/spec/{file_format_spec.rb → unit/file_format/file_format_api_spec.rb} +18 -6
  42. data/spec/unit/file_format/line_definition_spec.rb +64 -0
  43. data/spec/{merb_format_spec.rb → unit/file_format/merb_format_spec.rb} +3 -3
  44. data/spec/{rails_format_spec.rb → unit/file_format/rails_format_spec.rb} +24 -15
  45. data/spec/unit/filter/anonymize_filter_spec.rb +22 -0
  46. data/spec/unit/filter/field_filter_spec.rb +69 -0
  47. data/spec/unit/filter/timespan_filter_spec.rb +61 -0
  48. data/spec/{log_parser_spec.rb → unit/source/log_parser_spec.rb} +9 -10
  49. data/spec/{request_spec.rb → unit/source/request_spec.rb} +5 -5
  50. data/spec/unit/tracker/duration_tracker_spec.rb +99 -0
  51. data/spec/unit/tracker/frequency_tracker_spec.rb +83 -0
  52. data/spec/unit/tracker/hourly_spread_spec.rb +75 -0
  53. data/spec/unit/tracker/timespan_tracker_spec.rb +65 -0
  54. data/spec/unit/tracker/tracker_api_test.rb +45 -0
  55. data/tasks/rspec.rake +12 -0
  56. metadata +56 -28
  57. data/spec/controller_spec.rb +0 -45
  58. data/spec/file_formats/spec_format.rb +0 -27
  59. data/spec/filter_spec.rb +0 -157
  60. data/spec/line_definition_spec.rb +0 -124
  61. data/spec/log_processor_spec.rb +0 -57
  62. data/spec/summarizer_spec.rb +0 -9
@@ -1,112 +1,57 @@
1
1
  module RequestLogAnalyzer::FileFormat
2
2
 
3
- class RailsDevelopment < Base
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
- # Processing EmployeeController#index (for 123.123.123.123 at 2008-07-13 06:00:00) [GET]
6
- line_definition :processing do |line|
7
- line.header = true # this line is the first log line for a request
8
- line.teaser = /Processing /
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
- 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
  # User Load (0.4ms) SELECT * FROM `users` WHERE (`users`.`id` = 18205844) 
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)?(.+) (?:\e\[0m)?/
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 => :string }
26
+ << { :name => :query_duration, :type => :duration, :unit => :msec } \
27
+ << { :name => :query_sql, :type => :sql }
37
28
  end
38
29
 
39
30
  # CACHE (0.0ms) SELECT * FROM `users` WHERE (`users`.`id` = 0) 
40
31
  line_definition :query_cached do |line|
41
- line.teaser = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?(.+) (?:\e\[0m)?/
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 } \
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
- # Rails < 2.1 completed line example
60
- # Completed in 0.21665 (4 reqs/sec) | Rendering: 0.00926 (4%) | DB: 0.00000 (0%) | 200 OK [http://demo.nu/employees]
61
- RAILS_21_COMPLETED = /Completed in (\d+\.\d{5}) \(\d+ reqs\/sec\) (?:\| Rendering: (\d+\.\d{5}) \(\d+\%\) )?(?:\| DB: (\d+\.\d{5}) \(\d+\%\) )?\| (\d\d\d).+\[(http.+)\]/
62
-
63
- # Rails > 2.1 completed line example
64
- # Completed in 614ms (View: 120, DB: 31) | 200 OK [http://floorplanner.local/demo]
65
- RAILS_22_COMPLETED = /Completed in (\d+)ms \((?:View: (\d+), )?DB: (\d+)\) \| (\d\d\d).+\[(http.+)\]/
66
-
67
- # The completed line uses a kind of hack to ensure that both old style logs and new style logs
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
- analyze.category :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
106
- :if => lambda { |request| request[:duration] && request[:duration] > 1.0 }, :amount => 20
107
-
108
- analyze.hourly_spread :line_type => :processing
109
- analyze.category :error, :title => 'Failed requests', :line_type => :failed, :amount => 20
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
- klass = Object.const_get(RequestLogAnalyzer::to_camelcase(File.basename(file_format, '.rb')))
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
- raise if klass.nil?
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
- subclass.instance_variable_set(:@line_definer, RequestLogAnalyzer::LineDefinition::Definer.new)
60
- subclass.class_eval { class << self; attr_accessor :line_definer; end }
61
- subclass.class_eval { class << self; attr_accessor :report_definer; end }
62
-
63
- unless subclass.const_defined?('Request')
64
- subclass.const_set('Request', Class.new(RequestLogAnalyzer::Request))
65
- end
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 create_request(*hashes)
74
- self.class::Request.create(self, *hashes)
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(@line_definer)
109
+ yield self.line_definer
81
110
  else
82
- return @line_definer
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
- @report_definer = RequestLogAnalyzer::Aggregator::Summarizer::Definer.new
89
- yield(@report_definer)
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.instance_variable_get(:@report_definer).trackers rescue []
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 select or reject a specific field
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 Anonimize < Base
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 prepare
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
- return nil unless request
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 prepare
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
@@ -23,10 +23,6 @@ module RequestLogAnalyzer::Filter
23
23
  register_file_format(format)
24
24
  end
25
25
 
26
- # Initialize the filter
27
- def prepare
28
- end
29
-
30
26
  # Return the request if the request should be kept.
31
27
  # Return nil otherwise.
32
28
  def filter(request)
@@ -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
- request_info = { :line_type => name, :lineno => lineno }
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 anonymize_value(value, capture_definition)
102
- if capture_definition[:anonymize].respond_to?(:call)
103
- capture_definition[:anonymize].call(value, capture_definition)
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
- case capture_definition[:anonymize]
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
- # Anonymize a log line
136
- def anonymize(line, options = {})
137
- if self.teaser.nil? || self.teaser =~ line
138
- if self.regexp =~ line
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
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, two processors are supported, :strip and :anonymize.
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 can either be
25
- # :strip or :anonymize. This will set the processing mode.
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 (:anonymize or :strip)
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 (:strip, :anonymize)
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)] = width_left
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
- row_values.push("%#{alignment}#{width}s" % row[index].to_s[0...width])
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]} " : ' ')
@@ -1,5 +1,6 @@
1
1
  module RequestLogAnalyzer::Output
2
2
 
3
+ # HTML Output class. Generated a HTML-formatted report, including CSS.
3
4
  class HTML < Base
4
5
 
5
6
  # def initialize(io, options = {})