lackac-request-log-analyzer 0.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Willem van Bergen / Bart ten Brinke
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,132 @@
1
+ Request log analyzer
2
+ --------------------------------
3
+
4
+ This is a simple command line tool to analyze request log files of both Rails and
5
+ Merb. Its purpose is to find what actions are best candidates for optimization.
6
+
7
+ This tool will parse all requests in the logfile and aggregate the
8
+ information. Once it is finished parsing the log file, it will show the
9
+ requests that take op most server time. Different metrics are used (cumulative
10
+ time, average time, blockers, DB time, etc)
11
+
12
+
13
+ Installation
14
+ --------------------------------
15
+ gem sources -a http://gems.github.com
16
+ sudo gem install wvanbergen-request-log-analyzer
17
+
18
+ Usage
19
+ --------------------------------
20
+
21
+ Usage: request-log-analyzer [FILE] [OPTION]
22
+ Analyze the given log FILE with the given OPTION
23
+ Example: request-log-analyzer mongrel.log
24
+
25
+ --fast, -t: Only use completed requests
26
+ --guess-database-time, -g: Guesses the database duration of requests if they are not in the log
27
+ --output, -o: Comma-separated list of reports to show
28
+ --amount, -c: Displays the top <amount> elements in the reports
29
+ --colorize, -z: Fancy bash coloring
30
+
31
+
32
+
33
+ Example
34
+ --------------------------------
35
+
36
+ Note that this example was shortened for your viewing pleasure.
37
+ $ request-log-analyzer /var/log/my_app.log
38
+
39
+ Request log analyzer, by Willem van Bergen and Bart ten Brinke
40
+
41
+ Processing all log lines...
42
+ ========================================================================
43
+ Successfully analyzed 58908 requests from log file
44
+
45
+ Timestamp first request: 2008-07-13T06:25:58+00:00
46
+ Timestamp last request: 2008-07-20T06:18:53+00:00
47
+ Total time analyzed: 7 days
48
+ Methods: DELETE (1%), GET (50%), POST (22%), PUT (25%).
49
+
50
+ Top 10 most requested actions
51
+ ========================================================================
52
+ /overview/:date/ : 19359 requests
53
+ /overview/day/:date/ : 6365 requests
54
+ /overview/:date/set/ : 5589 requests
55
+ /overview/ : 3985 requests
56
+ /clients/:id/ : 1976 requests
57
+ ........
58
+
59
+ Top 10 actions by time - cumulative
60
+ ========================================================================
61
+ /overview/:date/ : 9044.582s [19359 requests]
62
+ /overview/ : 8478.767s [3985 requests]
63
+ /overview/:date/set/ : 3309.041s [5589 requests]
64
+ /clients/:id/products/:id/ : 1479.911s [924 requests]
65
+ /clients/:id/ : 750.080s [1976 requests]
66
+ ........
67
+
68
+ Top 10 actions by time - per request mean
69
+ ========================================================================
70
+ /overview/ : 2.128s [3985 requests]
71
+ /clients/:id/products/:id/ : 1.602s [924 requests]
72
+ /overview/:date/set/ : 0.592s [5589 requests]
73
+ /overview/:date/ : 0.467s [19359 requests]
74
+ /clients/:id/ : 0.380s [1976 requests]
75
+ ........
76
+
77
+ Top 10 worst DB offenders - cumulative time
78
+ ========================================================================
79
+ /overview/:date/ : 8773.993s [19359 requests]
80
+ /overview/ : 8394.754s [3985 requests]
81
+ /overview/:date/set/ : 3307.928s [5589 requests]
82
+ /clients/:id/products/:id/ : 1425.220s [924 requests]
83
+ /clients/:id/ : 535.229s [1976 requests]
84
+ ........
85
+
86
+ Top 10 worst DB offenders - mean time
87
+ ========================================================================
88
+ /overview/:id/:id/:id/print/ : 6.994s [448 requests]
89
+ /overview/ : 2.128s [3985 requests]
90
+ /clients/:id/products/:id/ : 1.602s [924 requests]
91
+ /overview/:date/set/ : 0.592s [5589 requests]
92
+ /overview/:date/ : 0.467s [19359 requests]
93
+ ........
94
+
95
+ Mongrel process blockers (> 1.0 seconds)
96
+ ========================================================================
97
+ /overview/:date/ : 7494.233s [3144 requests]
98
+ /overview/ : 8320.293s [1549 requests]
99
+ /overview/:date/set/ : 1149.235s [803 requests]
100
+ /overview/:id/:id/:id/print/new/ : 613.693s [341 requests]
101
+ /clients/:id/products/:id/ : 1370.693s [313 requests]
102
+ ........
103
+
104
+ Requests graph - per hour
105
+ ========================================================================
106
+ ........
107
+ 7:00 - 2731 : XXXXXXX
108
+ 8:00 - 6139 : XXXXXXXXXXXXXXXX
109
+ 9:00 - 7465 : XXXXXXXXXXXXXXXXXXXX
110
+ 10:00 - 7118 : XXXXXXXXXXXXXXXXXXX
111
+ 11:00 - 7409 : XXXXXXXXXXXXXXXXXXX
112
+ 12:00 - 6450 : XXXXXXXXXXXXXXXXX
113
+ 13:00 - 5377 : XXXXXXXXXXXXXX
114
+ 14:00 - 6058 : XXXXXXXXXXXXXXXX
115
+ 15:00 - 4156 : XXXXXXXXXXX
116
+ 16:00 - 2767 : XXXXXXX
117
+ 17:00 - 1598 : XXXX
118
+ 18:00 - 792 : XX
119
+ ........
120
+
121
+ Errors
122
+ ========================================================================
123
+ ArgumentError: [237 requests]
124
+ -> invalid date
125
+ StaleObjectError: [28 requests]
126
+ -> Attempted to update a stale object
127
+ RuntimeError: [3 requests]
128
+ -> Cannot destroy rule before it was created
129
+ StatementError: [2 requests]
130
+ -> Mysql::Error: Deadlock found when trying to get lock; try restarting transaction
131
+ NoMethodError: [1 requests]
132
+ -> undefined method `code' for nil:NilClass
@@ -0,0 +1,72 @@
1
+ require 'rubygems'
2
+
3
+ load 'test/tasks.rake'
4
+
5
+ desc 'Default: run unit tests for request-log-analyzer.'
6
+ task :default => :test
7
+
8
+
9
+ namespace :gem do
10
+
11
+ desc "Sets the version and date of the gem. Requires the VERSION environment variable."
12
+ task :version => [:manifest] do
13
+
14
+ require 'date'
15
+
16
+ new_version = ENV['VERSION']
17
+ raise "VERSION is required" unless /\d+(\.\d+)*/ =~ new_version
18
+
19
+ spec_file = Dir['*.gemspec'].first
20
+
21
+ spec = File.read(spec_file)
22
+ spec.gsub!(/^(\s*s\.version\s*=\s*)('|")(.+)('|")(\s*)$/) { "#{$1}'#{new_version}'#{$5}" }
23
+ spec.gsub!(/^(\s*s\.date\s*=\s*)('|")(.+)('|")(\s*)$/) { "#{$1}'#{Date.today.strftime('%Y-%m-%d')}'#{$5}" }
24
+ File.open(spec_file, 'w') { |f| f << spec }
25
+ end
26
+
27
+ desc "Creates a git tag for the provided VERSION"
28
+ task :tag => [:version] do
29
+
30
+ new_version = ENV['VERSION']
31
+ raise "VERSION is required" unless /\d+(\.\d+)*/ =~ new_version
32
+
33
+ sh "git add request-log-analyzer.gemspec"
34
+ sh "git commit -m \"Set gem version to #{new_version}\""
35
+ sh "git push origin"
36
+ sh "git tag -a \"request-log-analyzer-#{new_version}\" -m \"Tagged version #{new_version}\""
37
+ sh "git push --tags"
38
+ end
39
+
40
+ desc "Builds a ruby gem for request-log-analyzer"
41
+ task :build => [:manifest] do
42
+ system %[gem build request-log-analyzer.gemspec]
43
+ end
44
+
45
+ desc %{Update ".manifest" with the latest list of project filenames. Respect\
46
+ .gitignore by excluding everything that git ignores. Update `files` and\
47
+ `test_files` arrays in "*.gemspec" file if it's present.}
48
+ task :manifest do
49
+ list = Dir['**/*'].sort
50
+ spec_file = Dir['*.gemspec'].first
51
+ list -= [spec_file] if spec_file
52
+
53
+ File.read('.gitignore').each_line do |glob|
54
+ glob = glob.chomp.sub(/^\//, '')
55
+ list -= Dir[glob]
56
+ list -= Dir["#{glob}/**/*"] if File.directory?(glob) and !File.symlink?(glob)
57
+ puts "excluding #{glob}"
58
+ end
59
+
60
+ if spec_file
61
+ spec = File.read spec_file
62
+ spec.gsub! /^(\s* s.(test_)?files \s* = \s* )( \[ [^\]]* \] | %w\( [^)]* \) )/mx do
63
+ assignment = $1
64
+ bunch = $2 ? list.grep(/^test.*_test\.rb$/) : list
65
+ '%s%%w(%s)' % [assignment, bunch.join(' ')]
66
+ end
67
+
68
+ File.open(spec_file, 'w') {|f| f << spec }
69
+ end
70
+ File.open('.manifest', 'w') {|f| f << list.join("\n") }
71
+ end
72
+ end
data/TODO ADDED
@@ -0,0 +1,18 @@
1
+ TODO items for Rails-log-analyzer
2
+ =================================
3
+ Contact willem AT vanbergen DOT org if you want to help out with the development.
4
+
5
+ Database:
6
+ - Add query functionality for the resulting database file (interactive reports?)
7
+ - Link request processing line to request completed line
8
+
9
+ Rails integration:
10
+ - Create script that calls request-log-analyzer
11
+ - Optionally use local or specific routes.rb file to parse URLs
12
+ - Add rake tasks to Rails application when included
13
+
14
+ General:
15
+ - Add useful rake tasks
16
+ - Add more tests
17
+ - World domination
18
+ - Fix multiple file handling
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/ruby
2
+ require File.dirname(__FILE__) + '/../lib/command_line/arguments'
3
+ require File.dirname(__FILE__) + '/../lib/base/log_parser'
4
+ require File.dirname(__FILE__) + '/../lib/base/summarizer'
5
+ require File.dirname(__FILE__) + '/../lib/rails_analyzer/log_parser'
6
+ require File.dirname(__FILE__) + '/../lib/rails_analyzer/summarizer'
7
+ require File.dirname(__FILE__) + '/../lib/merb_analyzer/log_parser'
8
+ require File.dirname(__FILE__) + '/../lib/merb_analyzer/summarizer'
9
+ require File.dirname(__FILE__) + '/../lib/bashcolorizer'
10
+ require File.dirname(__FILE__) + '/../lib/ruby-progressbar/progressbar.rb'
11
+
12
+ puts "Request log analyzer, by Willem van Bergen and Bart ten Brinke\n\n"
13
+
14
+ # Substitutes variable elements in a url (like the id field) with a fixed string (like ":id")
15
+ # This is used to aggregate simular requests.
16
+ # <tt>request</tt> The request to evaluate.
17
+ # Returns uniformed url string.
18
+ # Raises on mailformed request.
19
+ def request_hasher(request)
20
+ if request[:url]
21
+ url = request[:url].downcase.split(/^http[s]?:\/\/[A-z0-9\.-]+/).last.split('?').first # only the relevant URL part
22
+ url << '/' if url[-1] != '/'[0] && url.length > 1 # pad a trailing slash for consistency
23
+
24
+ url.gsub!(/\/\d+-\d+-\d+(\/|$)/, '/:date') # Combine all (year-month-day) queries
25
+ url.gsub!(/\/\d+-\d+(\/|$)/, '/:month') # Combine all date (year-month) queries
26
+ url.gsub!(/\/\d+[\w-]*/, '/:id') # replace identifiers in URLs
27
+
28
+ return url
29
+ elsif request[:controller] && request[:action]
30
+ return "#{request[:controller]}##{request[:action]}"
31
+ else
32
+ raise 'Cannot hash this request! ' + request.inspect
33
+ end
34
+ end
35
+
36
+ # Print results using a ASCII table.
37
+ # <tt>summarizer</tt> The summarizer containg information to draw the table.
38
+ # <tt>field</tt> The field containing the data to be printed
39
+ # <tt>amount</tt> The length of the table (defaults to 20)
40
+ def print_table(summarizer, field, amount = 20)
41
+ summarizer.sort_actions_by(field).reverse[0, amount.to_i].each do |a|
42
+ # As we show count by default, show totaltime if we sort by count
43
+ field = :total_time if field == :count
44
+
45
+ puts "%-50s: %10.03fs [#{green("%d requests")}]" % [a[0], a[1][field], a[1][:count]]
46
+ end
47
+ end
48
+
49
+ # Parse the arguments given via commandline
50
+ begin
51
+ $arguments = CommandLine::Arguments.parse do |command_line|
52
+ command_line.switch(:guess_database_time, :g)
53
+ command_line.switch(:fast, :f)
54
+ command_line.switch(:colorize, :z)
55
+ command_line.switch(:merb, :m)
56
+ command_line.flag(:output, :alias => :o)
57
+ command_line.flag(:amount, :alias => :c)
58
+ command_line.required_files = 1
59
+ end
60
+
61
+ rescue CommandLine::Error => e
62
+ puts "ARGUMENT ERROR: " + e.message
63
+ puts
64
+ load File.dirname(__FILE__) + "/../output/usage.rb"
65
+ exit(0)
66
+ end
67
+
68
+
69
+ if $arguments[:merb]
70
+ $summarizer = MerbAnalyzer::Summarizer.new(:calculate_database => $arguments[:guess_database_time])
71
+ else
72
+ $summarizer = RailsAnalyzer::Summarizer.new(:calculate_database => $arguments[:guess_database_time])
73
+ end
74
+
75
+ if $arguments[:fast]
76
+ line_types = [:completed]
77
+ elsif $arguments[:merb]
78
+ line_types = MerbAnalyzer::LogParser::LOG_LINES.keys
79
+ else
80
+ line_types = RailsAnalyzer::LogParser::LOG_LINES.keys
81
+ end
82
+
83
+ # Walk through al the files given via the arguments.
84
+ $arguments.files.each do |log_file|
85
+ puts "Processing #{line_types.join(', ')} log lines from #{log_file}..."
86
+
87
+ if $arguments[:merb]
88
+ parser = MerbAnalyzer::LogParser.new(log_file)
89
+ else
90
+ parser = RailsAnalyzer::LogParser.new(log_file)
91
+ end
92
+
93
+ # add progress bar
94
+ unless $arguments[:fast]
95
+ pbar = ProgressBar.new(green(log_file), File.size(log_file))
96
+ parser.progress { |pos, total| (pos == :finished) ? pbar.finish : pbar.set(pos) }
97
+ end
98
+
99
+ parser.each(*line_types) do |request|
100
+ $summarizer.group(request) { |r| request_hasher(r) }
101
+ end
102
+ end
103
+
104
+ # Select the reports to output and generate them.
105
+ output_reports = $arguments[:output].split(',') rescue [:timespan, :most_requested, :total_time, :mean_time, :total_db_time, :mean_db_time, :mean_rendering_time, :blockers, :hourly_spread, :errors]
106
+
107
+ output_reports.each do |report|
108
+ report_location = "#{File.dirname(__FILE__)}/../output/#{report}.rb"
109
+
110
+ if File.exist?(report_location)
111
+ load report_location
112
+ else
113
+ puts "\nERROR: Output report #{report} not found!"
114
+ end
115
+ end
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/command_line/arguments'
4
+ require File.dirname(__FILE__) + '/../lib/base/log_parser'
5
+ require File.dirname(__FILE__) + '/../lib/base/record_inserter'
6
+ require File.dirname(__FILE__) + '/../lib/rails_analyzer/log_parser'
7
+ require File.dirname(__FILE__) + '/../lib/rails_analyzer/record_inserter'
8
+ require File.dirname(__FILE__) + '/../lib/bashcolorizer'
9
+ require File.dirname(__FILE__) + '/../lib/ruby-progressbar/progressbar.rb'
10
+
11
+
12
+ puts "Rails log parser, by Willem van Bergen and Bart ten Brinke\n\n"
13
+
14
+ begin
15
+
16
+ $arguments = CommandLine::Arguments.parse do |command_line|
17
+ command_line.switch(:guess_database_time, :g)
18
+ command_line.switch(:reset_database, :r)
19
+ command_line.flag(:database, :alias => :d, :required => false)
20
+ command_line.required_files = 1
21
+ end
22
+
23
+ rescue CommandLine::Error => e
24
+ puts "ARGUMENT ERROR: " + e.message
25
+ puts
26
+ puts "Usage: ruby parsetodb.rb [LOGFILES*] <OPTIONS>"
27
+ puts
28
+ puts "Options:"
29
+ puts " --database, -t: The database file to use"
30
+ puts " --reset-database, -r: Resets the database before inserting new records"
31
+ puts " --guess-database-time, -g: Guesses the database duration of requests"
32
+ puts
33
+ puts "Examples:"
34
+ puts " ./parsetodb.rb development.log"
35
+ puts " ./parsetodb.rb mongrel.0.log mongrel.1.log mongrel.2.log -g -d mongrel.db"
36
+ puts
37
+
38
+ exit(0)
39
+ end
40
+
41
+ log_files = $arguments.files
42
+ db_file = $arguments[:database] || log_files.first + '.db'
43
+
44
+ if $arguments[:reset_database] && File.exist?(db_file)
45
+ File.delete(db_file)
46
+ puts "Database file cleared."
47
+ end
48
+
49
+ records_inserted = 0
50
+ inserter = RailsAnalyzer::RecordInserter.insert_batch_into(db_file) do |db|
51
+ log_files.each do |log_file|
52
+
53
+ puts "Processing all log lines from #{log_file}..."
54
+ parser = RailsAnalyzer::LogParser.new(log_file)
55
+
56
+ pbar = ProgressBar.new(green(log_file), File.size(log_file))
57
+ parser.progress { |pos, total| (pos == :finished) ? pbar.finish : pbar.set(pos) }
58
+
59
+ parser.each do |request|
60
+ db.insert(request)
61
+ records_inserted += 1
62
+ end
63
+ end
64
+
65
+ if $arguments[:guess_database_time]
66
+ puts "Calculating database times..."
67
+ db.calculate_db_durations!
68
+ end
69
+ end
70
+
71
+ started = inserter.count(:started)
72
+ completed = inserter.count(:completed)
73
+ failed = inserter.count(:failed)
74
+
75
+ puts
76
+ puts "Inserted #{records_inserted} records from #{log_files.length} files."
77
+ puts "Parse warnings: #{inserter.warning_count}. Check the parse_warnings table in the database for details."
78
+ puts
79
+ puts "Requests started: #{started}"
80
+ puts "Requests completed: #{completed}"
81
+ puts "Requests failed: #{failed}"
@@ -0,0 +1,68 @@
1
+ module Base
2
+ # Parse a log file
3
+ class LogParser
4
+
5
+ LOG_LINES = {}
6
+
7
+ # LogParser initializer
8
+ # <tt>file</tt> The fileobject this LogParser wil operate on.
9
+ def initialize(file, options = {})
10
+ @file_name = file
11
+ @options = options
12
+ @file_size = File.size(@file_name)
13
+
14
+ self.initialize_hook(options) if self.respond_to?(:initialize_hook)
15
+ end
16
+
17
+ def progress(&block)
18
+ @progress_handler = block
19
+ end
20
+
21
+ # Output a warning
22
+ # <tt>message</tt> The warning message (object)
23
+ def warn(message)
24
+ puts " -> " + message.to_s
25
+ end
26
+
27
+ # Finds a log line and then parses the information in the line.
28
+ # Yields a hash containing the information found.
29
+ # <tt>*line_types</tt> The log line types to look for (defaults to LOG_LINES.keys).
30
+ # Yeilds a Hash when it encounters a chunk of information.
31
+ def each(*line_types, &block)
32
+ log_lines_hash = self.class::LOG_LINES
33
+
34
+
35
+ # parse everything by default
36
+ line_types = log_lines_hash.keys if line_types.empty?
37
+
38
+ File.open(@file_name) do |file|
39
+
40
+ file.each_line do |line|
41
+
42
+ #@progress_handler.call(file.pos, @file_size) if @progress_handler
43
+
44
+ line_types.each do |line_type|
45
+ if log_lines_hash[line_type][:teaser] =~ line
46
+ if log_lines_hash[line_type][:regexp] =~ line
47
+ request = { :type => line_type, :line => file.lineno }
48
+ log_lines_hash[line_type][:params].each do |key, value|
49
+ request[key] = case value
50
+ when Numeric; $~[value]
51
+ when Array; $~[value.first].send(value.last)
52
+ else; nil
53
+ end
54
+
55
+ end
56
+
57
+ yield(request) if block_given?
58
+ else
59
+ warn("Unparsable #{line_type} line: " + line[0..79]) unless line_type == :failed
60
+ end
61
+ end
62
+ end
63
+ end
64
+ @progress_handler.call(:finished, @file_size) if @progress_handler
65
+ end
66
+ end
67
+ end
68
+ end