request-log-analyzer 1.3.4 → 1.4.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 (86) hide show
  1. data/LICENSE +3 -3
  2. data/README.rdoc +1 -1
  3. data/bin/request-log-analyzer +17 -14
  4. data/lib/cli/command_line_arguments.rb +51 -51
  5. data/lib/cli/database_console.rb +3 -3
  6. data/lib/cli/database_console_init.rb +4 -3
  7. data/lib/cli/progressbar.rb +10 -10
  8. data/lib/cli/tools.rb +3 -3
  9. data/lib/request_log_analyzer/aggregator/database_inserter.rb +19 -17
  10. data/lib/request_log_analyzer/aggregator/echo.rb +14 -9
  11. data/lib/request_log_analyzer/aggregator/summarizer.rb +26 -26
  12. data/lib/request_log_analyzer/aggregator.rb +11 -15
  13. data/lib/request_log_analyzer/controller.rb +162 -89
  14. data/lib/request_log_analyzer/database/base.rb +22 -21
  15. data/lib/request_log_analyzer/database/connection.rb +3 -3
  16. data/lib/request_log_analyzer/database/request.rb +22 -0
  17. data/lib/request_log_analyzer/database/source.rb +13 -0
  18. data/lib/request_log_analyzer/database/warning.rb +14 -0
  19. data/lib/request_log_analyzer/database.rb +28 -103
  20. data/lib/request_log_analyzer/file_format/amazon_s3.rb +20 -20
  21. data/lib/request_log_analyzer/file_format/apache.rb +37 -30
  22. data/lib/request_log_analyzer/file_format/merb.rb +30 -13
  23. data/lib/request_log_analyzer/file_format/rack.rb +11 -0
  24. data/lib/request_log_analyzer/file_format/rails.rb +143 -73
  25. data/lib/request_log_analyzer/file_format/rails_development.rb +4 -49
  26. data/lib/request_log_analyzer/file_format.rb +16 -28
  27. data/lib/request_log_analyzer/filter/anonymize.rb +8 -7
  28. data/lib/request_log_analyzer/filter/field.rb +9 -9
  29. data/lib/request_log_analyzer/filter/timespan.rb +12 -10
  30. data/lib/request_log_analyzer/filter.rb +12 -16
  31. data/lib/request_log_analyzer/line_definition.rb +15 -14
  32. data/lib/request_log_analyzer/log_processor.rb +27 -29
  33. data/lib/request_log_analyzer/mailer.rb +15 -9
  34. data/lib/request_log_analyzer/output/fixed_width.rb +48 -48
  35. data/lib/request_log_analyzer/output/html.rb +20 -20
  36. data/lib/request_log_analyzer/output.rb +53 -12
  37. data/lib/request_log_analyzer/request.rb +88 -69
  38. data/lib/request_log_analyzer/source/database_loader.rb +10 -14
  39. data/lib/request_log_analyzer/source/log_parser.rb +57 -50
  40. data/lib/request_log_analyzer/source.rb +10 -12
  41. data/lib/request_log_analyzer/tracker/duration.rb +41 -134
  42. data/lib/request_log_analyzer/tracker/frequency.rb +31 -32
  43. data/lib/request_log_analyzer/tracker/hourly_spread.rb +21 -21
  44. data/lib/request_log_analyzer/tracker/timespan.rb +17 -17
  45. data/lib/request_log_analyzer/tracker/traffic.rb +106 -0
  46. data/lib/request_log_analyzer/tracker.rb +139 -32
  47. data/lib/request_log_analyzer.rb +5 -5
  48. data/request-log-analyzer.gemspec +19 -15
  49. data/spec/database.yml +6 -0
  50. data/spec/fixtures/rails_22.log +1 -1
  51. data/spec/integration/command_line_usage_spec.rb +7 -1
  52. data/spec/lib/helpers.rb +7 -7
  53. data/spec/lib/macros.rb +3 -3
  54. data/spec/lib/matchers.rb +41 -27
  55. data/spec/lib/mocks.rb +19 -15
  56. data/spec/lib/testing_format.rb +9 -9
  57. data/spec/spec_helper.rb +6 -6
  58. data/spec/unit/aggregator/database_inserter_spec.rb +16 -16
  59. data/spec/unit/aggregator/summarizer_spec.rb +4 -4
  60. data/spec/unit/controller/controller_spec.rb +2 -2
  61. data/spec/unit/controller/log_processor_spec.rb +1 -1
  62. data/spec/unit/database/base_class_spec.rb +26 -33
  63. data/spec/unit/database/connection_spec.rb +3 -3
  64. data/spec/unit/database/database_spec.rb +33 -38
  65. data/spec/unit/file_format/amazon_s3_format_spec.rb +5 -5
  66. data/spec/unit/file_format/apache_format_spec.rb +48 -11
  67. data/spec/unit/file_format/file_format_api_spec.rb +13 -13
  68. data/spec/unit/file_format/line_definition_spec.rb +24 -17
  69. data/spec/unit/file_format/merb_format_spec.rb +41 -45
  70. data/spec/unit/file_format/rails_format_spec.rb +157 -117
  71. data/spec/unit/filter/anonymize_filter_spec.rb +2 -2
  72. data/spec/unit/filter/field_filter_spec.rb +13 -13
  73. data/spec/unit/filter/filter_spec.rb +1 -1
  74. data/spec/unit/filter/timespan_filter_spec.rb +15 -15
  75. data/spec/unit/mailer_spec.rb +30 -0
  76. data/spec/unit/{source/request_spec.rb → request_spec.rb} +30 -30
  77. data/spec/unit/source/log_parser_spec.rb +27 -27
  78. data/spec/unit/tracker/duration_tracker_spec.rb +115 -78
  79. data/spec/unit/tracker/frequency_tracker_spec.rb +74 -63
  80. data/spec/unit/tracker/hourly_spread_spec.rb +28 -20
  81. data/spec/unit/tracker/timespan_tracker_spec.rb +25 -13
  82. data/spec/unit/tracker/tracker_api_spec.rb +117 -42
  83. data/spec/unit/tracker/traffic_tracker_spec.rb +107 -0
  84. data/tasks/github-gem.rake +125 -75
  85. data/tasks/request_log_analyzer.rake +2 -2
  86. metadata +19 -10
