module RequestLogAnalyzer::Tracker class NumericValue < Base attr_reader :categories # Sets up the numeric value tracker. It will check whether the value and category # options are set that are used to extract and categorize the values during # parsing. Two lambda procedures are created for these tasks def prepare raise "No value field set up for numeric tracker #{self.inspect}" unless options[:value] raise "No categorizer set up for numeric tracker #{self.inspect}" unless options[:category] unless options[:multiple] @categorizer = create_lambda(options[:category]) @valueizer = create_lambda(options[:value]) end @categories = {} end # Get the value information from the request and store it in the respective categories. # # If a request can contain multiple usable values for this tracker, the :multiple option # should be set to true. In this case, all the values and respective categories will be # read from the request using the #every method from the fields given in the :value and # :category option. # # If the request contains only one suitable value and the :multiple is not set, it will # read the single value and category from the fields provided in the :value and :category # option, or calculate it with any lambda procedure that is assigned to these options. The # request will be passed to procedure as input for the calculation. # # @param [RequestLogAnalyzer::Request] request The request to get the information from. def update(request) if options[:multiple] found_categories = request.every(options[:category]) found_values = request.every(options[:value]) raise "Capture mismatch for multiple values in a request" unless found_categories.length == found_values.length found_categories.each_with_index do |cat, index| update_statistics(cat, found_values[index]) if cat && found_values[index].kind_of?(Numeric) end else category = @categorizer.call(request) value = @valueizer.call(request) update_statistics(category, value) if value.kind_of?(Numeric) && category 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, sort, options = {}, &block) output.puts top_categories = output.slice_results(sorted_by(sort)) output.with_style(:top_line => true) do output.table(*statistics_header(:title => options[:title], :highlight => sort)) do |rows| top_categories.each { |(cat, info)| rows << statistics_row(cat) } end end end # Display a value def display_value(value) return "- " if value.nil? return "0 " if value.zero? case [Math.log10(value.abs).floor, 0].max when 0...4 then '%d ' % value when 4...7 then '%dk' % (value / 1000) when 7...10 then '%dM' % (value / 1000_000) when 10...13 then '%dG' % (value / 1000_000_000) when 13...16 then '%dT' % (value / 1000_000_000_000) else '%dP' % (value / 1000_000_000_000_000) end end # Generate a request 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) sortings = output.options[:sort] || [:sum, :mean] sortings.each do |sorting| report_table(output, sorting, :title => "#{title} - by #{sorting}") end if options[:total] output.puts output.puts "#{output.colorize(title, :white, :bold)} - total: " + output.colorize(display_value(sum_overall), :brown, :bold) end end # Returns the title of this tracker for reports def title @title ||= begin if options[:title] options[:title] else title_builder = "" title_builder << "#{options[:value]} " if options[:value].kind_of?(Symbol) title_builder << (options[:category].kind_of?(Symbol) ? "per #{options[:category]}" : "per request") title_builder end end 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 # Update sthe running calculation of statistics with the newly found numeric value. # category:: The category for which to update the running statistics calculations # number:: The numeric value to update the calculations with. def update_statistics(category, number) @categories[category] ||= {:hits => 0, :sum => 0, :mean => 0.0, :sum_of_squares => 0.0, :min => number, :max => number } delta = number - @categories[category][:mean] @categories[category][:hits] += 1 @categories[category][:mean] += (delta / @categories[category][:hits]) @categories[category][:sum_of_squares] += delta * (number - @categories[category][:mean]) @categories[category][:sum] += number @categories[category][:min] = number if number < @categories[category][:min] @categories[category][:max] = number if number > @categories[category][:max] 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 sum(cat) @categories[cat][:sum] end # Get the minimal duration of a specific category. # cat The category def min(cat) @categories[cat][:min] end # Get the maximum duration of a specific category. # cat The category def max(cat) @categories[cat][:max] end # Get the average duration of a specific category. # cat The category def mean(cat) @categories[cat][:mean] end # Get the standard deviation of the duration of a specific category. # cat The category def stddev(cat) Math.sqrt(variance(cat)) end # Get the variance of the duration of a specific category. # cat The category def variance(cat) return 0.0 if @categories[cat][:hits] <= 1 (@categories[cat][:sum_of_squares] / (@categories[cat][:hits] - 1)) end # Get the average duration of a all categories. def mean_overall sum_overall / hits_overall end # Get the cumlative duration of a all categories. def sum_overall @categories.inject(0.0) { |sum, (name, cat)| sum + cat[:sum] } end # Get the total hits of a all categories. def hits_overall @categories.inject(0) { |sum, (name, cat)| sum + cat[:hits] } end # Return categories sorted by a given key. # by The key to sort on. This parameter can be omitted if a sorting block is provided instead def sorted_by(by = nil) if block_given? categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) } else categories.sort { |a, b| send(by, b[0]) <=> send(by, a[0]) } end end # Returns the column header for a statistics table to report on the statistics result def statistics_header(options) [ {:title => options[:title], :width => :rest}, {:title => 'Hits', :align => :right, :highlight => (options[:highlight] == :hits), :min_width => 4}, {:title => 'Sum', :align => :right, :highlight => (options[:highlight] == :sum), :min_width => 6}, {:title => 'Mean', :align => :right, :highlight => (options[:highlight] == :mean), :min_width => 6}, {:title => 'StdDev', :align => :right, :highlight => (options[:highlight] == :stddev), :min_width => 6}, {:title => 'Min', :align => :right, :highlight => (options[:highlight] == :min), :min_width => 6}, {:title => 'Max', :align => :right, :highlight => (options[:highlight] == :max), :min_width => 6} ] end # Returns a row of statistics information for a report table, given a category def statistics_row(cat) [cat, hits(cat), display_value(sum(cat)), display_value(mean(cat)), display_value(stddev(cat)), display_value(min(cat)), display_value(max(cat))] end end end