request-log-analyzer 1.1.2 → 1.1.3
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/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
|