request-log-analyzer 1.3.4 → 1.3.5
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/request_log_analyzer/aggregator/database_inserter.rb +8 -5
- data/lib/request_log_analyzer/database/base.rb +3 -2
- data/lib/request_log_analyzer/file_format/amazon_s3.rb +6 -5
- data/lib/request_log_analyzer/file_format/apache.rb +18 -10
- data/lib/request_log_analyzer/file_format/rack.rb +11 -0
- data/lib/request_log_analyzer/output/fixed_width.rb +8 -7
- data/lib/request_log_analyzer/request.rb +18 -6
- data/lib/request_log_analyzer/tracker/duration.rb +3 -3
- data/lib/request_log_analyzer/tracker/traffic.rb +186 -0
- data/lib/request_log_analyzer.rb +3 -3
- data/request-log-analyzer.gemspec +4 -4
- data/spec/lib/mocks.rb +5 -2
- data/spec/unit/file_format/apache_format_spec.rb +38 -1
- data/spec/unit/tracker/traffic_tracker_spec.rb +105 -0
- metadata +8 -4
@@ -57,11 +57,14 @@ module RequestLogAnalyzer::Aggregator
|
|
57
57
|
|
58
58
|
# Records source changes in the sources table
|
59
59
|
def source_change(change, filename)
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
60
|
+
if File.exist?(filename)
|
61
|
+
case change
|
62
|
+
when :started
|
63
|
+
p database.source_class
|
64
|
+
@sources[filename] = database.source_class.create!(:filename => filename)
|
65
|
+
when :finished
|
66
|
+
@sources[filename].update_attributes!(:filesize => File.size(filename), :mtime => File.mtime(filename))
|
67
|
+
end
|
65
68
|
end
|
66
69
|
end
|
67
70
|
|
@@ -13,8 +13,9 @@ class RequestLogAnalyzer::Database::Base < ActiveRecord::Base
|
|
13
13
|
def line_type
|
14
14
|
self.class.name.underscore.gsub(/_line$/, '').to_sym
|
15
15
|
end
|
16
|
-
|
17
|
-
|
16
|
+
|
17
|
+
class_inheritable_accessor :line_definition
|
18
|
+
cattr_accessor :database
|
18
19
|
|
19
20
|
def self.subclass_from_line_definition(definition)
|
20
21
|
klass = Class.new(RequestLogAnalyzer::Database::Base)
|
@@ -9,7 +9,7 @@ module RequestLogAnalyzer::FileFormat
|
|
9
9
|
line_definition :access do |line|
|
10
10
|
line.header = true
|
11
11
|
line.footer = true
|
12
|
-
line.regexp = /^([^\ ]+) ([^\ ]+) \[([
|
12
|
+
line.regexp = /^([^\ ]+) ([^\ ]+) \[(\d{2}\/[A-Za-z]{3}\/\d{4}.\d{2}:\d{2}:\d{2})(?: .\d{4})?\] (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) ([^\ ]+) ([^\ ]+) (\w+(?:\.\w+)*) ([^\ ]+) "([^"]+)" (\d+) ([^\ ]+) (\d+) (\d+) (\d+) (\d+) "([^"]+)" "([^"]+)"/
|
13
13
|
line.captures << { :name => :bucket_owner, :type => :string } <<
|
14
14
|
{ :name => :bucket, :type => :string } <<
|
15
15
|
{ :name => :timestamp, :type => :timestamp } <<
|
@@ -21,8 +21,8 @@ module RequestLogAnalyzer::FileFormat
|
|
21
21
|
{ :name => :request_uri, :type => :string } <<
|
22
22
|
{ :name => :http_status, :type => :integer } <<
|
23
23
|
{ :name => :error_code, :type => :nillable_string } <<
|
24
|
-
{ :name => :bytes_sent, :type => :
|
25
|
-
{ :name => :object_size, :type => :
|
24
|
+
{ :name => :bytes_sent, :type => :traffic, :unit => :byte } <<
|
25
|
+
{ :name => :object_size, :type => :traffic, :unit => :byte } <<
|
26
26
|
{ :name => :total_time, :type => :duration, :unit => :msec } <<
|
27
27
|
{ :name => :turnaround_time, :type => :duration, :unit => :msec } <<
|
28
28
|
{ :name => :referer, :type => :referer } <<
|
@@ -34,7 +34,8 @@ module RequestLogAnalyzer::FileFormat
|
|
34
34
|
analyze.hourly_spread
|
35
35
|
|
36
36
|
analyze.frequency :category => lambda { |r| "#{r[:bucket]}/#{r[:key]}"}, :amount => 20, :title => "Most popular items"
|
37
|
-
analyze.duration :duration => :total_time, :category => lambda { |r| "#{r[:bucket]}/#{r[:key]}"}, :amount => 20, :title => "
|
37
|
+
analyze.duration :duration => :total_time, :category => lambda { |r| "#{r[:bucket]}/#{r[:key]}"}, :amount => 20, :title => "Request duration"
|
38
|
+
analyze.traffic :traffic => :bytes_sent, :category => lambda { |r| "#{r[:bucket]}/#{r[:key]}"}, :amount => 20, :title => "Traffic"
|
38
39
|
analyze.frequency :category => :http_status, :title => 'HTTP status codes'
|
39
40
|
analyze.frequency :category => :error_code, :title => 'Error codes'
|
40
41
|
end
|
@@ -47,7 +48,7 @@ module RequestLogAnalyzer::FileFormat
|
|
47
48
|
# Do not use DateTime.parse, but parse the timestamp ourselves to return a integer
|
48
49
|
# to speed up parsing.
|
49
50
|
def convert_timestamp(value, definition)
|
50
|
-
d = /^(\d{2})\/(
|
51
|
+
d = /^(\d{2})\/([A-Za-z]{3})\/(\d{4}).(\d{2}):(\d{2}):(\d{2})/.match(value).captures
|
51
52
|
"#{d[2]}#{MONTHS[d[1]]}#{d[0]}#{d[3]}#{d[4]}#{d[5]}".to_i
|
52
53
|
end
|
53
54
|
|
@@ -17,26 +17,30 @@ module RequestLogAnalyzer::FileFormat
|
|
17
17
|
# A hash of predefined Apache log formats
|
18
18
|
LOG_FORMAT_DEFAULTS = {
|
19
19
|
:common => '%h %l %u %t "%r" %>s %b',
|
20
|
-
:combined => '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'
|
20
|
+
:combined => '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"',
|
21
|
+
:rack => '%h %l %u %t "%r" %>s %b %T',
|
22
|
+
:referer => '%{Referer}i -> %U',
|
23
|
+
:agent => '%{User-agent}i'
|
21
24
|
}
|
22
|
-
|
25
|
+
|
23
26
|
# A hash that defines how the log format directives should be parsed.
|
24
27
|
LOG_DIRECTIVES = {
|
25
28
|
'%' => { :regexp => '%', :captures => [] },
|
26
29
|
'h' => { :regexp => '([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+)', :captures => [{:name => :remote_host, :type => :string}] },
|
27
30
|
'a' => { :regexp => '(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', :captures => [{:name => :remote_ip, :type => :string}] },
|
28
|
-
'b' => { :regexp => '(\d+|-)', :captures => [{:name => :bytes_sent, :type => :
|
31
|
+
'b' => { :regexp => '(\d+|-)', :captures => [{:name => :bytes_sent, :type => :traffic}] },
|
29
32
|
'c' => { :regexp => '(\+|\-|\X)', :captures => [{:name => :connection_status, :type => :integer}] },
|
30
|
-
'D' => { :regexp => '(\d+|-)', :captures => [{:name => :duration, :type => :duration, :unit => :
|
33
|
+
'D' => { :regexp => '(\d+|-)', :captures => [{:name => :duration, :type => :duration, :unit => :musec}] },
|
31
34
|
'l' => { :regexp => '([\w-]+)', :captures => [{:name => :remote_logname, :type => :nillable_string}] },
|
32
|
-
'T' => { :regexp => '(
|
33
|
-
't' => { :regexp => '\[([
|
35
|
+
'T' => { :regexp => '((?:\d+(?:\.\d+))|-)', :captures => [{:name => :duration, :type => :duration, :unit => :sec}] },
|
36
|
+
't' => { :regexp => '\[(\d{2}\/[A-Za-z]{3}\/\d{4}.\d{2}:\d{2}:\d{2})(?: .\d{4})?\]', :captures => [{:name => :timestamp, :type => :timestamp}] },
|
34
37
|
's' => { :regexp => '(\d{3})', :captures => [{:name => :http_status, :type => :integer}] },
|
35
38
|
'u' => { :regexp => '(\w+|-)', :captures => [{:name => :user, :type => :nillable_string}] },
|
36
|
-
'
|
39
|
+
'U' => { :regexp => '(\/\S*)', :captures => [{:name => :path, :type => :string}] },
|
40
|
+
'r' => { :regexp => '([A-Z]+) (\S+) HTTP\/(\d+(?:\.\d+)*)', :captures => [{:name => :http_method, :type => :string},
|
37
41
|
{:name => :path, :type => :path}, {:name => :http_version, :type => :string}]},
|
38
|
-
'i' => { 'Referer' => { :regexp => '(
|
39
|
-
'User-agent' => { :regexp => '(.*)',
|
42
|
+
'i' => { 'Referer' => { :regexp => '(\S+)', :captures => [{:name => :referer, :type => :nillable_string}] },
|
43
|
+
'User-agent' => { :regexp => '(.*)', :captures => [{:name => :user_agent, :type => :user_agent}] }
|
40
44
|
}
|
41
45
|
}
|
42
46
|
|
@@ -97,6 +101,10 @@ module RequestLogAnalyzer::FileFormat
|
|
97
101
|
analyze.duration :duration => :duration, :category => :path , :title => 'Request duration'
|
98
102
|
end
|
99
103
|
|
104
|
+
if line_definition.captures?(:path) && line_definition.captures?(:bytes_sent)
|
105
|
+
analyze.traffic :traffic => :bytes_sent, :category => :path , :title => 'Traffic'
|
106
|
+
end
|
107
|
+
|
100
108
|
return analyze.trackers
|
101
109
|
end
|
102
110
|
|
@@ -109,7 +117,7 @@ module RequestLogAnalyzer::FileFormat
|
|
109
117
|
# Do not use DateTime.parse, but parse the timestamp ourselves to return a integer
|
110
118
|
# to speed up parsing.
|
111
119
|
def convert_timestamp(value, definition)
|
112
|
-
d = /^(\d{2})\/(
|
120
|
+
d = /^(\d{2})\/([A-Za-z]{3})\/(\d{4}).(\d{2}):(\d{2}):(\d{2})/.match(value).captures
|
113
121
|
"#{d[2]}#{MONTHS[d[1]]}#{d[0]}#{d[3]}#{d[4]}#{d[5]}".to_i
|
114
122
|
end
|
115
123
|
|
@@ -104,7 +104,7 @@ module RequestLogAnalyzer::Output
|
|
104
104
|
end
|
105
105
|
|
106
106
|
# Write a link
|
107
|
-
# <tt>text</tt> The text in the link
|
107
|
+
# <tt>text</tt> The text in the link, or the URL itself if no text is given
|
108
108
|
# <tt>url</tt> The url to link to.
|
109
109
|
def link(text, url = nil)
|
110
110
|
if url.nil?
|
@@ -114,13 +114,13 @@ module RequestLogAnalyzer::Output
|
|
114
114
|
end
|
115
115
|
end
|
116
116
|
|
117
|
-
# Generate a header for a report
|
117
|
+
# Generate a header for a report
|
118
118
|
def header
|
119
119
|
if io.kind_of?(File)
|
120
|
-
puts "Request-log-analyzer summary report"
|
121
|
-
line
|
120
|
+
puts colorize("Request-log-analyzer summary report", :white, :bold)
|
121
|
+
line(:green)
|
122
122
|
puts "Version #{RequestLogAnalyzer::VERSION} - written by Willem van Bergen and Bart ten Brinke"
|
123
|
-
puts "
|
123
|
+
puts "Website: #{link('http://github.com/wvanbergen/request-log-analyzer')}"
|
124
124
|
end
|
125
125
|
end
|
126
126
|
|
@@ -128,8 +128,9 @@ module RequestLogAnalyzer::Output
|
|
128
128
|
def footer
|
129
129
|
puts
|
130
130
|
puts "Need an expert to analyze your application?"
|
131
|
-
puts "Mail to contact@railsdoctors.com or visit us at http://railsdoctors.com"
|
132
|
-
|
131
|
+
puts "Mail to #{link('contact@railsdoctors.com')} or visit us at #{link('http://railsdoctors.com')}."
|
132
|
+
line(:green)
|
133
|
+
puts "Thanks for using #{colorize('request-log-analyzer', :white, :bold)}!"
|
133
134
|
end
|
134
135
|
|
135
136
|
# Generate a report table and push it into the output object.
|
@@ -41,13 +41,25 @@ module RequestLogAnalyzer
|
|
41
41
|
DateTime.parse(value).strftime('%Y%m%d%H%M%S').to_i unless value.nil?
|
42
42
|
end
|
43
43
|
|
44
|
+
def convert_traffic(value, capture_definition)
|
45
|
+
return nil if value.nil?
|
46
|
+
case capture_definition[:unit]
|
47
|
+
when :GB, :G, :gigabyte then (value.to_f * 1000_000_000).round
|
48
|
+
when :GiB, :gibibyte then (value.to_f * (2 ** 30)).round
|
49
|
+
when :MB, :M, :megabyte then (value.to_f * 1000_000).round
|
50
|
+
when :MiB, :mebibyte then (value.to_f * (2 ** 20)).round
|
51
|
+
when :KB, :K, :kilobyte, :kB then (value.to_f * 1000).round
|
52
|
+
when :KiB, :kibibyte then (value.to_f * (2 ** 10)).round
|
53
|
+
else value.to_i
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
44
57
|
def convert_duration(value, capture_definition)
|
45
|
-
if value.nil?
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
else
|
50
|
-
value.to_f
|
58
|
+
return nil if value.nil?
|
59
|
+
case capture_definition[:unit]
|
60
|
+
when :microsec, :musec then value.to_f / 1000000.0
|
61
|
+
when :msec, :millisec then value.to_f / 1000.0
|
62
|
+
else value.to_f
|
51
63
|
end
|
52
64
|
end
|
53
65
|
end
|
@@ -51,10 +51,10 @@ module RequestLogAnalyzer::Tracker
|
|
51
51
|
raise "Capture mismatch for multiple values in a request"
|
52
52
|
end
|
53
53
|
else
|
54
|
-
category = @categorizer.call(request)
|
55
|
-
duration = @durationizer.call(request)
|
54
|
+
category = @categorizer.call(request)
|
55
|
+
duration = @durationizer.call(request)
|
56
56
|
|
57
|
-
if duration.kind_of?(
|
57
|
+
if duration.kind_of?(Numeric) && !category.nil?
|
58
58
|
@categories[category] ||= {:hits => 0, :cumulative => 0.0, :min => duration, :max => duration }
|
59
59
|
@categories[category][:hits] += 1
|
60
60
|
@categories[category][:cumulative] += duration
|
@@ -0,0 +1,186 @@
|
|
1
|
+
module RequestLogAnalyzer::Tracker
|
2
|
+
|
3
|
+
# Analyze the average and total traffic of requests
|
4
|
+
#
|
5
|
+
# === Options
|
6
|
+
# * <tt>:amount</tt> The amount of lines in the report
|
7
|
+
# * <tt>:category</tt> Proc that handles request categorization for given fileformat (REQUEST_CATEGORIZER)
|
8
|
+
# * <tt>:traffic</tt> The field containing the duration in the request hash.
|
9
|
+
# * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
|
10
|
+
# * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
|
11
|
+
# * <tt>:title</tt> Title do be displayed above the report
|
12
|
+
# * <tt>:unless</tt> Handle request if this proc is false for the handled request.
|
13
|
+
class Traffic < Base
|
14
|
+
|
15
|
+
attr_reader :categories
|
16
|
+
|
17
|
+
# Check if duration and catagory option have been received,
|
18
|
+
def prepare
|
19
|
+
raise "No traffic field set up for category tracker #{self.inspect}" unless options[:traffic]
|
20
|
+
raise "No categorizer set up for duration tracker #{self.inspect}" unless options[:category]
|
21
|
+
|
22
|
+
@categorizer = options[:category].respond_to?(:call) ? options[:category] : lambda { |request| request[options[:category]] }
|
23
|
+
@trafficizer = options[:traffic].respond_to?(:call) ? options[:traffic] : lambda { |request| request[options[:traffic]] }
|
24
|
+
@categories = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get the duration information fron the request and store it in the different categories.
|
28
|
+
# <tt>request</tt> The request.
|
29
|
+
def update(request)
|
30
|
+
category = @categorizer.call(request)
|
31
|
+
traffic = @trafficizer.call(request)
|
32
|
+
|
33
|
+
if traffic.kind_of?(Numeric) && !category.nil?
|
34
|
+
@categories[category] ||= {:hits => 0, :cumulative => 0, :min => traffic, :max => traffic }
|
35
|
+
@categories[category][:hits] += 1
|
36
|
+
@categories[category][:cumulative] += traffic
|
37
|
+
@categories[category][:min] = traffic if traffic < @categories[category][:min]
|
38
|
+
@categories[category][:max] = traffic if traffic > @categories[category][:max]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Get the number of hits of a specific category.
|
43
|
+
# <tt>cat</tt> The category
|
44
|
+
def hits(cat)
|
45
|
+
categories[cat][:hits]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Get the total duration of a specific category.
|
49
|
+
# <tt>cat</tt> The category
|
50
|
+
def cumulative_traffic(cat)
|
51
|
+
categories[cat][:cumulative]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get the minimal duration of a specific category.
|
55
|
+
# <tt>cat</tt> The category
|
56
|
+
def min_traffic(cat)
|
57
|
+
categories[cat][:min]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get the maximum duration of a specific category.
|
61
|
+
# <tt>cat</tt> The category
|
62
|
+
def max_traffic(cat)
|
63
|
+
categories[cat][:max]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get the average duration of a specific category.
|
67
|
+
# <tt>cat</tt> The category
|
68
|
+
def average_traffic(cat)
|
69
|
+
categories[cat][:cumulative].to_f / categories[cat][:hits]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get the average duration of a all categories.
|
73
|
+
def overall_average_traffic
|
74
|
+
overall_cumulative_duration.to_f / overall_hits
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get the cumlative duration of a all categories.
|
78
|
+
def overall_cumulative_traffic
|
79
|
+
categories.inject(0) { |sum, (name, cat)| sum + cat[:cumulative] }
|
80
|
+
end
|
81
|
+
|
82
|
+
# Get the total hits of a all categories.
|
83
|
+
def overall_hits
|
84
|
+
categories.inject(0) { |sum, (name, cat)| sum + cat[:hits] }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return categories sorted by hits.
|
88
|
+
def sorted_by_hits
|
89
|
+
sorted_by(:hits)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return categories sorted by cumulative duration.
|
93
|
+
def sorted_by_cumulative
|
94
|
+
sorted_by(:cumulative)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return categories sorted by cumulative duration.
|
98
|
+
def sorted_by_average
|
99
|
+
sorted_by { |cat| cat[:cumulative].to_f / cat[:hits] }
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return categories sorted by a given key.
|
103
|
+
# <tt>by</tt> The key.
|
104
|
+
def sorted_by(by = nil)
|
105
|
+
if block_given?
|
106
|
+
categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }
|
107
|
+
else
|
108
|
+
categories.sort { |a, b| b[1][by] <=> a[1][by] }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Block function to build a result table using a provided sorting function.
|
113
|
+
# <tt>output</tt> The output object.
|
114
|
+
# <tt>amount</tt> The number of rows in the report table (default 10).
|
115
|
+
# === Options
|
116
|
+
# * </tt>:title</tt> The title of the table
|
117
|
+
# * </tt>:sort</tt> The key to sort on (:hits, :cumulative, :average, :min or :max)
|
118
|
+
def report_table(output, amount = 10, options = {}, &block)
|
119
|
+
|
120
|
+
output.title(options[:title])
|
121
|
+
|
122
|
+
top_categories = @categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }.slice(0...amount)
|
123
|
+
output.table({:title => 'Category', :width => :rest},
|
124
|
+
{:title => 'Hits', :align => :right, :highlight => (options[:sort] == :hits), :min_width => 4},
|
125
|
+
{:title => 'Cumulative', :align => :right, :highlight => (options[:sort] == :cumulative), :min_width => 10},
|
126
|
+
{:title => 'Average', :align => :right, :highlight => (options[:sort] == :average), :min_width => 8},
|
127
|
+
{:title => 'Min', :align => :right, :highlight => (options[:sort] == :min)},
|
128
|
+
{:title => 'Max', :align => :right, :highlight => (options[:sort] == :max)}) do |rows|
|
129
|
+
|
130
|
+
top_categories.each do |(cat, info)|
|
131
|
+
rows << [cat, info[:hits], format_traffic(info[:cumulative]), format_traffic((info[:cumulative] / info[:hits]).round),
|
132
|
+
format_traffic(info[:min]), format_traffic(info[:max])]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Formats the traffic number using x B/kB/MB/GB etc notation
|
138
|
+
def format_traffic(bytes)
|
139
|
+
return "0 B" if bytes.zero?
|
140
|
+
case Math.log10(bytes).floor
|
141
|
+
when 1...4 then '%d B' % bytes
|
142
|
+
when 4...7 then '%d kB' % (bytes / 1000)
|
143
|
+
when 7...10 then '%d MB' % (bytes / 1000_000)
|
144
|
+
when 10...13 then '%d GB' % (bytes / 1000_000_000)
|
145
|
+
else '%d TB' % (bytes / 1000_000_000_000)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Generate a request duration report to the given output object
|
150
|
+
# By default colulative and average duration are generated.
|
151
|
+
# Any options for the report should have been set during initialize.
|
152
|
+
# <tt>output</tt> The output object
|
153
|
+
def report(output)
|
154
|
+
|
155
|
+
options[:report] ||= [:cumulative, :average]
|
156
|
+
options[:top] ||= 20
|
157
|
+
|
158
|
+
options[:report].each do |report|
|
159
|
+
case report
|
160
|
+
when :average
|
161
|
+
report_table(output, options[:top], :title => "#{title} - top #{options[:top]} by average", :sort => :average) { |cat| cat[:cumulative] / cat[:hits] }
|
162
|
+
when :cumulative
|
163
|
+
report_table(output, options[:top], :title => "#{title} - top #{options[:top]} by sum", :sort => :cumulative) { |cat| cat[:cumulative] }
|
164
|
+
when :hits
|
165
|
+
report_table(output, options[:top], :title => "#{title} - top #{options[:top]} by hits", :sort => :hits) { |cat| cat[:hits] }
|
166
|
+
else
|
167
|
+
raise "Unknown duration report specified: #{report}!"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
output.puts
|
172
|
+
output.puts "#{output.colorize(title, :white, :bold)} - observed total: " + output.colorize(format_traffic(overall_cumulative_traffic), :brown, :bold)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns the title of this tracker for reports
|
176
|
+
def title
|
177
|
+
options[:title] || 'Request traffic'
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns all the categories and the tracked duration as a hash than can be exported to YAML
|
181
|
+
def to_yaml_object
|
182
|
+
return nil if @categories.empty?
|
183
|
+
@categories
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
data/lib/request_log_analyzer.rb
CHANGED
@@ -10,8 +10,8 @@ Encoding.default_external = 'binary' if defined? Encoding and Encoding.respond_t
|
|
10
10
|
module RequestLogAnalyzer
|
11
11
|
|
12
12
|
# The current version of request-log-analyzer.
|
13
|
-
# This will be diplayed in output reports etc.
|
14
|
-
VERSION = "1.3.
|
13
|
+
# This will be diplayed in output reports etc.
|
14
|
+
VERSION = "1.3.5"
|
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.
|
@@ -25,7 +25,7 @@ module RequestLogAnalyzer
|
|
25
25
|
# <tt>const</tt>:: The constant to load from the base constant as a string or symbol. This should be 'Bar' or :Bar when the constant Foo::Bar is being loaded.
|
26
26
|
def self.load_default_class_file(base, const)
|
27
27
|
require "#{to_underscore("#{base.name}::#{const}")}"
|
28
|
-
base.const_get(const)
|
28
|
+
base.const_get(const) if base.const_defined?(const)
|
29
29
|
end
|
30
30
|
|
31
31
|
# Convert a string/symbol in camelcase (RequestLogAnalyzer::Controller) to underscores (request_log_analyzer/controller)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "request-log-analyzer"
|
3
|
-
s.version = "1.3.
|
4
|
-
s.date = "2009-09-
|
3
|
+
s.version = "1.3.5"
|
4
|
+
s.date = "2009-09-16"
|
5
5
|
|
6
6
|
s.rubyforge_project = 'r-l-a'
|
7
7
|
|
@@ -31,6 +31,6 @@ Gem::Specification.new do |s|
|
|
31
31
|
s.email = ['willem@railsdoctors.com', 'bart@railsdoctors.com']
|
32
32
|
s.homepage = 'http://railsdoctors.com'
|
33
33
|
|
34
|
-
s.files = %w(spec/unit/filter/anonymize_filter_spec.rb lib/request_log_analyzer/line_definition.rb lib/request_log_analyzer/output/html.rb lib/request_log_analyzer/controller.rb spec/fixtures/rails_22_cached.log spec/lib/macros.rb lib/request_log_analyzer/file_format/rails_development.rb spec/fixtures/apache_combined.log spec/fixtures/apache_common.log spec/fixtures/merb_prefixed.log lib/request_log_analyzer/file_format/amazon_s3.rb tasks/request_log_analyzer.rake spec/unit/file_format/file_format_api_spec.rb spec/unit/file_format/apache_format_spec.rb spec/integration/command_line_usage_spec.rb lib/request_log_analyzer/database.rb spec/fixtures/decompression.log.bz2 lib/request_log_analyzer/log_processor.rb lib/request_log_analyzer/tracker.rb lib/request_log_analyzer/filter.rb
|
35
|
-
s.test_files = %w(spec/unit/filter/anonymize_filter_spec.rb spec/unit/file_format/file_format_api_spec.rb spec/unit/file_format/apache_format_spec.rb spec/integration/command_line_usage_spec.rb spec/unit/filter/timespan_filter_spec.rb spec/unit/aggregator/database_inserter_spec.rb spec/unit/tracker/tracker_api_spec.rb spec/unit/tracker/duration_tracker_spec.rb spec/unit/file_format/amazon_s3_format_spec.rb spec/unit/controller/log_processor_spec.rb spec/unit/filter/filter_spec.rb spec/unit/filter/field_filter_spec.rb spec/unit/database/base_class_spec.rb spec/unit/tracker/timespan_tracker_spec.rb spec/unit/tracker/hourly_spread_spec.rb spec/unit/file_format/merb_format_spec.rb spec/unit/file_format/line_definition_spec.rb spec/unit/database/connection_spec.rb spec/unit/controller/controller_spec.rb spec/unit/source/request_spec.rb spec/unit/source/log_parser_spec.rb spec/unit/database/database_spec.rb spec/unit/aggregator/summarizer_spec.rb spec/unit/tracker/frequency_tracker_spec.rb spec/unit/file_format/rails_format_spec.rb)
|
34
|
+
s.files = %w(spec/unit/filter/anonymize_filter_spec.rb lib/request_log_analyzer/line_definition.rb lib/request_log_analyzer/output/html.rb lib/request_log_analyzer/controller.rb spec/fixtures/rails_22_cached.log spec/lib/macros.rb lib/request_log_analyzer/file_format/rails_development.rb spec/fixtures/apache_combined.log spec/fixtures/apache_common.log spec/fixtures/merb_prefixed.log lib/request_log_analyzer/file_format/amazon_s3.rb tasks/request_log_analyzer.rake spec/unit/file_format/file_format_api_spec.rb spec/unit/file_format/apache_format_spec.rb spec/integration/command_line_usage_spec.rb lib/request_log_analyzer/database.rb spec/fixtures/decompression.log.bz2 spec/fixtures/rails_unordered.log lib/request_log_analyzer/log_processor.rb lib/request_log_analyzer/tracker.rb lib/request_log_analyzer/filter.rb bin/request-log-analyzer request-log-analyzer.gemspec DESIGN.rdoc spec/unit/filter/timespan_filter_spec.rb spec/unit/aggregator/database_inserter_spec.rb spec/lib/matchers.rb lib/request_log_analyzer/filter/field.rb lib/request_log_analyzer/tracker/frequency.rb spec/fixtures/decompression.log.gz spec/fixtures/decompression.log spec/lib/testing_format.rb spec/fixtures/test_order.log spec/fixtures/rails.db lib/request_log_analyzer/output/fixed_width.rb lib/request_log_analyzer/filter/anonymize.rb lib/request_log_analyzer/tracker/timespan.rb lib/request_log_analyzer/database/base.rb lib/request_log_analyzer/aggregator.rb lib/cli/progressbar.rb lib/request_log_analyzer/mailer.rb README.rdoc spec/fixtures/merb.log lib/request_log_analyzer/tracker/hourly_spread.rb .gitignore spec/unit/tracker/tracker_api_spec.rb spec/unit/tracker/duration_tracker_spec.rb spec/unit/file_format/amazon_s3_format_spec.rb lib/request_log_analyzer/aggregator/echo.rb spec/unit/controller/log_processor_spec.rb spec/spec_helper.rb lib/request_log_analyzer.rb spec/database.yml Rakefile lib/request_log_analyzer/database/connection.rb spec/unit/filter/filter_spec.rb spec/fixtures/test_language_combined.log lib/request_log_analyzer/aggregator/database_inserter.rb lib/request_log_analyzer/aggregator/summarizer.rb lib/request_log_analyzer/file_format/rack.rb lib/request_log_analyzer/file_format/rails.rb spec/fixtures/decompression.tar.gz spec/unit/tracker/traffic_tracker_spec.rb spec/unit/filter/field_filter_spec.rb spec/unit/database/base_class_spec.rb lib/request_log_analyzer/filter/timespan.rb lib/request_log_analyzer/source/log_parser.rb spec/fixtures/decompression.tgz spec/unit/tracker/timespan_tracker_spec.rb spec/unit/tracker/hourly_spread_spec.rb spec/fixtures/header_and_footer.log lib/cli/tools.rb lib/request_log_analyzer/file_format/merb.rb spec/fixtures/multiple_files_1.log spec/unit/file_format/merb_format_spec.rb spec/unit/file_format/line_definition_spec.rb lib/request_log_analyzer/source.rb lib/request_log_analyzer/request.rb lib/cli/database_console.rb spec/unit/database/connection_spec.rb spec/unit/controller/controller_spec.rb spec/lib/mocks.rb spec/lib/helpers.rb lib/cli/database_console_init.rb lib/request_log_analyzer/output.rb lib/request_log_analyzer/file_format/apache.rb spec/fixtures/rails_1x.log spec/fixtures/decompression.log.zip spec/unit/source/request_spec.rb spec/unit/source/log_parser_spec.rb spec/fixtures/test_file_format.log tasks/github-gem.rake spec/unit/database/database_spec.rb lib/request_log_analyzer/tracker/duration.rb lib/request_log_analyzer/tracker/traffic.rb lib/request_log_analyzer/file_format.rb spec/unit/aggregator/summarizer_spec.rb spec/fixtures/syslog_1x.log spec/fixtures/rails_22.log spec/fixtures/multiple_files_2.log LICENSE lib/request_log_analyzer/source/database_loader.rb spec/unit/tracker/frequency_tracker_spec.rb spec/unit/file_format/rails_format_spec.rb lib/cli/command_line_arguments.rb)
|
35
|
+
s.test_files = %w(spec/unit/filter/anonymize_filter_spec.rb spec/unit/file_format/file_format_api_spec.rb spec/unit/file_format/apache_format_spec.rb spec/integration/command_line_usage_spec.rb spec/unit/filter/timespan_filter_spec.rb spec/unit/aggregator/database_inserter_spec.rb spec/unit/tracker/tracker_api_spec.rb spec/unit/tracker/duration_tracker_spec.rb spec/unit/file_format/amazon_s3_format_spec.rb spec/unit/controller/log_processor_spec.rb spec/unit/filter/filter_spec.rb spec/unit/tracker/traffic_tracker_spec.rb spec/unit/filter/field_filter_spec.rb spec/unit/database/base_class_spec.rb spec/unit/tracker/timespan_tracker_spec.rb spec/unit/tracker/hourly_spread_spec.rb spec/unit/file_format/merb_format_spec.rb spec/unit/file_format/line_definition_spec.rb spec/unit/database/connection_spec.rb spec/unit/controller/controller_spec.rb spec/unit/source/request_spec.rb spec/unit/source/log_parser_spec.rb spec/unit/database/database_spec.rb spec/unit/aggregator/summarizer_spec.rb spec/unit/tracker/frequency_tracker_spec.rb spec/unit/file_format/rails_format_spec.rb)
|
36
36
|
end
|
data/spec/lib/mocks.rb
CHANGED
@@ -25,7 +25,7 @@ module RequestLogAnalyzer::Spec::Mocks
|
|
25
25
|
def mock_io
|
26
26
|
mio = mock('IO')
|
27
27
|
mio.stub!(:print)
|
28
|
-
mio.stub!(:puts)
|
28
|
+
mio.stub!(:puts)
|
29
29
|
mio.stub!(:write)
|
30
30
|
return mio
|
31
31
|
end
|
@@ -35,12 +35,15 @@ module RequestLogAnalyzer::Spec::Mocks
|
|
35
35
|
output.stub!(:header)
|
36
36
|
output.stub!(:footer)
|
37
37
|
output.stub!(:puts)
|
38
|
-
output.stub!(:<<)
|
38
|
+
output.stub!(:<<)
|
39
|
+
output.stub!(:colorize).and_return("Fancy text")
|
40
|
+
output.stub!(:link)
|
39
41
|
output.stub!(:title)
|
40
42
|
output.stub!(:line)
|
41
43
|
output.stub!(:with_style)
|
42
44
|
output.stub!(:table).and_yield([])
|
43
45
|
output.stub!(:io).and_return(mock_io)
|
46
|
+
|
44
47
|
return output
|
45
48
|
end
|
46
49
|
|
@@ -99,12 +99,49 @@ describe RequestLogAnalyzer::FileFormat::Apache do
|
|
99
99
|
@log_parser.parse_file(log_fixture(:apache_common)) { counter.hit! }
|
100
100
|
end
|
101
101
|
end
|
102
|
+
|
103
|
+
context '"Rack" access log parser' do
|
104
|
+
before(:each) do
|
105
|
+
@file_format = RequestLogAnalyzer::FileFormat.load(:rack)
|
106
|
+
@log_parser = RequestLogAnalyzer::Source::LogParser.new(@file_format)
|
107
|
+
@sample_1 = '127.0.0.1 - - [16/Sep/2009 06:40:08] "GET /favicon.ico HTTP/1.1" 500 63183 0.0453'
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should create a kind of an Apache file format" do
|
111
|
+
@file_format.should be_kind_of(RequestLogAnalyzer::FileFormat::Apache)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should have a valid language definitions" do
|
115
|
+
@file_format.should be_valid
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should parse a valid access log line" do
|
119
|
+
@file_format.line_definitions[:access].matches(@sample_1).should be_kind_of(Hash)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should not parse a valid access log line" do
|
123
|
+
@file_format.line_definitions[:access].matches('addasdsasadadssadasd').should be_false
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should read the correct values from a valid 404 access log line" do
|
127
|
+
@log_parser.parse_io(StringIO.new(@sample_1)) do |request|
|
128
|
+
request[:remote_host].should == '127.0.0.1'
|
129
|
+
request[:timestamp].should == 20090916064008
|
130
|
+
request[:http_status].should == 500
|
131
|
+
request[:http_method].should == 'GET'
|
132
|
+
request[:http_version].should == '1.1'
|
133
|
+
request[:bytes_sent].should == 63183
|
134
|
+
request[:user].should == nil
|
135
|
+
request[:duration].should == 0.0453
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
102
139
|
|
103
140
|
context '"Combined" access log parsing' do
|
104
141
|
|
105
142
|
before(:all) do
|
106
143
|
@file_format = RequestLogAnalyzer::FileFormat.load(:apache, :combined)
|
107
|
-
@log_parser
|
144
|
+
@log_parser = RequestLogAnalyzer::Source::LogParser.new(@file_format)
|
108
145
|
@sample_1 = '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)"'
|
109
146
|
@sample_2 = '10.0.1.1 - - [02/Sep/2009:05:08:33 +0200] "GET / HTTP/1.1" 200 30 "-" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-us) AppleWebKit/531.9 (KHTML, like Gecko) Version/4.0.3 Safari/531.9"'
|
110
147
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../spec_helper'
|
2
|
+
|
3
|
+
describe RequestLogAnalyzer::Tracker::Traffic do
|
4
|
+
|
5
|
+
describe '#update' do
|
6
|
+
|
7
|
+
context 'using a field-based category' do
|
8
|
+
before(:each) do
|
9
|
+
@tracker = RequestLogAnalyzer::Tracker::Traffic.new(:traffic => :traffic, :category => :category)
|
10
|
+
@tracker.prepare
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should register a request in the right category" do
|
14
|
+
@tracker.update(request(:category => 'a', :traffic => 200))
|
15
|
+
@tracker.categories.should include('a')
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should register a hit in the right category" do
|
19
|
+
@tracker.update(request(:category => 'a', :traffic => 1))
|
20
|
+
@tracker.update(request(:category => 'b', :traffic => 2))
|
21
|
+
@tracker.update(request(:category => 'b', :traffic => 3))
|
22
|
+
|
23
|
+
@tracker.hits('a').should == 1
|
24
|
+
@tracker.hits('b').should == 2
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should sum the traffics of the same category as cumulative traffic" do
|
28
|
+
@tracker.update(request(:category => 'a', :traffic => 1))
|
29
|
+
@tracker.update(request(:category => 'b', :traffic => 2))
|
30
|
+
@tracker.update(request(:category => 'b', :traffic => 3))
|
31
|
+
|
32
|
+
@tracker.cumulative_traffic('a').should == 1
|
33
|
+
@tracker.cumulative_traffic('b').should == 5
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should calculate the average traffic correctly" do
|
37
|
+
@tracker.update(request(:category => 'a', :traffic => 1))
|
38
|
+
@tracker.update(request(:category => 'b', :traffic => 2))
|
39
|
+
@tracker.update(request(:category => 'b', :traffic => 3))
|
40
|
+
|
41
|
+
@tracker.average_traffic('a').should == 1.0
|
42
|
+
@tracker.average_traffic('b').should == 2.5
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should set min and max traffic correctly" do
|
46
|
+
@tracker.update(request(:category => 'a', :traffic => 1))
|
47
|
+
@tracker.update(request(:category => 'b', :traffic => 2))
|
48
|
+
@tracker.update(request(:category => 'b', :traffic => 3))
|
49
|
+
|
50
|
+
@tracker.min_traffic('b').should == 2
|
51
|
+
@tracker.max_traffic('b').should == 3
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'using a dynamic category' do
|
57
|
+
before(:each) do
|
58
|
+
@categorizer = Proc.new { |request| request[:traffic] < 2 ? 'few' : 'lots' }
|
59
|
+
@tracker = RequestLogAnalyzer::Tracker::Traffic.new(:traffic => :traffic, :category => @categorizer)
|
60
|
+
@tracker.prepare
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should use the categorizer to determine the right category" do
|
64
|
+
@tracker.update(request(:category => 'a', :traffic => 1))
|
65
|
+
@tracker.update(request(:category => 'b', :traffic => 2))
|
66
|
+
@tracker.update(request(:category => 'b', :traffic => 3))
|
67
|
+
|
68
|
+
@tracker.categories.should include('few', 'lots')
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should use the categorizer to aggregate the values correctly" do
|
72
|
+
@tracker.update(request(:category => 'a', :traffic => 1))
|
73
|
+
@tracker.update(request(:category => 'b', :traffic => 2))
|
74
|
+
@tracker.update(request(:category => 'b', :traffic => 3))
|
75
|
+
|
76
|
+
@tracker.max_traffic('few').should == 1
|
77
|
+
@tracker.min_traffic('lots').should == 2
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#report' do
|
83
|
+
before(:each) do
|
84
|
+
@tracker = RequestLogAnalyzer::Tracker::Traffic.new(:category => :category, :traffic => :traffic)
|
85
|
+
@tracker.prepare
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should generate a report without errors when one category is present" do
|
89
|
+
@tracker.update(request(:category => 'a', :traffic => 2))
|
90
|
+
@tracker.report(mock_output)
|
91
|
+
lambda { @tracker.report(mock_output) }.should_not raise_error
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should generate a report without errors when no category is present" do
|
95
|
+
lambda { @tracker.report(mock_output) }.should_not raise_error
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should generate a report without errors when multiple categories are present" do
|
99
|
+
@tracker.update(request(:category => 'a', :traffic => 2))
|
100
|
+
@tracker.update(request(:category => 'b', :traffic => 2))
|
101
|
+
lambda { @tracker.report(mock_output) }.should_not raise_error
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
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.3.
|
4
|
+
version: 1.3.5
|
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-16 00:00:00 +02:00
|
14
14
|
default_executable: request-log-analyzer
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -61,10 +61,10 @@ files:
|
|
61
61
|
- spec/integration/command_line_usage_spec.rb
|
62
62
|
- lib/request_log_analyzer/database.rb
|
63
63
|
- spec/fixtures/decompression.log.bz2
|
64
|
+
- spec/fixtures/rails_unordered.log
|
64
65
|
- lib/request_log_analyzer/log_processor.rb
|
65
66
|
- lib/request_log_analyzer/tracker.rb
|
66
67
|
- lib/request_log_analyzer/filter.rb
|
67
|
-
- spec/fixtures/rails_unordered.log
|
68
68
|
- bin/request-log-analyzer
|
69
69
|
- request-log-analyzer.gemspec
|
70
70
|
- DESIGN.rdoc
|
@@ -103,8 +103,10 @@ files:
|
|
103
103
|
- spec/fixtures/test_language_combined.log
|
104
104
|
- lib/request_log_analyzer/aggregator/database_inserter.rb
|
105
105
|
- lib/request_log_analyzer/aggregator/summarizer.rb
|
106
|
+
- lib/request_log_analyzer/file_format/rack.rb
|
106
107
|
- lib/request_log_analyzer/file_format/rails.rb
|
107
108
|
- spec/fixtures/decompression.tar.gz
|
109
|
+
- spec/unit/tracker/traffic_tracker_spec.rb
|
108
110
|
- spec/unit/filter/field_filter_spec.rb
|
109
111
|
- spec/unit/database/base_class_spec.rb
|
110
112
|
- lib/request_log_analyzer/filter/timespan.rb
|
@@ -136,11 +138,12 @@ files:
|
|
136
138
|
- tasks/github-gem.rake
|
137
139
|
- spec/unit/database/database_spec.rb
|
138
140
|
- lib/request_log_analyzer/tracker/duration.rb
|
141
|
+
- lib/request_log_analyzer/tracker/traffic.rb
|
139
142
|
- lib/request_log_analyzer/file_format.rb
|
140
143
|
- spec/unit/aggregator/summarizer_spec.rb
|
144
|
+
- spec/fixtures/syslog_1x.log
|
141
145
|
- spec/fixtures/rails_22.log
|
142
146
|
- spec/fixtures/multiple_files_2.log
|
143
|
-
- spec/fixtures/syslog_1x.log
|
144
147
|
- LICENSE
|
145
148
|
- lib/request_log_analyzer/source/database_loader.rb
|
146
149
|
- spec/unit/tracker/frequency_tracker_spec.rb
|
@@ -191,6 +194,7 @@ test_files:
|
|
191
194
|
- spec/unit/file_format/amazon_s3_format_spec.rb
|
192
195
|
- spec/unit/controller/log_processor_spec.rb
|
193
196
|
- spec/unit/filter/filter_spec.rb
|
197
|
+
- spec/unit/tracker/traffic_tracker_spec.rb
|
194
198
|
- spec/unit/filter/field_filter_spec.rb
|
195
199
|
- spec/unit/database/base_class_spec.rb
|
196
200
|
- spec/unit/tracker/timespan_tracker_spec.rb
|