request-log-analyzer 1.0.2 → 1.6.3

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 (144) hide show
  1. data/.gitignore +10 -0
  2. data/DESIGN.rdoc +41 -0
  3. data/LICENSE +4 -4
  4. data/README.rdoc +38 -0
  5. data/Rakefile +6 -3
  6. data/bin/request-log-analyzer +70 -72
  7. data/lib/cli/command_line_arguments.rb +53 -53
  8. data/lib/cli/database_console.rb +26 -0
  9. data/lib/cli/database_console_init.rb +43 -0
  10. data/lib/cli/progressbar.rb +166 -189
  11. data/lib/cli/tools.rb +49 -0
  12. data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
  13. data/lib/request_log_analyzer/aggregator/echo.rb +17 -12
  14. data/lib/request_log_analyzer/aggregator/summarizer.rb +101 -63
  15. data/lib/request_log_analyzer/{aggregator/base.rb → aggregator.rb} +17 -13
  16. data/lib/request_log_analyzer/controller.rb +251 -98
  17. data/lib/request_log_analyzer/database/base.rb +114 -0
  18. data/lib/request_log_analyzer/database/connection.rb +38 -0
  19. data/lib/request_log_analyzer/database/request.rb +22 -0
  20. data/lib/request_log_analyzer/database/source.rb +13 -0
  21. data/lib/request_log_analyzer/database/warning.rb +14 -0
  22. data/lib/request_log_analyzer/database.rb +102 -0
  23. data/lib/request_log_analyzer/file_format/amazon_s3.rb +74 -0
  24. data/lib/request_log_analyzer/file_format/apache.rb +147 -0
  25. data/lib/request_log_analyzer/file_format/delayed_job.rb +55 -0
  26. data/lib/request_log_analyzer/file_format/merb.rb +65 -29
  27. data/lib/request_log_analyzer/file_format/mysql.rb +101 -0
  28. data/lib/request_log_analyzer/file_format/postgresql.rb +68 -0
  29. data/lib/request_log_analyzer/file_format/rack.rb +9 -0
  30. data/lib/request_log_analyzer/file_format/rails.rb +164 -78
  31. data/lib/request_log_analyzer/file_format/rails3.rb +86 -0
  32. data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
  33. data/lib/request_log_analyzer/file_format.rb +252 -58
  34. data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
  35. data/lib/request_log_analyzer/filter/field.rb +19 -13
  36. data/lib/request_log_analyzer/filter/timespan.rb +25 -12
  37. data/lib/request_log_analyzer/filter.rb +30 -0
  38. data/lib/request_log_analyzer/line_definition.rb +69 -96
  39. data/lib/request_log_analyzer/log_processor.rb +31 -53
  40. data/lib/request_log_analyzer/mailer.rb +65 -0
  41. data/lib/request_log_analyzer/output/fancy_html.rb +49 -0
  42. data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
  43. data/lib/request_log_analyzer/output/html.rb +187 -0
  44. data/lib/request_log_analyzer/output.rb +117 -0
  45. data/lib/request_log_analyzer/request.rb +125 -40
  46. data/lib/request_log_analyzer/source/database_loader.rb +87 -0
  47. data/lib/request_log_analyzer/source/log_parser.rb +297 -0
  48. data/lib/request_log_analyzer/source.rb +72 -0
  49. data/lib/request_log_analyzer/tracker/duration.rb +28 -64
  50. data/lib/request_log_analyzer/tracker/frequency.rb +108 -0
  51. data/lib/request_log_analyzer/tracker/hourly_spread.rb +76 -49
  52. data/lib/request_log_analyzer/tracker/numeric_value.rb +223 -0
  53. data/lib/request_log_analyzer/tracker/timespan.rb +54 -27
  54. data/lib/request_log_analyzer/tracker/traffic.rb +40 -0
  55. data/lib/request_log_analyzer/tracker.rb +101 -0
  56. data/lib/request_log_analyzer.rb +43 -13
  57. data/request-log-analyzer.gemspec +41 -0
  58. data/spec/database.yml +23 -0
  59. data/spec/fixtures/apache_combined.log +5 -0
  60. data/spec/fixtures/apache_common.log +10 -0
  61. data/spec/fixtures/decompression.log +12 -0
  62. data/spec/fixtures/decompression.log.bz2 +0 -0
  63. data/spec/fixtures/decompression.log.gz +0 -0
  64. data/spec/fixtures/decompression.log.zip +0 -0
  65. data/spec/fixtures/decompression.tar.gz +0 -0
  66. data/spec/fixtures/decompression.tgz +0 -0
  67. data/spec/fixtures/header_and_footer.log +6 -0
  68. data/spec/fixtures/merb_prefixed.log +9 -0
  69. data/spec/fixtures/mysql_slow_query.log +110 -0
  70. data/spec/fixtures/postgresql.log +2980 -0
  71. data/spec/fixtures/rails.db +0 -0
  72. data/spec/fixtures/rails_22.log +1 -1
  73. data/spec/fixtures/sinatra.log +99 -0
  74. data/spec/integration/command_line_usage_spec.rb +84 -0
  75. data/spec/integration/mailer_spec.rb +179 -0
  76. data/spec/integration/munin_plugins_rails_spec.rb +58 -0
  77. data/spec/integration/scout_spec.rb +152 -0
  78. data/spec/lib/helpers.rb +72 -0
  79. data/spec/lib/macros.rb +18 -0
  80. data/spec/lib/matchers.rb +77 -0
  81. data/spec/lib/mocks.rb +77 -0
  82. data/spec/lib/testing_format.rb +46 -0
  83. data/spec/spec_helper.rb +16 -59
  84. data/spec/unit/aggregator/database_inserter_spec.rb +93 -0
  85. data/spec/unit/aggregator/summarizer_spec.rb +26 -0
  86. data/spec/unit/controller/controller_spec.rb +41 -0
  87. data/spec/unit/controller/log_processor_spec.rb +18 -0
  88. data/spec/unit/database/base_class_spec.rb +183 -0
  89. data/spec/unit/database/connection_spec.rb +34 -0
  90. data/spec/unit/database/database_spec.rb +133 -0
  91. data/spec/unit/file_format/amazon_s3_format_spec.rb +67 -0
  92. data/spec/unit/file_format/apache_format_spec.rb +203 -0
  93. data/spec/unit/file_format/common_regular_expressions_spec.rb +53 -0
  94. data/spec/unit/file_format/delayed_job_format_spec.rb +83 -0
  95. data/spec/unit/file_format/file_format_api_spec.rb +69 -0
  96. data/spec/unit/file_format/format_autodetection_spec.rb +40 -0
  97. data/spec/unit/file_format/line_definition_spec.rb +75 -0
  98. data/spec/unit/file_format/merb_format_spec.rb +52 -0
  99. data/spec/unit/file_format/mysql_format_spec.rb +154 -0
  100. data/spec/unit/file_format/postgresql_format_spec.rb +65 -0
  101. data/spec/unit/file_format/rack_format_spec.rb +50 -0
  102. data/spec/unit/file_format/rails3_format_spec.rb +120 -0
  103. data/spec/unit/file_format/rails_format_spec.rb +180 -0
  104. data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
  105. data/spec/unit/filter/field_filter_spec.rb +66 -0
  106. data/spec/unit/filter/filter_spec.rb +17 -0
  107. data/spec/unit/filter/timespan_filter_spec.rb +58 -0
  108. data/spec/unit/mailer_spec.rb +42 -0
  109. data/spec/unit/request_spec.rb +111 -0
  110. data/spec/unit/source/log_parser_spec.rb +119 -0
  111. data/spec/unit/tracker/duration_tracker_spec.rb +49 -0
  112. data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
  113. data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
  114. data/spec/unit/tracker/numeric_value_tracker_spec.rb +166 -0
  115. data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
  116. data/spec/unit/tracker/tracker_api_spec.rb +125 -0
  117. data/spec/unit/tracker/traffic_tracker_spec.rb +28 -0
  118. data/tasks/github-gem.rake +290 -139
  119. data/tasks/request_log_analyzer.rake +23 -7
  120. metadata +221 -94
  121. data/DESIGN +0 -14
  122. data/HACKING +0 -7
  123. data/README.textile +0 -36
  124. data/lib/cli/bashcolorizer.rb +0 -60
  125. data/lib/request_log_analyzer/aggregator/database.rb +0 -148
  126. data/lib/request_log_analyzer/filter/base.rb +0 -29
  127. data/lib/request_log_analyzer/log_parser.rb +0 -173
  128. data/lib/request_log_analyzer/source/base.rb +0 -42
  129. data/lib/request_log_analyzer/source/log_file.rb +0 -170
  130. data/lib/request_log_analyzer/tracker/base.rb +0 -54
  131. data/lib/request_log_analyzer/tracker/category.rb +0 -71
  132. data/spec/controller_spec.rb +0 -40
  133. data/spec/database_inserter_spec.rb +0 -101
  134. data/spec/file_format_spec.rb +0 -78
  135. data/spec/file_formats/spec_format.rb +0 -26
  136. data/spec/filter_spec.rb +0 -137
  137. data/spec/line_definition_spec.rb +0 -124
  138. data/spec/log_parser_spec.rb +0 -68
  139. data/spec/log_processor_spec.rb +0 -57
  140. data/spec/merb_format_spec.rb +0 -38
  141. data/spec/rails_format_spec.rb +0 -76
  142. data/spec/request_spec.rb +0 -72
  143. data/spec/summarizer_spec.rb +0 -9
  144. data/tasks/rspec.rake +0 -6
