lackac-request-log-analyzer 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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