request-log-analyzer 1.2.7 → 1.2.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|