@@ -5,13 +5,49 @@ module RequestLogAnalyzer::Output
5
5
  def self.const_missing(const)
6
6
  RequestLogAnalyzer::load_default_class_file(self, const)
7
7
  end
8
+
9
+ # Loads a Output::Base subclass instance.
10
+ def self.load(file_format, *args)
11
+
12
+ klass = nil
13
+ if file_format.kind_of?(RequestLogAnalyzer::Output::Base)
14
+ # this already is a file format! return itself
15
+ return file_format
16
+
17
+ elsif file_format.kind_of?(Class) && file_format.ancestors.include?(RequestLogAnalyzer::Output::Base)
18
+ # a usable class is provided. Use this format class.
19
+ klass = file_format
20
+
21
+ elsif file_format.kind_of?(String) && File.exist?(file_format)
22
+ # load a format from a ruby file
23
+ require file_format
24
+ const = RequestLogAnalyzer::to_camelcase(File.basename(file_format, '.rb'))
25
+ if RequestLogAnalyzer::FileFormat.const_defined?(const)
26
+ klass = RequestLogAnalyzer::Output.const_get(const)
27
+ elsif Object.const_defined?(const)
28
+ klass = Object.const_get(const)
29
+ else
30
+ raise "Cannot load class #{const} from #{file_format}!"
31
+ end
32
+
33
+ else
34
+ # load a provided file format
35
+ klass = RequestLogAnalyzer::Output.const_get(RequestLogAnalyzer::to_camelcase(file_format))
36
+ end
37
+
38
+ # check the returned klass to see if it can be used
39
+ raise "Could not load a file format from #{file_format.inspect}" if klass.nil?
40
+ raise "Invalid FileFormat class" unless klass.kind_of?(Class) && klass.ancestors.include?(RequestLogAnalyzer::Output::Base)
41
+
42
+ klass.create(*args) # return an instance of the class
43
+ end
8
44
 
9
45
  # Base Class used for generating output for reports.
10
46
  # All output should inherit fromt this class.
11
47
  class Base
12
-
48
+
13
49
  attr_accessor :io, :options, :style
14
-
50
+
15
51
  # Initialize a report
16
52
  # <tt>io</tt> iO Object (file, STDOUT, etc.)
17
53
  # <tt>options</tt> Specific style options
@@ -27,32 +63,37 @@ module RequestLogAnalyzer::Output
27
63
  @style = @style.merge(temp_style)
28
64
  yield(self) if block_given?
29
65
  @style = old_style
30
- end
31
-
66
+ end
67
+
32
68
  # Generate a header for a report
33
69
  def header
34
70
  end
35
-
36
- # Generate the footer of a report
71
+
72
+ # Generate the footer of a report
37
73
  def footer
38
74
  end
39
75
 
76
+ def slice_results(array)
77
+ return array if options[:amount] == :all
78
+ return array.slice(0, options[:amount]) # otherwise
79
+ end
80
+
40
81
  # Generate a report table and push it into the output object.