@@ -1,27 +1,90 @@
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
-
12
- include RequestLogAnalyzer::FileFormat::Awareness
13
-
14
- attr_reader :lines
15
- attr_reader :attributes
16
-
17
- # Initializes a new Request object.
11
+
12
+ module Converters
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.
17
+ def convert_value(value, capture_definition)
18
+ return capture_definition[:default] if value.nil?
19
+ custom_converter_method = :"convert_#{capture_definition[:type]}"
20
+ send(custom_converter_method, value, capture_definition)
21
+ end
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.
32
+ def convert_eval(value, capture_definition)
33
+ eval(value).inject({}) { |h, (k, v)| h[k.to_sym] = v; h}
34
+ rescue SyntaxError
35
+ nil
36
+ end
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.
41
+ def convert_timestamp(value, capture_definition)
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
57
+ end
58
+
59
+ # Convert duration fields to float, and make sure the values are in seconds.
60
+ def convert_duration(value, capture_definition)
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"
66
+ end
67
+ end
68
+
69
+ # Convert an epoch to an integer
70
+ def convert_epoch(value, capture_definition)
71
+ Time.at(value.to_i).strftime('%Y%m%d%H%M%S').to_i
72
+ end
73
+ end
74
+
75
+ # Install the default converter methods
76
+ include Converters
77
+
78
+ attr_reader :lines, :attributes, :file_format
79
+
80
+ # Initializes a new Request object.
18
81
  # It will apply the the provided FileFormat module to this instance.
