request-log-analyzer 1.2.7 → 1.2.8
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/Rakefile +5 -2
- data/bin/request-log-analyzer +2 -0
- data/lib/request_log_analyzer/aggregator/database.rb +18 -4
- data/lib/request_log_analyzer/aggregator/summarizer.rb +1 -1
- data/lib/request_log_analyzer/controller.rb +5 -1
- data/lib/request_log_analyzer/file_format/apache.rb +62 -21
- data/lib/request_log_analyzer/file_format.rb +57 -34
- data/lib/request_log_analyzer/line_definition.rb +14 -6
- data/lib/request_log_analyzer/source/log_parser.rb +12 -17
- data/lib/request_log_analyzer.rb +1 -1
- data/request-log-analyzer.gemspec +2 -2
- data/spec/integration/command_line_usage_spec.rb +24 -20
- data/spec/lib/helpers.rb +1 -1
- data/spec/lib/matchers.rb +3 -3
- data/spec/unit/aggregator/database_spec.rb +12 -1
- data/spec/unit/file_format/apache_format_spec.rb +71 -25
- data/spec/unit/file_format/file_format_api_spec.rb +1 -1
- data/spec/unit/source/log_parser_spec.rb +1 -1
- data/tasks/github-gem.rake +1 -1
- metadata +2 -2
data/Rakefile
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
Dir[File.dirname(__FILE__) + "/tasks/*.rake"].each { |file| load(file) }
|
2
2
|
|
3
|
+
# Create rake tasks for a gem manages by github. The tasks are created in the
|
4
|
+
# gem namespace
|
3
5
|
GithubGem::RakeTasks.new(:gem)
|
4
|
-
|
5
|
-
|
6
|
+
|
7
|
+
# Set the RSpec runner with specdoc output as default task.
|
8
|
+
task :default => "spec:specdoc"
|
data/bin/request-log-analyzer
CHANGED
@@ -21,6 +21,8 @@ begin
|
|
21
21
|
end
|
22
22
|
|
23
23
|
command_line.option(:format, :alias => :f, :default => 'rails')
|
24
|
+
command_line.option(:apache_format)
|
25
|
+
|
24
26
|
command_line.option(:file, :alias => :e)
|
25
27
|
command_line.option(:parse_strategy, :default => 'assume-correct')
|
26
28
|
command_line.option(:dump)
|
@@ -143,10 +143,10 @@ module RequestLogAnalyzer::Aggregator
|
|
143
143
|
|
144
144
|
orm_module.const_set(class_name, klass) unless orm_module.const_defined?(class_name)
|
145
145
|
request_class.send(:has_many, "#{definition.name}_lines".to_sym)
|
146
|
-
end
|
147
|
-
|
148
|
-
# Creates a requests table, in which a record is created for every request.
|
149
|
-
# ActiveRecord::Base class to communicate with this table.
|
146
|
+
end
|
147
|
+
|
148
|
+
# Creates a requests table, in which a record is created for every parsed request.
|
149
|
+
# It also creates an ActiveRecord::Base class to communicate with this table.
|
150
150
|
def create_request_table_and_class
|
151
151
|
connection.create_table("requests") do |t|
|
152
152
|
t.column :first_lineno, :integer
|
@@ -157,6 +157,19 @@ module RequestLogAnalyzer::Aggregator
|
|
157
157
|
@request_class = orm_module.const_get('Request')
|
158
158
|
end
|
159
159
|
|
160
|
+
# Creates a sources table, in which a record is created for every file that is parsed.
|
161
|
+
# It also creates an ActiveRecord::Base ORM class for the table.
|
162
|
+
def create_source_table_and_class
|
163
|
+
connection.create_table('sources') do |t|
|
164
|
+
t.column :filename, :string
|
165
|
+
t.column :mtime, :datetime
|
166
|
+
t.column :filesize, :integer
|
167
|
+
end
|
168
|
+
|
169
|
+
orm_module.const_set('Source', Class.new(orm_module::Base)) unless orm_module.const_defined?('Source')
|
170
|
+
@source_class = orm_module.const_get('Source')
|
171
|
+
end
|
172
|
+
|
160
173
|
# Creates a warnings table and a corresponding Warning class to communicate with this table using ActiveRecord.
|
161
174
|
def create_warning_table_and_class
|
162
175
|
connection.create_table("warnings") do |t|
|
@@ -172,6 +185,7 @@ module RequestLogAnalyzer::Aggregator
|
|
172
185
|
# Creates the database schema and related ActiveRecord::Base subclasses that correspond to the
|
173
186
|
# file format definition. These ORM classes will later be used to create records in the database.
|
174
187
|
def create_database_schema!
|
188
|
+
create_source_table_and_class
|
175
189
|
create_request_table_and_class
|
176
190
|
create_warning_table_and_class
|
177
191
|
|
@@ -24,7 +24,7 @@ module RequestLogAnalyzer::Aggregator
|
|
24
24
|
|
25
25
|
# Include missing trackers through method missing.
|
26
26
|
def method_missing(tracker_method, *args)
|
27
|
-
track(tracker_method, args
|
27
|
+
track(tracker_method, *args)
|
28
28
|
end
|
29
29
|
|
30
30
|
# Track the frequency of a specific category
|
@@ -46,7 +46,11 @@ module RequestLogAnalyzer
|
|
46
46
|
end
|
47
47
|
|
48
48
|
# Create the controller with the correct file format
|
49
|
-
file_format =
|
49
|
+
file_format = if arguments[:apache_format]
|
50
|
+
RequestLogAnalyzer::FileFormat.load(:apache, arguments[:apache_format])
|
51
|
+
else
|
52
|
+
RequestLogAnalyzer::FileFormat.load(arguments[:format])
|
53
|
+
end
|
50
54
|
|
51
55
|
# register sources
|
52
56
|
if arguments.parameters.length == 1
|
@@ -8,28 +8,69 @@ module RequestLogAnalyzer::FileFormat
|
|
8
8
|
|
9
9
|
class Apache < Base
|
10
10
|
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
11
|
+
# A hash of predefined Apache log format strings
|
12
|
+
LOG_FORMAT_DEFAULTS = {
|
13
|
+
:common => '%h %l %u %t "%r" %>s %b',
|
14
|
+
:combined => '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'
|
15
|
+
}
|
16
|
+
|
17
|
+
# A hash that defines how the log format directives should be parsed.
|
18
|
+
LOG_DIRECTIVES = {
|
19
|
+
'h' => { :regexp => '(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', :captures => [{:name => :ip_address, :type => :string}] },
|
20
|
+
't' => { :regexp => '\[([^\]]{26})\]', :captures => [{:name => :timestamp, :type => :timestamp}] },
|
21
|
+
's' => { :regexp => '(\d{3})', :captures => [{:name => :http_status, :type => :integer}] },
|
22
|
+
'r' => { :regexp => '([A-Z]+) ([^\s]+) HTTP\/(\d+(?:\.\d+)*)', :captures => [{:name => :http_method, :type => :string},
|
23
|
+
{:name => :path, :type => :string}, {:name => :http_version, :type => :string}]}
|
24
|
+
}
|
25
|
+
|
26
|
+
# Creates the Apache log format language based on a Apache log format string.
|
27
|
+
# It will set up the line definition and the report trackers according to the Apache access log format,
|
28
|
+
# which should be passed as first argument. By default, is uses the 'combined' log format.
|
29
|
+
def self.create(*args)
|
30
|
+
access_line = access_line_definition(args.first)
|
31
|
+
self.new({ :access => access_line}, report_trackers(access_line))
|
31
32
|
end
|
32
|
-
|
33
|
+
|
34
|
+
# Creates the access log line definition based on the Apache log format string
|
35
|
+
def self.access_line_definition(format_string)
|
36
|
+
format_string ||= :combined
|
37
|
+
format_string = LOG_FORMAT_DEFAULTS[format_string.to_sym] || format_string
|
38
|
+
|
39
|
+
line_regexp = ''
|
40
|
+
captures = []
|
41
|
+
format_string.scan(/([^%]*)(?:%(?:\{([^\}]+)\})?>?([A-Za-z]))?/) do |literal, arg, variable|
|
42
|
+
|
43
|
+
line_regexp << Regexp.quote(literal) # Make sure to parse the literal before the directive
|
44
|
+
if variable
|
45
|
+
# Check if we recognize the log directive
|
46
|
+
if directive = LOG_DIRECTIVES[variable]
|
47
|
+
line_regexp << directive[:regexp] # Parse the value of the directive
|
48
|
+
captures += directive[:captures] # Add the directive's information to the captures
|
49
|
+
else
|
50
|
+
line_regexp << '.*' # Just accept any input for this literal
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return a new line definition object
|
56
|
+
return RequestLogAnalyzer::LineDefinition.new(:access, :regexp => Regexp.new(line_regexp),
|
57
|
+
:captures => captures, :header => true, :footer => true)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Sets up the report trackers according to the access line definition.
|
61
|
+
def self.report_trackers(line_definition)
|
62
|
+
analyze = RequestLogAnalyzer::Aggregator::Summarizer::Definer.new
|
63
|
+
|
64
|
+
analyze.timespan if line_definition.captures?(:timestamp)
|
65
|
+
analyze.hourly_spread if line_definition.captures?(:timestamp)
|
66
|
+
|
67
|
+
analyze.frequency :category => :http_method, :amount => 20, :title => "HTTP methods" if line_definition.captures?(:http_method)
|
68
|
+
analyze.frequency :category => :http_status, :amount => 20, :title => "HTTP statuses" if line_definition.captures?(:http_status)
|
69
|
+
analyze.frequency :category => :path, :amount => 20, :title => "Most popular URIs" if line_definition.captures?(:path)
|
70
|
+
|
71
|
+
return analyze.trackers
|
72
|
+
end
|
73
|
+
|
33
74
|
# Define a custom Request class for the Apache file format to speed up timestamp handling.
|
34
75
|
class Request < RequestLogAnalyzer::Request
|
35
76
|
|
@@ -10,7 +10,7 @@ module RequestLogAnalyzer::FileFormat
|
|
10
10
|
# * A FileFormat class (of which an imstance will be returned)
|
11
11
|
# * A filename (from which the FileFormat class is loaded)
|
12
12
|
# * A symbol of a built-in file format (e.g. :rails)
|
13
|
-
def self.load(file_format)
|
13
|
+
def self.load(file_format, *args)
|
14
14
|
klass = nil
|
15
15
|
if file_format.kind_of?(RequestLogAnalyzer::FileFormat::Base)
|
16
16
|
# this already is a file format! return itself
|
@@ -41,7 +41,7 @@ module RequestLogAnalyzer::FileFormat
|
|
41
41
|
raise "Could not load a file format from #{file_format.inspect}" if klass.nil?
|
42
42
|
raise "Invalid FileFormat class" unless klass.kind_of?(Class) && klass.ancestors.include?(RequestLogAnalyzer::FileFormat::Base)
|
43
43
|
|
44
|
-
@current_file_format = klass.
|
44
|
+
@current_file_format = klass.create(*args) # return an instance of the class
|
45
45
|
end
|
46
46
|
|
47
47
|
# Makes classes aware of a file format by registering the file_format variable
|
@@ -62,11 +62,17 @@ module RequestLogAnalyzer::FileFormat
|
|
62
62
|
# A subclass of this class is instantiated when request-log-analyzer is started and this instance
|
63
63
|
# is shared with all components of the application so they can act on the specifics of the format
|
64
64
|
class Base
|
65
|
-
|
65
|
+
|
66
|
+
attr_reader :line_definitions, :report_trackers
|
67
|
+
|
68
|
+
####################################################################################
|
69
|
+
# CLASS METHODS for format definition
|
70
|
+
####################################################################################
|
71
|
+
|
66
72
|
# Registers the line definer instance for a subclass.
|
67
73
|
def self.inherited(subclass)
|
68
74
|
if subclass.superclass == RequestLogAnalyzer::FileFormat::Base
|
69
|
-
|
75
|
+
|
70
76
|
# Create aline and report definer for this class
|
71
77
|
subclass.class_eval do
|
72
78
|
instance_variable_set(:@line_definer, RequestLogAnalyzer::LineDefinition::Definer.new)
|
@@ -77,32 +83,24 @@ module RequestLogAnalyzer::FileFormat
|
|
77
83
|
# Create a custom Request class for this file format
|
78
84
|
subclass.const_set('Request', Class.new(RequestLogAnalyzer::Request)) unless subclass.const_defined?('Request')
|
79
85
|
else
|
80
|
-
|
86
|
+
|
81
87
|
# Copy the line and report definer from the parent class.
|
82
88
|
subclass.class_eval do
|
83
89
|
instance_variable_set(:@line_definer, superclass.line_definer.clone)
|
84
90
|
instance_variable_set(:@report_definer, superclass.report_definer.clone)
|
85
91
|
class << self; attr_accessor :line_definer, :report_definer; end
|
86
|
-
end
|
87
|
-
|
92
|
+
end
|
93
|
+
|
88
94
|
# Create a custom Request class based on the superclass's Request class
|
89
95
|
subclass.const_set('Request', Class.new(subclass.superclass::Request)) unless subclass.const_defined?('Request')
|
90
96
|
end
|
91
|
-
end
|
92
|
-
|
97
|
+
end
|
98
|
+
|
93
99
|
# Specifies a single line defintions.
|
94
100
|
def self.line_definition(name, &block)
|
95
101
|
@line_definer.send(name, &block)
|
96
102
|
end
|
97
|
-
|
98
|
-
def request_class
|
99
|
-
self.class::Request
|
100
|
-
end
|
101
|
-
|
102
|
-
def request(*hashes)
|
103
|
-
request_class.create(self, *hashes)
|
104
|
-
end
|
105
|
-
|
103
|
+
|
106
104
|
# Specifies multiple line definitions at once using a block
|
107
105
|
def self.format_definition(&block)
|
108
106
|
if block_given?
|
@@ -111,36 +109,61 @@ module RequestLogAnalyzer::FileFormat
|
|
111
109
|
return self.line_definer
|
112
110
|
end
|
113
111
|
end
|
114
|
-
|
112
|
+
|
115
113
|
# Specifies the summary report using a block.
|
116
114
|
def self.report(mode = :append, &block)
|
117
|
-
if mode == :overwrite
|
118
|
-
self.report_definer.reset!
|
119
|
-
end
|
120
|
-
|
115
|
+
self.report_definer.reset! if mode == :overwrite
|
121
116
|
yield(self.report_definer)
|
122
117
|
end
|
123
118
|
|
124
|
-
|
125
|
-
|
126
|
-
|
119
|
+
####################################################################################
|
120
|
+
# Instantiation
|
121
|
+
####################################################################################
|
122
|
+
|
123
|
+
def self.create(*args)
|
124
|
+
# Ignore arguments
|
125
|
+
return self.new(line_definer.line_definitions, report_definer.trackers)
|
127
126
|
end
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
self.class.report_definer.trackers# => rescue []
|
127
|
+
|
128
|
+
def initialize(line_definitions = [], report_trackers = [])
|
129
|
+
@line_definitions, @report_trackers = line_definitions, report_trackers
|
132
130
|
end
|
133
|
-
|
131
|
+
|
132
|
+
####################################################################################
|
133
|
+
# INSTANCE methods
|
134
|
+
####################################################################################
|
135
|
+
|
136
|
+
# Returns the Request class of this file format
|
137
|
+
def request_class
|
138
|
+
self.class::Request
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns a Request instance with the given parsed lines that should be provided as hashes.
|
142
|
+
def request(*hashes)
|
143
|
+
request_class.create(self, *hashes)
|
144
|
+
end
|
145
|
+
|
134
146
|
# Checks whether the line definitions form a valid language.
|
135
147
|
# A file format should have at least a header and a footer line type
|
136
148
|
def valid?
|
137
|
-
line_definitions.
|
149
|
+
line_definitions.any? { |(name, ld)| ld.header } && line_definitions.any? { |(name, ld)| ld.footer }
|
138
150
|
end
|
139
|
-
|
151
|
+
|
152
|
+
# Returns true if this language captures the given symbol in one of its line definitions
|
153
|
+
def captures?(name)
|
154
|
+
line_definitions.any? { |(name, ld)| ld.captures?(name) }
|
155
|
+
end
|
156
|
+
|
140
157
|
# Function that a file format con implement to monkey patch the environment.
|
141
158
|
# * <tt>controller</tt> The environment is provided as a controller instance
|
142
159
|
def setup_environment(controller)
|
143
|
-
|
160
|
+
end
|
161
|
+
|
162
|
+
# Parses a line by trying to parse it using every line definition in this file format
|
163
|
+
def parse_line(line, &warning_handler)
|
164
|
+
request_data = nil
|
165
|
+
self.line_definitions.any? { |lt, definition| request_data = definition.matches(line, &warning_handler) }
|
166
|
+
return request_data
|
144
167
|
end
|
145
168
|
end
|
146
169
|
end
|
@@ -30,6 +30,9 @@ module RequestLogAnalyzer
|
|
30
30
|
attr_accessor :teaser, :regexp, :captures
|
31
31
|
attr_accessor :header, :footer
|
32
32
|
|
33
|
+
alias_method :header?, :header
|
34
|
+
alias_method :footer?, :footer
|
35
|
+
|
33
36
|
# Initializes the LineDefinition instance with a hash containing the different elements of
|
34
37
|
# the definition.
|
35
38
|
def initialize(name, definition = {})
|
@@ -49,13 +52,13 @@ module RequestLogAnalyzer
|
|
49
52
|
# with all the fields parsed from that line as content.
|
50
53
|
# If the line definition has a teaser-check, a :teaser_check_failed warning will be emitted
|
51
54
|
# if this teaser-check is passed, but the full regular exprssion does not ,atch.
|
52
|
-
def matches(line,
|
55
|
+
def matches(line, &warning_handler)
|
53
56
|
if @teaser.nil? || @teaser =~ line
|
54
57
|
if match_data = line.match(@regexp)
|
55
|
-
return { :line_definition => self, :
|
58
|
+
return { :line_definition => self, :captures => match_data.captures}
|
56
59
|
else
|
57
|
-
if @teaser &&
|
58
|
-
|
60
|
+
if @teaser && warning_handler
|
61
|
+
warning_handler.call(:teaser_check_failed, "Teaser matched for #{name.inspect}, but full line did not:\n#{line.inspect}")
|
59
62
|
end
|
60
63
|
return false
|
61
64
|
end
|
@@ -68,8 +71,8 @@ module RequestLogAnalyzer
|
|
68
71
|
|
69
72
|
# matches the line and converts the captured values using the request's
|
70
73
|
# convert_value function.
|
71
|
-
def match_for(line, request,
|
72
|
-
if match_info = matches(line,
|
74
|
+
def match_for(line, request, &warning_handler)
|
75
|
+
if match_info = matches(line, &warning_handler)
|
73
76
|
convert_captured_values(match_info[:captures], request)
|
74
77
|
else
|
75
78
|
false
|
@@ -97,6 +100,11 @@ module RequestLogAnalyzer
|
|
97
100
|
return value_hash
|
98
101
|
end
|
99
102
|
|
103
|
+
# Returns true if this line captures values of the given name
|
104
|
+
def captures?(name)
|
105
|
+
captures.any? { |c| c[:name] == name }
|
106
|
+
end
|
107
|
+
|
100
108
|
end
|
101
109
|
|
102
110
|
end
|
@@ -17,7 +17,7 @@ module RequestLogAnalyzer::Source
|
|
17
17
|
# All available parse strategies.
|
18
18
|
PARSE_STRATEGIES = ['cautious', 'assume-correct']
|
19
19
|
|
20
|
-
attr_reader :source_files
|
20
|
+
attr_reader :source_files, :current_file, :current_lineno
|
21
21
|
|
22
22
|
# Initializes the log file parser instance.
|
23
23
|
# It will apply the language specific FileFormat module to this instance. It will use the line
|
@@ -32,7 +32,8 @@ module RequestLogAnalyzer::Source
|
|
32
32
|
@parsed_requests = 0
|
33
33
|
@skipped_lines = 0
|
34
34
|
@skipped_requests = 0
|
35
|
-
@
|
35
|
+
@current_file = nil
|
36
|
+
@current_lineno = nil
|
36
37
|
@source_files = options[:source_files]
|
37
38
|
|
38
39
|
@options[:parse_strategy] ||= DEFAULT_PARSE_STRATEGY
|
@@ -124,27 +125,21 @@ module RequestLogAnalyzer::Source
|
|
124
125
|
# <tt>options</tt>:: A hash of options that can be used by the parser.
|
125
126
|
def parse_io(io, options = {}, &block) # :yields: request
|
126
127
|
|
127
|
-
@
|
128
|
-
|
129
|
-
|
130
|
-
@progress_handler.call(:progress, @current_io.pos) if @progress_handler && @current_io.kind_of?(File)
|
128
|
+
@current_lineno = 0
|
129
|
+
io.each_line do |line|
|
130
|
+
@progress_handler.call(:progress, io.pos) if @progress_handler && io.kind_of?(File)
|
131
131
|
|
132
|
-
request_data =
|
133
|
-
file_format.line_definitions.each do |line_type, definition|
|
134
|
-
request_data = definition.matches(line, lineno, self)
|
135
|
-
break if request_data
|
136
|
-
end
|
137
|
-
|
138
|
-
if request_data
|
132
|
+
if request_data = file_format.parse_line(line) { |wt, message| warn(wt, message) }
|
139
133
|
@parsed_lines += 1
|
140
|
-
update_current_request(request_data, &block)
|
134
|
+
update_current_request(request_data.merge(:lineno => @current_lineno), &block)
|
141
135
|
end
|
142
|
-
|
136
|
+
|
137
|
+
@current_lineno += 1
|
143
138
|
end
|
144
139
|
|
145
140
|
warn(:unfinished_request_on_eof, "End of file reached, but last request was not completed!") unless @current_request.nil?
|
146
141
|
|
147
|
-
@
|
142
|
+
@current_lineno = nil
|
148
143
|
end
|
149
144
|
|
150
145
|
# Add a block to this method to install a progress handler while parsing.
|
@@ -169,7 +164,7 @@ module RequestLogAnalyzer::Source
|
|
169
164
|
# <tt>type</tt>:: The warning type (a Symbol)
|
170
165
|
# <tt>message</tt>:: A message explaining the warning
|
171
166
|
def warn(type, message)
|
172
|
-
@warning_handler.call(type, message, @
|
167
|
+
@warning_handler.call(type, message, @current_lineno) if @warning_handler
|
173
168
|
end
|
174
169
|
|
175
170
|
protected
|
data/lib/request_log_analyzer.rb
CHANGED
@@ -11,7 +11,7 @@ module RequestLogAnalyzer
|
|
11
11
|
|
12
12
|
# The current version of request-log-analyzer.
|
13
13
|
# This will be diplayed in output reports etc.
|
14
|
-
VERSION = "1.2.
|
14
|
+
VERSION = "1.2.8"
|
15
15
|
|
16
16
|
# Loads constants in the RequestLogAnalyzer namespace using self.load_default_class_file(base, const)
|
17
17
|
# <tt>const</tt>:: The constant that is not yet loaded in the RequestLogAnalyzer namespace. This should be passed as a string or symbol.
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
2
|
|
3
3
|
describe RequestLogAnalyzer, 'running from command line' do
|
4
|
-
|
4
|
+
|
5
5
|
before(:each) do
|
6
6
|
cleanup_temp_files!
|
7
7
|
end
|
@@ -10,27 +10,27 @@ describe RequestLogAnalyzer, 'running from command line' do
|
|
10
10
|
cleanup_temp_files!
|
11
11
|
end
|
12
12
|
|
13
|
-
it "should find 4 requests in default mode" do
|
13
|
+
it "should find 4 requests in default mode" do
|
14
14
|
output = run("#{log_fixture(:rails_1x)}")
|
15
15
|
output.detect { |line| /Parsed requests\:\s*4/ =~ line }.should_not be_nil
|
16
16
|
end
|
17
17
|
|
18
|
-
it "should find 3 requests with a --select option" do
|
18
|
+
it "should find 3 requests with a --select option" do
|
19
19
|
output = run("#{log_fixture(:rails_1x)} --select controller PeopleController")
|
20
20
|
output.detect { |line| /Parsed requests\:\s*4/ =~ line }.should_not be_nil
|
21
21
|
end
|
22
22
|
|
23
|
-
it "should find 1 requests with a --reject option" do
|
23
|
+
it "should find 1 requests with a --reject option" do
|
24
24
|
output = run("#{log_fixture(:rails_1x)} --reject controller PeopleController")
|
25
25
|
output.detect { |line| /Parsed requests\:\s*4/ =~ line }.should_not be_nil
|
26
26
|
end
|
27
|
-
|
28
|
-
it "should write output to a file with the --file option" do
|
27
|
+
|
28
|
+
it "should write output to a file with the --file option" do
|
29
29
|
run("#{log_fixture(:rails_1x)} --file #{temp_output_file(:report)}")
|
30
30
|
File.exist?(temp_output_file(:report)).should be_true
|
31
31
|
end
|
32
32
|
|
33
|
-
it "should write only ASCII characters to a file with the --file option" do
|
33
|
+
it "should write only ASCII characters to a file with the --file option" do
|
34
34
|
run("#{log_fixture(:rails_1x)} --file #{temp_output_file(:report)}")
|
35
35
|
/^[\x00-\x7F]*$/.match(File.read(temp_output_file(:report))).should_not be_nil
|
36
36
|
end
|
@@ -39,31 +39,35 @@ describe RequestLogAnalyzer, 'running from command line' do
|
|
39
39
|
output = run("#{log_fixture(:rails_1x)} --output HTML")
|
40
40
|
output.any? { |line| /<html.*>/ =~ line}
|
41
41
|
end
|
42
|
-
|
43
|
-
it "should run with the --database option" do
|
42
|
+
|
43
|
+
it "should run with the --database option" do
|
44
44
|
run("#{log_fixture(:rails_1x)} --database #{temp_output_file(:database)}")
|
45
45
|
File.exist?(temp_output_file(:database)).should be_true
|
46
46
|
end
|
47
47
|
|
48
|
-
it "should use no colors in the report with the --boring option" do
|
48
|
+
it "should use no colors in the report with the --boring option" do
|
49
49
|
output = run("#{log_fixture(:rails_1x)} --boring")
|
50
50
|
output.any? { |line| /\e/ =~ line }.should be_false
|
51
51
|
end
|
52
|
-
|
53
|
-
it "should use only ASCII characters in the report with the --boring option" do
|
52
|
+
|
53
|
+
it "should use only ASCII characters in the report with the --boring option" do
|
54
54
|
output = run("#{log_fixture(:rails_1x)} --boring")
|
55
55
|
output.all? { |line| /^[\x00-\x7F]*$/ =~ line }.should be_true
|
56
56
|
end
|
57
|
-
|
58
|
-
it "should parse a Merb file if --format
|
57
|
+
|
58
|
+
it "should parse a Merb file if --apache-format is set" do
|
59
59
|
output = run("#{log_fixture(:merb)} --format merb")
|
60
|
-
output.detect { |line| /Parsed requests\:\s*11/ =~ line }.should_not be_nil
|
61
|
-
end
|
62
|
-
|
60
|
+
output.detect { |line| /Parsed requests\:\s*11/ =~ line }.should_not be_nil
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should parse a Apache access log file if --format merb is set" do
|
64
|
+
output = run("#{log_fixture(:apache)} --apache-format combined")
|
65
|
+
output.detect { |line| /Parsed requests\:\s*5/ =~ line }.should_not be_nil
|
66
|
+
end
|
67
|
+
|
63
68
|
it "should dump the results to a YAML file" do
|
64
69
|
run("#{log_fixture(:rails_1x)} --dump #{temp_output_file(:dump)}")
|
65
70
|
File.exist?(temp_output_file(:dump)).should be_true
|
66
71
|
YAML::load(File.read(temp_output_file(:dump))).should have_at_least(1).item
|
67
|
-
end
|
68
|
-
|
69
|
-
end
|
72
|
+
end
|
73
|
+
end
|
data/spec/lib/helpers.rb
CHANGED
data/spec/lib/matchers.rb
CHANGED
@@ -13,9 +13,9 @@ module RequestLogAnalyzer::Spec::Matchers
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def matches?(file_format)
|
16
|
-
if file_format.
|
17
|
-
|
18
|
-
@captures.all? { |c| ld.captures.include?(c) }
|
16
|
+
file_format = file_format.create if file_format.kind_of?(Class)
|
17
|
+
if ld = file_format.line_definitions[@line_type]
|
18
|
+
@captures.all? { |c| ld.captures.map { |cd| cd[:name] }.include?(c) }
|
19
19
|
else
|
20
20
|
false
|
21
21
|
end
|
@@ -183,10 +183,21 @@ describe RequestLogAnalyzer::Aggregator::Database do
|
|
183
183
|
@database_inserter.send :create_database_schema!
|
184
184
|
end
|
185
185
|
|
186
|
-
it "should create a
|
186
|
+
it "should create a Warning class inheriting from ActiveRecord and the base class of the ORM module" do
|
187
187
|
@database_inserter.send :create_database_schema!
|
188
188
|
@database_inserter.warning_class.ancestors.should include(ActiveRecord::Base, @database_inserter.orm_module::Base)
|
189
189
|
end
|
190
|
+
|
191
|
+
it "should create a sources table to track parsed files" do
|
192
|
+
@connection.should_receive(:create_table).with("sources")
|
193
|
+
@database_inserter.send :create_database_schema!
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should create a Source ORM class" do
|
197
|
+
@database_inserter.send :create_database_schema!
|
198
|
+
@database_inserter.orm_module::Source.ancestors.should include(ActiveRecord::Base, @database_inserter.orm_module::Base)
|
199
|
+
end
|
200
|
+
|
190
201
|
|
191
202
|
it "should create a table for every line type" do
|
192
203
|
@database_inserter.should_receive(:create_database_table).with(an_instance_of(RequestLogAnalyzer::LineDefinition)).exactly(@line_type_cnt).times
|
@@ -1,35 +1,81 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/../../spec_helper'
|
2
2
|
|
3
3
|
describe RequestLogAnalyzer::FileFormat::Apache do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
4
|
+
|
5
|
+
describe '.access_line_definition' do
|
6
|
+
before(:each) do
|
7
|
+
@format_string = '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'
|
8
|
+
@line_definition = RequestLogAnalyzer::FileFormat::Apache.access_line_definition(@format_string)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should create a Regexp to match the line" do
|
12
|
+
@line_definition.regexp.should be_kind_of(Regexp)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should create a list of captures for the values in the lines" do
|
16
|
+
@line_definition.captures.should be_kind_of(Array)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should make it a header line" do
|
20
|
+
@line_definition.should be_header
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should make it a footer line" do
|
24
|
+
@line_definition.should be_footer
|
25
|
+
end
|
17
26
|
end
|
18
27
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
28
|
+
describe '.create' do
|
29
|
+
|
30
|
+
before(:each) do
|
31
|
+
@format_string = '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'
|
32
|
+
@format = RequestLogAnalyzer::FileFormat::Apache.create(@format_string)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should create the :access line definition" do
|
36
|
+
@format.should have_line_definition(:access).capturing(:timestamp, :ip_address, :http_method, :path, :http_version, :http_status)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should be a valid file format" do
|
40
|
+
@format.should be_valid
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should setup report trackers" do
|
44
|
+
@format.report_trackers.should_not be_empty
|
26
45
|
end
|
27
46
|
end
|
47
|
+
|
48
|
+
context 'log parsing' do
|
49
|
+
|
50
|
+
before(:each) do
|
51
|
+
@file_format = RequestLogAnalyzer::FileFormat.load(:apache)
|
52
|
+
@log_parser = RequestLogAnalyzer::Source::LogParser.new(@file_format)
|
53
|
+
@sample = '69.41.0.45 - - [02/Sep/2009:12:02:40 +0200] "GET //phpMyAdmin/ HTTP/1.1" 404 209 "-" "Mozilla/4.0 (compatible; MSIE 6.0; Windows 98)"'
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should have a valid language definitions" do
|
57
|
+
@file_format.should be_valid
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should parse a valid access log line" do
|
61
|
+
@file_format.line_definitions[:access].matches(@sample).should be_kind_of(Hash)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should read the correct values from a valid access log line" do
|
65
|
+
@log_parser.parse_io(@sample) do |request|
|
66
|
+
request[:ip_address].should == '69.41.0.45'
|
67
|
+
request[:timestamp].should == 20090902120240
|
68
|
+
request[:http_status].should == 404
|
69
|
+
request[:http_method].should == 'GET'
|
70
|
+
request[:http_version].should == '1.1'
|
71
|
+
end
|
72
|
+
end
|
28
73
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
74
|
+
it "should parse 5 request from fixture access log" do
|
75
|
+
counter = mock('counter')
|
76
|
+
counter.should_receive(:hit!).exactly(5).times
|
77
|
+
@log_parser.parse_file(log_fixture(:apache)) { counter.hit! }
|
78
|
+
end
|
33
79
|
end
|
34
|
-
|
35
80
|
end
|
81
|
+
|
@@ -16,7 +16,7 @@ describe RequestLogAnalyzer::FileFormat do
|
|
16
16
|
|
17
17
|
it "specify lines with a block for the format definition" do
|
18
18
|
@first_file_format.format_definition do |format|
|
19
|
-
format.block_test :regexp => /test (\w+)/, :captures => [:tester]
|
19
|
+
format.block_test :regexp => /test (\w+)/, :captures => [{:name => :tester, :type => :string}]
|
20
20
|
end
|
21
21
|
|
22
22
|
@first_file_format.should have_line_definition(:block_test).capturing(:tester)
|
@@ -71,7 +71,7 @@ end
|
|
71
71
|
describe RequestLogAnalyzer::Source::LogParser, :decompression do
|
72
72
|
|
73
73
|
before(:each) do
|
74
|
-
@log_parser = RequestLogAnalyzer::Source::LogParser.new(RequestLogAnalyzer::FileFormat::Rails.
|
74
|
+
@log_parser = RequestLogAnalyzer::Source::LogParser.new(RequestLogAnalyzer::FileFormat::Rails.create)
|
75
75
|
end
|
76
76
|
|
77
77
|
it "should parse a rails gzipped log file" do
|
data/tasks/github-gem.rake
CHANGED
@@ -203,7 +203,7 @@ module GithubGem
|
|
203
203
|
end
|
204
204
|
|
205
205
|
def rubyforge_release_task
|
206
|
-
sh 'rubyforge', 'add_release', gemspec.rubyforge_project, gemspec.name, gemspec.version, "
|
206
|
+
sh 'rubyforge', 'add_release', gemspec.rubyforge_project, gemspec.name, gemspec.version.to_s, "pkg/#{gemspec.name}-#{gemspec.version}.gem"
|
207
207
|
end
|
208
208
|
|
209
209
|
def release_task
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: request-log-analyzer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Willem van Bergen
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2009-09-
|
13
|
+
date: 2009-09-08 00:00:00 +02:00
|
14
14
|
default_executable: request-log-analyzer
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|