41
82
  # Yeilds a rows array into which the rows can be pushed
42
83
  # <tt>*colums<tt> Array of Column hashes (see Column options).
43
84
  # <tt>&block</tt>: A block yeilding the rows.
44
- #
85
+ #
45
86
  # === Column options
46
87
  # Columns is an array of hashes containing the column definitions.
47
88
  # * <tt>:align</tt> Alignment :left or :right
48
89
  # * <tt>:treshold</tt> Width in characters or :rest
49
90
  # * <tt>:type</tt> :ratio or nil
50
91
  # * <tt>:width</tt> Width in characters or :rest
51
- #
92
+ #
52
93
  # === Example
53
94
  # The output object should support table definitions:
54
95
  #
55
- # output.table({:align => :left}, {:align => :right }, {:align => :right}, {:type => :ratio, :width => :rest}) do |rows|
96
+ # output.table({:align => :left}, {:align => :right }, {:align => :right}, {:type => :ratio, :width => :rest}) do |rows|
56
97
  # sorted_frequencies.each do |(cat, count)|
57
98
  # rows << [cat, "#{count} hits", '%0.1f%%' % ((count.to_f / total_hits.to_f) * 100.0), (count.to_f / total_hits.to_f)]
58
99
  # end
@@ -60,13 +101,13 @@ module RequestLogAnalyzer::Output
60
101
  #
61
102
  def table(*columns, &block)
62
103
  end
63
-
104
+
64
105
  protected
65
106
  # Check if a given table defination hash includes a header (title)
66
107
  # <tt>columns</tt> The columns hash
67
108
  def table_has_header?(columns)
68
- columns.any? { |column| !column[:title].nil? }
109
+ columns.any? { |column| !column[:title].nil? }
69
110
  end
70
-
111
+
71
112
  end
72
113
  end
@@ -1,71 +1,85 @@
1
1
  module RequestLogAnalyzer
2
-
3
- # The Request class represents a parsed request from the log file.
2
+
3
+ # The Request class represents a parsed request from the log file.
4
4
  # Instances are created by the LogParser and are passed to the different aggregators, so they
5
- # can do their aggregating work.
5
+ # can do their aggregating work.
6
6
  #
7
7
  # This class provides several methods to access the data that was parsed from the log files.
8
8
  # Request#first(field_name) returns the first (only) value corresponding to the given field
9
9
  # Request#every(field_name) returns all values corresponding to the given field name as array.
10
10
  class Request
11
-
11
+
12
12
  module Converters
13
-
13
+
14
+ # Default converter function, which converts the parsed strings to a native Ruby type
15
+ # using the type indication in the line definition. It will use a custom connverter
16
+ # method if one is available.
14
17
  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
18
+ return capture_definition[:default] if value.nil?
19
+ custom_converter_method = :"convert_#{capture_definition[:type]}"
20
+ send(custom_converter_method, value, capture_definition)
31
21
  end
32
-
22
+
23
+ def convert_string(value, capture_definition); value; end
24
+ def convert_float(value, capture_definition); value.to_f; end
25
+ def convert_decimal(value, capture_definition); value.to_f; end
26
+ def convert_int(value, capture_definition); value.to_i; end
27
+ def convert_integer(value, capture_definition); value.to_i; end
28
+ def convert_sym(value, capture_definition); value.to_sym; end
29
+ def convert_symbol(value, capture_definition); value.to_sym; end
30
+
31
+ # Converts :eval field, which should evaluate to a hash.
33
32
  def convert_eval(value, capture_definition)
34
- eval(value).inject({}) { |h, (k, v)| h[k.to_sym] = v; h}
33
+ eval(value).inject({}) { |h, (k, v)| h[k.to_sym] = v; h}
35
34
  rescue SyntaxError
36
35
  nil
37
36
  end
38
-
39
- # Slow default method to parse timestamps
37
+
38
+ # Slow default method to parse timestamps.
39
+ # Reimplement this function in a file format specific Request class
40
+ # to improve the timestamp parsing speed.
40
41
  def convert_timestamp(value, capture_definition)
41
- DateTime.parse(value).strftime('%Y%m%d%H%M%S').to_i unless value.nil?
42
+ DateTime.parse(value).strftime('%Y%m%d%H%M%S').to_i
43
+ end
44
+
45
+ # Converts traffic fields to (whole) bytes based on the given unit.
46
+ def convert_traffic(value, capture_definition)
47
+ case capture_definition[:unit]
48
+ when nil, :b, :B, :byte then value.to_i
49
+ when :GB, :G, :gigabyte then (value.to_f * 1000_000_000).round
50
+ when :GiB, :gibibyte then (value.to_f * (2 ** 30)).round
51
+ when :MB, :M, :megabyte then (value.to_f * 1000_000).round
52
+ when :MiB, :mebibyte then (value.to_f * (2 ** 20)).round
53
+ when :KB, :K, :kilobyte, :kB then (value.to_f * 1000).round
54
+ when :KiB, :kibibyte then (value.to_f * (2 ** 10)).round
55
+ else raise "Unknown traffic unit"
56
+ end
42
57
  end