19
- def initialize(file_format)
20
- @lines = []
21
- @attributes = {}
22
- register_file_format(file_format)
82
+ def initialize(file_format, attributes = {})
83
+ @lines = []
84
+ @attributes = attributes
85
+ @file_format = file_format
23
86
  end
24
-
87
+
25
88
  # Creates a new request that was parsed from the log with the given FileFormat. The hashes
26
89
  # that are passed to this function are added as lines to this request.
27
90
  def self.create(file_format, *hashes)
@@ -29,67 +92,89 @@ module RequestLogAnalyzer
29
92
  hashes.flatten.each { |hash| request << hash }
30
93
  return request
31
94
  end
32
-
33
- # Adds another line to the request.
95
+
96
+ # Adds another line to the request when it is parsed in the LogParser.
97
+ #
98
+ # The line should be provided as a hash with the attributes line_definition, :captures,
99
+ # :lineno and :source set. This function is called from LogParser.
100
+ def add_parsed_line (parsed_line)
101
+ value_hash = parsed_line[:line_definition].convert_captured_values(parsed_line[:captures], self)
102
+ value_hash[:line_type] = parsed_line[:line_definition].name
103
+ value_hash[:lineno] = parsed_line[:lineno]
104
+ value_hash[:source] = parsed_line[:source]
105
+ add_line_hash(value_hash)
106
+ end
107
+
108
+ # Adds another line to the request using a plain hash.
109
+ #
34
110
  # The line should be provides as a hash of the fields parsed from the line.
35
- def << (request_info_hash)
36
- @lines << request_info_hash
37
- @attributes = request_info_hash.merge(@attributes)
111
+ def add_line_hash(value_hash)
112
+ @lines << value_hash
113
+ @attributes = value_hash.merge(@attributes)
114
+ end
115
+
116
+ # Adds another line to the request. This method switches automatically between
117
+ # the add_line_hash and add_parsed_line based on the keys of the provided hash.
118
+ def <<(hash)
119
+ hash[:line_definition] ? add_parsed_line(hash) : add_line_hash(hash)
38
120
  end
39
-
121
+
40
122
  # Checks whether the given line type was parsed from the log file for this request
41
123
  def has_line_type?(line_type)
42
124
  return true if @lines.length == 1 && @lines[0][:line_type] == line_type.to_sym
43
-
44
125
  @lines.detect { |l| l[:line_type] == line_type.to_sym }
