request-log-analyzer 1.0.2
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 +14 -0
- data/HACKING +7 -0
- data/LICENSE +20 -0
- data/README.textile +36 -0
- data/Rakefile +5 -0
- data/bin/request-log-analyzer +123 -0
- data/lib/cli/bashcolorizer.rb +60 -0
- data/lib/cli/command_line_arguments.rb +301 -0
- data/lib/cli/progressbar.rb +236 -0
- data/lib/request_log_analyzer.rb +14 -0
- data/lib/request_log_analyzer/aggregator/base.rb +45 -0
- data/lib/request_log_analyzer/aggregator/database.rb +148 -0
- data/lib/request_log_analyzer/aggregator/echo.rb +25 -0
- data/lib/request_log_analyzer/aggregator/summarizer.rb +116 -0
- data/lib/request_log_analyzer/controller.rb +201 -0
- data/lib/request_log_analyzer/file_format.rb +81 -0
- data/lib/request_log_analyzer/file_format/merb.rb +33 -0
- data/lib/request_log_analyzer/file_format/rails.rb +90 -0
- data/lib/request_log_analyzer/filter/base.rb +29 -0
- data/lib/request_log_analyzer/filter/field.rb +36 -0
- data/lib/request_log_analyzer/filter/timespan.rb +32 -0
- data/lib/request_log_analyzer/line_definition.rb +159 -0
- data/lib/request_log_analyzer/log_parser.rb +173 -0
- data/lib/request_log_analyzer/log_processor.rb +121 -0
- data/lib/request_log_analyzer/request.rb +95 -0
- data/lib/request_log_analyzer/source/base.rb +42 -0
- data/lib/request_log_analyzer/source/log_file.rb +170 -0
- data/lib/request_log_analyzer/tracker/base.rb +54 -0
- data/lib/request_log_analyzer/tracker/category.rb +71 -0
- data/lib/request_log_analyzer/tracker/duration.rb +81 -0
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +80 -0
- data/lib/request_log_analyzer/tracker/timespan.rb +54 -0
- data/spec/controller_spec.rb +40 -0
- data/spec/database_inserter_spec.rb +101 -0
- data/spec/file_format_spec.rb +78 -0
- data/spec/file_formats/spec_format.rb +26 -0
- data/spec/filter_spec.rb +137 -0
- data/spec/fixtures/merb.log +84 -0
- data/spec/fixtures/multiple_files_1.log +5 -0
- data/spec/fixtures/multiple_files_2.log +2 -0
- data/spec/fixtures/rails_1x.log +59 -0
- data/spec/fixtures/rails_22.log +12 -0
- data/spec/fixtures/rails_22_cached.log +10 -0
- data/spec/fixtures/rails_unordered.log +24 -0
- data/spec/fixtures/syslog_1x.log +5 -0
- data/spec/fixtures/test_file_format.log +13 -0
- data/spec/fixtures/test_language_combined.log +14 -0
- data/spec/fixtures/test_order.log +16 -0
- data/spec/line_definition_spec.rb +124 -0
- data/spec/log_parser_spec.rb +68 -0
- data/spec/log_processor_spec.rb +57 -0
- data/spec/merb_format_spec.rb +38 -0
- data/spec/rails_format_spec.rb +76 -0
- data/spec/request_spec.rb +72 -0
- data/spec/spec_helper.rb +67 -0
- data/spec/summarizer_spec.rb +9 -0
- data/tasks/github-gem.rake +177 -0
- data/tasks/request_log_analyzer.rake +10 -0
- data/tasks/rspec.rake +6 -0
- metadata +135 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../tracker/base'
|
2
|
+
|
3
|
+
module RequestLogAnalyzer::Aggregator
|
4
|
+
|
5
|
+
class Summarizer < Base
|
6
|
+
|
7
|
+
class Definer
|
8
|
+
|
9
|
+
attr_reader :trackers
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@trackers = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(tracker_method, *args)
|
16
|
+
track(tracker_method, args.first)
|
17
|
+
end
|
18
|
+
|
19
|
+
def category(category_field, options = {})
|
20
|
+
if category_field.kind_of?(Symbol)
|
21
|
+
track(:category, options.merge(:category => category_field))
|
22
|
+
elsif category_field.kind_of?(Hash)
|
23
|
+
track(:category, category_field.merge(options))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def duration(duration_field, options = {})
|
28
|
+
if duration_field.kind_of?(Symbol)
|
29
|
+
track(:duration, options.merge(:duration => duration_field))
|
30
|
+
elsif duration_field.kind_of?(Hash)
|
31
|
+
track(:duration, duration_field.merge(options))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def track(tracker_klass, options = {})
|
36
|
+
require "#{File.dirname(__FILE__)}/../tracker/#{tracker_klass}"
|
37
|
+
tracker_klass = RequestLogAnalyzer::Tracker.const_get(tracker_klass.to_s.split(/[^a-z0-9]/i).map{ |w| w.capitalize }.join('')) if tracker_klass.kind_of?(Symbol)
|
38
|
+
@trackers << tracker_klass.new(options)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :trackers
|
43
|
+
attr_reader :warnings_encountered
|
44
|
+
|
45
|
+
def initialize(source, options = {})
|
46
|
+
super(source, options)
|
47
|
+
@warnings_encountered = {}
|
48
|
+
@trackers = source.file_format.report_trackers
|
49
|
+
setup
|
50
|
+
end
|
51
|
+
|
52
|
+
def setup
|
53
|
+
end
|
54
|
+
|
55
|
+
def prepare
|
56
|
+
raise "No trackers set up in Summarizer!" if @trackers.nil? || @trackers.empty?
|
57
|
+
@trackers.each { |tracker| tracker.prepare }
|
58
|
+
end
|
59
|
+
|
60
|
+
def aggregate(request)
|
61
|
+
@trackers.each do |tracker|
|
62
|
+
tracker.update(request) if tracker.should_update?(request)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def finalize
|
67
|
+
@trackers.each { |tracker| tracker.finalize }
|
68
|
+
end
|
69
|
+
|
70
|
+
def report(output=STDOUT, report_width = 80, color = false)
|
71
|
+
report_header(output, report_width, color)
|
72
|
+
if source.parsed_requests > 0
|
73
|
+
@trackers.each { |tracker| tracker.report(output, report_width, color) }
|
74
|
+
else
|
75
|
+
output << "\n"
|
76
|
+
output << "There were no requests analyzed.\n"
|
77
|
+
end
|
78
|
+
report_footer(output, report_width, color)
|
79
|
+
end
|
80
|
+
|
81
|
+
def report_header(output=STDOUT, report_width = 80, color = false)
|
82
|
+
output << "Request summary\n"
|
83
|
+
output << green("━" * report_width, color) + "\n"
|
84
|
+
output << "Parsed lines: #{green(source.parsed_lines, color)}\n"
|
85
|
+
output << "Parsed requests: #{green(source.parsed_requests, color)}\n"
|
86
|
+
output << "Skipped lines: #{green(source.skipped_lines, color)}\n" if source.skipped_lines > 0
|
87
|
+
if has_warnings?
|
88
|
+
output << "Warnings: " + @warnings_encountered.map { |(key, value)| "#{key.inspect}: #{blue(value, color)}" }.join(', ') + "\n"
|
89
|
+
end
|
90
|
+
output << "\n"
|
91
|
+
end
|
92
|
+
|
93
|
+
def report_footer(output=STDOUT, report_width = 80, color = false)
|
94
|
+
output << "\n"
|
95
|
+
if has_serious_warnings?
|
96
|
+
output << green("━" * report_width, color) + "\n"
|
97
|
+
output << "Multiple warnings were encountered during parsing. Possibly, your logging " + "\n"
|
98
|
+
output << "is not setup correctly. Visit this website for logging configuration tips:" + "\n"
|
99
|
+
output << blue("http://github.com/wvanbergen/request-log-analyzer/wikis/configure-logging", color) + "\n"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def has_warnings?
|
104
|
+
@warnings_encountered.inject(0) { |result, (key, value)| result += value } > 0
|
105
|
+
end
|
106
|
+
|
107
|
+
def has_serious_warnings?
|
108
|
+
@warnings_encountered.inject(0) { |result, (key, value)| result += value } > 10
|
109
|
+
end
|
110
|
+
|
111
|
+
def warning(type, message, lineno)
|
112
|
+
@warnings_encountered[type] ||= 0
|
113
|
+
@warnings_encountered[type] += 1
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
module RequestLogAnalyzer
|
2
|
+
|
3
|
+
# The RequestLogAnalyzer::Controller class creates a LogParser instance for the
|
4
|
+
# requested file format, and connect it with sources and aggregators.
|
5
|
+
#
|
6
|
+
# Sources are streams or files from which the requests will be parsed.
|
7
|
+
# Aggregators will handle every passed request to yield a meaningfull results.
|
8
|
+
#
|
9
|
+
# - Use the build-function to build a controller instance using command line arguments.
|
10
|
+
# - Use add_aggregator to register a new aggregator
|
11
|
+
# - Use add_source to register a new aggregator
|
12
|
+
# - Use the run! method to start the parser and send the requests to the aggregators.
|
13
|
+
#
|
14
|
+
# Note that the order of sources can be imported if you have log files than succeed
|
15
|
+
# eachother. Requests that span over succeeding files will be parsed correctly if the
|
16
|
+
# sources are registered in the correct order. This can be helpful to parse requests
|
17
|
+
# from several logrotated log files.
|
18
|
+
class Controller
|
19
|
+
|
20
|
+
include RequestLogAnalyzer::FileFormat::Awareness
|
21
|
+
|
22
|
+
attr_reader :aggregators
|
23
|
+
attr_reader :filters
|
24
|
+
attr_reader :log_parser
|
25
|
+
attr_reader :source
|
26
|
+
attr_reader :output
|
27
|
+
attr_reader :options
|
28
|
+
|
29
|
+
# Builds a RequestLogAnalyzer::Controller given parsed command line arguments
|
30
|
+
# <tt>arguments<tt> A CommandLine::Arguments hash containing parsed commandline parameters.
|
31
|
+
# <rr>report_with</tt> Width of the report. Defaults to 80.
|
32
|
+
def self.build(arguments, report_width = 80)
|
33
|
+
|
34
|
+
options = { :report_width => arguments[:report_width].to_i, :output => STDOUT}
|
35
|
+
|
36
|
+
options[:database] = arguments[:database] if arguments[:database]
|
37
|
+
options[:debug] = arguments[:debug]
|
38
|
+
options[:colorize] = !arguments[:boring]
|
39
|
+
|
40
|
+
if arguments[:file]
|
41
|
+
options[:output] = File.new(arguments[:file], "w+")
|
42
|
+
options[:colorize] = false
|
43
|
+
end
|
44
|
+
|
45
|
+
# Create the controller with the correct file format
|
46
|
+
file_format = RequestLogAnalyzer::FileFormat.load(arguments[:format])
|
47
|
+
|
48
|
+
# register sources
|
49
|
+
if arguments.parameters.length == 1
|
50
|
+
file = arguments.parameters[0]
|
51
|
+
if file == '-' || file == 'STDIN'
|
52
|
+
options.store(:source_files, $stdin)
|
53
|
+
elsif File.exist?(file)
|
54
|
+
options.store(:source_files, file)
|
55
|
+
else
|
56
|
+
puts "File not found: #{file}"
|
57
|
+
exit(0)
|
58
|
+
end
|
59
|
+
else
|
60
|
+
options.store(:source_files, arguments.parameters)
|
61
|
+
end
|
62
|
+
|
63
|
+
controller = Controller.new(RequestLogAnalyzer::Source::LogFile.new(file_format, options), options)
|
64
|
+
|
65
|
+
options[:assume_correct_order] = arguments[:assume_correct_order]
|
66
|
+
|
67
|
+
# register filters
|
68
|
+
if arguments[:after] || arguments[:before]
|
69
|
+
filter_options = {}
|
70
|
+
filter_options[:after] = DateTime.parse(arguments[:after])
|
71
|
+
filter_options[:before] = DateTime.parse(arguments[:before]) if arguments[:before]
|
72
|
+
controller.add_filter(:timespan, filter_options)
|
73
|
+
end
|
74
|
+
|
75
|
+
arguments[:reject].each do |(field, value)|
|
76
|
+
controller.add_filter(:field, :mode => :reject, :field => field, :value => value)
|
77
|
+
end
|
78
|
+
|
79
|
+
arguments[:select].each do |(field, value)|
|
80
|
+
controller.add_filter(:field, :mode => :select, :field => field, :value => value)
|
81
|
+
end
|
82
|
+
|
83
|
+
# register aggregators
|
84
|
+
arguments[:aggregator].each { |agg| controller.add_aggregator(agg.to_sym) }
|
85
|
+
|
86
|
+
# register the database
|
87
|
+
controller.add_aggregator(:database) if arguments[:database] && !arguments[:aggregator].include?('database')
|
88
|
+
controller.add_aggregator(:summarizer) if arguments[:aggregator].empty?
|
89
|
+
|
90
|
+
# register the echo aggregator in debug mode
|
91
|
+
controller.add_aggregator(:echo) if arguments[:debug]
|
92
|
+
|
93
|
+
file_format.setup_environment(controller)
|
94
|
+
|
95
|
+
return controller
|
96
|
+
end
|
97
|
+
|
98
|
+
# Builds a new Controller for the given log file format.
|
99
|
+
# <tt>format</tt> Logfile format. Defaults to :rails
|
100
|
+
# Options are passd on to the LogParser.
|
101
|
+
# * <tt>:aggregator</tt> Aggregator array.
|
102
|
+
# * <tt>:database</tt> Database the controller should use.
|
103
|
+
# * <tt>:echo</tt> Output debug information.
|
104
|
+
# * <tt>:silent</tt> Do not output any warnings.
|
105
|
+
# * <tt>:colorize</tt> Colorize output
|
106
|
+
# * <tt>:output</tt> All report outputs get << through this output.
|
107
|
+
def initialize(source, options = {})
|
108
|
+
|
109
|
+
@source = source
|
110
|
+
@options = options
|
111
|
+
@aggregators = []
|
112
|
+
@filters = []
|
113
|
+
@output = options[:output]
|
114
|
+
|
115
|
+
# Requester format through RequestLogAnalyzer::FileFormat and construct the parser
|
116
|
+
register_file_format(@source.file_format)
|
117
|
+
|
118
|
+
# Pass all warnings to every aggregator so they can do something useful with them.
|
119
|
+
@source.warning = lambda { |type, message, lineno| @aggregators.each { |agg| agg.warning(type, message, lineno) } } if @source
|
120
|
+
|
121
|
+
# Handle progress messagess
|
122
|
+
@source.progress = lambda { |message, value| handle_progress(message, value) } if @source
|
123
|
+
end
|
124
|
+
|
125
|
+
# Progress function.
|
126
|
+
# Expects :started with file, :progress with current line and :finished or :interrupted when done.
|
127
|
+
# <tt>message</tt> Current state (:started, :finished, :interupted or :progress).
|
128
|
+
# <tt>value</tt> File or current line.
|
129
|
+
def handle_progress(message, value = nil)
|
130
|
+
case message
|
131
|
+
when :started
|
132
|
+
@progress_bar = ProgressBar.new(green(File.basename(value), options[:colorize]), File.size(value))
|
133
|
+
when :finished
|
134
|
+
@progress_bar.finish
|
135
|
+
@progress_bar = nil
|
136
|
+
when :interrupted
|
137
|
+
if @progress_bar
|
138
|
+
@progress_bar.halt
|
139
|
+
@progress_bar = nil
|
140
|
+
end
|
141
|
+
when :progress
|
142
|
+
@progress_bar.set(value)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Adds an aggregator to the controller. The aggregator will be called for every request
|
147
|
+
# that is parsed from the provided sources (see add_source)
|
148
|
+
def add_aggregator(agg)
|
149
|
+
if agg.kind_of?(Symbol)
|
150
|
+
require File.dirname(__FILE__) + "/aggregator/#{agg}"
|
151
|
+
agg = RequestLogAnalyzer::Aggregator.const_get(agg.to_s.split(/[^a-z0-9]/i).map{ |w| w.capitalize }.join(''))
|
152
|
+
end
|
153
|
+
|
154
|
+
@aggregators << agg.new(@source, @options)
|
155
|
+
end
|
156
|
+
|
157
|
+
alias :>> :add_aggregator
|
158
|
+
|
159
|
+
# Adds a request filter to the controller.
|
160
|
+
def add_filter(filter, filter_options = {})
|
161
|
+
if filter.kind_of?(Symbol)
|
162
|
+
require File.dirname(__FILE__) + "/filter/#{filter}"
|
163
|
+
filter = RequestLogAnalyzer::Filter.const_get(filter.to_s.split(/[^a-z0-9]/i).map{ |w| w.capitalize }.join(''))
|
164
|
+
end
|
165
|
+
|
166
|
+
@filters << filter.new(file_format, @options.merge(filter_options))
|
167
|
+
end
|
168
|
+
|
169
|
+
# Runs RequestLogAnalyzer
|
170
|
+
# 1. Call prepare on every aggregator
|
171
|
+
# 2. Generate requests from source object
|
172
|
+
# 3. Filter out unwanted requests
|
173
|
+
# 4. Call aggregate for remaning requests on every aggregator
|
174
|
+
# 4. Call finalize on every aggregator
|
175
|
+
# 5. Call report on every aggregator
|
176
|
+
# 6. Finalize Source
|
177
|
+
def run!
|
178
|
+
|
179
|
+
@filters.each { |filter| filter.prepare }
|
180
|
+
@aggregators.each { |agg| agg.prepare }
|
181
|
+
|
182
|
+
begin
|
183
|
+
@source.requests do |request|
|
184
|
+
#@filters.each { |filter| request = filter.filter(request) }
|
185
|
+
@aggregators.each { |agg| agg.aggregate(request) } if request
|
186
|
+
end
|
187
|
+
rescue Interrupt => e
|
188
|
+
handle_progress(:interrupted)
|
189
|
+
puts "Caught interrupt! Stopped parsing."
|
190
|
+
end
|
191
|
+
|
192
|
+
puts "\n"
|
193
|
+
|
194
|
+
@aggregators.each { |agg| agg.finalize }
|
195
|
+
@aggregators.each { |agg| agg.report(@output, options[:report_width], options[:colorize]) }
|
196
|
+
|
197
|
+
@source.finalize
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module RequestLogAnalyzer
|
2
|
+
|
3
|
+
class FileFormat
|
4
|
+
|
5
|
+
# Makes classes aware of a file format by registering the file_format variable
|
6
|
+
module Awareness
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.send(:attr_reader, :file_format)
|
10
|
+
end
|
11
|
+
|
12
|
+
def register_file_format(format)
|
13
|
+
@file_format = format
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.inherited(subclass)
|
18
|
+
subclass.instance_variable_set(:@line_definer, RequestLogAnalyzer::LineDefinition::Definer.new)
|
19
|
+
subclass.class_eval { class << self; attr_accessor :line_definer; end }
|
20
|
+
subclass.class_eval { class << self; attr_accessor :report_definer; end }
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.line_definition(name, &block)
|
24
|
+
@line_definer.send(name, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.format_definition(&block)
|
28
|
+
if block_given?
|
29
|
+
yield(@line_definer)
|
30
|
+
else
|
31
|
+
return @line_definer
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.report(&block)
|
36
|
+
@report_definer = RequestLogAnalyzer::Aggregator::Summarizer::Definer.new
|
37
|
+
yield(@report_definer)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.load(file_format)
|
41
|
+
if file_format.kind_of?(RequestLogAnalyzer::FileFormat)
|
42
|
+
# this already is a file format! return itself
|
43
|
+
return file_format
|
44
|
+
|
45
|
+
elsif file_format.kind_of?(Class) && file_format.ancestors.include?(RequestLogAnalyzer::FileFormat)
|
46
|
+
klass = file_format
|
47
|
+
|
48
|
+
elsif file_format.kind_of?(String) && File.exist?(file_format)
|
49
|
+
# load a format from a ruby file
|
50
|
+
require file_format
|
51
|
+
klass_name = File.basename(file_format, '.rb').split(/[^a-z0-9]/i).map{ |w| w.capitalize }.join('')
|
52
|
+
klass = Object.const_get(klass_name)
|
53
|
+
|
54
|
+
elsif File.exist?("#{File.dirname(__FILE__)}/file_format/#{file_format}.rb")
|
55
|
+
# load a provided file format
|
56
|
+
require "#{File.dirname(__FILE__)}/file_format/#{file_format}"
|
57
|
+
klass_name = file_format.to_s.split(/[^a-z0-9]/i).map{ |w| w.capitalize }.join('')
|
58
|
+
klass = RequestLogAnalyzer::FileFormat.const_get(klass_name)
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
klass.new # return an instance of the class
|
63
|
+
end
|
64
|
+
|
65
|
+
def line_definitions
|
66
|
+
@line_definitions ||= self.class.line_definer.line_definitions
|
67
|
+
end
|
68
|
+
|
69
|
+
def report_trackers
|
70
|
+
self.class.instance_variable_get(:@report_definer).trackers rescue []
|
71
|
+
end
|
72
|
+
|
73
|
+
def valid?
|
74
|
+
line_definitions.detect { |(name, ld)| ld.header } && line_definitions.detect { |(name, ld)| ld.footer }
|
75
|
+
end
|
76
|
+
|
77
|
+
def setup_environment(controller)
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module RequestLogAnalyzer::FileFormat::Merb
|
2
|
+
|
3
|
+
LINE_DEFINITIONS = {
|
4
|
+
|
5
|
+
# ~ Started request handling: Fri Aug 29 11:10:23 +0200 2008
|
6
|
+
:started => {
|
7
|
+
:header => true,
|
8
|
+
:teaser => /Started/,
|
9
|
+
:regexp => /Started request handling\:\ (.+)/,
|
10
|
+
:captures => [{ :name => :timestamp, :type => :timestamp, :anonymize => :slightly }]
|
11
|
+
},
|
12
|
+
|
13
|
+
# ~ Params: {"action"=>"create", "controller"=>"session"}
|
14
|
+
# ~ Params: {"_method"=>"delete", "authenticity_token"=>"[FILTERED]", "action"=>"d}
|
15
|
+
:params => {
|
16
|
+
:teaser => /Params/,
|
17
|
+
:regexp => /Params\:\ \{(.+)\}/,
|
18
|
+
:captures => [{ :name => :raw_hash, :type => :string}]
|
19
|
+
},
|
20
|
+
|
21
|
+
# ~ {:dispatch_time=>0.006117, :after_filters_time=>6.1e-05, :before_filters_time=>0.000712, :action_time=>0.005833}
|
22
|
+
:completed => {
|
23
|
+
:footer => true,
|
24
|
+
:teaser => /\{:dispatch_time/,
|
25
|
+
:regexp => /\{\:dispatch_time=>(\d+\.\d+(?:e-?\d+)?), (?:\:after_filters_time=>(\d+\.\d+(?:e-?\d+)?), )?(?:\:before_filters_time=>(\d+\.\d+(?:e-?\d+)?), )?\:action_time=>(\d+\.\d+(?:e-?\d+)?)\}/,
|
26
|
+
:captures => [{ :name => :dispatch_time, :type => :sec, :anonymize => :slightly },
|
27
|
+
{ :name => :after_filters_time, :type => :sec, :anonymize => :slightly },
|
28
|
+
{ :name => :before_filters_time, :type => :sec, :anonymize => :slightly },
|
29
|
+
{ :name => :action_time, :type => :sec, :anonymize => :slightly }]
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
class RequestLogAnalyzer::FileFormat::Rails < RequestLogAnalyzer::FileFormat
|
2
|
+
|
3
|
+
# Processing EmployeeController#index (for 123.123.123.123 at 2008-07-13 06:00:00) [GET]
|
4
|
+
line_definition :processing do |line|
|
5
|
+
line.header = true # this line is the first log line for a request
|
6
|
+
line.teaser = /Processing /
|
7
|
+
line.regexp = /Processing ((?:\w+::)?\w+)#(\w+)(?: to (\w+))? \(for (\d+\.\d+\.\d+\.\d+) at (\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)\) \[([A-Z]+)\]/
|
8
|
+
line.captures << { :name => :controller, :type => :string } \
|
9
|
+
<< { :name => :action, :type => :string } \
|
10
|
+
<< { :name => :format, :type => :string } \
|
11
|
+
<< { :name => :ip, :type => :string, :anonymize => :ip } \
|
12
|
+
<< { :name => :timestamp, :type => :timestamp, :anonymize => :slightly } \
|
13
|
+
<< { :name => :method, :type => :string }
|
14
|
+
end
|
15
|
+
|
16
|
+
# 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.
|
17
|
+
line_definition :cache_hit do |line|
|
18
|
+
line.regexp = /Filter chain halted as \[\#<ActionController::Caching::Actions::ActionCacheFilter:.+>\] rendered_or_redirected/
|
19
|
+
end
|
20
|
+
|
21
|
+
# RuntimeError (Cannot destroy employee): /app/models/employee.rb:198:in `before_destroy'
|
22
|
+
line_definition :failed do |line|
|
23
|
+
line.footer = true
|
24
|
+
line.regexp = /((?:[A-Z]\w+\:\:)*[A-Z]\w+) \((.*)\)(?: on line #(\d+) of .+)?\:(.*)/
|
25
|
+
line.captures << { :name => :error, :type => :string } \
|
26
|
+
<< { :name => :message, :type => :string } \
|
27
|
+
<< { :name => :line, :type => :integer } \
|
28
|
+
<< { :name => :file, :type => :string } \
|
29
|
+
<< { :name => :stack_trace, :type => :string, :anonymize => true }
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Rails < 2.1 completed line example
|
34
|
+
# Completed in 0.21665 (4 reqs/sec) | Rendering: 0.00926 (4%) | DB: 0.00000 (0%) | 200 OK [http://demo.nu/employees]
|
35
|
+
RAILS_21_COMPLETED = /Completed in (\d+\.\d{5}) \(\d+ reqs\/sec\) (?:\| Rendering: (\d+\.\d{5}) \(\d+\%\) )?(?:\| DB: (\d+\.\d{5}) \(\d+\%\) )?\| (\d\d\d).+\[(http.+)\]/
|
36
|
+
|
37
|
+
# Rails > 2.1 completed line example
|
38
|
+
# Completed in 614ms (View: 120, DB: 31) | 200 OK [http://floorplanner.local/demo]
|
39
|
+
RAILS_22_COMPLETED = /Completed in (\d+)ms \((?:View: (\d+), )?DB: (\d+)\) \| (\d\d\d).+\[(http.+)\]/
|
40
|
+
|
41
|
+
# The completed line uses a kind of hack to ensure that both old style logs and new style logs
|
42
|
+
# are both parsed by the same regular expression. The format in Rails 2.2 was slightly changed,
|
43
|
+
# but the line contains exactly the same information.
|
44
|
+
line_definition :completed do |line|
|
45
|
+
|
46
|
+
line.footer = true
|
47
|
+
line.teaser = /Completed in /
|
48
|
+
line.regexp = Regexp.new("(?:#{RAILS_21_COMPLETED}|#{RAILS_22_COMPLETED})")
|
49
|
+
|
50
|
+
line.captures << { :name => :duration, :type => :sec, :anonymize => :slightly } \
|
51
|
+
<< { :name => :view, :type => :sec, :anonymize => :slightly } \
|
52
|
+
<< { :name => :db, :type => :sec, :anonymize => :slightly } \
|
53
|
+
<< { :name => :status, :type => :integer } \
|
54
|
+
<< { :name => :url, :type => :string, :anonymize => :url } # Old variant
|
55
|
+
|
56
|
+
line.captures << { :name => :duration, :type => :msec, :anonymize => :slightly } \
|
57
|
+
<< { :name => :view, :type => :msec, :anonymize => :slightly } \
|
58
|
+
<< { :name => :db, :type => :msec, :anonymize => :slightly } \
|
59
|
+
<< { :name => :status, :type => :integer} \
|
60
|
+
<< { :name => :url, :type => :string, :anonymize => :url } # 2.2 variant
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
REQUEST_CATEGORIZER = Proc.new do |request|
|
66
|
+
format = request[:format] || 'html'
|
67
|
+
"#{request[:controller]}##{request[:action]}.#{format} [#{request[:method]}]"
|
68
|
+
end
|
69
|
+
|
70
|
+
report do |analyze|
|
71
|
+
analyze.timespan :line_type => :processing
|
72
|
+
analyze.category :category => REQUEST_CATEGORIZER, :title => 'Top 20 hits', :amount => 20, :line_type => :processing
|
73
|
+
analyze.category :method, :title => 'HTTP methods'
|
74
|
+
analyze.category :status, :title => 'HTTP statuses returned'
|
75
|
+
analyze.category :category => lambda { |request| request =~ :cache_hit ? 'Cache hit' : 'No hit' }, :title => 'Rails action cache hits'
|
76
|
+
|
77
|
+
analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
|
78
|
+
analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
|
79
|
+
analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
|
80
|
+
|
81
|
+
analyze.category :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
|
82
|
+
:if => lambda { |request| request[:duration] && request[:duration] > 1.0 }, :amount => 20
|
83
|
+
|
84
|
+
analyze.hourly_spread :line_type => :processing
|
85
|
+
analyze.category :error, :title => 'Failed requests', :line_type => :failed, :amount => 20
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
end
|