request-log-analyzer 1.7.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -1
- data/DESIGN.rdoc +0 -1
- data/Gemfile +2 -0
- data/README.rdoc +16 -5
- data/bin/request-log-analyzer +4 -2
- data/lib/cli/database_console_init.rb +1 -1
- data/lib/cli/tools.rb +2 -1
- data/lib/mixins/gets_memory_protection.rb +1 -1
- data/lib/request_log_analyzer/aggregator/summarizer.rb +2 -2
- data/lib/request_log_analyzer/aggregator.rb +3 -3
- data/lib/request_log_analyzer/controller.rb +42 -36
- data/lib/request_log_analyzer/database.rb +5 -3
- data/lib/request_log_analyzer/file_format/apache.rb +19 -28
- data/lib/request_log_analyzer/file_format/delayed_job2.rb +38 -0
- data/lib/request_log_analyzer/file_format/delayed_job21.rb +39 -0
- data/lib/request_log_analyzer/file_format/haproxy.rb +138 -0
- data/lib/request_log_analyzer/file_format/oink.rb +96 -0
- data/lib/request_log_analyzer/file_format/postgresql.rb +7 -6
- data/lib/request_log_analyzer/file_format/rails.rb +4 -4
- data/lib/request_log_analyzer/file_format/rails3.rb +34 -11
- data/lib/request_log_analyzer/file_format/w3c.rb +56 -0
- data/lib/request_log_analyzer/file_format.rb +33 -10
- data/lib/request_log_analyzer/filter.rb +3 -5
- data/lib/request_log_analyzer/output/fancy_html.rb +0 -5
- data/lib/request_log_analyzer/output.rb +6 -7
- data/lib/request_log_analyzer/request.rb +17 -3
- data/lib/request_log_analyzer/source/log_parser.rb +3 -1
- data/lib/request_log_analyzer/source.rb +2 -6
- data/lib/request_log_analyzer/tracker/duration.rb +7 -0
- data/lib/request_log_analyzer/tracker/numeric_value.rb +123 -12
- data/lib/request_log_analyzer/tracker/traffic.rb +7 -0
- data/lib/request_log_analyzer/tracker.rb +6 -5
- data/lib/request_log_analyzer.rb +36 -17
- data/request-log-analyzer.gemspec +9 -6
- data/spec/fixtures/oink_22.log +19 -0
- data/spec/fixtures/oink_22_failure.log +17 -0
- data/spec/fixtures/postgresql.log +29 -2980
- data/spec/integration/command_line_usage_spec.rb +4 -4
- data/spec/integration/mailer_spec.rb +20 -4
- data/spec/integration/munin_plugins_rails_spec.rb +1 -1
- data/spec/integration/scout_spec.rb +1 -1
- data/spec/lib/helpers.rb +2 -2
- data/spec/lib/macros.rb +1 -1
- data/spec/lib/matchers.rb +1 -1
- data/spec/lib/mocks.rb +1 -1
- data/spec/spec_helper.rb +10 -10
- data/spec/unit/aggregator/database_inserter_spec.rb +1 -1
- data/spec/unit/aggregator/summarizer_spec.rb +1 -1
- data/spec/unit/controller/controller_spec.rb +1 -1
- data/spec/unit/controller/log_processor_spec.rb +1 -1
- data/spec/unit/database/base_class_spec.rb +1 -1
- data/spec/unit/database/connection_spec.rb +1 -1
- data/spec/unit/database/database_spec.rb +1 -1
- data/spec/unit/file_format/amazon_s3_format_spec.rb +1 -1
- data/spec/unit/file_format/apache_format_spec.rb +18 -1
- data/spec/unit/file_format/common_regular_expressions_spec.rb +1 -1
- data/spec/unit/file_format/delayed_job21_format_spec.rb +60 -0
- data/spec/unit/file_format/delayed_job2_format_spec.rb +60 -0
- data/spec/unit/file_format/delayed_job_format_spec.rb +2 -2
- data/spec/unit/file_format/file_format_api_spec.rb +2 -2
- data/spec/unit/file_format/format_autodetection_spec.rb +7 -7
- data/spec/unit/file_format/haproxy_format_spec.rb +84 -0
- data/spec/unit/file_format/line_definition_spec.rb +1 -1
- data/spec/unit/file_format/merb_format_spec.rb +1 -1
- data/spec/unit/file_format/mysql_format_spec.rb +1 -1
- data/spec/unit/file_format/oink_format_spec.rb +105 -0
- data/spec/unit/file_format/postgresql_format_spec.rb +24 -46
- data/spec/unit/file_format/rack_format_spec.rb +1 -1
- data/spec/unit/file_format/rails3_format_spec.rb +41 -14
- data/spec/unit/file_format/rails_format_spec.rb +11 -1
- data/spec/unit/file_format/w3c_format_spec.rb +46 -0
- data/spec/unit/filter/anonymize_filter_spec.rb +1 -1
- data/spec/unit/filter/field_filter_spec.rb +1 -1
- data/spec/unit/filter/filter_spec.rb +1 -1
- data/spec/unit/filter/timespan_filter_spec.rb +1 -1
- data/spec/unit/mailer_spec.rb +1 -1
- data/spec/unit/request_spec.rb +1 -1
- data/spec/unit/source/log_parser_spec.rb +1 -1
- data/spec/unit/tracker/duration_tracker_spec.rb +4 -2
- data/spec/unit/tracker/frequency_tracker_spec.rb +1 -1
- data/spec/unit/tracker/hourly_spread_spec.rb +1 -1
- data/spec/unit/tracker/numeric_value_tracker_spec.rb +38 -17
- data/spec/unit/tracker/timespan_tracker_spec.rb +1 -1
- data/spec/unit/tracker/tracker_api_spec.rb +1 -1
- data/spec/unit/tracker/traffic_tracker_spec.rb +1 -1
- data/tasks/github-gem.rake +97 -60
- data/tasks/request_log_analyzer.rake +102 -0
- metadata +198 -153
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module RequestLogAnalyzer::FileFormat
|
|
2
|
+
|
|
3
|
+
class Haproxy < RequestLogAnalyzer::FileFormat::Base
|
|
4
|
+
|
|
5
|
+
extend CommonRegularExpressions
|
|
6
|
+
|
|
7
|
+
# Define line types
|
|
8
|
+
line_definition :haproxy do |line|
|
|
9
|
+
line.header = true
|
|
10
|
+
line.footer = true
|
|
11
|
+
|
|
12
|
+
line.regexp = %r{
|
|
13
|
+
(#{ip_address}):\d+\s # client_ip ':' client_port
|
|
14
|
+
\[(#{timestamp('%d/%b/%Y:%H:%M:%S')})\.\d{3}\]\s # '[' accept_date ']'
|
|
15
|
+
(\S+)\s # frontend_name
|
|
16
|
+
(\S+)\/(\S+)\s # backend_name '/' server_name
|
|
17
|
+
(\d+|-1)\/(\d+|-1)\/(\d+|-1)\/(\d+|-1)\/\+?(\d+)\s # Tq '/' Tw '/' Tc '/' Tr '/' Tt
|
|
18
|
+
(\d+)\s # status_code
|
|
19
|
+
\+?(\d+)\s # bytes_read
|
|
20
|
+
(\S+)\s # captured_request_cookie
|
|
21
|
+
(\S+)\s # captured_response_cookie
|
|
22
|
+
(\w|-)(\w|-)(\w|-)(\w|-)\s # termination_state
|
|
23
|
+
(\d+)\/(\d+)\/(\d+)\/(\d+)\/\+?(\d+)\s # actconn '/' feconn '/' beconn '/' srv_conn '/' retries
|
|
24
|
+
(\d+)\/(\d+)\s # srv_queue '/' backend_queue
|
|
25
|
+
(\S*)\s? # captured_request_headers
|
|
26
|
+
(\S*)\s? # captured_response_headers
|
|
27
|
+
"([^"]*)" # '"' http_request '"'
|
|
28
|
+
}x
|
|
29
|
+
|
|
30
|
+
line.capture(:client_ip).as(:string)
|
|
31
|
+
line.capture(:accept_date).as(:timestamp)
|
|
32
|
+
line.capture(:frontend_name).as(:string)
|
|
33
|
+
line.capture(:backend_name).as(:string)
|
|
34
|
+
line.capture(:server_name).as(:string)
|
|
35
|
+
line.capture(:tq).as(:nillable_duration, :unit => :msec)
|
|
36
|
+
line.capture(:tw).as(:nillable_duration, :unit => :msec)
|
|
37
|
+
line.capture(:tc).as(:nillable_duration, :unit => :msec)
|
|
38
|
+
line.capture(:tr).as(:nillable_duration, :unit => :msec)
|
|
39
|
+
line.capture(:tt).as(:duration, :unit => :msec)
|
|
40
|
+
line.capture(:status_code).as(:integer)
|
|
41
|
+
line.capture(:bytes_read).as(:traffic, :unit => :byte)
|
|
42
|
+
line.capture(:captured_request_cookie).as(:nillable_string)
|
|
43
|
+
line.capture(:captured_response_cookie).as(:nillable_string)
|
|
44
|
+
line.capture(:termination_event_code).as(:nillable_string)
|
|
45
|
+
line.capture(:terminated_session_state).as(:nillable_string)
|
|
46
|
+
line.capture(:clientside_persistence_cookie).as(:nillable_string)
|
|
47
|
+
line.capture(:serverside_persistence_cookie).as(:nillable_string)
|
|
48
|
+
line.capture(:actconn).as(:integer)
|
|
49
|
+
line.capture(:feconn).as(:integer)
|
|
50
|
+
line.capture(:beconn).as(:integer)
|
|
51
|
+
line.capture(:srv_conn).as(:integer)
|
|
52
|
+
line.capture(:retries).as(:integer)
|
|
53
|
+
line.capture(:srv_queue).as(:integer)
|
|
54
|
+
line.capture(:backend_queue).as(:integer)
|
|
55
|
+
line.capture(:captured_request_headers).as(:nillable_string)
|
|
56
|
+
line.capture(:captured_response_headers).as(:nillable_string)
|
|
57
|
+
line.capture(:http_request).as(:nillable_string)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Define the summary report
|
|
61
|
+
report do |analyze|
|
|
62
|
+
analyze.hourly_spread :field => :accept_date
|
|
63
|
+
|
|
64
|
+
analyze.frequency :client_ip,
|
|
65
|
+
:title => "Hits per IP"
|
|
66
|
+
|
|
67
|
+
analyze.frequency :frontend_name,
|
|
68
|
+
:title => "Hits per frontend service"
|
|
69
|
+
|
|
70
|
+
analyze.frequency :backend_name,
|
|
71
|
+
:title => "Hits per backend service"
|
|
72
|
+
|
|
73
|
+
analyze.frequency :server_name,
|
|
74
|
+
:title => "Hits per backend server"
|
|
75
|
+
|
|
76
|
+
analyze.frequency :status_code,
|
|
77
|
+
:title => "HTTP response code frequency"
|
|
78
|
+
|
|
79
|
+
analyze.frequency :http_request,
|
|
80
|
+
:title => "Most popular requests"
|
|
81
|
+
|
|
82
|
+
analyze.frequency :http_request,
|
|
83
|
+
:title => "Most frequent HTTP 40x errors",
|
|
84
|
+
:category => lambda { |r| "#{r[:http_request]}"},
|
|
85
|
+
:if => lambda { |r| r[:status_code] >= 400 and r[:status_code] <= 417 }
|
|
86
|
+
|
|
87
|
+
analyze.frequency :http_request,
|
|
88
|
+
:title => "Most frequent HTTP 50x errors",
|
|
89
|
+
:category => lambda { |r| "#{r[:http_request]}"},
|
|
90
|
+
:if => lambda { |r| r[:status_code] >= 500 and r[:status_code] <= 505 }
|
|
91
|
+
|
|
92
|
+
analyze.traffic :bytes_read,
|
|
93
|
+
:title => "Traffic per frontend service",
|
|
94
|
+
:category => lambda { |r| "#{r[:frontend_name]}"}
|
|
95
|
+
|
|
96
|
+
analyze.traffic :bytes_read,
|
|
97
|
+
:title => "Traffic per backend service",
|
|
98
|
+
:category => lambda { |r| "#{r[:backend_name]}"}
|
|
99
|
+
|
|
100
|
+
analyze.traffic :bytes_read,
|
|
101
|
+
:title => "Traffic per backend server",
|
|
102
|
+
:category => lambda { |r| "#{r[:server_name]}"}
|
|
103
|
+
|
|
104
|
+
analyze.duration :tr,
|
|
105
|
+
:title => "Time waiting for backend response",
|
|
106
|
+
:category => lambda { |r| "#{r[:http_request]}"}
|
|
107
|
+
|
|
108
|
+
analyze.duration :tt,
|
|
109
|
+
:title => "Total time spent on request",
|
|
110
|
+
:category => lambda { |r| "#{r[:http_request]}"}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Define a custom Request class for the HAProxy file format to speed up
|
|
114
|
+
# timestamp handling. Shamelessly copied from apache.rb
|
|
115
|
+
class Request < RequestLogAnalyzer::Request
|
|
116
|
+
|
|
117
|
+
MONTHS = {'Jan' => '01', 'Feb' => '02', 'Mar' => '03', 'Apr' => '04', 'May' => '05', 'Jun' => '06',
|
|
118
|
+
'Jul' => '07', 'Aug' => '08', 'Sep' => '09', 'Oct' => '10', 'Nov' => '11', 'Dec' => '12' }
|
|
119
|
+
|
|
120
|
+
# Do not use DateTime.parse, but parse the timestamp ourselves to return
|
|
121
|
+
# a integer to speed up parsing.
|
|
122
|
+
def convert_timestamp(value, definition)
|
|
123
|
+
"#{value[7,4]}#{MONTHS[value[3,3]]}#{value[0,2]}#{value[12,2]}#{value[15,2]}#{value[18,2]}".to_i
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Make sure that the strings '-' or '{}' or '' are parsed as a nil value.
|
|
127
|
+
def convert_nillable_string(value, definition)
|
|
128
|
+
value =~ /-|\{\}|^$/ ? nil : value
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Make sure that -1 is parsed as a nil value.
|
|
132
|
+
def convert_nillable_duration(value, definition)
|
|
133
|
+
value == '-1' ? nil : convert_duration(value, definition)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
class RequestLogAnalyzer::FileFormat::Oink < RequestLogAnalyzer::FileFormat::Rails
|
|
2
|
+
# capture the PID
|
|
3
|
+
HODEL3000_PROCESSING = /\[(\d+)\]: Processing ((?:\w+::)*\w+)#(\w+)(?: to (\w+))? \(for (#{ip_address}) at (#{timestamp('%Y-%m-%d %H:%M:%S')})\) \[([A-Z]+)\]/
|
|
4
|
+
|
|
5
|
+
# TODO: fix me!
|
|
6
|
+
line_definition :processing do |line|
|
|
7
|
+
line.header = true
|
|
8
|
+
line.regexp = Regexp.union(LINE_DEFINITIONS[:processing].regexp,HODEL3000_PROCESSING)
|
|
9
|
+
line.captures = [ { :name => :controller, :type => :string }, # Default Rails Processing
|
|
10
|
+
{ :name => :action, :type => :string },
|
|
11
|
+
{ :name => :format, :type => :string, :default => 'html' },
|
|
12
|
+
{ :name => :ip, :type => :string },
|
|
13
|
+
{ :name => :timestamp, :type => :timestamp },
|
|
14
|
+
{ :name => :method, :type => :string },
|
|
15
|
+
{ :name => :pid, :type => :integer }, # Hodel 3000 Processing w/PID
|
|
16
|
+
{ :name => :controller, :type => :string },
|
|
17
|
+
{ :name => :action, :type => :string },
|
|
18
|
+
{ :name => :format, :type => :string, :default => 'html' },
|
|
19
|
+
{ :name => :ip, :type => :string },
|
|
20
|
+
{ :name => :timestamp, :type => :timestamp },
|
|
21
|
+
{ :name => :method, :type => :string }]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
line_definition :memory_usage do |line|
|
|
25
|
+
line.regexp = /\[(\d+)\]: Memory usage: (\d+)/
|
|
26
|
+
line.capture(:pid).as(:integer)
|
|
27
|
+
line.capture(:memory).as(:traffic)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
line_definition :instance_type_counter do |line|
|
|
31
|
+
line.regexp = /\[(\d+)\]: Instantiation Breakdown: (.*)$/
|
|
32
|
+
line.capture(:pid).as(:integer)
|
|
33
|
+
line.capture(:instance_counts).as(:pipe_separated_counts)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
report(:append) do |analyze|
|
|
37
|
+
analyze.traffic :memory_diff, :category => REQUEST_CATEGORIZER, :title => "Largest Memory Increases", :line_type => :memory_usage
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Keep a record of PIDs and their memory usage when validating requests.
|
|
41
|
+
def pids
|
|
42
|
+
@pids ||= {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Request < RequestLogAnalyzer::FileFormat::Rails::Request
|
|
46
|
+
# Overrides the #validate method to handle PID updating.
|
|
47
|
+
def validate
|
|
48
|
+
update_pids
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Accessor for memory information associated with the specified request PID. If no memory exists
|
|
53
|
+
# for this request's :pid, the memory tracking is initialized.
|
|
54
|
+
def pid_memory
|
|
55
|
+
file_format.pids[self[:pid]] ||= { :last_memory_reading => -1, :current_memory_reading => -1 }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Calculates :memory_diff for each request based on the last completed request that was not a failure.
|
|
59
|
+
def update_pids
|
|
60
|
+
# memory isn't recorded with exceptions. need to set #last_memory_reading+ to -1 as
|
|
61
|
+
# the memory used could have changed. for the next request the memory change will not be recorded.
|
|
62
|
+
#
|
|
63
|
+
# NOTE - the failure regex was not matching with a Rails Development log file.
|
|
64
|
+
if has_line_type?(:failure) and processing = has_line_type?(:processing)
|
|
65
|
+
pid_memory[:last_memory_reading] = -1
|
|
66
|
+
elsif mem_line = has_line_type?(:memory_usage)
|
|
67
|
+
memory_reading = mem_line[:memory]
|
|
68
|
+
pid_memory[:current_memory_reading] = memory_reading
|
|
69
|
+
# calcuate the change in memory
|
|
70
|
+
unless pid_memory[:current_memory_reading] == -1 || pid_memory[:last_memory_reading] == -1
|
|
71
|
+
# logged as kB, need to convert to bytes for the :traffic Tracker
|
|
72
|
+
memory_diff = (pid_memory[:current_memory_reading] - pid_memory[:last_memory_reading])*1024
|
|
73
|
+
if memory_diff > 0
|
|
74
|
+
self.attributes[:memory_diff] = memory_diff
|
|
75
|
+
end # if memory_diff > 0
|
|
76
|
+
end # unless
|
|
77
|
+
|
|
78
|
+
pid_memory[:last_memory_reading] = pid_memory[:current_memory_reading]
|
|
79
|
+
pid_memory[:current_memory_reading] = -1
|
|
80
|
+
end # if mem_line
|
|
81
|
+
return true
|
|
82
|
+
end # def update_pids
|
|
83
|
+
|
|
84
|
+
def convert_pipe_separated_counts(value, capture_definition)
|
|
85
|
+
count_strings = value.split(' | ')
|
|
86
|
+
count_arrays = count_strings.map do |count_string|
|
|
87
|
+
if count_string =~ /^(\w+): (\d+)/
|
|
88
|
+
[$1, $2.to_i]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Hash[count_arrays]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
end # class Request
|
|
96
|
+
end
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
module RequestLogAnalyzer::FileFormat
|
|
2
2
|
|
|
3
|
+
# PostgresQL spec 8.3.7
|
|
3
4
|
class Postgresql < Base
|
|
4
5
|
|
|
5
6
|
extend CommonRegularExpressions
|
|
6
7
|
|
|
7
8
|
line_definition :query do |line|
|
|
8
9
|
line.header = true
|
|
9
|
-
line.teaser =
|
|
10
|
-
line.regexp = /(#{timestamp('%
|
|
10
|
+
line.teaser = /.*LOG\:/
|
|
11
|
+
line.regexp = /(#{timestamp('%Y-%m-%d %k:%M:%S')})\ \S+ \[\d+\]\:\ \[.*\]\ LOG\:\ \ \d+\:\ duration\: (.*)\ ms\ \ statement:\ (.*)/
|
|
11
12
|
|
|
12
13
|
line.capture(:timestamp).as(:timestamp)
|
|
14
|
+
line.capture(:query_time).as(:duration, :unit => :sec)
|
|
13
15
|
line.capture(:query_fragment)
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
line_definition :
|
|
18
|
+
line_definition :location do |line|
|
|
17
19
|
line.footer = true
|
|
18
|
-
line.teaser =
|
|
19
|
-
line.regexp =
|
|
20
|
+
line.teaser = /.*LOCATION:/
|
|
21
|
+
line.regexp = /.*(\ )LOCATION:/
|
|
20
22
|
|
|
21
|
-
line.capture(:query_time).as(:duration, :unit => :sec)
|
|
22
23
|
line.capture(:query).as(:sql) # Hack to gather up fragments
|
|
23
24
|
end
|
|
24
25
|
|
|
@@ -25,9 +25,9 @@ module RequestLogAnalyzer::FileFormat
|
|
|
25
25
|
lines.each do |line|
|
|
26
26
|
line = line.to_sym
|
|
27
27
|
if LINE_COLLECTIONS.has_key?(line)
|
|
28
|
-
LINE_COLLECTIONS[line].each { |l| definitions_hash[l]
|
|
28
|
+
LINE_COLLECTIONS[line].each { |l| definitions_hash[l] ||= LINE_DEFINITIONS[l] }
|
|
29
29
|
elsif LINE_DEFINITIONS.has_key?(line)
|
|
30
|
-
definitions_hash[line]
|
|
30
|
+
definitions_hash[line] ||= LINE_DEFINITIONS[line]
|
|
31
31
|
else
|
|
32
32
|
raise "Unrecognized Rails log line name: #{line.inspect}!"
|
|
33
33
|
end
|
|
@@ -82,8 +82,8 @@ module RequestLogAnalyzer::FileFormat
|
|
|
82
82
|
|
|
83
83
|
# Rails > 2.1 completed line example
|
|
84
84
|
# Completed in 614ms (View: 120, DB: 31) | 200 OK [http://floorplanner.local/demo]
|
|
85
|
-
RAILS_22_COMPLETED = /Completed in (\d+)ms \((?:View: (\d+)
|
|
86
|
-
|
|
85
|
+
RAILS_22_COMPLETED = /Completed in (\d+)ms \((?:View: (\d+))?,?(?:.?DB: (\d+))?\)? \| (\d{3}).+\[(http.+)\]/
|
|
86
|
+
|
|
87
87
|
# A hash of definitions for all common lines in Rails logs.
|
|
88
88
|
LINE_DEFINITIONS = {
|
|
89
89
|
:processing => RequestLogAnalyzer::LineDefinition.new(:processing, :header => true,
|
|
@@ -8,11 +8,11 @@ module RequestLogAnalyzer::FileFormat
|
|
|
8
8
|
|
|
9
9
|
extend CommonRegularExpressions
|
|
10
10
|
|
|
11
|
-
# Started GET "/
|
|
11
|
+
# beta4: Started GET "/" for 127.0.0.1 at Wed Jul 07 09:13:27 -0700 2010 (different time format)
|
|
12
12
|
line_definition :started do |line|
|
|
13
13
|
line.header = true
|
|
14
14
|
line.teaser = /Started /
|
|
15
|
-
line.regexp = /Started ([A-Z]+) "([^"]+)" for (#{ip_address}) at (#{timestamp('%Y-%m-%d %H:%M:%S')})/
|
|
15
|
+
line.regexp = /Started ([A-Z]+) "([^"]+)" for (#{ip_address}) at (#{timestamp('%a %b %d %H:%M:%S %z %Y')}|#{timestamp('%Y-%m-%d %H:%M:%S %z')})/
|
|
16
16
|
|
|
17
17
|
line.capture(:method)
|
|
18
18
|
line.capture(:path)
|
|
@@ -23,21 +23,25 @@ module RequestLogAnalyzer::FileFormat
|
|
|
23
23
|
# Processing by QueriesController#index as HTML
|
|
24
24
|
line_definition :processing do |line|
|
|
25
25
|
line.teaser = /Processing by /
|
|
26
|
-
line.regexp = /Processing by (
|
|
26
|
+
line.regexp = /Processing by ([A-Za-z0-9\-:]+)\#(\w+) as ([\w\/\*]+)/
|
|
27
27
|
|
|
28
28
|
line.capture(:controller)
|
|
29
29
|
line.capture(:action)
|
|
30
30
|
line.capture(:format)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
# Completed in
|
|
33
|
+
# Completed 200 OK in 224ms (Views: 200.2ms | ActiveRecord: 3.4ms)
|
|
34
|
+
# Completed 302 Found in 23ms
|
|
35
|
+
# Completed in 189ms
|
|
34
36
|
line_definition :completed do |line|
|
|
35
37
|
line.footer = true
|
|
36
38
|
line.teaser = /Completed /
|
|
37
|
-
line.regexp = /Completed (\d+) .*
|
|
38
|
-
|
|
39
|
+
line.regexp = /Completed (\d+)? .*in (\d+(?:\.\d+)?)ms(?:[^\(]*\(Views: (\d+(?:\.\d+)?)ms .* ActiveRecord: (\d+(?:\.\d+)?)ms.*\))?/
|
|
40
|
+
|
|
39
41
|
line.capture(:status).as(:integer)
|
|
40
42
|
line.capture(:duration).as(:duration, :unit => :msec)
|
|
43
|
+
line.capture(:view).as(:duration, :unit => :msec)
|
|
44
|
+
line.capture(:db).as(:duration, :unit => :msec)
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
# ActionView::Template::Error (undefined local variable or method `field' for #<Class>) on line #3 of /Users/willem/Code/warehouse/app/views/queries/execute.csv.erb:
|
|
@@ -69,18 +73,37 @@ module RequestLogAnalyzer::FileFormat
|
|
|
69
73
|
analyze.frequency :status, :title => 'HTTP statuses returned'
|
|
70
74
|
|
|
71
75
|
analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
|
|
77
|
+
analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
|
|
74
78
|
|
|
75
79
|
analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
|
|
76
80
|
:if => lambda { |request| request[:duration] && request[:duration] > 1.0 }
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
class Request < RequestLogAnalyzer::Request
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
# Used to handle conversion of abbrev. month name to a digit
|
|
85
|
+
MONTHS = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
|
|
86
|
+
|
|
87
|
+
def convert_timestamp(value, definition)
|
|
88
|
+
# the time value can be in 2 formats:
|
|
89
|
+
# - 2010-10-26 02:27:15 +0000 (ruby 1.9.2)
|
|
90
|
+
# - Thu Oct 25 16:15:18 -0800 2010
|
|
91
|
+
if value =~ /^#{CommonRegularExpressions::TIMESTAMP_PARTS['Y']}/
|
|
92
|
+
value.gsub!(/\W/,'')
|
|
93
|
+
value[0..13].to_i
|
|
94
|
+
else
|
|
95
|
+
value.gsub!(/\W/,'')
|
|
96
|
+
time_as_str = value[-4..-1] # year
|
|
97
|
+
# convert the month to a 2-digit representation
|
|
98
|
+
month = MONTHS.index(value[3..5])+1
|
|
99
|
+
month < 10 ? time_as_str << "0#{month}" : time_as_str << month.to_s
|
|
100
|
+
|
|
101
|
+
time_as_str << value[6..13] # day of month + time
|
|
102
|
+
time_as_str.to_i
|
|
103
|
+
end
|
|
104
|
+
|
|
82
105
|
end
|
|
83
106
|
end
|
|
84
107
|
|
|
85
108
|
end
|
|
86
|
-
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module RequestLogAnalyzer::FileFormat
|
|
2
|
+
|
|
3
|
+
# FileFormat for W3C access logs.
|
|
4
|
+
class W3c < Base
|
|
5
|
+
|
|
6
|
+
extend CommonRegularExpressions
|
|
7
|
+
|
|
8
|
+
line_definition :access do |line|
|
|
9
|
+
line.header = true
|
|
10
|
+
line.footer = true
|
|
11
|
+
line.regexp = /^(#{timestamp('%Y-%m-%d %H:%M:%S')}) (#{ip_address}) (.*) (#{ip_address}) (\d+) (\w+) ([\w|\/|.]+) \- (\d+) (\d+) (\d+) (\d+) (.*) (\S+)/
|
|
12
|
+
|
|
13
|
+
line.capture(:timestamp).as(:timestamp)
|
|
14
|
+
line.capture(:remote_ip)
|
|
15
|
+
line.capture(:username).as(:nillable_string)
|
|
16
|
+
line.capture(:local_ip)
|
|
17
|
+
line.capture(:port).as(:integer)
|
|
18
|
+
line.capture(:method)
|
|
19
|
+
line.capture(:path).as(:path)
|
|
20
|
+
line.capture(:http_status).as(:integer)
|
|
21
|
+
line.capture(:bytes_sent).as(:traffic, :unit => :byte)
|
|
22
|
+
line.capture(:bytes_received).as(:traffic, :unit => :byte)
|
|
23
|
+
line.capture(:duration).as(:duration, :unit => :msec)
|
|
24
|
+
line.capture(:user_agent)
|
|
25
|
+
line.capture(:referer)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
report do |analyze|
|
|
29
|
+
analyze.timespan
|
|
30
|
+
analyze.hourly_spread
|
|
31
|
+
|
|
32
|
+
analyze.frequency :category => :http_method, :title => "HTTP methods"
|
|
33
|
+
analyze.frequency :category => :http_status, :title => "HTTP statuses"
|
|
34
|
+
|
|
35
|
+
analyze.frequency :category => :path, :title => "Most popular URIs"
|
|
36
|
+
|
|
37
|
+
analyze.frequency :category => :user_agent, :title => "User agents"
|
|
38
|
+
analyze.frequency :category => :referer, :title => "Referers"
|
|
39
|
+
|
|
40
|
+
analyze.duration :duration => :duration, :category => :path, :title => 'Request duration'
|
|
41
|
+
analyze.traffic :traffic => :bytes_sent, :category => :path, :title => 'Traffic out'
|
|
42
|
+
analyze.traffic :traffic => :bytes_received, :category => :path, :title => 'Traffic in'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Request < RequestLogAnalyzer::Request
|
|
46
|
+
# Do not use DateTime.parse, but parse the timestamp ourselves to return a integer
|
|
47
|
+
# to speed up parsing.
|
|
48
|
+
def convert_timestamp(value, definition)
|
|
49
|
+
"#{value[0,4]}#{value[5,2]}#{value[8,2]}#{value[11,2]}#{value[14,2]}#{value[17,2]}".to_i
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
module RequestLogAnalyzer::FileFormat
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
autoload :Rails, 'request_log_analyzer/file_format/rails'
|
|
4
|
+
autoload :Rails3, 'request_log_analyzer/file_format/rails3'
|
|
5
|
+
autoload :RailsDevelopment, 'request_log_analyzer/file_format/rails_development'
|
|
6
|
+
autoload :Oink, 'request_log_analyzer/file_format/oink'
|
|
7
|
+
autoload :Rack, 'request_log_analyzer/file_format/rack'
|
|
8
|
+
autoload :Merb, 'request_log_analyzer/file_format/merb'
|
|
9
|
+
autoload :Mysql, 'request_log_analyzer/file_format/mysql'
|
|
10
|
+
autoload :Postgresql, 'request_log_analyzer/file_format/postgresql'
|
|
11
|
+
autoload :DelayedJob, 'request_log_analyzer/file_format/delayed_job'
|
|
12
|
+
autoload :DelayedJob2, 'request_log_analyzer/file_format/delayed_job2'
|
|
13
|
+
autoload :DelayedJob21, 'request_log_analyzer/file_format/delayed_job21'
|
|
14
|
+
autoload :Apache, 'request_log_analyzer/file_format/apache'
|
|
15
|
+
autoload :AmazonS3, 'request_log_analyzer/file_format/amazon_s3'
|
|
16
|
+
autoload :W3c, 'request_log_analyzer/file_format/w3c'
|
|
17
|
+
autoload :Haproxy, 'request_log_analyzer/file_format/haproxy'
|
|
6
18
|
|
|
7
19
|
# Loads a FileFormat::Base subclass instance.
|
|
8
20
|
# You can provide:
|
|
@@ -20,10 +32,11 @@ module RequestLogAnalyzer::FileFormat
|
|
|
20
32
|
# a usable class is provided. Use this format class.
|
|
21
33
|
klass = file_format
|
|
22
34
|
|
|
23
|
-
elsif file_format.kind_of?(String) && File.exist?(file_format)
|
|
35
|
+
elsif file_format.kind_of?(String) && File.exist?(file_format) && File.file?(file_format)
|
|
24
36
|
# load a format from a ruby file
|
|
25
37
|
require file_format
|
|
26
|
-
|
|
38
|
+
|
|
39
|
+
const = RequestLogAnalyzer.to_camelcase(File.basename(file_format, '.rb'))
|
|
27
40
|
if RequestLogAnalyzer::FileFormat.const_defined?(const)
|
|
28
41
|
klass = RequestLogAnalyzer::FileFormat.const_get(const)
|
|
29
42
|
elsif Object.const_defined?(const)
|
|
@@ -34,7 +47,7 @@ module RequestLogAnalyzer::FileFormat
|
|
|
34
47
|
|
|
35
48
|
else
|
|
36
49
|
# load a provided file format
|
|
37
|
-
klass = RequestLogAnalyzer::FileFormat.const_get(RequestLogAnalyzer
|
|
50
|
+
klass = RequestLogAnalyzer::FileFormat.const_get(RequestLogAnalyzer.to_camelcase(file_format))
|
|
38
51
|
end
|
|
39
52
|
|
|
40
53
|
# check the returned klass to see if it can be used
|
|
@@ -46,7 +59,7 @@ module RequestLogAnalyzer::FileFormat
|
|
|
46
59
|
|
|
47
60
|
# Returns an array of all FileFormat instances that are shipped with request-log-analyzer by default.
|
|
48
61
|
def self.all_formats
|
|
49
|
-
@all_formats ||= Dir[File.
|
|
62
|
+
@all_formats ||= Dir[File.expand_path('file_format/*.rb', File.dirname(__FILE__))].map do |file|
|
|
50
63
|
self.load(File.basename(file, '.rb'))
|
|
51
64
|
end
|
|
52
65
|
end
|
|
@@ -246,11 +259,21 @@ module RequestLogAnalyzer::FileFormat
|
|
|
246
259
|
request_class.create(self, *hashes)
|
|
247
260
|
end
|
|
248
261
|
|
|
249
|
-
# Checks whether the
|
|
250
|
-
# A file format should have at least a header and a footer line type
|
|
262
|
+
# Checks whether the file format is valid so it can be safely used with RLA.
|
|
251
263
|
def valid?
|
|
264
|
+
valid_line_definitions? && valid_request_class?
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Checks whether the line definitions form a valid language.
|
|
268
|
+
# A file format should have at least a header and a footer line type
|
|
269
|
+
def valid_line_definitions?
|
|
252
270
|
line_definitions.any? { |(name, ld)| ld.header } && line_definitions.any? { |(name, ld)| ld.footer }
|
|
253
271
|
end
|
|
272
|
+
|
|
273
|
+
# Checks whether the request class inherits from the base Request class.
|
|
274
|
+
def valid_request_class?
|
|
275
|
+
request_class.ancestors.include?(RequestLogAnalyzer::Request)
|
|
276
|
+
end
|
|
254
277
|
|
|
255
278
|
# Returns true if this language captures the given symbol in one of its line definitions
|
|
256
279
|
def captures?(name)
|
|
@@ -272,4 +295,4 @@ module RequestLogAnalyzer::FileFormat
|
|
|
272
295
|
return nil
|
|
273
296
|
end
|
|
274
297
|
end
|
|
275
|
-
end
|
|
298
|
+
end
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
module RequestLogAnalyzer::Filter
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
RequestLogAnalyzer::load_default_class_file(self, const)
|
|
7
|
-
end
|
|
3
|
+
autoload :Field, 'request_log_analyzer/filter/field'
|
|
4
|
+
autoload :Timespan, 'request_log_analyzer/filter/timespan'
|
|
5
|
+
autoload :Anonymize, 'request_log_analyzer/filter/anonymize'
|
|
8
6
|
|
|
9
7
|
# Base filter class used to filter input requests.
|
|
10
8
|
# All filters should interit from this base.
|
|
@@ -10,11 +10,6 @@ module RequestLogAnalyzer::Output
|
|
|
10
10
|
|
|
11
11
|
class FancyHTML < HTML
|
|
12
12
|
|
|
13
|
-
# Load class files if needed
|
|
14
|
-
def self.const_missing(const)
|
|
15
|
-
RequestLogAnalyzer::load_default_class_file(self, const)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
13
|
def report_tracker(tracker)
|
|
19
14
|
case tracker
|
|
20
15
|
when RequestLogAnalyzer::Tracker::HourlySpread then report_hourly_spread(tracker)
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# Module for generating output
|
|
2
2
|
module RequestLogAnalyzer::Output
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
autoload :FixedWidth, 'request_log_analyzer/output/fixed_width'
|
|
5
|
+
autoload :HTML, 'request_log_analyzer/output/html'
|
|
6
|
+
autoload :FancyHTML, 'request_log_analyzer/output/fancy_html'
|
|
7
|
+
|
|
9
8
|
# Loads a Output::Base subclass instance.
|
|
10
9
|
def self.load(file_format, *args)
|
|
11
10
|
|
|
@@ -21,7 +20,7 @@ module RequestLogAnalyzer::Output
|
|
|
21
20
|
elsif file_format.kind_of?(String) && File.exist?(file_format)
|
|
22
21
|
# load a format from a ruby file
|
|
23
22
|
require file_format
|
|
24
|
-
const = RequestLogAnalyzer
|
|
23
|
+
const = RequestLogAnalyzer.to_camelcase(File.basename(file_format, '.rb'))
|
|
25
24
|
if RequestLogAnalyzer::FileFormat.const_defined?(const)
|
|
26
25
|
klass = RequestLogAnalyzer::Output.const_get(const)
|
|
27
26
|
elsif Object.const_defined?(const)
|
|
@@ -32,7 +31,7 @@ module RequestLogAnalyzer::Output
|
|
|
32
31
|
|
|
33
32
|
else
|
|
34
33
|
# load a provided file format
|
|
35
|
-
klass = RequestLogAnalyzer::Output.const_get(RequestLogAnalyzer
|
|
34
|
+
klass = RequestLogAnalyzer::Output.const_get(RequestLogAnalyzer.to_camelcase(file_format))
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
# check the returned klass to see if it can be used
|
|
@@ -27,14 +27,28 @@ module RequestLogAnalyzer
|
|
|
27
27
|
def convert_integer(value, capture_definition); value.to_i; end
|
|
28
28
|
def convert_sym(value, capture_definition); value.to_sym; end
|
|
29
29
|
def convert_symbol(value, capture_definition); value.to_sym; end
|
|
30
|
+
def convert_nillable_string(value, definition); value == '-' ? nil : value ; end
|
|
31
|
+
|
|
32
|
+
# This function can be overridden to rewrite the path for better categorization in the
|
|
33
|
+
# reports.
|
|
34
|
+
def convert_path(value, definition)
|
|
35
|
+
value
|
|
36
|
+
end
|
|
30
37
|
|
|
31
38
|
# Converts :eval field, which should evaluate to a hash.
|
|
32
39
|
def convert_eval(value, capture_definition)
|
|
33
|
-
eval(value).inject({}) { |h, (k, v)| h[k.to_sym] = v; h}
|
|
34
|
-
|
|
40
|
+
eval(sanitize_parameters(value)).inject({}) { |h, (k, v)| h[k.to_sym] = v; h}
|
|
41
|
+
# Wide range of errors possible with wild eval. ATM we choose to crash on this.
|
|
42
|
+
rescue SyntaxError
|
|
35
43
|
nil
|
|
36
44
|
end
|
|
37
45
|
|
|
46
|
+
# Removes certain string sequences which would be problematic for eval.
|
|
47
|
+
# TODO remove all characters not valid in ruby symbols
|
|
48
|
+
def sanitize_parameters(parameter_string)
|
|
49
|
+
parameter_string.gsub(/#</, '"').gsub(/>,/, '", ').gsub(/\\0/, '')
|
|
50
|
+
end
|
|
51
|
+
|
|
38
52
|
# Slow default method to parse timestamps.
|
|
39
53
|
# Reimplement this function in a file format specific Request class
|
|
40
54
|
# to improve the timestamp parsing speed.
|
|
@@ -177,4 +191,4 @@ module RequestLogAnalyzer
|
|
|
177
191
|
@lines.map { |line| line[:lineno] }.reject { |v| v.nil? }.max
|
|
178
192
|
end
|
|
179
193
|
end
|
|
180
|
-
end
|
|
194
|
+
end
|
|
@@ -43,7 +43,9 @@ module RequestLogAnalyzer::Source
|
|
|
43
43
|
@progress_handler = nil
|
|
44
44
|
|
|
45
45
|
@options[:parse_strategy] ||= DEFAULT_PARSE_STRATEGY
|
|
46
|
-
|
|
46
|
+
unless PARSE_STRATEGIES.include?(@options[:parse_strategy])
|
|
47
|
+
raise "Unknown parse strategy: #{@options[@parse_strategy]}"
|
|
48
|
+
end
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
# Reads the input, which can either be a file, sequence of files or STDIN to parse
|
|
@@ -8,12 +8,8 @@
|
|
|
8
8
|
# - Currently, RequestLogAnalyzer::Source::LogParser is the only implemented source.
|
|
9
9
|
module RequestLogAnalyzer::Source
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# <tt>const</tt>:: The constant to load in the RequestLogAnalyzer::Source namespace.
|
|
14
|
-
def self.const_missing(const)
|
|
15
|
-
RequestLogAnalyzer::load_default_class_file(self, const)
|
|
16
|
-
end
|
|
11
|
+
autoload :LogParser, 'request_log_analyzer/source/log_parser'
|
|
12
|
+
autoload :DatabaseLoader, 'request_log_analyzer/source/database_loader'
|
|
17
13
|
|
|
18
14
|
# The base Source class. All other sources should inherit from this class.
|
|
19
15
|
#
|
|
@@ -25,6 +25,13 @@ module RequestLogAnalyzer::Tracker
|
|
|
25
25
|
def prepare
|
|
26
26
|
options[:value] = options[:duration] if options[:duration]
|
|
27
27
|
super
|
|
28
|
+
|
|
29
|
+
@number_of_buckets = options[:number_of_buckets] || 1000
|
|
30
|
+
@min_bucket_value = options[:min_bucket_value] ? options[:min_bucket_value].to_f : 0.0001
|
|
31
|
+
@max_bucket_value = options[:max_bucket_value] ? options[:max_bucket_value].to_f : 1000
|
|
32
|
+
|
|
33
|
+
# precalculate the bucket size
|
|
34
|
+
@bucket_size = (Math.log(@max_bucket_value) - Math.log(@min_bucket_value)) / @number_of_buckets.to_f
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
# Display a duration
|