45
126
  end
46
-
127
+
47
128
  alias :=~ :has_line_type?
48
-
129
+
49
130
  # Returns the value that was captured for the "field" of this request.
50
131
  # This function will return the first value that was captured if the field
51
132
  # was captured in multiple lines
52
133
  def first(field)
53
134
  @attributes[field]
54
135
  end
55
-
136
+
56
137
  alias :[] :first
57
-
138
+
58
139
  # Returns an array of all the "field" values that were captured for this request
59
140
  def every(field)
60
141
  @lines.inject([]) { |result, fields| result << fields[field] if fields.has_key?(field); result }
61
- end
62
-
63
- # Returns true if this request does not yet contain any parsed lines. This should only occur
142
+ end
143
+
144
+ # Returns true if this request does not yet contain any parsed lines. This should only occur
64
145
  # during parsing. An empty request should never be sent to the aggregators
65
146
  def empty?
66
147
  @lines.length == 0
67
148
  end
68
-
149
+
69
150
  # Checks whether this request is completed. A completed request contains both a parsed header
70
- # line and a parsed footer line. Not that calling this function in single line mode will always
151
+ # line and a parsed footer line. Not that calling this function in single line mode will always
71
152
  # return false.
72
153
  def completed?
73
154
  header_found, footer_found = false, false
74
- @lines.each do |line|
155
+ @lines.each do |line|
75
156
  line_def = file_format.line_definitions[line[:line_type]]
76
157
  header_found = true if line_def.header
77
- footer_found = true if line_def.footer
158
+ footer_found = true if line_def.footer
78
159
  end
79
- header_found && footer_found
160
+ header_found && footer_found
161
+ end
162
+
163
+ # This function is called before a Requests is yielded.
164
+ def validate
80
165
  end
81
-
82
- # Returns the first timestamp encountered in a request.
166
+
167
+ # Returns the first timestamp encountered in a request.
83
168
  def timestamp
84
169
  first(:timestamp)
85
170
  end
86
-
171
+
87
172
  def first_lineno
88
- @lines.first[:lineno]
173
+ @lines.map { |line| line[:lineno] }.reject { |v| v.nil? }.min
89
174
  end
90
-
175
+
91
176
  def last_lineno
92
- @lines.last[:lineno]
177
+ @lines.map { |line| line[:lineno] }.reject { |v| v.nil? }.max
93
178
  end
94
179
  end
95
180
  end
