request-log-analyzer 1.1.2 → 1.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/DESIGN +24 -10
- data/bin/request-log-analyzer +2 -27
- data/lib/cli/progressbar.rb +2 -19
- data/lib/cli/tools.rb +46 -0
- data/lib/request_log_analyzer/aggregator/database.rb +16 -5
- data/lib/request_log_analyzer/aggregator/echo.rb +1 -0
- data/lib/request_log_analyzer/aggregator/summarizer.rb +15 -13
- data/lib/request_log_analyzer/controller.rb +8 -4
- data/lib/request_log_analyzer/file_format/merb.rb +17 -4
- data/lib/request_log_analyzer/file_format/rails_development.rb +30 -92
- data/lib/request_log_analyzer/file_format.rb +8 -4
- data/lib/request_log_analyzer/filter/anonymize.rb +0 -3
- data/lib/request_log_analyzer/filter/field.rb +6 -1
- data/lib/request_log_analyzer/filter/timespan.rb +7 -1
- data/lib/request_log_analyzer/filter.rb +0 -4
- data/lib/request_log_analyzer/line_definition.rb +13 -2
- data/lib/request_log_analyzer/output/fixed_width.rb +7 -1
- data/lib/request_log_analyzer/output/html.rb +1 -0
- data/lib/request_log_analyzer/request.rb +4 -0
- data/lib/request_log_analyzer/source/log_parser.rb +19 -21
- data/lib/request_log_analyzer/source.rb +2 -1
- data/lib/request_log_analyzer/tracker/duration.rb +74 -14
- data/lib/request_log_analyzer/tracker/frequency.rb +22 -10
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +18 -6
- data/lib/request_log_analyzer/tracker/timespan.rb +15 -8
- data/lib/request_log_analyzer.rb +4 -8
- data/spec/integration/command_line_usage_spec.rb +71 -0
- data/spec/lib/helper.rb +33 -0
- data/spec/lib/mocks.rb +47 -0
- data/spec/{file_formats/spec_format.rb → lib/testing_format.rb} +6 -1
- data/spec/spec_helper.rb +5 -41
- data/spec/{database_inserter_spec.rb → unit/aggregator/database_inserter_spec.rb} +40 -37
- data/spec/unit/aggregator/summarizer_spec.rb +28 -0
- data/spec/unit/controller/controller_spec.rb +43 -0
- data/spec/{log_processor_spec.rb → unit/controller/log_processor_spec.rb} +4 -3
- data/spec/{file_format_spec.rb → unit/file_format/file_format_api_spec.rb} +16 -4
- data/spec/{line_definition_spec.rb → unit/file_format/line_definition_spec.rb} +13 -6
- data/spec/{merb_format_spec.rb → unit/file_format/merb_format_spec.rb} +2 -2
- data/spec/{rails_format_spec.rb → unit/file_format/rails_format_spec.rb} +19 -11
- data/spec/unit/filter/anonymize_filter_spec.rb +22 -0
- data/spec/unit/filter/field_filter_spec.rb +69 -0
- data/spec/unit/filter/timespan_filter_spec.rb +61 -0
- data/spec/{log_parser_spec.rb → unit/source/log_parser_spec.rb} +7 -7
- data/spec/{request_spec.rb → unit/source/request_spec.rb} +5 -5
- data/spec/unit/tracker/duration_tracker_spec.rb +90 -0
- data/spec/unit/tracker/frequency_tracker_spec.rb +83 -0
- data/spec/unit/tracker/timespan_tracker_spec.rb +64 -0
- data/spec/unit/tracker/tracker_api_test.rb +45 -0
- metadata +50 -26
- data/spec/controller_spec.rb +0 -64
- data/spec/filter_spec.rb +0 -157
- data/spec/summarizer_spec.rb +0 -9
data/DESIGN
CHANGED
@@ -4,21 +4,35 @@ This allows you to easily add extra reports, filters and outputs.
|
|
4
4
|
|
5
5
|
1) Build pipeline.
|
6
6
|
-> Aggregator (database)
|
7
|
-
Source -> Filter -> Filter -> Aggregator (summary report)
|
7
|
+
Source -> Filter -> Filter -> Aggregator (summary report) -> Output
|
8
8
|
-> Aggregator (...)
|
9
|
-
|
9
|
+
|
10
10
|
2) Start chunk producer and push chunks through pipeline.
|
11
|
-
|
11
|
+
Controller.start
|
12
|
+
|
13
|
+
RequestLogAnalyzer::Source is an Object that pushes requests into the chain.
|
14
|
+
At the moment you can only use the log-parser as a source.
|
15
|
+
It accepts files or stdin and can parse then into request objects using a RequestLogAnalyzer::FileFormat definition.
|
16
|
+
In the future we want to be able to have a generated request database as source as this will make interactive
|
17
|
+
down drilling possible.
|
18
|
+
|
19
|
+
The filters are all subclasses of the RequestLogAnalyzer::Filter class.
|
20
|
+
They accept a request object, manipulate or drop it, and then pass the request object on to the next filter
|
21
|
+
in the chain.
|
22
|
+
At the moment there are three types of filters available: Anonymize, Field and Timespan.
|
23
|
+
|
24
|
+
The Aggregators all inherit from the RequestLogAnalyzer::Aggregator class.
|
25
|
+
All the requests that come out of the Filterchain are fed into all the aggregators in parallel.
|
26
|
+
These aggregators can do anything what they want with the given request.
|
27
|
+
For example: the Database aggregator will just store all the requests into a SQLite database while the Summarizer will
|
28
|
+
generate a wide range of statistical reports from them.
|
12
29
|
|
13
30
|
3) Gather output from pipeline.
|
14
31
|
Controller.report
|
15
32
|
|
16
|
-
|
17
|
-
|
18
|
-
This will make interactive downdrilling possible.
|
19
|
-
|
33
|
+
All Aggregators are asked to report what they have done. For example the database will report: I stuffed x requests
|
34
|
+
into SQLite database Y. The Summarizer will output its reports.
|
20
35
|
|
21
|
-
|
22
|
-
|
23
|
-
tables, lines and comments and push them into the output class.
|
36
|
+
The output is pushed to a RequestLogAnalyzer::Output object, which takes care of the output.
|
37
|
+
It can generate either ASCII, UTF8 or even HTML output.
|
24
38
|
|
data/bin/request-log-analyzer
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
2
|
require File.dirname(__FILE__) + '/../lib/request_log_analyzer'
|
3
3
|
require File.dirname(__FILE__) + '/../lib/cli/command_line_arguments'
|
4
|
-
|
5
|
-
def terminal_width(default = 81)
|
6
|
-
IO.popen('stty -a') do |pipe|
|
7
|
-
column_line = pipe.detect { |line| /(\d+) columns/ =~ line }
|
8
|
-
width = column_line ? $1.to_i : default
|
9
|
-
end
|
10
|
-
rescue
|
11
|
-
default
|
12
|
-
end
|
4
|
+
require File.dirname(__FILE__) + '/../lib/cli/tools'
|
13
5
|
|
14
6
|
# Parse the arguments given via commandline
|
15
7
|
begin
|
@@ -29,7 +21,7 @@ begin
|
|
29
21
|
|
30
22
|
command_line.option(:format, :alias => :f, :default => 'rails')
|
31
23
|
command_line.option(:file, :alias => :e)
|
32
|
-
command_line.
|
24
|
+
command_line.option(:parse_strategy, :default => 'assume-correct')
|
33
25
|
|
34
26
|
command_line.option(:aggregator, :alias => :a, :multiple => true)
|
35
27
|
command_line.option(:database, :alias => :d)
|
@@ -80,23 +72,6 @@ rescue CommandLine::Error => e
|
|
80
72
|
exit(0)
|
81
73
|
end
|
82
74
|
|
83
|
-
def install_rake_tasks(install_type)
|
84
|
-
if install_type == 'rails'
|
85
|
-
require 'ftools'
|
86
|
-
if File.directory?('./lib/tasks/')
|
87
|
-
File.copy(File.dirname(__FILE__) + '/../tasks/request_log_analyzer.rake', './lib/tasks/request_log_analyze.rake')
|
88
|
-
puts "Installed rake tasks."
|
89
|
-
puts "To use, run: rake log:analyze"
|
90
|
-
else
|
91
|
-
puts "Cannot find /lib/tasks folder. Are you in your Rails directory?"
|
92
|
-
puts "Installation aborted."
|
93
|
-
end
|
94
|
-
else
|
95
|
-
raise "Cannot perform this install type! (#{install_type})"
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
|
100
75
|
case arguments.command
|
101
76
|
when :install
|
102
77
|
install_rake_tasks(arguments.parameters[0])
|
data/lib/cli/progressbar.rb
CHANGED
@@ -119,23 +119,6 @@ module CommandLine
|
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
|
-
def get_width
|
123
|
-
# FIXME: I don't know how portable it is.
|
124
|
-
default_width = 80
|
125
|
-
begin
|
126
|
-
tiocgwinsz = 0x5413
|
127
|
-
data = [0, 0, 0, 0].pack("SSSS")
|
128
|
-
if @out.ioctl(tiocgwinsz, data) >= 0 then
|
129
|
-
rows, cols, xpixels, ypixels = data.unpack("SSSS")
|
130
|
-
if cols >= 0 then cols else default_width end
|
131
|
-
else
|
132
|
-
default_width
|
133
|
-
end
|
134
|
-
rescue Exception
|
135
|
-
default_width
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
122
|
def show
|
140
123
|
arguments = @format_arguments.map {|method|
|
141
124
|
method = sprintf("fmt_%s", method)
|
@@ -143,7 +126,7 @@ module CommandLine
|
|
143
126
|
}
|
144
127
|
line = sprintf(@format, *arguments)
|
145
128
|
|
146
|
-
width =
|
129
|
+
width = terminal_width(80)
|
147
130
|
if line.length == width - 1
|
148
131
|
@out.print(line + eol)
|
149
132
|
@out.flush
|
@@ -176,7 +159,7 @@ module CommandLine
|
|
176
159
|
public
|
177
160
|
def clear
|
178
161
|
@out.print "\r"
|
179
|
-
@out.print(" " * (
|
162
|
+
@out.print(" " * (terminal_width(80) - 1))
|
180
163
|
@out.print "\r"
|
181
164
|
end
|
182
165
|
|
data/lib/cli/tools.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# Try to determine the terminal with.
|
2
|
+
# If it is not possible to to so, it returns the default_width.
|
3
|
+
# <tt>default_width</tt> Defaults to 81
|
4
|
+
def terminal_width(default_width = 81)
|
5
|
+
tiocgwinsz = 0x5413
|
6
|
+
data = [0, 0, 0, 0].pack("SSSS")
|
7
|
+
if @out.ioctl(tiocgwinsz, data) >= 0
|
8
|
+
rows, cols, xpixels, ypixels = data.unpack("SSSS")
|
9
|
+
raise unless cols > 0
|
10
|
+
cols
|
11
|
+
else
|
12
|
+
raise
|
13
|
+
end
|
14
|
+
rescue
|
15
|
+
begin
|
16
|
+
IO.popen('stty -a') do |pipe|
|
17
|
+
column_line = pipe.detect { |line| /(\d+) columns/ =~ line }
|
18
|
+
raise unless column_line
|
19
|
+
$1.to_i
|
20
|
+
end
|
21
|
+
rescue
|
22
|
+
default_width
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Copies request-log-analyzer analyzer rake tasks into the /lib/tasks folder of a project, for easy access and
|
27
|
+
# environment integration.
|
28
|
+
# <tt>install_type</tt> Type of project to install into. Defaults to :rails.
|
29
|
+
# Raises if it cannot find the project folder or if the install_type is now known.
|
30
|
+
def install_rake_tasks(install_type = :rails)
|
31
|
+
if install_type.to_sym == :rails
|
32
|
+
require 'ftools'
|
33
|
+
if File.directory?('./lib/tasks/')
|
34
|
+
File.copy(File.dirname(__FILE__) + '/../tasks/request_log_analyzer.rake', './lib/tasks/request_log_analyze.rake')
|
35
|
+
puts "Installed rake tasks."
|
36
|
+
puts "To use, run: rake log:analyze"
|
37
|
+
else
|
38
|
+
puts "Cannot find /lib/tasks folder. Are you in your Rails directory?"
|
39
|
+
puts "Installation aborted."
|
40
|
+
end
|
41
|
+
else
|
42
|
+
raise "Cannot perform this install type! (#{install_type.to_s})"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
@@ -30,7 +30,8 @@ module RequestLogAnalyzer::Aggregator
|
|
30
30
|
def aggregate(request)
|
31
31
|
@request_object = @request_class.new(:first_lineno => request.first_lineno, :last_lineno => request.last_lineno)
|
32
32
|
request.lines.each do |line|
|
33
|
-
|
33
|
+
class_columns = @orm_module.const_get("#{line[:line_type]}_line".classify).column_names.reject { |column| ['id'].include?(column) }
|
34
|
+
attributes = Hash[*line.select { |(k, v)| class_columns.include?(k.to_s) }.flatten]
|
34
35
|
@request_object.send("#{line[:line_type]}_lines").build(attributes)
|
35
36
|
end
|
36
37
|
@request_object.save!
|
@@ -72,7 +73,16 @@ module RequestLogAnalyzer::Aggregator
|
|
72
73
|
t.column(:request_id, :integer)
|
73
74
|
t.column(:lineno, :integer)
|
74
75
|
definition.captures.each do |capture|
|
75
|
-
|
76
|
+
|
77
|
+
# Add a field for every capture
|
78
|
+
t.column(capture[:name], column_type(capture[:type]))
|
79
|
+
|
80
|
+
# If the capture provides other field as well, create them too
|
81
|
+
if capture[:provides].kind_of?(Hash)
|
82
|
+
capture[:provides].each do |field, field_type|
|
83
|
+
t.column(field, column_type(field_type))
|
84
|
+
end
|
85
|
+
end
|
76
86
|
end
|
77
87
|
end
|
78
88
|
ActiveRecord::Migration.add_index("#{name}_lines", [:request_id])
|
@@ -136,12 +146,13 @@ module RequestLogAnalyzer::Aggregator
|
|
136
146
|
|
137
147
|
# Function to determine the column type for a field
|
138
148
|
# TODO: make more robust / include in file-format definition
|
139
|
-
def column_type(
|
140
|
-
case
|
149
|
+
def column_type(type_indicator)
|
150
|
+
case type_indicator
|
151
|
+
when :eval; :text
|
141
152
|
when :sec; :double
|
142
153
|
when :msec; :double
|
143
154
|
when :float; :double
|
144
|
-
else
|
155
|
+
else type_indicator
|
145
156
|
end
|
146
157
|
end
|
147
158
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/../tracker'
|
2
|
-
|
3
1
|
module RequestLogAnalyzer::Aggregator
|
4
2
|
|
5
3
|
class Summarizer < Base
|
@@ -11,6 +9,10 @@ module RequestLogAnalyzer::Aggregator
|
|
11
9
|
def initialize
|
12
10
|
@trackers = []
|
13
11
|
end
|
12
|
+
|
13
|
+
def initialize_copy(other)
|
14
|
+
@trackers = other.trackers.dup
|
15
|
+
end
|
14
16
|
|
15
17
|
def reset!
|
16
18
|
@trackers = []
|
@@ -58,7 +60,7 @@ module RequestLogAnalyzer::Aggregator
|
|
58
60
|
def prepare
|
59
61
|
raise "No trackers set up in Summarizer!" if @trackers.nil? || @trackers.empty?
|
60
62
|
@trackers.each { |tracker| tracker.prepare }
|
61
|
-
|
63
|
+
end
|
62
64
|
|
63
65
|
def aggregate(request)
|
64
66
|
@trackers.each do |tracker|
|
@@ -86,23 +88,23 @@ module RequestLogAnalyzer::Aggregator
|
|
86
88
|
|
87
89
|
output.with_style(:cell_separator => false) do
|
88
90
|
output.table({:width => 20}, {:font => :bold}) do |rows|
|
89
|
-
rows << ['Parsed lines:',
|
90
|
-
rows << ['Parsed
|
91
|
-
rows << ['Skipped lines:',
|
91
|
+
rows << ['Parsed lines:', source.parsed_lines]
|
92
|
+
rows << ['Parsed requests:', source.parsed_requests]
|
93
|
+
rows << ['Skipped lines:', source.skipped_lines]
|
92
94
|
|
93
|
-
rows <<
|
95
|
+
rows << ["Warnings:", @warnings_encountered.map { |(key, value)| "#{key}: #{value}" }.join(', ')] if has_warnings?
|
94
96
|
end
|
95
97
|
end
|
96
98
|
output << "\n"
|
97
99
|
end
|
98
100
|
|
99
101
|
def report_footer(output)
|
100
|
-
if
|
101
|
-
|
102
|
+
if has_log_ordering_warnings?
|
102
103
|
output.title("Parse warnings")
|
103
104
|
|
104
|
-
output.puts "
|
105
|
-
output.puts "is not setup correctly
|
105
|
+
output.puts "Parseable lines were ancountered without a header line before it. It"
|
106
|
+
output.puts "could be that logging is not setup correctly for your application."
|
107
|
+
output.puts "Visit this website for logging configuration tips:"
|
106
108
|
output.puts output.link("http://github.com/wvanbergen/request-log-analyzer/wikis/configure-logging")
|
107
109
|
output.puts
|
108
110
|
end
|
@@ -112,8 +114,8 @@ module RequestLogAnalyzer::Aggregator
|
|
112
114
|
@warnings_encountered.inject(0) { |result, (key, value)| result += value } > 0
|
113
115
|
end
|
114
116
|
|
115
|
-
def
|
116
|
-
@warnings_encountered
|
117
|
+
def has_log_ordering_warnings?
|
118
|
+
@warnings_encountered[:no_current_request] && @warnings_encountered[:no_current_request] > 0
|
117
119
|
end
|
118
120
|
|
119
121
|
def warning(type, message, lineno)
|
@@ -64,7 +64,7 @@ module RequestLogAnalyzer
|
|
64
64
|
|
65
65
|
controller = Controller.new(RequestLogAnalyzer::Source::LogParser.new(file_format, options), options)
|
66
66
|
|
67
|
-
options[:
|
67
|
+
options[:parse_strategy] = arguments[:parse_strategy]
|
68
68
|
|
69
69
|
# register filters
|
70
70
|
if arguments[:after] || arguments[:before]
|
@@ -160,6 +160,9 @@ module RequestLogAnalyzer
|
|
160
160
|
@filters << filter.new(file_format, @options.merge(filter_options))
|
161
161
|
end
|
162
162
|
|
163
|
+
# Push a request through the entire filterchain (@filters).
|
164
|
+
# <tt>request</tt> The request to filter.
|
165
|
+
# Returns the filtered request or nil.
|
163
166
|
def filter_request(request)
|
164
167
|
@filters.each do |filter|
|
165
168
|
request = filter.filter(request)
|
@@ -168,7 +171,10 @@ module RequestLogAnalyzer
|
|
168
171
|
return request
|
169
172
|
end
|
170
173
|
|
174
|
+
# Push a request to all the aggregators (@aggregators).
|
175
|
+
# <tt>request</tt> The request to push to the aggregators.
|
171
176
|
def aggregate_request(request)
|
177
|
+
return unless request
|
172
178
|
@aggregators.each { |agg| agg.aggregate(request) }
|
173
179
|
end
|
174
180
|
|
@@ -182,13 +188,11 @@ module RequestLogAnalyzer
|
|
182
188
|
# 6. Finalize Source
|
183
189
|
def run!
|
184
190
|
|
185
|
-
@filters.each { |filter| filter.prepare }
|
186
191
|
@aggregators.each { |agg| agg.prepare }
|
187
192
|
|
188
193
|
begin
|
189
194
|
@source.each_request do |request|
|
190
|
-
|
191
|
-
aggregate_request(request) unless request.nil?
|
195
|
+
aggregate_request(filter_request(request))
|
192
196
|
end
|
193
197
|
rescue Interrupt => e
|
194
198
|
handle_progress(:interrupted)
|
@@ -14,8 +14,9 @@ module RequestLogAnalyzer::FileFormat
|
|
14
14
|
# ~ Params: {"_method"=>"delete", "authenticity_token"=>"[FILTERED]", "action"=>"d}
|
15
15
|
line_definition :params do |line|
|
16
16
|
line.teaser = /Params/
|
17
|
-
line.regexp = /Params\:\ \{
|
18
|
-
line.captures << { :name => :
|
17
|
+
line.regexp = /Params\:\ (\{.+\})/
|
18
|
+
line.captures << { :name => :params, :type => :eval, :provides => {
|
19
|
+
:namespace => :string, :controller => :string, :action => :string, :format => :string } }
|
19
20
|
end
|
20
21
|
|
21
22
|
# ~ {:dispatch_time=>0.006117, :after_filters_time=>6.1e-05, :before_filters_time=>0.000712, :action_time=>0.005833}
|
@@ -29,11 +30,23 @@ module RequestLogAnalyzer::FileFormat
|
|
29
30
|
<< { :name => :action_time, :type => :duration }
|
30
31
|
end
|
31
32
|
|
33
|
+
REQUEST_CATEGORIZER = Proc.new do |request|
|
34
|
+
category = "#{request[:controller]}##{request[:action]}"
|
35
|
+
category = "#{request[:namespace]}::#{category}" if request[:namespace]
|
36
|
+
category = "#{category}.#{request[:format]}" if request[:format]
|
37
|
+
category
|
38
|
+
end
|
32
39
|
|
33
40
|
report do |analyze|
|
34
|
-
|
41
|
+
analyze.timespan :line_type => :started
|
42
|
+
analyze.hourly_spread :line_type => :started
|
43
|
+
|
44
|
+
analyze.duration :dispatch_time, :category => REQUEST_CATEGORIZER, :title => 'Request dispatch duration'
|
45
|
+
# analyze.duration :action_time, :category => REQUEST_CATEGORIZER, :title => 'Request action duration'
|
46
|
+
# analyze.duration :after_filters_time, :category => REQUEST_CATEGORIZER, :title => 'Request after_filter duration'
|
47
|
+
# analyze.duration :before_filters_time, :category => REQUEST_CATEGORIZER, :title => 'Request before_filter duration'
|
35
48
|
end
|
36
|
-
|
49
|
+
|
37
50
|
end
|
38
51
|
|
39
52
|
end
|
@@ -1,28 +1,19 @@
|
|
1
1
|
module RequestLogAnalyzer::FileFormat
|
2
2
|
|
3
|
-
|
3
|
+
# The RailsDevelopment FileFormat is an extention to the default Rails file format. It includes
|
4
|
+
# all lines of the normal Rails file format, but parses SQL queries and partial rendering lines
|
5
|
+
# as well.
|
6
|
+
class RailsDevelopment < Rails
|
4
7
|
|
5
|
-
#
|
6
|
-
line_definition :
|
7
|
-
line.
|
8
|
-
line.
|
9
|
-
line.
|
10
|
-
line.captures << { :name => :controller, :type => :string } \
|
11
|
-
<< { :name => :action, :type => :string } \
|
12
|
-
<< { :name => :format, :type => :string } \
|
13
|
-
<< { :name => :ip, :type => :string } \
|
14
|
-
<< { :name => :timestamp, :type => :timestamp } \
|
15
|
-
<< { :name => :method, :type => :string }
|
16
|
-
end
|
17
|
-
|
18
|
-
# Filter chain halted as [#<ActionController::Caching::Actions::ActionCacheFilter:0x2a999ad620 @check=nil, @options={:store_options=>{}, :layout=>nil, :cache_path=>#<Proc:0x0000002a999b8890@/app/controllers/cached_controller.rb:8>}>] rendered_or_redirected.
|
19
|
-
line_definition :cache_hit do |line|
|
20
|
-
line.regexp = /Filter chain halted as \[\#<ActionController::Caching::Actions::ActionCacheFilter:.+>\] rendered_or_redirected/
|
8
|
+
# Parameters: {"action"=>"demo", "controller"=>"page"}
|
9
|
+
line_definition :parameters do |line|
|
10
|
+
line.teaser = /Parameters/
|
11
|
+
line.regexp = /\s+Parameters:\s+(\{.*\})/
|
12
|
+
line.captures << { :name => :params, :type => :eval }
|
21
13
|
end
|
22
14
|
|
23
15
|
# Rendered layouts/_footer (2.9ms)
|
24
16
|
line_definition :rendered do |line|
|
25
|
-
line.teaser = /Rendered /
|
26
17
|
line.regexp = /Rendered (\w+(?:\/\w+)+) \((\d+\.\d+)ms\)/
|
27
18
|
line.captures << { :name => :render_file, :type => :string } \
|
28
19
|
<< { :name => :render_duration, :type => :duration, :unit => :msec }
|
@@ -30,90 +21,37 @@ module RequestLogAnalyzer::FileFormat
|
|
30
21
|
|
31
22
|
# [4;36;1mUser Load (0.4ms)[0m [0;1mSELECT * FROM `users` WHERE (`users`.`id` = 18205844) [0m
|
32
23
|
line_definition :query_executed do |line|
|
33
|
-
line.regexp = /\s+(?:\e\[4;36;1m)?((?:\w+::)*\w+) Load \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0;1m)?(
|
24
|
+
line.regexp = /\s+(?:\e\[4;36;1m)?((?:\w+::)*\w+) Load \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0;1m)?([^\e]+) ?(?:\e\[0m)?/
|
34
25
|
line.captures << { :name => :query_class, :type => :string } \
|
35
26
|
<< { :name => :query_duration, :type => :duration, :unit => :msec } \
|
36
|
-
<< { :name => :query_sql, :type => :
|
27
|
+
<< { :name => :query_sql, :type => :sql }
|
37
28
|
end
|
38
29
|
|
39
30
|
# [4;35;1mCACHE (0.0ms)[0m [0mSELECT * FROM `users` WHERE (`users`.`id` = 0) [0m
|
40
31
|
line_definition :query_cached do |line|
|
41
|
-
line.
|
42
|
-
line.regexp = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?(.+) (?:\e\[0m)?/
|
32
|
+
line.regexp = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?([^\e]+) ?(?:\e\[0m)?/
|
43
33
|
line.captures << { :name => :cached_duration, :type => :duration, :unit => :msec } \
|
44
|
-
<< { :name => :cached_sql, :type => :
|
34
|
+
<< { :name => :cached_sql, :type => :sql }
|
45
35
|
end
|
46
|
-
|
47
|
-
# RuntimeError (Cannot destroy employee): /app/models/employee.rb:198:in `before_destroy'
|
48
|
-
line_definition :failed do |line|
|
49
|
-
line.footer = true
|
50
|
-
line.regexp = /((?:[A-Z]\w+\:\:)*[A-Z]\w+) \((.*)\)(?: on line #(\d+) of .+)?\:(.*)/
|
51
|
-
line.captures << { :name => :error, :type => :string } \
|
52
|
-
<< { :name => :message, :type => :string } \
|
53
|
-
<< { :name => :line, :type => :integer } \
|
54
|
-
<< { :name => :file, :type => :string } \
|
55
|
-
<< { :name => :stack_trace, :type => :string }
|
56
|
-
end
|
57
|
-
|
58
36
|
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
# but the line contains exactly the same information.
|
70
|
-
line_definition :completed do |line|
|
71
|
-
|
72
|
-
line.footer = true
|
73
|
-
line.teaser = /Completed in /
|
74
|
-
line.regexp = Regexp.new("(?:#{RAILS_21_COMPLETED}|#{RAILS_22_COMPLETED})")
|
75
|
-
|
76
|
-
line.captures << { :name => :duration, :type => :duration } \
|
77
|
-
<< { :name => :view, :type => :duration } \
|
78
|
-
<< { :name => :db, :type => :duration } \
|
79
|
-
<< { :name => :status, :type => :integer } \
|
80
|
-
<< { :name => :url, :type => :string } # Old variant
|
81
|
-
|
82
|
-
line.captures << { :name => :duration, :type => :duration, :unit => :msec } \
|
83
|
-
<< { :name => :view, :type => :duration, :unit => :msec } \
|
84
|
-
<< { :name => :db, :type => :duration, :unit => :msec } \
|
85
|
-
<< { :name => :status, :type => :integer} \
|
86
|
-
<< { :name => :url, :type => :string } # 2.2 variant
|
87
|
-
end
|
88
|
-
|
89
|
-
REQUEST_CATEGORIZER = Proc.new do |request|
|
90
|
-
format = request[:format] || 'html'
|
91
|
-
"#{request[:controller]}##{request[:action]}.#{format} [#{request[:method]}]"
|
92
|
-
end
|
93
|
-
|
94
|
-
report do |analyze|
|
95
|
-
analyze.timespan :line_type => :processing
|
96
|
-
analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Top 20 hits', :amount => 20, :line_type => :processing
|
97
|
-
analyze.frequency :method, :title => 'HTTP methods'
|
98
|
-
analyze.frequency :status, :title => 'HTTP statuses returned'
|
99
|
-
analyze.frequency :category => lambda { |request| request =~ :cache_hit ? 'Cache hit' : 'No hit' }, :title => 'Rails action cache hits'
|
100
|
-
|
101
|
-
analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
|
102
|
-
analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
|
103
|
-
analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
|
104
|
-
|
105
|
-
analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
|
106
|
-
:if => lambda { |request| request[:duration] && request[:duration] > 1.0 }, :amount => 20
|
107
|
-
|
108
|
-
analyze.hourly_spread :line_type => :processing
|
109
|
-
analyze.frequency :error, :title => 'Failed requests', :line_type => :failed, :amount => 20
|
110
|
-
end
|
37
|
+
# Define the reporting for the additional parsed lines
|
38
|
+
report(:append) do |analyze|
|
39
|
+
|
40
|
+
analyze.duration :render_duration, :category => :render_file, :multiple_per_request => true,
|
41
|
+
:amount => 20, :title => 'Partial rendering duration'
|
42
|
+
|
43
|
+
analyze.duration :query_duration, :category => :query_sql, :multiple_per_request => true,
|
44
|
+
:amount => 20, :title => 'Query duration'
|
45
|
+
|
46
|
+
end
|
111
47
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
48
|
+
# Add a converter method for the SQL fields the the Rails request class
|
49
|
+
class Request < Rails::Request
|
50
|
+
|
51
|
+
# Sanitizes SQL queries so that they can be grouped
|
52
|
+
def convert_sql(sql, definition)
|
53
|
+
sql.gsub(/\b\d+\b/, ':int').gsub(/`([^`]+)`/, '\1').gsub(/'[^']*'/, ':string').rstrip
|
116
54
|
end
|
117
|
-
end
|
55
|
+
end
|
118
56
|
end
|
119
57
|
end
|
@@ -80,8 +80,8 @@ module RequestLogAnalyzer::FileFormat
|
|
80
80
|
|
81
81
|
# Copy the line and report definer from the parent class.
|
82
82
|
subclass.class_eval do
|
83
|
-
instance_variable_set(:@line_definer, superclass.line_definer)
|
84
|
-
instance_variable_set(:@report_definer, superclass.report_definer)
|
83
|
+
instance_variable_set(:@line_definer, superclass.line_definer.clone)
|
84
|
+
instance_variable_set(:@report_definer, superclass.report_definer.clone)
|
85
85
|
class << self; attr_accessor :line_definer, :report_definer; end
|
86
86
|
end
|
87
87
|
|
@@ -95,8 +95,12 @@ module RequestLogAnalyzer::FileFormat
|
|
95
95
|
@line_definer.send(name, &block)
|
96
96
|
end
|
97
97
|
|
98
|
-
def
|
99
|
-
self.class::Request
|
98
|
+
def request_class
|
99
|
+
self.class::Request
|
100
|
+
end
|
101
|
+
|
102
|
+
def request(*hashes)
|
103
|
+
request_class.create(self, *hashes)
|
100
104
|
end
|
101
105
|
|
102
106
|
# Specifies multiple line definitions at once using a block
|
@@ -9,8 +9,13 @@ module RequestLogAnalyzer::Filter
|
|
9
9
|
|
10
10
|
attr_reader :field, :value, :mode
|
11
11
|
|
12
|
+
def initialize(file_format, options = {})
|
13
|
+
super(file_format, options)
|
14
|
+
setup_filter
|
15
|
+
end
|
16
|
+
|
12
17
|
# Setup mode, field and value.
|
13
|
-
def
|
18
|
+
def setup_filter
|
14
19
|
@mode = (@options[:mode] || :accept).to_sym
|
15
20
|
@field = @options[:field].to_sym
|
16
21
|
|
@@ -8,9 +8,15 @@ module RequestLogAnalyzer::Filter
|
|
8
8
|
|
9
9
|
attr_reader :before, :after
|
10
10
|
|
11
|
+
def initialize(file_format, options = {})
|
12
|
+
super(file_format, options)
|
13
|
+
setup_filter
|
14
|
+
end
|
15
|
+
|
16
|
+
|
11
17
|
# Convert the timestamp to the correct formats for quick timestamp comparisons.
|
12
18
|
# These are stored in the before and after attr_reader fields.
|
13
|
-
def
|
19
|
+
def setup_filter
|
14
20
|
@after = @options[:after].strftime('%Y%m%d%H%M%S').to_i if options[:after]
|
15
21
|
@before = @options[:before].strftime('%Y%m%d%H%M%S').to_i if options[:before]
|
16
22
|
end
|
@@ -12,6 +12,10 @@ module RequestLogAnalyzer
|
|
12
12
|
def initialize
|
13
13
|
@line_definitions = {}
|
14
14
|
end
|
15
|
+
|
16
|
+
def initialize_copy(other)
|
17
|
+
@line_definitions = other.line_definitions.dup
|
18
|
+
end
|
15
19
|
|
16
20
|
def method_missing(name, *args, &block)
|
17
21
|
if block_given?
|
@@ -33,7 +37,7 @@ module RequestLogAnalyzer
|
|
33
37
|
@captures = []
|
34
38
|
definition.each { |key, value| self.send("#{key.to_s}=".to_sym, value) }
|
35
39
|
end
|
36
|
-
|
40
|
+
|
37
41
|
def self.define(name, &block)
|
38
42
|
definition = self.new(name)
|
39
43
|
yield(definition) if block_given?
|
@@ -73,7 +77,14 @@ module RequestLogAnalyzer
|
|
73
77
|
def convert_captured_values(values, request)
|
74
78
|
value_hash = {}
|
75
79
|
captures.each_with_index do |capture, index|
|
76
|
-
|
80
|
+
converted = request.convert_value(values[index], capture)
|
81
|
+
if converted.kind_of?(Hash)
|
82
|
+
value_hash[capture[:name]] = values[index]
|
83
|
+
converted = converted.inject({}) { |h, (key, value)| h[key.to_sym] = value; h }
|
84
|
+
value_hash = converted.merge(value_hash)
|
85
|
+
else
|
86
|
+
value_hash[capture[:name]] ||= converted
|
87
|
+
end
|
77
88
|
end
|
78
89
|
return value_hash
|
79
90
|
end
|