request-log-analyzer 1.3.5 → 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.
- data/LICENSE +3 -3
- data/README.rdoc +1 -1
- data/bin/request-log-analyzer +17 -14
- data/lib/cli/command_line_arguments.rb +51 -51
- data/lib/cli/database_console.rb +3 -3
- data/lib/cli/database_console_init.rb +4 -3
- data/lib/cli/progressbar.rb +10 -10
- data/lib/cli/tools.rb +3 -3
- data/lib/request_log_analyzer/aggregator/database_inserter.rb +13 -14
- data/lib/request_log_analyzer/aggregator/echo.rb +14 -9
- data/lib/request_log_analyzer/aggregator/summarizer.rb +26 -26
- data/lib/request_log_analyzer/aggregator.rb +11 -15
- data/lib/request_log_analyzer/controller.rb +162 -89
- data/lib/request_log_analyzer/database/base.rb +21 -21
- data/lib/request_log_analyzer/database/connection.rb +3 -3
- data/lib/request_log_analyzer/database/request.rb +22 -0
- data/lib/request_log_analyzer/database/source.rb +13 -0
- data/lib/request_log_analyzer/database/warning.rb +14 -0
- data/lib/request_log_analyzer/database.rb +28 -103
- data/lib/request_log_analyzer/file_format/amazon_s3.rb +17 -18
- data/lib/request_log_analyzer/file_format/apache.rb +26 -27
- data/lib/request_log_analyzer/file_format/merb.rb +30 -13
- data/lib/request_log_analyzer/file_format/rack.rb +4 -4
- data/lib/request_log_analyzer/file_format/rails.rb +143 -73
- data/lib/request_log_analyzer/file_format/rails_development.rb +4 -49
- data/lib/request_log_analyzer/file_format.rb +16 -28
- data/lib/request_log_analyzer/filter/anonymize.rb +8 -7
- data/lib/request_log_analyzer/filter/field.rb +9 -9
- data/lib/request_log_analyzer/filter/timespan.rb +12 -10
- data/lib/request_log_analyzer/filter.rb +12 -16
- data/lib/request_log_analyzer/line_definition.rb +15 -14
- data/lib/request_log_analyzer/log_processor.rb +27 -29
- data/lib/request_log_analyzer/mailer.rb +15 -9
- data/lib/request_log_analyzer/output/fixed_width.rb +40 -41
- data/lib/request_log_analyzer/output/html.rb +20 -20
- data/lib/request_log_analyzer/output.rb +53 -12
- data/lib/request_log_analyzer/request.rb +75 -68
- data/lib/request_log_analyzer/source/database_loader.rb +10 -14
- data/lib/request_log_analyzer/source/log_parser.rb +57 -50
- data/lib/request_log_analyzer/source.rb +10 -12
- data/lib/request_log_analyzer/tracker/duration.rb +39 -132
- data/lib/request_log_analyzer/tracker/frequency.rb +31 -32
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +21 -21
- data/lib/request_log_analyzer/tracker/timespan.rb +17 -17
- data/lib/request_log_analyzer/tracker/traffic.rb +36 -116
- data/lib/request_log_analyzer/tracker.rb +139 -32
- data/lib/request_log_analyzer.rb +4 -4
- data/request-log-analyzer.gemspec +19 -15
- data/spec/database.yml +6 -0
- data/spec/fixtures/rails_22.log +1 -1
- data/spec/integration/command_line_usage_spec.rb +7 -1
- data/spec/lib/helpers.rb +7 -7
- data/spec/lib/macros.rb +3 -3
- data/spec/lib/matchers.rb +41 -27
- data/spec/lib/mocks.rb +15 -14
- data/spec/lib/testing_format.rb +9 -9
- data/spec/spec_helper.rb +6 -6
- data/spec/unit/aggregator/database_inserter_spec.rb +16 -16
- data/spec/unit/aggregator/summarizer_spec.rb +4 -4
- data/spec/unit/controller/controller_spec.rb +2 -2
- data/spec/unit/controller/log_processor_spec.rb +1 -1
- data/spec/unit/database/base_class_spec.rb +26 -33
- data/spec/unit/database/connection_spec.rb +3 -3
- data/spec/unit/database/database_spec.rb +33 -38
- data/spec/unit/file_format/amazon_s3_format_spec.rb +5 -5
- data/spec/unit/file_format/apache_format_spec.rb +13 -13
- data/spec/unit/file_format/file_format_api_spec.rb +13 -13
- data/spec/unit/file_format/line_definition_spec.rb +24 -17
- data/spec/unit/file_format/merb_format_spec.rb +41 -45
- data/spec/unit/file_format/rails_format_spec.rb +157 -117
- data/spec/unit/filter/anonymize_filter_spec.rb +2 -2
- data/spec/unit/filter/field_filter_spec.rb +13 -13
- data/spec/unit/filter/filter_spec.rb +1 -1
- data/spec/unit/filter/timespan_filter_spec.rb +15 -15
- data/spec/unit/mailer_spec.rb +30 -0
- data/spec/unit/{source/request_spec.rb → request_spec.rb} +30 -30
- data/spec/unit/source/log_parser_spec.rb +27 -27
- data/spec/unit/tracker/duration_tracker_spec.rb +115 -78
- data/spec/unit/tracker/frequency_tracker_spec.rb +74 -63
- data/spec/unit/tracker/hourly_spread_spec.rb +28 -20
- data/spec/unit/tracker/timespan_tracker_spec.rb +25 -13
- data/spec/unit/tracker/tracker_api_spec.rb +117 -42
- data/spec/unit/tracker/traffic_tracker_spec.rb +81 -79
- data/tasks/github-gem.rake +125 -75
- data/tasks/request_log_analyzer.rake +2 -2
- metadata +13 -8
|
@@ -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,83 +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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
42
|
+
DateTime.parse(value).strftime('%Y%m%d%H%M%S').to_i
|
|
42
43
|
end
|
|
43
|
-
|
|
44
|
+
|
|
45
|
+
# Converts traffic fields to (whole) bytes based on the given unit.
|
|
44
46
|
def convert_traffic(value, capture_definition)
|
|
45
|
-
return nil if value.nil?
|
|
46
47
|
case capture_definition[:unit]
|
|
48
|
+
when nil, :b, :B, :byte then value.to_i
|
|
47
49
|
when :GB, :G, :gigabyte then (value.to_f * 1000_000_000).round
|
|
48
50
|
when :GiB, :gibibyte then (value.to_f * (2 ** 30)).round
|
|
49
51
|
when :MB, :M, :megabyte then (value.to_f * 1000_000).round
|
|
50
52
|
when :MiB, :mebibyte then (value.to_f * (2 ** 20)).round
|
|
51
53
|
when :KB, :K, :kilobyte, :kB then (value.to_f * 1000).round
|
|
52
54
|
when :KiB, :kibibyte then (value.to_f * (2 ** 10)).round
|
|
53
|
-
else
|
|
55
|
+
else raise "Unknown traffic unit"
|
|
54
56
|
end
|
|
55
57
|
end
|
|
56
|
-
|
|
58
|
+
|
|
59
|
+
# Convert duration fields to float, and make sure the values are in seconds.
|
|
57
60
|
def convert_duration(value, capture_definition)
|
|
58
|
-
return nil if value.nil?
|
|
59
61
|
case capture_definition[:unit]
|
|
62
|
+
when nil, :sec, :s then value.to_f
|
|
60
63
|
when :microsec, :musec then value.to_f / 1000000.0
|
|
61
64
|
when :msec, :millisec then value.to_f / 1000.0
|
|
62
|
-
else
|
|
65
|
+
else raise "Unknown duration unit"
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
68
|
end
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
|
|
70
|
+
# Install the default converter methods
|
|
68
71
|
include Converters
|
|
69
|
-
|
|
70
|
-
attr_reader :lines
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# Initializes a new Request object.
|
|
72
|
+
|
|
73
|
+
attr_reader :lines, :attributes, :file_format
|
|
74
|
+
|
|
75
|
+
# Initializes a new Request object.
|
|
74
76
|
# It will apply the the provided FileFormat module to this instance.
|
|
75
77
|
def initialize(file_format, attributes = {})
|
|
76
|
-
@lines
|
|
77
|
-
@attributes
|
|
78
|
-
|
|
78
|
+
@lines = []
|
|
79
|
+
@attributes = attributes
|
|
80
|
+
@file_format = file_format
|
|
79
81
|
end
|
|
80
|
-
|
|
82
|
+
|
|
81
83
|
# Creates a new request that was parsed from the log with the given FileFormat. The hashes
|
|
82
84
|
# that are passed to this function are added as lines to this request.
|
|
83
85
|
def self.create(file_format, *hashes)
|
|
@@ -85,9 +87,11 @@ module RequestLogAnalyzer
|
|
|
85
87
|
hashes.flatten.each { |hash| request << hash }
|
|
86
88
|
return request
|
|
87
89
|
end
|
|
88
|
-
|
|
89
|
-
# Adds another line to the request.
|
|
90
|
-
#
|
|
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.
|
|
91
95
|
def add_parsed_line (parsed_line)
|
|
92
96
|
value_hash = parsed_line[:line_definition].convert_captured_values(parsed_line[:captures], self)
|
|
93
97
|
value_hash[:line_type] = parsed_line[:line_definition].name
|
|
@@ -95,72 +99,75 @@ module RequestLogAnalyzer
|
|
|
95
99
|
value_hash[:source] = parsed_line[:source]
|
|
96
100
|
add_line_hash(value_hash)
|
|
97
101
|
end
|
|
98
|
-
|
|
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.
|
|
99
106
|
def add_line_hash(value_hash)
|
|
100
107
|
@lines << value_hash
|
|
101
108
|
@attributes = value_hash.merge(@attributes)
|
|
102
109
|
end
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
105
113
|
def <<(hash)
|
|
106
114
|
hash[:line_definition] ? add_parsed_line(hash) : add_line_hash(hash)
|
|
107
115
|
end
|
|
108
|
-
|
|
116
|
+
|
|
109
117
|
# Checks whether the given line type was parsed from the log file for this request
|
|
110
118
|
def has_line_type?(line_type)
|
|
111
119
|
return true if @lines.length == 1 && @lines[0][:line_type] == line_type.to_sym
|
|
112
|
-
|
|
113
120
|
@lines.detect { |l| l[:line_type] == line_type.to_sym }
|
|
114
121
|
end
|
|
115
|
-
|
|
122
|
+
|
|
116
123
|
alias :=~ :has_line_type?
|
|
117
|
-
|
|
124
|
+
|
|
118
125
|
# Returns the value that was captured for the "field" of this request.
|
|
119
126
|
# This function will return the first value that was captured if the field
|
|
120
127
|
# was captured in multiple lines
|
|
121
128
|
def first(field)
|
|
122
129
|
@attributes[field]
|
|
123
130
|
end
|
|
124
|
-
|
|
131
|
+
|
|
125
132
|
alias :[] :first
|
|
126
|
-
|
|
133
|
+
|
|
127
134
|
# Returns an array of all the "field" values that were captured for this request
|
|
128
135
|
def every(field)
|
|
129
136
|
@lines.inject([]) { |result, fields| result << fields[field] if fields.has_key?(field); result }
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# 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
|
|
133
140
|
# during parsing. An empty request should never be sent to the aggregators
|
|
134
141
|
def empty?
|
|
135
142
|
@lines.length == 0
|
|
136
143
|
end
|
|
137
|
-
|
|
144
|
+
|
|
138
145
|
# Checks whether this request is completed. A completed request contains both a parsed header
|
|
139
|
-
# 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
|
|
140
147
|
# return false.
|
|
141
148
|
def completed?
|
|
142
149
|
header_found, footer_found = false, false
|
|
143
|
-
@lines.each do |line|
|
|
150
|
+
@lines.each do |line|
|
|
144
151
|
line_def = file_format.line_definitions[line[:line_type]]
|
|
145
152
|
header_found = true if line_def.header
|
|
146
|
-
footer_found = true if line_def.footer
|
|
153
|
+
footer_found = true if line_def.footer
|
|
147
154
|
end
|
|
148
|
-
header_found && footer_found
|
|
155
|
+
header_found && footer_found
|
|
149
156
|
end
|
|
150
|
-
|
|
157
|
+
|
|
151
158
|
# This function is called before a Requests is yielded.
|
|
152
159
|
def validate
|
|
153
160
|
end
|
|
154
|
-
|
|
155
|
-
# Returns the first timestamp encountered in a request.
|
|
161
|
+
|
|
162
|
+
# Returns the first timestamp encountered in a request.
|
|
156
163
|
def timestamp
|
|
157
164
|
first(:timestamp)
|
|
158
165
|
end
|
|
159
|
-
|
|
166
|
+
|
|
160
167
|
def first_lineno
|
|
161
168
|
@lines.map { |line| line[:lineno] }.reject { |v| v.nil? }.min
|
|
162
169
|
end
|
|
163
|
-
|
|
170
|
+
|
|
164
171
|
def last_lineno
|
|
165
172
|
@lines.map { |line| line[:lineno] }.reject { |v| v.nil? }.max
|
|
166
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
|
-
|
|
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
|
#
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
module RequestLogAnalyzer::Source
|
|
2
|
-
|
|
2
|
+
|
|
3
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
|
|
4
|
+
# to parse all relevent information about requests from the file. A FileFormat module should
|
|
5
5
|
# be provided that contains the definitions of the lines that occur in the log data.
|
|
6
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
|
|
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
11
|
# that deal differently with this problem.
|
|
12
12
|
class LogParser < Base
|
|
13
13
|
|
|
@@ -15,7 +15,7 @@ module RequestLogAnalyzer::Source
|
|
|
15
15
|
|
|
16
16
|
# The default parse strategy that will be used to parse the input.
|
|
17
17
|
DEFAULT_PARSE_STRATEGY = 'assume-correct'
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
# All available parse strategies.
|
|
20
20
|
PARSE_STRATEGIES = ['cautious', 'assume-correct']
|
|
21
21
|
|
|
@@ -27,42 +27,44 @@ module RequestLogAnalyzer::Source
|
|
|
27
27
|
#
|
|
28
28
|
# <tt>format</tt>:: The current file format instance
|
|
29
29
|
# <tt>options</tt>:: A hash of options that are used by the parser
|
|
30
|
-
def initialize(format, options = {})
|
|
31
|
-
|
|
32
|
-
@options = options
|
|
30
|
+
def initialize(format, options = {})
|
|
31
|
+
super(format, options)
|
|
33
32
|
@parsed_lines = 0
|
|
34
33
|
@parsed_requests = 0
|
|
35
34
|
@skipped_lines = 0
|
|
36
35
|
@skipped_requests = 0
|
|
36
|
+
@current_request = nil
|
|
37
|
+
@current_source = nil
|
|
37
38
|
@current_file = nil
|
|
38
39
|
@current_lineno = nil
|
|
39
40
|
@source_files = options[:source_files]
|
|
40
|
-
|
|
41
|
+
@progress_handler = nil
|
|
42
|
+
|
|
41
43
|
@options[:parse_strategy] ||= DEFAULT_PARSE_STRATEGY
|
|
42
44
|
raise "Unknown parse strategy" unless PARSE_STRATEGIES.include?(@options[:parse_strategy])
|
|
43
|
-
|
|
44
|
-
self.register_file_format(format)
|
|
45
45
|
end
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
# Reads the input, which can either be a file, sequence of files or STDIN to parse
|
|
48
48
|
# lines specified in the FileFormat. This lines will be combined into Request instances,
|
|
49
49
|
# that will be yielded. The actual parsing occurs in the parse_io method.
|
|
50
50
|
# <tt>options</tt>:: A Hash of options that will be pased to parse_io.
|
|
51
51
|
def each_request(options = {}, &block) # :yields: :request, request
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
case @source_files
|
|
54
54
|
when IO
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
if @source_files == $stdin
|
|
56
|
+
puts "Parsing from the standard input. Press CTRL+C to finish." # FIXME: not here
|
|
57
|
+
end
|
|
58
|
+
parse_stream(@source_files, options, &block)
|
|
57
59
|
when String
|
|
58
|
-
parse_file(@source_files, options, &block)
|
|
60
|
+
parse_file(@source_files, options, &block)
|
|
59
61
|
when Array
|
|
60
|
-
parse_files(@source_files, options, &block)
|
|
62
|
+
parse_files(@source_files, options, &block)
|
|
61
63
|
else
|
|
62
64
|
raise "Unknown source provided"
|
|
63
65
|
end
|
|
64
66
|
end
|
|
65
|
-
|
|
67
|
+
|
|
66
68
|
# Make sure the Enumerable methods work as expected
|
|
67
69
|
alias_method :each, :each_request
|
|
68
70
|
|
|
@@ -73,12 +75,12 @@ module RequestLogAnalyzer::Source
|
|
|
73
75
|
def parse_files(files, options = {}, &block) # :yields: request
|
|
74
76
|
files.each { |file| parse_file(file, options, &block) }
|
|
75
77
|
end
|
|
76
|
-
|
|
78
|
+
|
|
77
79
|
# Check if a file has a compressed extention in the filename.
|
|
78
80
|
# If recognized, return the command string used to decompress the file
|
|
79
81
|
def decompress_file?(filename)
|
|
80
82
|
nice_command = "nice -n 5"
|
|
81
|
-
|
|
83
|
+
|
|
82
84
|
return "#{nice_command} gunzip -c -d #{filename}" if filename.match(/\.tar.gz$/) || filename.match(/\.tgz$/) || filename.match(/\.gz$/)
|
|
83
85
|
return "#{nice_command} bunzip2 -c -d #{filename}" if filename.match(/\.bz2$/)
|
|
84
86
|
return "#{nice_command} unzip -p #{filename}" if filename.match(/\.zip$/)
|
|
@@ -97,27 +99,32 @@ module RequestLogAnalyzer::Source
|
|
|
97
99
|
# <tt>options</tt>:: A Hash of options that will be pased to parse_io.
|
|
98
100
|
def parse_file(file, options = {}, &block)
|
|
99
101
|
|
|
100
|
-
@progress_handler = @dormant_progress_handler
|
|
101
102
|
@current_source = File.expand_path(file)
|
|
102
|
-
@progress_handler.call(:started, file) if @progress_handler
|
|
103
103
|
@source_changes_handler.call(:started, @current_source) if @source_changes_handler
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
if decompress_file?(file).empty?
|
|
106
|
+
|
|
107
|
+
@progress_handler = @dormant_progress_handler
|
|
108
|
+
@progress_handler.call(:started, file) if @progress_handler
|
|
109
|
+
|
|
106
110
|
File.open(file, 'r') { |f| parse_io(f, options, &block) }
|
|
111
|
+
|
|
112
|
+
@progress_handler.call(:finished, file) if @progress_handler
|
|
113
|
+
@progress_handler = nil
|
|
107
114
|
else
|
|
108
115
|
IO.popen(decompress_file?(file), 'r') { |f| parse_io(f, options, &block) }
|
|
109
116
|
end
|
|
110
|
-
|
|
117
|
+
|
|
111
118
|
@source_changes_handler.call(:finished, @current_source) if @source_changes_handler
|
|
112
|
-
|
|
119
|
+
|
|
113
120
|
@current_source = nil
|
|
114
|
-
|
|
121
|
+
|
|
115
122
|
end
|
|
116
123
|
|
|
117
124
|
# Parses an IO stream. It will simply call parse_io. This function does not support progress updates
|
|
118
125
|
# because the length of a stream is not known.
|
|
119
126
|
# <tt>stream</tt>:: The IO stream that should be parsed.
|
|
120
|
-
# <tt>options</tt>:: A Hash of options that will be pased to parse_io.
|
|
127
|
+
# <tt>options</tt>:: A Hash of options that will be pased to parse_io.
|
|
121
128
|
def parse_stream(stream, options = {}, &block)
|
|
122
129
|
parse_io(stream, options, &block)
|
|
123
130
|
end
|
|
@@ -137,16 +144,16 @@ module RequestLogAnalyzer::Source
|
|
|
137
144
|
def parse_io(io, options = {}, &block) # :yields: request
|
|
138
145
|
@current_lineno = 1
|
|
139
146
|
while line = io.gets
|
|
140
|
-
@progress_handler.call(:progress, io.pos) if @progress_handler && @current_lineno %
|
|
141
|
-
|
|
147
|
+
@progress_handler.call(:progress, io.pos) if @progress_handler && @current_lineno % 255 == 0
|
|
148
|
+
|
|
142
149
|
if request_data = file_format.parse_line(line) { |wt, message| warn(wt, message) }
|
|
143
150
|
@parsed_lines += 1
|
|
144
151
|
update_current_request(request_data.merge(:source => @current_source, :lineno => @current_lineno), &block)
|
|
145
152
|
end
|
|
146
|
-
|
|
153
|
+
|
|
147
154
|
@current_lineno += 1
|
|
148
155
|
end
|
|
149
|
-
|
|
156
|
+
|
|
150
157
|
warn(:unfinished_request_on_eof, "End of file reached, but last request was not completed!") unless @current_request.nil?
|
|
151
158
|
@current_lineno = nil
|
|
152
159
|
end
|
|
@@ -162,7 +169,7 @@ module RequestLogAnalyzer::Source
|
|
|
162
169
|
def warning=(proc)
|
|
163
170
|
@warning_handler = proc
|
|
164
171
|
end
|
|
165
|
-
|
|
172
|
+
|
|
166
173
|
# Add a block to this method to install a source change handler while parsing,
|
|
167
174
|
# <tt>proc</tt>:: The proc that will be called to handle source changes
|
|
168
175
|
def source_changes=(proc)
|
|
@@ -170,9 +177,9 @@ module RequestLogAnalyzer::Source
|
|
|
170
177
|
end
|
|
171
178
|
|
|
172
179
|
# This method is called by the parser if it encounteres any parsing problems.
|
|
173
|
-
# It will call the installed warning handler if any.
|
|
180
|
+
# It will call the installed warning handler if any.
|
|
174
181
|
#
|
|
175
|
-
# By default, RequestLogAnalyzer::Controller will install a warning handler
|
|
182
|
+
# By default, RequestLogAnalyzer::Controller will install a warning handler
|
|
176
183
|
# that will pass the warnings to each aggregator so they can do something useful
|
|
177
184
|
# with it.
|
|
178
185
|
#
|
|
@@ -184,24 +191,24 @@ module RequestLogAnalyzer::Source
|
|
|
184
191
|
|
|
185
192
|
protected
|
|
186
193
|
|
|
187
|
-
# Combines the different lines of a request into a single Request object. It will start a
|
|
188
|
-
# new request when a header line is encountered en will emit the request when a footer line
|
|
194
|
+
# Combines the different lines of a request into a single Request object. It will start a
|
|
195
|
+
# new request when a header line is encountered en will emit the request when a footer line
|
|
189
196
|
# is encountered.
|
|
190
197
|
#
|
|
191
198
|
# Combining the lines is done using heuristics. Problems can occur in this process. The
|
|
192
199
|
# current parse strategy defines how these cases are handled.
|
|
193
200
|
#
|
|
194
201
|
# When using the 'assume-correct' parse strategy (default):
|
|
195
|
-
# - Every line that is parsed before a header line is ignored as it cannot be included in
|
|
202
|
+
# - Every line that is parsed before a header line is ignored as it cannot be included in
|
|
196
203
|
# any request. It will emit a :no_current_request warning.
|
|
197
|
-
# - If a header line is found before the previous requests was closed, the previous request
|
|
204
|
+
# - If a header line is found before the previous requests was closed, the previous request
|
|
198
205
|
# will be yielded and a new request will be started.
|
|
199
206
|
#
|
|
200
207
|
# When using the 'cautious' parse strategy:
|
|
201
|
-
# - Every line that is parsed before a header line is ignored as it cannot be included in
|
|
208
|
+
# - Every line that is parsed before a header line is ignored as it cannot be included in
|
|
202
209
|
# any request. It will emit a :no_current_request warning.
|
|
203
210
|
# - A header line that is parsed before a request is closed by a footer line, is a sign of
|
|
204
|
-
# an unproperly ordered file. All data that is gathered for the request until then is
|
|
211
|
+
# an unproperly ordered file. All data that is gathered for the request until then is
|
|
205
212
|
# discarded and the next request is ignored as well. An :unclosed_request warning is
|
|
206
213
|
# emitted.
|
|
207
214
|
#
|
|
@@ -228,7 +235,7 @@ module RequestLogAnalyzer::Source
|
|
|
228
235
|
@current_request << request_data
|
|
229
236
|
if footer_line?(request_data)
|
|
230
237
|
handle_request(@current_request, &block) # yield @current_request
|
|
231
|
-
@current_request = nil
|
|
238
|
+
@current_request = nil
|
|
232
239
|
end
|
|
233
240
|
else
|
|
234
241
|
@skipped_lines += 1
|
|
@@ -236,8 +243,8 @@ module RequestLogAnalyzer::Source
|
|
|
236
243
|
end
|
|
237
244
|
end
|
|
238
245
|
end
|
|
239
|
-
|
|
240
|
-
# Handles the parsed request by sending it into the pipeline.
|
|
246
|
+
|
|
247
|
+
# Handles the parsed request by sending it into the pipeline.
|
|
241
248
|
#
|
|
242
249
|
# - It will call RequestLogAnalyzer::Request#validate on the request instance
|
|
243
250
|
# - It will send the request into the pipeline, checking whether it was accepted by all the filters.
|
|
@@ -249,19 +256,19 @@ module RequestLogAnalyzer::Source
|
|
|
249
256
|
request.validate
|
|
250
257
|
accepted = block_given? ? yield(request) : true
|
|
251
258
|
@skipped_requests += 1 unless accepted
|
|
252
|
-
end
|
|
259
|
+
end
|
|
253
260
|
|
|
254
261
|
# Checks whether a given line hash is a header line according to the current file format.
|
|
255
|
-
# <tt>hash</tt>:: A hash of data that was parsed from the line.
|
|
262
|
+
# <tt>hash</tt>:: A hash of data that was parsed from the line.
|
|
256
263
|
def header_line?(hash)
|
|
257
264
|
hash[:line_definition].header
|
|
258
265
|
end
|
|
259
266
|
|
|
260
|
-
# Checks whether a given line hash is a footer line according to the current file format.
|
|
261
|
-
# <tt>hash</tt>:: A hash of data that was parsed from the line.
|
|
267
|
+
# Checks whether a given line hash is a footer line according to the current file format.
|
|
268
|
+
# <tt>hash</tt>:: A hash of data that was parsed from the line.
|
|
262
269
|
def footer_line?(hash)
|
|
263
270
|
hash[:line_definition].footer
|
|
264
|
-
end
|
|
271
|
+
end
|
|
265
272
|
end
|
|
266
273
|
|
|
267
274
|
end
|