@@ -0,0 +1,87 @@
1
+ require 'rubygems'
2
+ require 'activerecord'
3
+
4
+ module RequestLogAnalyzer::Source
5
+
6
+ # Active Resource hook
7
+ class Request < ActiveRecord::Base
8
+ has_many :completed_lines
9
+ has_many :processing_lines
10
+ def convert(file_format)
11
+ send_attributes = self.attributes
12
+ send_attributes.merge!(self.completed_lines.first.attributes) if self.completed_lines.first
13
+ send_attributes.merge!(self.processing_lines.first.attributes) if self.processing_lines.first
14
+ return RequestLogAnalyzer::Request.new(file_format, send_attributes)
15
+ end
16
+ end
17
+
18
+ class CompletedLine < ActiveRecord::Base
19
+ belongs_to :request
20
+ end
21
+
22
+ class ProcessingLine < ActiveRecord::Base
23
+ belongs_to :request
24
+ end
25
+
26
+ # The Database class gets log data from the database.
27
+ class DatabaseLoader < Base
28
+
29
+ attr_reader :source_files, :file_format, :requests
30
+
31
+ # Initializes the log file parser instance.
32
+ # It will apply the language specific FileFormat module to this instance. It will use the line
33
+ # definitions in this module to parse any input that it is given (see parse_io).
34
+ #
35
+ # <tt>format</tt>:: The current file format instance
36
+ # <tt>options</tt>:: A hash of options that are used by the parser
37
+ def initialize(format, options = {})
38
+ super(format, options)
39
+ @source_files = options[:source_files]
40
+ @parsed_requests = 0
41
+ @requests = []
42
+ end
43
+
44
+ # Reads the input, which can either be a file, sequence of files or STDIN to parse
45
+ # lines specified in the FileFormat. This lines will be combined into Request instances,
46
+ # that will be yielded. The actual parsing occurs in the parse_io method.
47
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
48
+ def each_request(options = {}, &block) # :yields: request
49
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => @source_files)
50
+
51
+ @progress_handler.call(:started, @source_files) if @progress_handler
52
+ RequestLogAnalyzer::Source::Request.find(:all).each do |request|
53
+ @parsed_requests += 1
54
+ @progress_handler.call(:progress, @parsed_requests) if @progress_handler
55
+
56
+ yield request.convert(self.file_format)
57
+ end
58
+
59
+ @progress_handler.call(:finished, @source_files) if @progress_handler
60
+ end
61
+
62
+ # Add a block to this method to install a progress handler while parsing.
63
+ # <tt>proc</tt>:: The proc that will be called to handle progress update messages
64
+ def progress=(proc)
65
+ @progress_handler = proc
66
+ end
67
+
68
+ # Add a block to this method to install a warning handler while parsing,
69
+ # <tt>proc</tt>:: The proc that will be called to handle parse warning messages
70
+ def warning=(proc)
71
+ @warning_handler = proc
72
+ end
73
+
74
+ # This method is called by the parser if it encounteres any parsing problems.
75
+ # It will call the installed warning handler if any.
76
+ #
77
+ # By default, RequestLogAnalyzer::Controller will install a warning handler
78
+ # that will pass the warnings to each aggregator so they can do something useful
79
+ # with it.
80
+ #
81
+ # <tt>type</tt>:: The warning type (a Symbol)
82
+ # <tt>message</tt>:: A message explaining the warning
83
+ def warn(type, message)
84
+ @warning_handler.call(type, message, @current_io.lineno) if @warning_handler
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,297 @@
1
+ module RequestLogAnalyzer::Source
2
+
3
+ # The LogParser class reads log data from a given source and uses a file format definition
4
+ # to parse all relevent information about requests from the file. A FileFormat module should
5
+ # be provided that contains the definitions of the lines that occur in the log data.
6
+ #
7
+ # De order in which lines occur is used to combine lines to a single request. If these lines
8
+ # are mixed, requests cannot be combined properly. This can be the case if data is written to
9
+ # the log file simultaneously by different mongrel processes. This problem is detected by the
10
+ # parser. It will emit warnings when this occurs. LogParser supports multiple parse strategies
11
+ # that deal differently with this problem.
12
+ class LogParser < Base
13
+
14
+ include Enumerable
15
+
16
+ # The default parse strategy that will be used to parse the input.
17
+ DEFAULT_PARSE_STRATEGY = 'assume-correct'
18
+
19
+ # All available parse strategies.
20
+ PARSE_STRATEGIES = ['cautious', 'assume-correct']
21
+
22
+ attr_reader :source_files, :current_file, :current_lineno
23
+ attr_reader :warnings, :parsed_lines, :parsed_requests, :skipped_lines, :skipped_requests
24
+
25
+ # Initializes the log file parser instance.
26
+ # It will apply the language specific FileFormat module to this instance. It will use the line
27
+ # definitions in this module to parse any input that it is given (see parse_io).
28
+ #
29
+ # <tt>format</tt>:: The current file format instance
30
+ # <tt>options</tt>:: A hash of options that are used by the parser
31
+ def initialize(format, options = {})
32
+ super(format, options)
33
+ @warnings = 0
34
+ @parsed_lines = 0
35
+ @parsed_requests = 0
36
+ @skipped_lines = 0
37
+ @skipped_requests = 0
38
+ @current_request = nil
39
+ @current_source = nil
40
+ @current_file = nil
41
+ @current_lineno = nil
42
+ @source_files = options[:source_files]
43
+ @progress_handler = nil
44
+
45
+ @options[:parse_strategy] ||= DEFAULT_PARSE_STRATEGY
46
+ raise "Unknown parse strategy" unless PARSE_STRATEGIES.include?(@options[:parse_strategy])
47
+ end
48
+
49
+ # Reads the input, which can either be a file, sequence of files or STDIN to parse
50
+ # lines specified in the FileFormat. This lines will be combined into Request instances,
51
+ # that will be yielded. The actual parsing occurs in the parse_io method.
52
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
53
+ def each_request(options = {}, &block) # :yields: :request, request
54
+
55
+ case @source_files
56
+ when IO
57
+ if @source_files == $stdin
58
+ puts "Parsing from the standard input. Press CTRL+C to finish." # FIXME: not here
59
+ end
60
+ parse_stream(@source_files, options, &block)
61
+ when String
62
+ parse_file(@source_files, options, &block)
63
+ when Array
64
+ parse_files(@source_files, options, &block)
65
+ else
66
+ raise "Unknown source provided"
67
+ end
68
+ end
69
+
70
+ # Make sure the Enumerable methods work as expected
71
+ alias_method :each, :each_request
72
+
73
+ # Parses a list of subsequent files of the same format, by calling parse_file for every
74
+ # file in the array.
75
+ # <tt>files</tt>:: The Array of files that should be parsed
76
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
77
+ def parse_files(files, options = {}, &block) # :yields: request
78
+ files.each { |file| parse_file(file, options, &block) }
79
+ end
80
+
81
+ # Check if a file has a compressed extention in the filename.
82
+ # If recognized, return the command string used to decompress the file
83
+ def decompress_file?(filename)
84
+ nice_command = "nice -n 5"
85
+
86
+ return "#{nice_command} gunzip -c -d #{filename}" if filename.match(/\.tar.gz$/) || filename.match(/\.tgz$/) || filename.match(/\.gz$/)
87
+ return "#{nice_command} bunzip2 -c -d #{filename}" if filename.match(/\.bz2$/)
88
+ return "#{nice_command} unzip -p #{filename}" if filename.match(/\.zip$/)
89
+
90
+ return ""
91
+ end
92
+
93
+ # Parses a log file. Creates an IO stream for the provided file, and sends it to parse_io for
94
+ # further handling. This method supports progress updates that can be used to display a progressbar
95
+ #
96
+ # If the logfile is compressed, it is uncompressed to stdout and read.
97
+ # TODO: Check if IO.popen encounters problems with the given command line.
98
+ # TODO: Fix progress bar that is broken for IO.popen, as it returns a single string.
99
+ #
100
+ # <tt>file</tt>:: The file that should be parsed.
101
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
102
+ def parse_file(file, options = {}, &block)
103
+
104
+ @current_source = File.expand_path(file)
105
+ @source_changes_handler.call(:started, @current_source) if @source_changes_handler
106
+
107
+ if decompress_file?(file).empty?
108
+
109
+ @progress_handler = @dormant_progress_handler
110
+ @progress_handler.call(:started, file) if @progress_handler
111
+
112
+ File.open(file, 'rb') { |f| parse_io(f, options, &block) }
113
+
114
+ @progress_handler.call(:finished, file) if @progress_handler
115
+ @progress_handler = nil
116
+ else
117
+ IO.popen(decompress_file?(file), 'rb') { |f| parse_io(f, options, &block) }
118
+ end
119
+
120
+ @source_changes_handler.call(:finished, @current_source) if @source_changes_handler
121
+
122
+ @current_source = nil
123
+
124
+ end
125
+
126
+ # Parses an IO stream. It will simply call parse_io. This function does not support progress updates
127
+ # because the length of a stream is not known.
128
+ # <tt>stream</tt>:: The IO stream that should be parsed.
129
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
130
+ def parse_stream(stream, options = {}, &block)
131
+ parse_io(stream, options, &block)
132
+ end
133
+
134
+ # This method loops over each line of the input stream. It will try to parse this line as any of
135
+ # the lines that are defined by the current file format (see RequestLogAnalyazer::FileFormat).
136
+ # It will then combine these parsed line into requests using heuristics. These requests (see
137
+ # RequestLogAnalyzer::Request) will then be yielded for further processing in the pipeline.
138
+ #
139
+ # - RequestLogAnalyzer::LineDefinition#matches is called to test if a line matches a line definition of the file format.
140
+ # - update_current_request is used to combine parsed lines into requests using heuristics.
141
+ # - The method will yield progress updates if a progress handler is installed using progress=
142
+ # - The method will yield parse warnings if a warning handler is installed using warning=
143
+ #
144
+ # <tt>io</tt>:: The IO instance to use as source
145
+ # <tt>options</tt>:: A hash of options that can be used by the parser.
146
+ def parse_io(io, options = {}, &block) # :yields: request
147
+ @current_lineno = 0
148
+ while line = io.gets
149
+ @current_lineno += 1
150
+ @progress_handler.call(:progress, io.pos) if @progress_handler && @current_lineno % 255 == 0
151
+ parse_line(line, &block)
152
+ end
153
+
154
+ warn(:unfinished_request_on_eof, "End of file reached, but last request was not completed!") unless @current_request.nil?
155
+ @current_lineno = nil
156
+ end
157
+
158
+ # Parses a single line using the current file format. If successful, use the parsed
159
+ # information to build a request
160
+ # <tt>line</tt>:: The line to parse
161
+ # <tt>block</tt>:: The block to send fully parsed requests to.
162
+ def parse_line(line, &block) # :yields: request
163
+ if request_data = file_format.parse_line(line) { |wt, message| warn(wt, message) }
164
+ @parsed_lines += 1
165
+ update_current_request(request_data.merge(:source => @current_source, :lineno => @current_lineno), &block)
166
+ end
167
+ end
168
+
169
+ # Add a block to this method to install a progress handler while parsing.
170
+ # <tt>proc</tt>:: The proc that will be called to handle progress update messages
171
+ def progress=(proc)
172
+ @dormant_progress_handler = proc
173
+ end
174
+
175
+ # Add a block to this method to install a warning handler while parsing,
176
+ # <tt>proc</tt>:: The proc that will be called to handle parse warning messages
177
+ def warning=(proc)
178
+ @warning_handler = proc
179
+ end
180
+
181
+ # Add a block to this method to install a source change handler while parsing,
182
+ # <tt>proc</tt>:: The proc that will be called to handle source changes
183
+ def source_changes=(proc)
184
+ @source_changes_handler = proc
185
+ end
186
+
187
+ # This method is called by the parser if it encounteres any parsing problems.
188
+ # It will call the installed warning handler if any.
189
+ #
190
+ # By default, RequestLogAnalyzer::Controller will install a warning handler
191
+ # that will pass the warnings to each aggregator so they can do something useful
192
+ # with it.
193
+ #
194
+ # <tt>type</tt>:: The warning type (a Symbol)
195
+ # <tt>message</tt>:: A message explaining the warning
196
+ def warn(type, message)
197
+ @warnings += 1
198
+ @warning_handler.call(type, message, @current_lineno) if @warning_handler
199
+ end
200
+
201
+ protected
202
+
203
+ # Combines the different lines of a request into a single Request object. It will start a
204
+ # new request when a header line is encountered en will emit the request when a footer line
205
+ # is encountered.
206
+ #
207
+ # Combining the lines is done using heuristics. Problems can occur in this process. The
208
+ # current parse strategy defines how these cases are handled.
209
+ #
210
+ # When using the 'assume-correct' parse strategy (default):
211
+ # - Every line that is parsed before a header line is ignored as it cannot be included in
212
+ # any request. It will emit a :no_current_request warning.
213
+ # - If a header line is found before the previous requests was closed, the previous request
214
+ # will be yielded and a new request will be started.
215
+ #
216
+ # When using the 'cautious' parse strategy:
217
+ # - Every line that is parsed before a header line is ignored as it cannot be included in
218
+ # any request. It will emit a :no_current_request warning.
219
+ # - A header line that is parsed before a request is closed by a footer line, is a sign of
220
+ # an unproperly ordered file. All data that is gathered for the request until then is
221
+ # discarded and the next request is ignored as well. An :unclosed_request warning is
222
+ # emitted.
223
+ #
224
+ # <tt>request_data</tt>:: A hash of data that was parsed from the last line.
225
+ def update_current_request(request_data, &block) # :yields: request
226
+ if alternative_header_line?(request_data)
227
+ if @current_request
228
+ @current_request << request_data
229
+ else
230
+ @current_request = @file_format.request(request_data)
231
+ end
232
+ elsif header_line?(request_data)
233
+ if @current_request
234
+ case options[:parse_strategy]
235
+ when 'assume-correct'
236
+ handle_request(@current_request, &block)
237
+ @current_request = @file_format.request(request_data)
238
+ when 'cautious'
239
+ @skipped_lines += 1
240
+ warn(:unclosed_request, "Encountered header line (#{request_data[:line_definition].name.inspect}), but previous request was not closed!")
241
+ @current_request = nil # remove all data that was parsed, skip next request as well.
242
+ end
243
+ elsif footer_line?(request_data)
244
+ handle_request(@file_format.request(request_data), &block)
245
+ else
246
+ @current_request = @file_format.request(request_data)
247
+ end
248
+ else
249
+ if @current_request
250
+ @current_request << request_data
251
+ if footer_line?(request_data)
252
+ handle_request(@current_request, &block) # yield @current_request
253
+ @current_request = nil
254
+ end
255
+ else
256
+ @skipped_lines += 1
257
+ warn(:no_current_request, "Parsebale line (#{request_data[:line_definition].name.inspect}) found outside of a request!")
258
+ end
259
+ end
260
+ end
261
+
262
+ # Handles the parsed request by sending it into the pipeline.
263
+ #
264
+ # - It will call RequestLogAnalyzer::Request#validate on the request instance
265
+ # - It will send the request into the pipeline, checking whether it was accepted by all the filters.
266
+ # - It will update the parsed_requests and skipped_requests variables accordingly
267
+ #
268
+ # <tt>request</tt>:: The parsed request instance (RequestLogAnalyzer::Request)
269
+ def handle_request(request, &block) # :yields: :request, request
270
+ @parsed_requests += 1
271
+ request.validate
272
+ accepted = block_given? ? yield(request) : true
273
+ @skipped_requests += 1 unless accepted
274
+ end
275
+
276
+
277
+ # Checks whether a given line hash is an alternative header line according to the current file format.
278
+ # <tt>hash</tt>:: A hash of data that was parsed from the line.
279
+ def alternative_header_line?(hash)
280
+ hash[:line_definition].header == :alternative
281
+ end
282
+
283
+
284
+ # Checks whether a given line hash is a header line according to the current file format.
285
+ # <tt>hash</tt>:: A hash of data that was parsed from the line.
286
+ def header_line?(hash)
287
+ hash[:line_definition].header == true
288
+ end
289
+
290
+ # Checks whether a given line hash is a footer line according to the current file format.
291
+ # <tt>hash</tt>:: A hash of data that was parsed from the line.
292
+ def footer_line?(hash)
293
+ hash[:line_definition].footer
294
+ end
295
+ end
296
+
297
+ end
@@ -0,0 +1,72 @@
1
+ # The RequestLogAnalyzer::Source module contains all functionality that loads requests from a given source
2
+ # and feed them to the pipeline for further processing. The requests (see RequestLogAnalyzer::Request) that
3
+ # will be parsed from a source, will be piped throug filters (see RequestLogAnalyzer::Filter) and are then
4
+ # fed to an aggregator (see RequestLogAnalyzer::Aggregator). The source instance is thus the beginning of
5
+ # the RequestLogAnalyzer chain.
6
+ #
7
+ # - The base class for all sources is RequestLogAnalyzer::Source::Base. All source classes should inherit from this class.
8
+ # - Currently, RequestLogAnalyzer::Source::LogParser is the only implemented source.
9
+ module RequestLogAnalyzer::Source
10
+
11
+ # Loads constants that reside in the RequestLogAnalyzer::Source namespace. This function uses
12
+ # RequestLogAnalyzer::load_default_class_file to load the file in which the constant is declared.
13
+ # <tt>const</tt>:: The constant to load in the RequestLogAnalyzer::Source namespace.
14
+ def self.const_missing(const)
15
+ RequestLogAnalyzer::load_default_class_file(self, const)
16
+ end
17
+
18
+ # The base Source class. All other sources should inherit from this class.
19
+ #
20
+ # A source implememtation should at least implement the each_request method, which should yield
21
+ # RequestLogAnalyzer::Request instances that will be fed through the pipleine.
22
+ class Base
23
+
24
+ # A hash of options
25
+ attr_reader :options
26
+
27
+ # The current Request object that is being parsed
28
+ attr_reader :current_request
29
+
30
+ # The total number of parsed lines
31
+ attr_reader :parsed_lines
32
+
33
+ # The number of skipped lines because of warnings
34
+ attr_reader :skipped_lines
35
+
36
+ # The total number of parsed requests.
37
+ attr_reader :parsed_requests
38
+
39
+ # The total number of skipped requests because of filters.
40
+ attr_reader :skipped_requests
41
+
42
+ # The FileFormat instance that describes the format of this source.
43
+ attr_reader :file_format
44
+
45
+ # Initializer, which will register the file format and save any options given as a hash.
46
+ # <tt>format</tt>:: The file format instance
47
+ # <tt>options</tt>:: A hash of options that can be used by a specific Source implementation
48
+ def initialize(format, options = {})
49
+ @options = options
50
+ @file_format = format
51
+ end
52
+
53
+ # The prepare method is called before the RequestLogAnalyzer::Source::Base#each_request method is called.
54
+ # Use this method to implement any initialization that should occur before this source can produce Request
55
+ # instances.
56
+ def prepare
57
+ end
58
+
59
+ # This function is called to actually produce the requests that will be send into the pipeline.
60
+ # The implementation should yield instances of RequestLogAnalyzer::Request.
61
+ # <tt>options</tt>:: A Hash of options that can be used in the implementation.
62
+ def each_request(options = {}, &block) # :yields: request
63
+ return true
64
+ end
65
+
66
+ # This function is called after RequestLogAnalyzer::Source::Base#each_request finished. Any code to
67
+ # wrap up, free resources, etc. can be put in this method.
68
+ def finalize
69
+ end
70
+
71
+ end
72
+ end