43
-
58
+
59
+ # Convert duration fields to float, and make sure the values are in seconds.
44
60
  def convert_duration(value, capture_definition)
45
- if value.nil?
46
- nil
47
- elsif capture_definition[:unit] == :msec
48
- value.to_f / 1000.0
49
- else
50
- value.to_f
61
+ case capture_definition[:unit]
62
+ when nil, :sec, :s then value.to_f
63
+ when :microsec, :musec then value.to_f / 1000000.0
64
+ when :msec, :millisec then value.to_f / 1000.0
65
+ else raise "Unknown duration unit"
51
66
  end
52
67
  end
53
68
  end
54
-
55
- include RequestLogAnalyzer::FileFormat::Awareness
69
+
70
+ # Install the default converter methods
56
71
  include Converters
57
-
58
- attr_reader :lines
59
- attr_reader :attributes
60
-
61
- # Initializes a new Request object.
72
+
73
+ attr_reader :lines, :attributes, :file_format
74
+
75
+ # Initializes a new Request object.
62
76
  # It will apply the the provided FileFormat module to this instance.
63
77
  def initialize(file_format, attributes = {})
64
- @lines = []
65
- @attributes = attributes
66
- register_file_format(file_format)
78
+ @lines = []
79
+ @attributes = attributes
80
+ @file_format = file_format
67
81
  end
68
-
82
+
69
83
  # Creates a new request that was parsed from the log with the given FileFormat. The hashes
70
84
  # that are passed to this function are added as lines to this request.
71
85
  def self.create(file_format, *hashes)
@@ -73,9 +87,11 @@ module RequestLogAnalyzer
73
87
  hashes.flatten.each { |hash| request << hash }
74
88
  return request
75
89
  end
76
-
77
- # Adds another line to the request.
78
- # The line should be provides as a hash of the fields parsed from the line.
90
+
91
+ # Adds another line to the request when it is parsed in the LogParser.
92
+ #
93
+ # The line should be provided as a hash with the attributes line_definition, :captures,
94
+ # :lineno and :source set. This function is called from LogParser.
79
95
  def add_parsed_line (parsed_line)
80
96
  value_hash = parsed_line[:line_definition].convert_captured_values(parsed_line[:captures], self)
81
97
  value_hash[:line_type] = parsed_line[:line_definition].name
@@ -83,72 +99,75 @@ module RequestLogAnalyzer
83
99
  value_hash[:source] = parsed_line[:source]
84
100
  add_line_hash(value_hash)
85
101
  end
86
-
102
+
103
+ # Adds another line to the request using a plain hash.
104
+ #
105
+ # The line should be provides as a hash of the fields parsed from the line.
87
106
  def add_line_hash(value_hash)
88
107
  @lines << value_hash
89
108
  @attributes = value_hash.merge(@attributes)
90
109
  end
91
-
92
-
110
+
111
+ # Adds another line to the request. This method switches automatically between
112
+ # the add_line_hash and add_parsed_line based on the keys of the provided hash.
93
113
  def <<(hash)
94
114
  hash[:line_definition] ? add_parsed_line(hash) : add_line_hash(hash)
95
115
  end
96
-
116
+
97
117
  # Checks whether the given line type was parsed from the log file for this request
98
118
  def has_line_type?(line_type)
99
119
  return true if @lines.length == 1 && @lines[0][:line_type] == line_type.to_sym
100
-
101
120
  @lines.detect { |l| l[:line_type] == line_type.to_sym }
102
121
  end
103
-
122
+
104
123
  alias :=~ :has_line_type?
105
-
124
+
106
125
  # Returns the value that was captured for the "field" of this request.
107
126
  # This function will return the first value that was captured if the field
108
127
  # was captured in multiple lines
109
128
  def first(field)
110
129
  @attributes[field]
111
130
  end
112
-
131
+
113
132
  alias :[] :first
114
-
133
+
115
134
  # Returns an array of all the "field" values that were captured for this request
116
135
  def every(field)
117
136
  @lines.inject([]) { |result, fields| result << fields[field] if fields.has_key?(field); result }
