module RequestLogAnalyzer::Tracker
# Analyze the average and total traffic of requests
#
# === Options
# * :amount The amount of lines in the report
# * :category Proc that handles request categorization for given fileformat (REQUEST_CATEGORIZER)
# * :traffic The field containing the duration in the request hash.
# * :if Proc that has to return !nil for a request to be passed to the tracker.
# * :line_type The line type that contains the duration field (determined by the category proc).
# * :title Title do be displayed above the report
# * :unless Handle request if this proc is false for the handled request.
class Traffic < Base
attr_reader :categories
# Check if duration and catagory option have been received,
def prepare
raise "No traffic field set up for category tracker #{self.inspect}" unless options[:traffic]
raise "No categorizer set up for duration tracker #{self.inspect}" unless options[:category]
@categorizer = options[:category].respond_to?(:call) ? options[:category] : lambda { |request| request[options[:category]] }
@trafficizer = options[:traffic].respond_to?(:call) ? options[:traffic] : lambda { |request| request[options[:traffic]] }
@categories = {}
end
# Get the duration information fron the request and store it in the different categories.
# request The request.
def update(request)
category = @categorizer.call(request)
traffic = @trafficizer.call(request)
if traffic.kind_of?(Numeric) && !category.nil?
@categories[category] ||= {:hits => 0, :cumulative => 0, :min => traffic, :max => traffic }
@categories[category][:hits] += 1
@categories[category][:cumulative] += traffic
@categories[category][:min] = traffic if traffic < @categories[category][:min]
@categories[category][:max] = traffic if traffic > @categories[category][:max]
end
end
# Get the number of hits of a specific category.
# cat The category
def hits(cat)
categories[cat][:hits]
end
# Get the total duration of a specific category.
# cat The category
def cumulative_traffic(cat)
categories[cat][:cumulative]
end
# Get the minimal duration of a specific category.
# cat The category
def min_traffic(cat)
categories[cat][:min]
end
# Get the maximum duration of a specific category.
# cat The category
def max_traffic(cat)
categories[cat][:max]
end
# Get the average duration of a specific category.
# cat The category
def average_traffic(cat)
categories[cat][:cumulative].to_f / categories[cat][:hits]
end
# Get the average duration of a all categories.
def overall_average_traffic
overall_cumulative_duration.to_f / overall_hits
end
# Get the cumlative duration of a all categories.
def overall_cumulative_traffic
categories.inject(0) { |sum, (name, cat)| sum + cat[:cumulative] }
end
# Get the total hits of a all categories.
def overall_hits
categories.inject(0) { |sum, (name, cat)| sum + cat[:hits] }
end
# Return categories sorted by hits.
def sorted_by_hits
sorted_by(:hits)
end
# Return categories sorted by cumulative duration.
def sorted_by_cumulative
sorted_by(:cumulative)
end
# Return categories sorted by cumulative duration.
def sorted_by_average
sorted_by { |cat| cat[:cumulative].to_f / cat[:hits] }
end
# Return categories sorted by a given key.
# by The key.
def sorted_by(by = nil)
if block_given?
categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }
else
categories.sort { |a, b| b[1][by] <=> a[1][by] }
end
end
# Block function to build a result table using a provided sorting function.
# output The output object.
# amount The number of rows in the report table (default 10).
# === Options
# * :title The title of the table
# * :sort The key to sort on (:hits, :cumulative, :average, :min or :max)
def report_table(output, amount = 10, options = {}, &block)
output.title(options[:title])
top_categories = @categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }.slice(0...amount)
output.table({:title => 'Category', :width => :rest},
{:title => 'Hits', :align => :right, :highlight => (options[:sort] == :hits), :min_width => 4},
{:title => 'Cumulative', :align => :right, :highlight => (options[:sort] == :cumulative), :min_width => 10},
{:title => 'Average', :align => :right, :highlight => (options[:sort] == :average), :min_width => 8},
{:title => 'Min', :align => :right, :highlight => (options[:sort] == :min)},
{:title => 'Max', :align => :right, :highlight => (options[:sort] == :max)}) do |rows|
top_categories.each do |(cat, info)|
rows << [cat, info[:hits], format_traffic(info[:cumulative]), format_traffic((info[:cumulative] / info[:hits]).round),
format_traffic(info[:min]), format_traffic(info[:max])]
end
end
end
# Formats the traffic number using x B/kB/MB/GB etc notation
def format_traffic(bytes)
return "0 B" if bytes.zero?
case Math.log10(bytes).floor
when 1...4 then '%d B' % bytes
when 4...7 then '%d kB' % (bytes / 1000)
when 7...10 then '%d MB' % (bytes / 1000_000)
when 10...13 then '%d GB' % (bytes / 1000_000_000)
else '%d TB' % (bytes / 1000_000_000_000)
end
end
# Generate a request duration report to the given output object
# By default colulative and average duration are generated.
# Any options for the report should have been set during initialize.
# output The output object
def report(output)
options[:report] ||= [:cumulative, :average]
options[:top] ||= 20
options[:report].each do |report|
case report
when :average
report_table(output, options[:top], :title => "#{title} - top #{options[:top]} by average", :sort => :average) { |cat| cat[:cumulative] / cat[:hits] }
when :cumulative
report_table(output, options[:top], :title => "#{title} - top #{options[:top]} by sum", :sort => :cumulative) { |cat| cat[:cumulative] }
when :hits
report_table(output, options[:top], :title => "#{title} - top #{options[:top]} by hits", :sort => :hits) { |cat| cat[:hits] }
else
raise "Unknown duration report specified: #{report}!"
end
end
output.puts
output.puts "#{output.colorize(title, :white, :bold)} - observed total: " + output.colorize(format_traffic(overall_cumulative_traffic), :brown, :bold)
end
# Returns the title of this tracker for reports
def title
options[:title] || 'Request traffic'
end
# Returns all the categories and the tracked duration as a hash than can be exported to YAML
def to_yaml_object
return nil if @categories.empty?
@categories
end
end
end