Narnach-rails_analyzer 0.2.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/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