118
- end
119
-
120
- # Returns true if this request does not yet contain any parsed lines. This should only occur
137
+ end
138
+
139
+ # Returns true if this request does not yet contain any parsed lines. This should only occur
121
140
  # during parsing. An empty request should never be sent to the aggregators
122
141
  def empty?
123
142
  @lines.length == 0
124
143
  end
125
-
144
+
126
145
  # Checks whether this request is completed. A completed request contains both a parsed header
127
- # line and a parsed footer line. Not that calling this function in single line mode will always
146
+ # line and a parsed footer line. Not that calling this function in single line mode will always
128
147
  # return false.
129
148
  def completed?
130
149
  header_found, footer_found = false, false
131
- @lines.each do |line|
150
+ @lines.each do |line|
132
151
  line_def = file_format.line_definitions[line[:line_type]]
133
152
  header_found = true if line_def.header
134
- footer_found = true if line_def.footer
153
+ footer_found = true if line_def.footer
135
154
  end
136
- header_found && footer_found
155
+ header_found && footer_found
137
156
  end
138
-
157
+
139
158
  # This function is called before a Requests is yielded.
140
159
  def validate
141
160
  end
142
-
143
- # Returns the first timestamp encountered in a request.
161
+
162
+ # Returns the first timestamp encountered in a request.
144
163
  def timestamp
145
164
  first(:timestamp)
146
165
  end
147
-
166
+
148
167
  def first_lineno
149
168
  @lines.map { |line| line[:lineno] }.reject { |v| v.nil? }.min
150
169
  end
151
-
170
+
152
171
  def last_lineno
153
172
  @lines.map { |line| line[:lineno] }.reject { |v| v.nil? }.max
154
173
  end
@@ -2,7 +2,7 @@ require 'rubygems'
2
2
  require 'activerecord'
3
3
 
4
4
  module RequestLogAnalyzer::Source
5
-
5
+
6
6
  # Active Resource hook
7
7
  class Request < ActiveRecord::Base
8
8
  has_many :completed_lines
@@ -26,8 +26,7 @@ module RequestLogAnalyzer::Source
26
26
  # The Database class gets log data from the database.
27
27
  class DatabaseLoader < Base
28
28
 
29
- attr_reader :source_files
30
- attr_reader :requests
29
+ attr_reader :source_files, :file_format, :requests
31
30
 
32
31
  # Initializes the log file parser instance.
33
32
  # It will apply the language specific FileFormat module to this instance. It will use the line
@@ -35,16 +34,13 @@ module RequestLogAnalyzer::Source
35
34
  #
36
35
  # <tt>format</tt>:: The current file format instance
37
36
  # <tt>options</tt>:: A hash of options that are used by the parser
38
- def initialize(format, options = {})
39
- @line_definitions = {}
40
- @options = options
37
+ def initialize(format, options = {})
38
+ super(format, options)
41
39
  @source_files = options[:source_files]
42
40
  @parsed_requests = 0
43
41
  @requests = []
44
-
45
- self.register_file_format(format)
46
42
  end
47
-
43
+
48
44
  # Reads the input, which can either be a file, sequence of files or STDIN to parse
49
45
  # lines specified in the FileFormat. This lines will be combined into Request instances,
50
46
  # that will be yielded. The actual parsing occurs in the parse_io method.
@@ -56,10 +52,10 @@ module RequestLogAnalyzer::Source
56
52
  RequestLogAnalyzer::Source::Request.find(:all).each do |request|
57
53
  @parsed_requests += 1
58
54
  @progress_handler.call(:progress, @parsed_requests) if @progress_handler
59
-
55
+
60
56
  yield request.convert(self.file_format)
61
57
  end
62
-
58
+
63
59
  @progress_handler.call(:finished, @source_files) if @progress_handler
64
60
  end
65
61
 
@@ -70,15 +66,15 @@ module RequestLogAnalyzer::Source
70
66
  end
71
67
 
72
68
  # Add a block to this method to install a warning handler while parsing,
73
- # <tt>proc</tt>:: The proc that will be called to handle parse warning messages
69
+ # <tt>proc</tt>:: The proc that will be called to handle parse warning messages
74
70
  def warning=(proc)
75
71
  @warning_handler = proc
76
72
  end
77
73
 
78
74
  # This method is called by the parser if it encounteres any parsing problems.
79
- # It will call the installed warning handler if any.
75
+ # It will call the installed warning handler if any.
80
76
  #
81
- # By default, RequestLogAnalyzer::Controller will install a warning handler
77
+ # By default, RequestLogAnalyzer::Controller will install a warning handler
82
78
  # that will pass the warnings to each aggregator so they can do something useful
83
79
  # with it.
84
80
  #