Narnach-rails_analyzer 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Wes Oldenbeuving
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.rdoc ADDED
@@ -0,0 +1,58 @@
1
+ == RailsAnalyzer
2
+ RailsAnalyzer generates reports about requests processed by a Ruby on Rails server.
3
+
4
+ It analyzes the log files created by a Rails server. It can accept any number of files as its command line parameters. It defaults to use 'log/production.log' as its input when no filenames are provided.
5
+
6
+ Two different reporting engines process the log files:
7
+ * TimeStats, which is concerned with *when* the request was made. It produces reports with the amount of hits recorded in the log file per time interval, and the relative amount of requests within a time interval. The relative interval simply lowers all hit counts until one hits zero. This makes it easy to filter out periodic requests.
8
+ * HitStats, which is concerned with how *fast* the requests were handled. It produces reports with detailed time statistics for each request. It has two types of reports: with params and without params. Each type of report produces a number of files, each which is sorted by one of the following statistics for responses: fastest, slowest, sum, average, median, standard deviation and hit count.
9
+ Both engines produce .txt files as their only output.
10
+ * TimeStats produces log_times_*.txt and log_times_*_relative.txt output.
11
+ * HitStats produces the other log_*.txt and log_*_with_params.txt files.
12
+
13
+ == Recent changes
14
+
15
+ === Version 0.2.2
16
+ Added reports for slowest and fastest requests.
17
+ Documentation updates.
18
+
19
+ === Version 0.2.1
20
+ Bugfixes:
21
+ * HitStats reports were not generated. They are working again.
22
+ * Array#median always returns a float to prevent integer math (lack of) rounding problems.
23
+ Dependencies:
24
+ * No longer rely on ActiveSupport. It was used for Array#sum and Array#group_by, which are now implemented in ArrayExt.
25
+ Specs:
26
+ * Parts of UrlHits and Entries got specs to help debugging the HitStats bug.
27
+ * Added specs for all ArrayExt methods.
28
+
29
+ === Version 0.2.0
30
+ Split single-file script into one file per existing class.
31
+ Introduced new classes to handle responsibilities that were not yet handled.
32
+ Changed classes involved in generating URL hits-based reports to be more flexible.
33
+ Generally refactored a lot of non-DRY code to be at least a bit nicer.
34
+
35
+ === Version 0.1.0
36
+ Imported single-file script
37
+
38
+ == Installation
39
+ === From gem
40
+ The gem is located on github.
41
+ gem install Narnach-rails_analyzer -s http://gems.github.com
42
+ === From git
43
+ From the project root, use rake to install:
44
+ git clone git://github.com/Narnach/rails_analyzer.git
45
+ cd rails_analyzer
46
+ rake install
47
+ This will build the gem and install it for you.
48
+
49
+ == Syntax
50
+ rails_analyzer [log_file1] [log_file2] [..] [log_fileN]
51
+ When no log files are provided, log/production.log is used.
52
+
53
+ == About
54
+
55
+ Author:: Wes 'Narnach' Oldenbeuving (narnach@gmail.com)
56
+ Website:: http://www.github.com/Narnach/rails_analyzer
57
+ Copyright:: Copyright (c) 2008 Wes Oldenbeuving
58
+ License:: MIT license. See MIT-LICENSE (in the gem directory) for license details.
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require "rake"
2
+ require "rake/clean"
3
+ require "rake/gempackagetask"
4
+ require 'rubygems'
5
+
6
+ ################################################################################
7
+ ### Gem
8
+ ################################################################################
9
+
10
+ begin
11
+ # Parse gemspec using the github safety level.
12
+ file = Dir['*.gemspec'].first
13
+ data = File.read(file)
14
+ spec = nil
15
+ Thread.new { spec = eval("$SAFE = 3\n%s" % data)}.join
16
+
17
+ # Create the gem tasks
18
+ Rake::GemPackageTask.new(spec) do |package|
19
+ package.gem_spec = spec
20
+ end
21
+ rescue Exception => e
22
+ printf "WARNING: Error caught (%s): %s\n%s", e.class.name, e.message, e.backtrace[0...5].map {|l| ' %s' % l}.join("\n")
23
+ end
24
+
25
+ desc 'Package and install the gem for the current version'
26
+ task :install => :gem do
27
+ system "sudo gem install -l pkg/%s-%s.gem" % [spec.name, spec.version]
28
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rails_analyzer'
3
+
4
+ ra = RailsAnalyzer.new(ARGV)
5
+ ra.generate_reports
data/lib/array_ext.rb ADDED
@@ -0,0 +1,51 @@
1
+ module ArrayExt
2
+ module GroupBy
3
+ # Returns a Hash:
4
+ # - The keys are grouping values
5
+ # - The values are an Array with values grouped to that key.
6
+ def group_by(&block)
7
+ grouped_results = Hash.new { |hash, key| hash[key] = Array.new }
8
+ each do |element|
9
+ group_key = block.call(element)
10
+ grouped_results[group_key] << element
11
+ end
12
+ grouped_results
13
+ end
14
+ end
15
+
16
+ module Stats
17
+ def avg
18
+ sum / size.to_f
19
+ end
20
+
21
+ # Returns the median as a Float.
22
+ def median
23
+ return 0 if size == 0
24
+ if size%2==0
25
+ # Average two middle values
26
+ # [1,2,3,4,5,6].median #=> 3.5
27
+ (self[size / 2] + self[size / 2 - 1]) / 2.0
28
+ else
29
+ # Use middle value
30
+ # [1,2,3,4,5].median #=> 3
31
+ self[size / 2].to_f
32
+ end
33
+ end
34
+
35
+ def stddev
36
+ avg_cached = avg # prevent having to recompute it each time
37
+ squared_deviations = map {|n| (n - avg_cached) ** 2 }
38
+ variance = squared_deviations.avg
39
+ Math::sqrt(variance)
40
+ end
41
+
42
+ def sum
43
+ inject(0.0) {|s,n| s+n }
44
+ end
45
+ end
46
+ end
47
+
48
+ class Array
49
+ include ArrayExt::GroupBy
50
+ include ArrayExt::Stats
51
+ end
data/lib/entries.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'url_hits'
2
+
3
+ class Entries
4
+ attr_accessor :entries
5
+
6
+ def initialize
7
+ @entries = Hash.new { |hash, url| hash[url] = UrlHits.new(url) }
8
+ end
9
+
10
+ def add_hit(url, time)
11
+ entries[url].add_hit(time)
12
+ end
13
+
14
+ def to_s(sort=:size)
15
+ sorted = entries.values.sort {|a,b| b.send(sort) <=> a.send(sort) }
16
+ head = ('%6s ' + '%6s '*5 + '%9s %s') % %w[Hits Low Median Avg Stddev High Sum Url]
17
+ head << "\n" << sorted.join("\n")
18
+ end
19
+ end
data/lib/float_ext.rb ADDED
@@ -0,0 +1,18 @@
1
+ # Add decent rounding to X decimals.
2
+ module FloatExt
3
+ def round_to(x=4)
4
+ (self * 10**x).round.to_f / 10**x
5
+ end
6
+
7
+ def ceil_to(x=4)
8
+ (self * 10**x).ceil.to_f / 10**x
9
+ end
10
+
11
+ def floor_to(x=4)
12
+ (self * 10**x).floor.to_f / 10**x
13
+ end
14
+ end
15
+
16
+ class Float
17
+ include FloatExt
18
+ end
data/lib/hit_stats.rb ADDED
@@ -0,0 +1,64 @@
1
+ require 'entries'
2
+ require 'uri'
3
+
4
+ class HitStats
5
+ attr_accessor :logs, :hits_with_query, :hits_without_query
6
+
7
+ def initialize(logs)
8
+ @logs = logs
9
+ @hits_with_query = Entries.new
10
+ @hits_without_query = Entries.new
11
+ end
12
+
13
+ def self.generate(logs)
14
+ hs = self.new(logs)
15
+ hs.parse_logs
16
+ hs.save_reports
17
+ end
18
+
19
+ def parse_logs
20
+ logs.each do |log|
21
+ parse_log(log)
22
+ end
23
+ end
24
+
25
+ def save_reports
26
+ [:sum, :avg, :size, :median, :stddev, :min, :max].each do |sort_order|
27
+ File.open("log_%s.txt" % name_for_sort_order(sort_order),"wb") {|file| file.puts hits_without_query.to_s(sort_order) }
28
+ File.open('log_%s_with_params.txt' % name_for_sort_order(sort_order), 'wb') {|file| file.puts hits_with_query.to_s(sort_order)}
29
+ end
30
+ end
31
+
32
+ protected
33
+
34
+ def name_for_sort_order(order)
35
+ {
36
+ :size => 'hits',
37
+ :min => 'low',
38
+ :max => 'high',
39
+ }[order] || order
40
+ end
41
+
42
+ def parse_log(log)
43
+ results = `grep "Completed" #{log}`
44
+ results.each do |line|
45
+ parse_line(line)
46
+ end
47
+ end
48
+
49
+ def parse_line(line)
50
+ unless line =~ /\[(.*?)\]/
51
+ puts "Failed to parse: #{line}"
52
+ return
53
+ end
54
+ return unless uri=URI.parse($1) rescue nil
55
+ unless line =~ /Completed\ in\ ([0-9]+\.[0-9]+)/
56
+ puts "Could not extract time from line: #{line}"
57
+ return
58
+ end
59
+ time = $1.to_f
60
+ hits_with_query.add_hit(uri.to_s,time)
61
+ uri.query=nil
62
+ hits_without_query.add_hit(uri.to_s,time)
63
+ end
64
+ end
@@ -0,0 +1,23 @@
1
+ require 'time_stats'
2
+ require 'hit_stats'
3
+
4
+ class RailsAnalyzer
5
+ attr_accessor :logs
6
+
7
+ def initialize(logs=[])
8
+ @logs = logs
9
+ @logs = %w[log/production.log] if logs.size==0
10
+ end
11
+
12
+ def generate_reports
13
+ puts "RailsAnalyzer"
14
+ puts "Analyzes the following Rails log files:"
15
+ puts logs.map{|l| ' %s' % l}.join("\n")
16
+
17
+ puts 'Generating time-based reports'
18
+ TimeStats.generate(logs)
19
+
20
+ puts 'Generating hit-based reports'
21
+ HitStats.generate(logs)
22
+ end
23
+ end
data/lib/time_stats.rb ADDED
@@ -0,0 +1,79 @@
1
+ require 'array_ext'
2
+
3
+ class TimeStats
4
+ attr_accessor :logs, :times
5
+
6
+ def initialize(logs)
7
+ @times = []
8
+ @logs = logs
9
+ end
10
+
11
+ def self.generate(logs)
12
+ ts = new(logs)
13
+ ts.parse_files
14
+ ts.save_reports
15
+ end
16
+
17
+ def parse_files
18
+ logs.each do |log|
19
+ results = `grep "Processing" #{log}`
20
+ timestamps = results.map { |line| (line =~ /Processing.*\(for .* at (\d+-\d+-\d+ \d+:\d+:\d+)\)/) ? $1 : nil}.compact
21
+ times.concat(timestamps)
22
+ end
23
+ end
24
+
25
+ def save_reports
26
+ %w[day hour day_hour].each do |timeframe|
27
+ filename = 'log_times_%s.txt' % timeframe
28
+ save_hits_report(filename, timeframe)
29
+ end
30
+ %w[hour ten_min].each do |timeframe|
31
+ hits = self.send('per_%s' % timeframe).map {|frame, hits| hits.size}
32
+ offset = hits.min || 0
33
+ filename = 'log_times_%s_relative.txt' % timeframe
34
+ save_hits_report(filename, timeframe, offset)
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ def save_hits_report(filename, timeframe, offset = 0)
41
+ collection = self.send('per_%s' % timeframe)
42
+ File.open(filename,'wb') do |f|
43
+ collection.each do |frame, hits|
44
+ f.puts '%s: %s' % [frame, hits.size - offset]
45
+ end
46
+ end
47
+ end
48
+
49
+ def per_day_hour
50
+ group_times_by {|d, h, m, s| '%s %sh' % [d, h]}
51
+ end
52
+
53
+ def per_day
54
+ group_times_by {|d, h, m, s| d}
55
+ end
56
+
57
+ def per_hour
58
+ group_times_by {|d, h, m, s| h}
59
+ end
60
+
61
+ def per_ten_min
62
+ group_times_by {|d, h, m, s| '%sh%s0' % [h, m[0,1]]}
63
+ end
64
+
65
+ # Group times by date/time-related data.
66
+ # Sorts by the yield return value
67
+ # Returns an Array of two-element Arrays:
68
+ # - The first element is the sort key
69
+ # - The second element is the hit times for that key
70
+ # The return value is sorted by key.
71
+ def group_times_by(&block) # :yields: date, hour, min, sec
72
+ grouped_times = times.group_by do |t|
73
+ date, time = t.split(" ")
74
+ hour, min, sec = time.split(":")
75
+ block.call(date, hour, min, sec)
76
+ end
77
+ grouped_times.to_a.sort {|a,b| a.first <=> b.first}
78
+ end
79
+ end
data/lib/url_hits.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'float_ext'
2
+ require 'array_ext'
3
+
4
+ class UrlHits
5
+ attr_reader :url, :hits
6
+
7
+ def initialize(url)
8
+ @url = url
9
+ @hits = Array.new
10
+ end
11
+
12
+ def add_hit(time)
13
+ hits << time
14
+ end
15
+
16
+ def to_s
17
+ '%6i %s %s' % [size, time_stats, @url]
18
+ end
19
+
20
+ def ==(other)
21
+ (other.class == self.class) && (other.url == self.url) && (other.hits == self.hits)
22
+ end
23
+
24
+ [:size, :min, :max, :sum, :stddev, :median, :avg].each do |op|
25
+ define_method(op) do
26
+ hits.send(op)
27
+ end
28
+ end
29
+
30
+ protected
31
+
32
+ def time_stats
33
+ return nil unless size > 0
34
+ '%3.03f %3.03f %3.03f %3.03f %3.03f %6.03f' % [min, median, avg, stddev, max, sum]
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ Gem::Specification.new do |s|
2
+ # Project
3
+ s.name = 'rails_analyzer'
4
+ s.summary = "RailsAnalyzer generates reports about requests processed by a Ruby on Rails server."
5
+ s.description = "RailsAnalyzer generates reports about requests processed by a Ruby on Rails server."
6
+ s.version = '0.2.2'
7
+ s.date = '2008-09-15'
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Wes Oldenbeuving"]
10
+ s.email = "narnach@gmail.com"
11
+ s.homepage = "http://www.github.com/Narnach/rails_analyzer"
12
+
13
+ # Files
14
+ root_files = %w[MIT-LICENSE README.rdoc Rakefile rails_analyzer.gemspec]
15
+ bin_files = %w[rails_analyzer]
16
+ lib_files = %w[rails_analyzer array_ext float_ext time_stats hit_stats entries url_hits]
17
+ test_files = %w[]
18
+ spec_files = %w[rails_analyzer entries url_hits]
19
+ other_files = %w[spec/spec.opts spec/spec_helper.rb]
20
+ s.bindir = "bin"
21
+ s.require_path = "lib"
22
+ s.executables = bin_files
23
+ s.test_files = test_files.map {|f| 'test/%s_test.rb' % f} + spec_files.map {|f| 'spec/%s_spec.rb' % f}
24
+ s.files = root_files + s.test_files + other_files + bin_files.map {|f| 'bin/%s' % f} + lib_files.map {|f| 'lib/%s.rb' % f}
25
+
26
+ # rdoc
27
+ s.has_rdoc = true
28
+ s.extra_rdoc_files = %w[ README.rdoc MIT-LICENSE]
29
+ s.rdoc_options << '--inline-source' << '--line-numbers' << '--main' << 'README.rdoc'
30
+
31
+ # Requirements
32
+ s.required_ruby_version = ">= 1.8.0"
33
+ end
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'entries'
3
+
4
+ describe Entries do
5
+ describe '#add_hit' do
6
+ it 'should be stored grouped per URL' do
7
+ u = 'http://www.example.org'
8
+ u2 = 'http://www2.example.org'
9
+ uh1 = UrlHits.new(u)
10
+ uh1.add_hit(0.1)
11
+ uh2 = UrlHits.new(u)
12
+ uh2.add_hit(0.1)
13
+ uh2.add_hit(0.2)
14
+ uh3 = UrlHits.new(u)
15
+ uh3.add_hit(0.1)
16
+ uh3.add_hit(0.2)
17
+ uh3.add_hit(0.3)
18
+ uh4 = UrlHits.new(u2)
19
+ uh4.add_hit(0.1)
20
+
21
+ e = Entries.new
22
+ e.entries.should == {}
23
+ e.add_hit(u, 0.1)
24
+ e.entries.should == {u => uh1}
25
+ e.add_hit(u, 0.2)
26
+ e.entries.should == {u => uh2}
27
+ e.add_hit(u, 0.3)
28
+ e.entries.should == {u => uh3}
29
+ e.add_hit(u2, 0.1)
30
+ e.entries.should == {u => uh3, u2 => uh4}
31
+ end
32
+ end
33
+
34
+ describe '#to_s' do
35
+ it "should return a summary of the url hit times" do
36
+ e = Entries.new
37
+ e.add_hit('http://www.example.org', 0.1)
38
+ e.add_hit('http://www.example.org', 0.2)
39
+ e.add_hit('http://www.example.org', 0.3)
40
+ expected_output = '' + \
41
+ ' Hits Low Median Avg Stddev High Sum Url' + "\n" + \
42
+ ' 3 0.100 0.200 0.200 0.082 0.300 0.600 http://www.example.org'
43
+ e.to_s.should == expected_output
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'rails_analyzer'
3
+
4
+ describe RailsAnalyzer do
5
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --format specdoc
3
+ --diff unified
@@ -0,0 +1 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__),'..','lib'))
@@ -0,0 +1,16 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'url_hits'
3
+
4
+ describe UrlHits do
5
+ describe '#to_s' do
6
+ it "should return a summary of the url hit times" do
7
+ uh = UrlHits.new('http://www.example.org')
8
+ uh.add_hit(0.1)
9
+ uh.to_s.should == ' 1 0.100 0.100 0.100 0.000 0.100 0.100 http://www.example.org'
10
+ uh.add_hit(0.2)
11
+ uh.to_s.should == ' 2 0.100 0.150 0.150 0.050 0.200 0.300 http://www.example.org'
12
+ uh.add_hit(0.3)
13
+ uh.to_s.should == ' 3 0.100 0.200 0.200 0.082 0.300 0.600 http://www.example.org'
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: Narnach-rails_analyzer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Wes Oldenbeuving
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-09-15 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: RailsAnalyzer generates reports about requests processed by a Ruby on Rails server.
17
+ email: narnach@gmail.com
18
+ executables:
19
+ - rails_analyzer
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ - MIT-LICENSE
25
+ files:
26
+ - MIT-LICENSE
27
+ - README.rdoc
28
+ - Rakefile
29
+ - rails_analyzer.gemspec
30
+ - spec/rails_analyzer_spec.rb
31
+ - spec/entries_spec.rb
32
+ - spec/url_hits_spec.rb
33
+ - spec/spec.opts
34
+ - spec/spec_helper.rb
35
+ - bin/rails_analyzer
36
+ - lib/rails_analyzer.rb
37
+ - lib/array_ext.rb
38
+ - lib/float_ext.rb
39
+ - lib/time_stats.rb
40
+ - lib/hit_stats.rb
41
+ - lib/entries.rb
42
+ - lib/url_hits.rb
43
+ has_rdoc: true
44
+ homepage: http://www.github.com/Narnach/rails_analyzer
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --inline-source
48
+ - --line-numbers
49
+ - --main
50
+ - README.rdoc
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 1.8.0
58
+ version:
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.2.0
69
+ signing_key:
70
+ specification_version: 2
71
+ summary: RailsAnalyzer generates reports about requests processed by a Ruby on Rails server.
72
+ test_files:
73
+ - spec/rails_analyzer_spec.rb
74
+ - spec/entries_spec.rb
75
+ - spec/url_hits_spec.rb