request-log-analyzer 1.4.0 → 1.5.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/LICENSE +1 -1
- data/README.rdoc +19 -24
- data/bin/request-log-analyzer +10 -7
- data/lib/request_log_analyzer/aggregator/summarizer.rb +13 -2
- data/lib/request_log_analyzer/controller.rb +68 -36
- data/lib/request_log_analyzer/file_format/apache.rb +1 -1
- data/lib/request_log_analyzer/file_format/mysql.rb +93 -0
- data/lib/request_log_analyzer/file_format/rails_development.rb +1 -1
- data/lib/request_log_analyzer/file_format.rb +46 -2
- data/lib/request_log_analyzer/mailer.rb +25 -12
- data/lib/request_log_analyzer/output/fixed_width.rb +1 -1
- data/lib/request_log_analyzer/output/html.rb +2 -0
- data/lib/request_log_analyzer/request.rb +5 -0
- data/lib/request_log_analyzer/source/log_parser.rb +33 -10
- data/lib/request_log_analyzer/tracker/count.rb +93 -0
- data/lib/request_log_analyzer.rb +1 -1
- data/request-log-analyzer.gemspec +6 -6
- data/spec/fixtures/mysql_slow_query.log +110 -0
- data/spec/integration/command_line_usage_spec.rb +3 -3
- data/spec/integration/mailer_spec.rb +179 -0
- data/spec/integration/munin_plugins_rails_spec.rb +58 -0
- data/spec/integration/scout_spec.rb +152 -0
- data/spec/lib/helpers.rb +26 -1
- data/spec/lib/mocks.rb +3 -1
- data/spec/unit/file_format/apache_format_spec.rb +102 -95
- data/spec/unit/file_format/format_autodetection_spec.rb +35 -0
- data/spec/unit/file_format/mysql_format_spec.rb +155 -0
- data/spec/unit/file_format/rails_format_spec.rb +9 -12
- data/spec/unit/mailer_spec.rb +12 -0
- data/spec/unit/tracker/count_tracker_spec.rb +123 -0
- data/spec/unit/tracker/tracker_api_spec.rb +1 -1
- metadata +125 -110
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
|
@@ -1,42 +1,37 @@
|
|
|
1
1
|
= Request-log-analyzer
|
|
2
2
|
|
|
3
|
-
This is a simple command line tool to analyze request log files
|
|
4
|
-
Merb to produce a performance report. Its purpose is to find what actions are best candidates for optimization.
|
|
3
|
+
This is a simple command line tool to analyze request log files in various formats to produce a performance report. Its purpose is to find what actions are best candidates for optimization.
|
|
5
4
|
|
|
6
|
-
* Analyzes Rails
|
|
7
|
-
* Combines multiple files and decompresses compressed files
|
|
8
|
-
* Uses several metrics, including cumulative request time,
|
|
9
|
-
* Low memory footprint and reasonably fast
|
|
5
|
+
* Analyzes Rails request logs, Merb request logs, Apache access logs and more, or parses any other log format you specify.
|
|
6
|
+
* Combines multiple files and decompresses compressed files, which comes in handy if you are using logrotate.
|
|
7
|
+
* Uses several metrics, including cumulative request time, mean request time, process blockers, database and rendering time, HTTP methods and statuses, Rails action cache statistics, etc.) (Sample output: http://wiki.github.com/wvanbergen/request-log-analyzer/sample-output)
|
|
8
|
+
* Low memory footprint and reasonably fast, so it is safe to run on a production server.
|
|
10
9
|
* MIT licensed
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
See the project wiki at http://wiki.github.com/wvanbergen/request-log-analyzer for documentation and additional information.
|
|
13
12
|
|
|
14
|
-
== Installation
|
|
13
|
+
== Installation & basic usage
|
|
15
14
|
|
|
16
|
-
Install request-log-analyzer as a Ruby gem
|
|
15
|
+
Install request-log-analyzer as a Ruby gem (you might need to run this command
|
|
16
|
+
as root by prepending +sudo+ to it):
|
|
17
17
|
|
|
18
|
-
$
|
|
18
|
+
$ gem install request-log-analyzer
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
$ sudo gem install wvanbergen-request-log-analyzer --source http://gems.github.com
|
|
23
|
-
|
|
24
|
-
To get the best results out of request-log-analyzer, make sure to
|
|
25
|
-
set up logging correctly: http://wiki.github.com/wvanbergen/request-log-analyzer/configure-logging
|
|
26
|
-
for your application.
|
|
27
|
-
|
|
28
|
-
== Usage
|
|
29
|
-
|
|
30
|
-
To analyze a log file and produce a performance report, run request-log-analyzer like this:
|
|
20
|
+
To analyze a Rails log file and produce a performance report, run
|
|
21
|
+
request-log-analyzer like this:
|
|
31
22
|
|
|
32
23
|
$ request-log-analyzer log/production.log
|
|
33
24
|
|
|
34
|
-
For more details and available command line options, see the project's wiki
|
|
25
|
+
For more details, other file formats, and available command line options, see the project's wiki at http://wiki.github.com/wvanbergen/request-log-analyzer
|
|
35
26
|
|
|
36
27
|
== Additional information
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
Request-log-analyzer was designed and built by Willem van Bergen and Bart ten
|
|
30
|
+
Brinke.
|
|
31
|
+
|
|
32
|
+
Do you have a rails application that is not performing as it should? If you need
|
|
33
|
+
an expert to analyze your application, feel free to contact either Willem van
|
|
34
|
+
Bergen (willem@railsdoctors.com) or Bart ten Brinke (bart@railsdoctors.com).
|
|
40
35
|
|
|
41
36
|
* Project wiki at GitHub: http://wiki.github.com/wvanbergen/request-log-analyzer
|
|
42
37
|
* railsdoctors homepage: http://railsdoctors.com
|
data/bin/request-log-analyzer
CHANGED
|
@@ -27,14 +27,16 @@ begin
|
|
|
27
27
|
strip.switch(:keep_junk_lines, :j)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
command_line.option(:format, :alias => :f
|
|
30
|
+
command_line.option(:format, :alias => :f)
|
|
31
31
|
command_line.option(:apache_format)
|
|
32
32
|
command_line.option(:rails_format)
|
|
33
33
|
|
|
34
34
|
command_line.option(:file, :alias => :e)
|
|
35
35
|
command_line.option(:mail, :alias => :m)
|
|
36
|
+
command_line.option(:mailhost, :default => 'localhost')
|
|
36
37
|
command_line.option(:parse_strategy, :default => 'assume-correct')
|
|
37
|
-
command_line.option(:
|
|
38
|
+
command_line.option(:yaml)
|
|
39
|
+
command_line.option(:dump) # To be deprecated
|
|
38
40
|
|
|
39
41
|
command_line.option(:aggregator, :alias => :a, :multiple => true)
|
|
40
42
|
|
|
@@ -48,7 +50,7 @@ begin
|
|
|
48
50
|
command_line.option(:before)
|
|
49
51
|
|
|
50
52
|
command_line.switch(:boring, :b)
|
|
51
|
-
command_line.option(:output, :alias => :o, :default => '
|
|
53
|
+
command_line.option(:output, :alias => :o, :default => 'fixedwidth')
|
|
52
54
|
command_line.option(:report_width, :default => terminal_width - 1)
|
|
53
55
|
command_line.option(:report_amount, :default => 20)
|
|
54
56
|
command_line.option(:report_sort, :default => 'sum,mean')
|
|
@@ -68,7 +70,7 @@ rescue CommandLine::Error => e
|
|
|
68
70
|
puts "Usage: request-log-analyzer [LOGFILES*] <OPTIONS>"
|
|
69
71
|
puts
|
|
70
72
|
puts "Input options:"
|
|
71
|
-
puts " --format <format>, -f:
|
|
73
|
+
puts " --format <format>, -f: Log file format. amazon_s3, apache, merb, mysql, rack or rails (default)."
|
|
72
74
|
puts " --after <date> Only consider requests from <date> or later."
|
|
73
75
|
puts " --before <date> Only consider requests before <date>."
|
|
74
76
|
puts " --select <field> <value> Only consider requests where <field> matches <value>."
|
|
@@ -78,10 +80,11 @@ rescue CommandLine::Error => e
|
|
|
78
80
|
puts " --boring, -b Output reports without ASCII colors."
|
|
79
81
|
puts " --database <filename>, -d: Creates an SQLite3 database of all the parsed request information."
|
|
80
82
|
puts " --debug Print debug information while parsing."
|
|
81
|
-
puts " --file <filename>
|
|
83
|
+
puts " --file <filename> Redirect output to file."
|
|
82
84
|
puts " --mail <emailaddress> Send report to an email address."
|
|
83
|
-
puts " --
|
|
84
|
-
puts " --
|
|
85
|
+
puts " --mailhost <server> Use the given server as the SMTP server for sending email."
|
|
86
|
+
puts " --output <format> Output format. Supports 'html' and 'fixed_width' (default)."
|
|
87
|
+
puts " --yaml <filename> Dump the results in YAML format in the given file."
|
|
85
88
|
puts
|
|
86
89
|
puts "Examples:"
|
|
87
90
|
puts " request-log-analyzer development.log"
|
|
@@ -49,6 +49,17 @@ module RequestLogAnalyzer::Aggregator
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Track the frequency of a specific category
|
|
53
|
+
# <tt>category_field</tt> Field to track
|
|
54
|
+
# <tt>options</tt> options are passed to new frequency tracker
|
|
55
|
+
def count(category_field, options = {})
|
|
56
|
+
if category_field.kind_of?(Symbol)
|
|
57
|
+
track(:count, options.merge(:category => category_field))
|
|
58
|
+
elsif category_field.kind_of?(Hash)
|
|
59
|
+
track(:count, category_field.merge(options))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
# Helper function to initialize a tracker and add it to the tracker array.
|
|
53
64
|
# <tt>tracker_class</tt> The class to include
|
|
54
65
|
# <tt>optiont</tt> The options to pass to the trackers.
|
|
@@ -90,7 +101,7 @@ module RequestLogAnalyzer::Aggregator
|
|
|
90
101
|
# Call finalize on all trackers. Saves a YAML dump if this is set in the options.
|
|
91
102
|
def finalize
|
|
92
103
|
@trackers.each { |tracker| tracker.finalize }
|
|
93
|
-
save_results_dump(options[:
|
|
104
|
+
save_results_dump(options[:yaml]) if options[:yaml]
|
|
94
105
|
end
|
|
95
106
|
|
|
96
107
|
# Saves the results of all the trackers in YAML format to a file.
|
|
@@ -145,7 +156,7 @@ module RequestLogAnalyzer::Aggregator
|
|
|
145
156
|
if has_log_ordering_warnings?
|
|
146
157
|
output.title("Parse warnings")
|
|
147
158
|
|
|
148
|
-
output.puts "Parseable lines were
|
|
159
|
+
output.puts "Parseable lines were encountered without a header line before it. It"
|
|
149
160
|
output.puts "could be that logging is not setup correctly for your application."
|
|
150
161
|
output.puts "Visit this website for logging configuration tips:"
|
|
151
162
|
output.puts output.link("http://github.com/wvanbergen/request-log-analyzer/wikis/configure-logging")
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
module RequestLogAnalyzer
|
|
2
2
|
|
|
3
3
|
# The RequestLogAnalyzer::Controller class creates a LogParser instance for the
|
|
4
|
-
# requested file format
|
|
4
|
+
# requested file format and connect it with sources and aggregators.
|
|
5
5
|
#
|
|
6
6
|
# Sources are streams or files from which the requests will be parsed.
|
|
7
7
|
# Aggregators will handle every passed request to yield a meaningfull results.
|
|
8
8
|
#
|
|
9
|
-
# - Use the build-function to build a controller instance
|
|
10
|
-
# - Use add_aggregator to register a new aggregator
|
|
11
|
-
# - Use add_source to register a new aggregator
|
|
9
|
+
# - Use the build -function to build a new controller instance.
|
|
12
10
|
# - Use the run! method to start the parser and send the requests to the aggregators.
|
|
13
11
|
#
|
|
14
12
|
# Note that the order of sources can be imported if you have log files than succeed
|
|
@@ -21,7 +19,6 @@ module RequestLogAnalyzer
|
|
|
21
19
|
|
|
22
20
|
# Builds a RequestLogAnalyzer::Controller given parsed command line arguments
|
|
23
21
|
# <tt>arguments<tt> A CommandLine::Arguments hash containing parsed commandline parameters.
|
|
24
|
-
# <rr>report_with</tt> Width of the report. Defaults to 80.
|
|
25
22
|
def self.build_from_arguments(arguments)
|
|
26
23
|
|
|
27
24
|
options = {}
|
|
@@ -30,13 +27,12 @@ module RequestLogAnalyzer
|
|
|
30
27
|
options[:database] = arguments[:database]
|
|
31
28
|
options[:reset_database] = arguments[:reset_database]
|
|
32
29
|
options[:debug] = arguments[:debug]
|
|
33
|
-
options[:
|
|
34
|
-
options[:
|
|
30
|
+
options[:yaml] = arguments[:yaml] || arguments[:dump]
|
|
31
|
+
options[:mail] = arguments[:mail]
|
|
35
32
|
options[:no_progress] = arguments[:no_progress]
|
|
36
33
|
options[:format] = arguments[:format]
|
|
37
|
-
options[:output] = arguments[:output]
|
|
34
|
+
options[:output] = arguments[:output].downcase
|
|
38
35
|
options[:file] = arguments[:file]
|
|
39
|
-
options[:format] = arguments[:format]
|
|
40
36
|
options[:after] = arguments[:after]
|
|
41
37
|
options[:before] = arguments[:before]
|
|
42
38
|
options[:reject] = arguments[:reject]
|
|
@@ -46,6 +42,7 @@ module RequestLogAnalyzer
|
|
|
46
42
|
options[:report_width] = arguments[:report_width]
|
|
47
43
|
options[:report_sort] = arguments[:report_sort]
|
|
48
44
|
options[:report_amount] = arguments[:report_amount]
|
|
45
|
+
options[:mailhost] = arguments[:mailhost]
|
|
49
46
|
|
|
50
47
|
# Apache format workaround
|
|
51
48
|
if arguments[:rails_format]
|
|
@@ -54,6 +51,12 @@ module RequestLogAnalyzer
|
|
|
54
51
|
options[:format] = {:apache => arguments[:apache_format]}
|
|
55
52
|
end
|
|
56
53
|
|
|
54
|
+
# Handle output format casing
|
|
55
|
+
if options[:output].class == String
|
|
56
|
+
options[:output] = 'HTML' if options[:output] == 'html'
|
|
57
|
+
options[:output] = 'FixedWidth' if options[:output] == 'fixedwidth' || options[:output] == 'fixed_width'
|
|
58
|
+
end
|
|
59
|
+
|
|
57
60
|
# Register sources
|
|
58
61
|
if arguments.parameters.length == 1
|
|
59
62
|
file = arguments.parameters[0]
|
|
@@ -69,38 +72,60 @@ module RequestLogAnalyzer
|
|
|
69
72
|
options.store(:source_files, arguments.parameters)
|
|
70
73
|
end
|
|
71
74
|
|
|
75
|
+
# Guess file format
|
|
76
|
+
if !options[:format] && options[:source_files]
|
|
77
|
+
options[:format] = :rails # Default
|
|
78
|
+
|
|
79
|
+
if options[:source_files] != $stdin
|
|
80
|
+
if options[:source_files].class == String
|
|
81
|
+
options[:format] = RequestLogAnalyzer::FileFormat.autodetect(options[:source_files])
|
|
82
|
+
|
|
83
|
+
elsif options[:source_files].class == Array && options[:source_files].first != $stdin
|
|
84
|
+
options[:format] = RequestLogAnalyzer::FileFormat.autodetect(options[:source_files].first)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
72
89
|
build(options)
|
|
73
90
|
end
|
|
74
91
|
|
|
75
|
-
# Build a new controller
|
|
76
|
-
#
|
|
77
|
-
# Options are passd on to the LogParser.
|
|
92
|
+
# Build a new controller.
|
|
93
|
+
# Returns a new RequestLogAnalyzer::Controller object.
|
|
78
94
|
#
|
|
79
95
|
# Options
|
|
80
|
-
# * <tt>:database</tt> Database file
|
|
81
|
-
# * <tt>:reset_database</tt>
|
|
82
|
-
# * <tt>:debug</tt> Enables echo aggregator.
|
|
83
|
-
# * <tt>:dump</tt>
|
|
84
|
-
# * <tt>:parse_strategy</tt>
|
|
85
|
-
# * <tt>:no_progress</tt>
|
|
86
|
-
# * <tt>:output</tt> :fixed_width, :html or Output class. Defaults to fixed width.
|
|
87
|
-
# * <tt>:file</tt> Filestring or File or StringIO
|
|
88
|
-
# * <tt>:format</tt> :rails, {:apache => 'FORMATSTRING'}, :merb, etcetera or Format Class. Defaults to :rails.
|
|
89
|
-
# * <tt>:source_files</tt> File or STDIN
|
|
90
96
|
# * <tt>:after</tt> Drop all requests after this date (Date, DateTime, Time, or a String in "YYYY-MM-DD hh:mm:ss" format)
|
|
91
|
-
# * <tt>:
|
|
92
|
-
# * <tt>:
|
|
93
|
-
# * <tt>:
|
|
94
|
-
# * <tt>:
|
|
95
|
-
# * <tt>:
|
|
96
|
-
# * <tt>:
|
|
97
|
+
# * <tt>:aggregator</tt> Array of aggregators (ATM: STRINGS OR SYMBOLS ONLY! - Defaults to [:summarizer]).
|
|
98
|
+
# * <tt>:boring</tt> Do not show color on STDOUT (Defaults to false).
|
|
99
|
+
# * <tt>:before</tt> Drop all requests before this date (Date, DateTime, Time or a String in "YYYY-MM-DD hh:mm:ss" format)
|
|
100
|
+
# * <tt>:database</tt> Database file to insert encountered requests to.
|
|
101
|
+
# * <tt>:debug</tt> Enables echo aggregator which will echo each request analyzed.
|
|
102
|
+
# * <tt>:file</tt> Filestring, File or StringIO.
|
|
103
|
+
# * <tt>:format</tt> :rails, {:apache => 'FORMATSTRING'}, :merb, :amazon_s3, :mysql or RequestLogAnalyzer::FileFormat class. (Defaults to :rails).
|
|
104
|
+
# * <tt>:mail</tt> Email the results to this email address.
|
|
105
|
+
# * <tt>:mailhost</tt> Email the results to this mail server.
|
|
106
|
+
# * <tt>:no_progress</tt> Do not display the progress bar (increases parsing speed).
|
|
107
|
+
# * <tt>:output</tt> 'FixedWidth', 'HTML' or RequestLogAnalyzer::Output class. Defaults to 'FixedWidth'.
|
|
108
|
+
# * <tt>:reject</tt> Reject specific {:field => :value} combination (expects a single hash).
|
|
109
|
+
# * <tt>:report_width</tt> Width of reports in characters for FixedWidth reports. (Defaults to 80)
|
|
110
|
+
# * <tt>:reset_database</tt> Reset the database before starting.
|
|
111
|
+
# * <tt>:select</tt> Select specific {:field => :value} combination (expects a single hash).
|
|
112
|
+
# * <tt>:source_files</tt> Source files to analyze. Provide either File, array of files or STDIN.
|
|
113
|
+
# * <tt>:yaml</tt> Output to YAML file.
|
|
97
114
|
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
115
|
+
# === Example
|
|
116
|
+
# RequestLogAnalyzer::Controller.build(
|
|
117
|
+
# :output => :HTML,
|
|
118
|
+
# :mail => 'root@localhost',
|
|
119
|
+
# :after => Time.now - 24*60*60,
|
|
120
|
+
# :source_files => '/var/log/passenger.log'
|
|
121
|
+
# ).run!
|
|
122
|
+
#
|
|
123
|
+
# === Todo
|
|
124
|
+
# * Check if defaults work (Aggregator defaults seem wrong).
|
|
125
|
+
# * Refactor :database => options[:database], :dump => options[:dump] away from contoller intialization.
|
|
101
126
|
def self.build(options)
|
|
102
127
|
# Defaults
|
|
103
|
-
options[:output] ||=
|
|
128
|
+
options[:output] ||= 'FixedWidth'
|
|
104
129
|
options[:format] ||= :rails
|
|
105
130
|
options[:aggregator] ||= [:summarizer]
|
|
106
131
|
options[:report_width] ||= 80
|
|
@@ -108,6 +133,12 @@ module RequestLogAnalyzer
|
|
|
108
133
|
options[:report_sort] ||= 'sum,mean'
|
|
109
134
|
options[:boring] ||= false
|
|
110
135
|
|
|
136
|
+
# Deprecation warnings
|
|
137
|
+
if options[:dump]
|
|
138
|
+
warn "[DEPRECATION] `:dump` is deprecated. Please use `:yaml` instead."
|
|
139
|
+
options[:yaml] = options[:dump]
|
|
140
|
+
end
|
|
141
|
+
|
|
111
142
|
# Set the output class
|
|
112
143
|
output_args = {}
|
|
113
144
|
output_object = nil
|
|
@@ -124,7 +155,7 @@ module RequestLogAnalyzer
|
|
|
124
155
|
output_object = %w[File StringIO].include?(options[:file].class.name) ? options[:file] : File.new(options[:file], "w+")
|
|
125
156
|
output_args = {:width => 80, :color => false, :characters => :ascii, :sort => output_sort, :amount => output_amount }
|
|
126
157
|
elsif options[:mail]
|
|
127
|
-
output_object = RequestLogAnalyzer::Mailer.new(
|
|
158
|
+
output_object = RequestLogAnalyzer::Mailer.new(options[:mail], options[:mailhost])
|
|
128
159
|
output_args = {:width => 80, :color => false, :characters => :ascii, :sort => output_sort, :amount => output_amount }
|
|
129
160
|
else
|
|
130
161
|
output_object = STDOUT
|
|
@@ -145,8 +176,9 @@ module RequestLogAnalyzer
|
|
|
145
176
|
controller = Controller.new( RequestLogAnalyzer::Source::LogParser.new(file_format, :source_files => options[:source_files]),
|
|
146
177
|
{ :output => output_instance,
|
|
147
178
|
:database => options[:database], # FUGLY!
|
|
148
|
-
:
|
|
149
|
-
:reset_database => options[:reset_database]
|
|
179
|
+
:yaml => options[:yaml],
|
|
180
|
+
:reset_database => options[:reset_database],
|
|
181
|
+
:no_progress => options[:no_progress]})
|
|
150
182
|
|
|
151
183
|
# register filters
|
|
152
184
|
if options[:after] || options[:before]
|
|
@@ -188,7 +220,7 @@ module RequestLogAnalyzer
|
|
|
188
220
|
# <tt>format</tt> Logfile format. Defaults to :rails
|
|
189
221
|
# Options are passd on to the LogParser.
|
|
190
222
|
# * <tt>:database</tt> Database the controller should use.
|
|
191
|
-
# * <tt>:
|
|
223
|
+
# * <tt>:yaml</tt> Yaml Dump the contrller should use.
|
|
192
224
|
# * <tt>:output</tt> All report outputs get << through this output.
|
|
193
225
|
# * <tt>:no_progress</tt> No progress bar
|
|
194
226
|
def initialize(source, options = {})
|
|
@@ -55,7 +55,7 @@ module RequestLogAnalyzer::FileFormat
|
|
|
55
55
|
|
|
56
56
|
# Creates the access log line definition based on the Apache log format string
|
|
57
57
|
def self.access_line_definition(format_string)
|
|
58
|
-
format_string ||= :
|
|
58
|
+
format_string ||= :common
|
|
59
59
|
format_string = LOG_FORMAT_DEFAULTS[format_string.to_sym] || format_string
|
|
60
60
|
|
|
61
61
|
line_regexp = ''
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module RequestLogAnalyzer::FileFormat
|
|
2
|
+
|
|
3
|
+
class Mysql < Base
|
|
4
|
+
|
|
5
|
+
line_definition :time do |line|
|
|
6
|
+
line.header = :alternative
|
|
7
|
+
line.teaser = /\# Time: /
|
|
8
|
+
line.regexp = /\# Time: (\d{6}\s+\d{1,2}:\d{2}:\d{2})/
|
|
9
|
+
line.captures << { :name => :timestamp, :type => :timestamp }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
line_definition :user_host do |line|
|
|
13
|
+
line.header = :alternative
|
|
14
|
+
line.teaser = /\# User\@Host\: /
|
|
15
|
+
line.regexp = /\# User\@Host\: ([\w-]+)\[[\w-]+\] \@ ([\w\.-]*) \[([\d\.]*)\]/
|
|
16
|
+
line.captures << { :name => :user, :type => :string } <<
|
|
17
|
+
{ :name => :host, :type => :string } <<
|
|
18
|
+
{ :name => :ip, :type => :string }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
line_definition :query_statistics do |line|
|
|
22
|
+
line.header = :alternative
|
|
23
|
+
line.teaser = /\# Query_time: /
|
|
24
|
+
line.regexp = /\# Query_time: (\d+(?:\.\d+)?)\s+Lock_time: (\d+(?:\.\d+)?)\s+Rows_sent: (\d+)\s+Rows_examined: (\d+)/
|
|
25
|
+
line.captures << { :name => :query_time, :type => :duration, :unit => :sec } <<
|
|
26
|
+
{ :name => :lock_time, :type => :duration, :unit => :sec } <<
|
|
27
|
+
{ :name => :rows_sent, :type => :integer } <<
|
|
28
|
+
{ :name => :rows_examined, :type => :integer }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
line_definition :use_database do |line|
|
|
32
|
+
line.regexp = /^use (\w+);\s*$/
|
|
33
|
+
line.captures << { :name => :database, :type => :string }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
line_definition :query_part do |line|
|
|
37
|
+
line.regexp = /^(?!(?:use |\# |SET ))(.*[^;\s])\s*$/
|
|
38
|
+
line.captures << { :name => :query_fragment, :type => :string }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
line_definition :query do |line|
|
|
42
|
+
line.regexp = /^(?!(?:use |\# |SET ))(.*);\s*$/
|
|
43
|
+
line.captures << { :name => :query, :type => :sql }
|
|
44
|
+
line.footer = true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
PER_USER = :user
|
|
48
|
+
PER_QUERY = :query
|
|
49
|
+
PER_USER_QUERY = Proc.new { |request| "#{request[:user]}@#{request.host}: #{request[:query]}" }
|
|
50
|
+
|
|
51
|
+
report do |analyze|
|
|
52
|
+
analyze.timespan :line_type => :time
|
|
53
|
+
analyze.frequency :user, :title => "Users with most queries"
|
|
54
|
+
analyze.duration :query_time, :category => PER_USER, :title => 'Query time per user'
|
|
55
|
+
analyze.duration :query_time, :category => PER_USER_QUERY, :title => 'Query time'
|
|
56
|
+
# => analyze.duration :lock_time, :category => PER_USER_QUERY, :title => 'Lock time'
|
|
57
|
+
analyze.count :category => PER_USER_QUERY, :title => "Rows examined", :field => :rows_examined
|
|
58
|
+
analyze.count :category => PER_USER_QUERY, :title => "Rows sent", :field => :rows_sent
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Request < RequestLogAnalyzer::Request
|
|
62
|
+
|
|
63
|
+
def convert_sql(value, definition)
|
|
64
|
+
|
|
65
|
+
# Recreate the full SQL query by joining all the previous parts and this last line
|
|
66
|
+
sql = every(:query_fragment).join("\n") + value
|
|
67
|
+
|
|
68
|
+
# Sanitize an SQL query so that it can be used as a category field.
|
|
69
|
+
# sql.gsub!(/\/\*.*\*\//, '') # remove comments
|
|
70
|
+
sql.gsub!(/\s+/, ' ') # remove excessive whitespace
|
|
71
|
+
sql.gsub!(/`([^`]+)`/, '\1') # remove quotes from field names
|
|
72
|
+
sql.gsub!(/'\d{4}-\d{2}-\d{2}'/, ':date') # replace dates
|
|
73
|
+
sql.gsub!(/'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'/, ':datetime') # replace timestamps
|
|
74
|
+
sql.gsub!(/'[^']*'/, ':string') # replace strings
|
|
75
|
+
sql.gsub!(/\b\d+\b/, ':int') # replace integers
|
|
76
|
+
sql.gsub!(/(:int,)+:int/, ':ints') # replace multiple ints by a list
|
|
77
|
+
sql.gsub!(/(:string,)+:string/, ':strings') # replace multiple strings by a list
|
|
78
|
+
|
|
79
|
+
return sql.rstrip
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def host
|
|
83
|
+
self[:host] == '' || self[:host].nil? ? self[:ip] : self[:host]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Convert the timestamp to an integer
|
|
87
|
+
def convert_timestamp(value, definition)
|
|
88
|
+
all,y,m,d,h,i,s = value.split(/(\d\d)(\d\d)(\d\d)\s+(\d?\d):(\d\d):(\d\d)/)
|
|
89
|
+
('20%s%s%s%s%s%s' % [y,m,d,h.rjust(2, '0'),i,s]).to_i
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module RequestLogAnalyzer::FileFormat
|
|
2
2
|
|
|
3
|
-
def self.const_missing(const)
|
|
3
|
+
def self.const_missing(const) # :nodoc:
|
|
4
4
|
RequestLogAnalyzer::load_default_class_file(self, const)
|
|
5
5
|
end
|
|
6
6
|
|
|
@@ -39,10 +39,54 @@ module RequestLogAnalyzer::FileFormat
|
|
|
39
39
|
|
|
40
40
|
# check the returned klass to see if it can be used
|
|
41
41
|
raise "Could not load a file format from #{file_format.inspect}" if klass.nil?
|
|
42
|
-
raise "Invalid FileFormat class" unless klass.kind_of?(Class) && klass.ancestors.include?(RequestLogAnalyzer::FileFormat::Base)
|
|
42
|
+
raise "Invalid FileFormat class from #{file_format.inspect}" unless klass.kind_of?(Class) && klass.ancestors.include?(RequestLogAnalyzer::FileFormat::Base)
|
|
43
43
|
|
|
44
44
|
@current_file_format = klass.create(*args) # return an instance of the class
|
|
45
45
|
end
|
|
46
|
+
|
|
47
|
+
# Returns an array of all FileFormat instances that are shipped with request-log-analyzer by default.
|
|
48
|
+
def self.all_formats
|
|
49
|
+
@all_formats ||= Dir[File.dirname(__FILE__) + '/file_format/*.rb'].map do |file|
|
|
50
|
+
self.load(File.basename(file, '.rb'))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Autodetects the filetype of a given file.
|
|
55
|
+
#
|
|
56
|
+
# Returns a FileFormat instance, by parsing the first couple of lines of the provided file
|
|
57
|
+
# with avery known file format and return the most promosing file format based on the parser
|
|
58
|
+
# statistics. The <tt>autodetect_score</tt> method is used to score the fitness of a format.
|
|
59
|
+
#
|
|
60
|
+
# <tt>file</tt>:: The file to detect the file format for.
|
|
61
|
+
# <tt>line_count</tt>:: The number of lines to take into consideration
|
|
62
|
+
def self.autodetect(file, line_count = 50)
|
|
63
|
+
|
|
64
|
+
parsers = all_formats.map { |f| RequestLogAnalyzer::Source::LogParser.new(f, :parse_strategy => 'cautious') }
|
|
65
|
+
|
|
66
|
+
File.open(file, 'r') do |io|
|
|
67
|
+
while io.lineno < line_count && (line = io.gets)
|
|
68
|
+
parsers.each { |parser| parser.parse_line(line) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
parsers.select { |p| autodetect_score(p) > 0 }.max { |a, b| autodetect_score(a) <=> autodetect_score(b) }.file_format rescue nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Calculates a file format auto detection score based on the parser statistics.
|
|
76
|
+
#
|
|
77
|
+
# This method returns a score as an integer. Usually, the score will increase as more
|
|
78
|
+
# lines are parsed. Usually, a file_format with a score of zero or lower should not be
|
|
79
|
+
# considered.
|
|
80
|
+
#
|
|
81
|
+
# <tt>parser</tt>:: The parsed that was use to parse the initial lines of the log file.
|
|
82
|
+
def self.autodetect_score(parser)
|
|
83
|
+
score = 0
|
|
84
|
+
score -= parser.file_format.line_definitions.length
|
|
85
|
+
score -= parser.warnings * 3
|
|
86
|
+
score += parser.parsed_lines * 1
|
|
87
|
+
score += parser.parsed_requests * 10
|
|
88
|
+
score
|
|
89
|
+
end
|
|
46
90
|
|
|
47
91
|
# Base class for all log file format definitions. This class provides functions for subclasses to
|
|
48
92
|
# define their LineDefinitions and to define a summary report.
|
|
@@ -1,38 +1,51 @@
|
|
|
1
1
|
module RequestLogAnalyzer
|
|
2
2
|
|
|
3
|
+
# Mail report to a specified emailaddress
|
|
3
4
|
class Mailer
|
|
4
5
|
|
|
5
|
-
attr_accessor :data, :to, :host
|
|
6
|
+
attr_accessor :data, :to, :host, :port
|
|
6
7
|
|
|
7
8
|
# Initialize a mailer
|
|
8
|
-
# <tt>to</tt> to address
|
|
9
|
-
# <tt>host</tt> the mailer host
|
|
9
|
+
# <tt>to</tt> to email address to mail to
|
|
10
|
+
# <tt>host</tt> the mailer host (defaults to localhost)
|
|
10
11
|
# <tt>options</tt> Specific style options
|
|
12
|
+
#
|
|
11
13
|
# Options
|
|
12
|
-
# <tt>:debug</tt> Do not actually mail
|
|
14
|
+
# * <tt>:debug</tt> Do not actually mail
|
|
15
|
+
# * <tt>:from_alias</tt> The from alias
|
|
16
|
+
# * <tt>:to_alias</tt> The to alias
|
|
17
|
+
# * <tt>:subject</tt> The message subject
|
|
13
18
|
def initialize(to, host = 'localhost', options = {})
|
|
14
19
|
require 'net/smtp'
|
|
15
20
|
@to = to
|
|
16
21
|
@host = host
|
|
22
|
+
|
|
23
|
+
@port = 25
|
|
17
24
|
@options = options
|
|
18
|
-
@
|
|
25
|
+
@host, @port = host.split(':') if @host.include?(':')
|
|
26
|
+
@data = []
|
|
19
27
|
end
|
|
20
28
|
|
|
29
|
+
# Send all data in @data to the email address used during initialization.
|
|
30
|
+
# Returns array containg [message_data, from_email_address, to_email_address] of sent email.
|
|
21
31
|
def mail
|
|
22
|
-
from
|
|
23
|
-
from_alias
|
|
24
|
-
to_alias
|
|
25
|
-
subject
|
|
32
|
+
from = @options[:from] || 'contact@railsdoctors.com'
|
|
33
|
+
from_alias = @options[:from_alias] || 'Request-log-analyzer reporter'
|
|
34
|
+
to_alias = @options[:to_alias] || to
|
|
35
|
+
subject = @options[:subject] || "Request log analyzer report - generated on #{Time.now.to_s}"
|
|
36
|
+
content_type = ""
|
|
37
|
+
content_type = 'Content-Type: text/html; charset="ISO-8859-1";' if @data.map{|l| l.include?('html')}.include?(true)
|
|
26
38
|
msg = <<END_OF_MESSAGE
|
|
27
39
|
From: #{from_alias} <#{from}>
|
|
28
40
|
To: #{to_alias} <#{@to}>
|
|
29
41
|
Subject: #{subject}
|
|
42
|
+
#{content_type}
|
|
30
43
|
|
|
31
|
-
#{@data.
|
|
44
|
+
#{@data.join("\n")}
|
|
32
45
|
END_OF_MESSAGE
|
|
33
|
-
|
|
46
|
+
|
|
34
47
|
unless @options[:debug]
|
|
35
|
-
Net::SMTP.start(@host) do |smtp|
|
|
48
|
+
Net::SMTP.start(@host, @port) do |smtp|
|
|
36
49
|
smtp.send_message msg, from, to
|
|
37
50
|
end
|
|
38
51
|
end
|
|
@@ -69,6 +69,8 @@ module RequestLogAnalyzer::Output
|
|
|
69
69
|
|
|
70
70
|
# Genrate HTML header and associated stylesheet
|
|
71
71
|
def header
|
|
72
|
+
@io.content_type = content_type if @io.respond_to?(:content_type)
|
|
73
|
+
|
|
72
74
|
@io << "<html>"
|
|
73
75
|
@io << tag(:head) do |headers|
|
|
74
76
|
headers << tag(:title, 'Request-log-analyzer report')
|
|
@@ -65,6 +65,11 @@ module RequestLogAnalyzer
|
|
|
65
65
|
else raise "Unknown duration unit"
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
|
+
|
|
69
|
+
# Convert an epoch to an integer
|
|
70
|
+
def convert_epoch(value, capture_definition)
|
|
71
|
+
Time.at(value.to_i).strftime('%Y%m%d%H%M%S').to_i
|
|
72
|
+
end
|
|
68
73
|
end
|
|
69
74
|
|
|
70
75
|
# Install the default converter methods
|