Class: RequestLogAnalyzer::Tracker::NumericValue

Inherits:
Base
  • Object
show all
Defined in:
lib/request_log_analyzer/tracker/numeric_value.rb

Direct Known Subclasses

Duration, Traffic

Instance Attribute Summary collapse

Attributes inherited from Base

#options

Instance Method Summary collapse

Methods inherited from Base

#create_lambda, #finalize, #initialize, #setup_should_update_checks!, #should_update?

Constructor Details

This class inherits a constructor from RequestLogAnalyzer::Tracker::Base

Instance Attribute Details

#categoriesObject (readonly)

Returns the value of attribute categories.



3
4
5
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 3

def categories
  @categories
end

Instance Method Details

#bucket_average_value(index) ⇒ Object

Returns the average of the lower and upper bound of the bucket.



147
148
149
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 147

def bucket_average_value(index)
  (bucket_lower_bound(index) + bucket_upper_bound(index)) / 2
end

#bucket_index(value) ⇒ Object

Returns the bucket index for a value



129
130
131
132
133
134
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 129

def bucket_index(value)
  return 0 if value < @min_bucket_value
  return @number_of_buckets - 1 if value >= @max_bucket_value

  ((Math.log(value) - Math.log(@min_bucket_value)) / @bucket_size).floor
end

#bucket_interval(index) ⇒ Object

Returns the range of values for a bucket.



161
162
163
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 161

def bucket_interval(index)
  Range.new(bucket_lower_bound(index), bucket_upper_bound(index), true)
end

#bucket_lower_bound(index) ⇒ Object

Returns the lower value of a bucket given its index



137
138
139
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 137

def bucket_lower_bound(index)
  Math.exp((index * @bucket_size) + Math.log(@min_bucket_value))
end

#bucket_upper_bound(index) ⇒ Object

Returns the upper value of a bucket given its index



142
143
144
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 142

def bucket_upper_bound(index)
  bucket_lower_bound(index + 1)
end

#bucket_value(index, type = nil) ⇒ Object

Returns a single value representing a bucket.



152
153
154
155
156
157
158
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 152

def bucket_value(index, type = nil)
  case type
  when :begin, :start, :lower, :lower_bound then bucket_lower_bound(index)
  when :end, :finish, :upper, :upper_bound then  bucket_upper_bound(index)
  else bucket_average_value(index)
  end
end

#bucketize(category, value) ⇒ Object

Records a hit on a bucket that includes the given value.



166
167
168
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 166

def bucketize(category, value)
  @categories[category][:buckets][bucket_index(value)] += 1
end

#display_value(value) ⇒ Object

Display a value



73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 73

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 / 1_000_000)
    when 10...13 then '%dG' % (value / 1_000_000_000)
    when 13...16 then '%dT' % (value / 1_000_000_000_000)
    else              '%dP' % (value / 1_000_000_000_000_000)
  end
end

#hits(cat) ⇒ Object

Get the number of hits of a specific category. cat The category



244
245
246
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 244

def hits(cat)
  @categories[cat][:hits]
end

#hits_overallObject

Get the total hits of a all categories.



296
297
298
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 296

def hits_overall
  @categories.reduce(0) { |sum, (_, cat)| sum + cat[:hits] }
end

#max(cat) ⇒ Object

Get the maximum duration of a specific category. cat The category



262
263
264
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 262

def max(cat)
  @categories[cat][:max]
end

#mean(cat) ⇒ Object

Get the average duration of a specific category. cat The category



268
269
270
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 268

def mean(cat)
  @categories[cat][:mean]
end

#mean_overallObject

Get the average duration of a all categories.



286
287
288
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 286

def mean_overall
  sum_overall / hits_overall
end

#median(category) ⇒ Object



199
200
201
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 199

def median(category)
  percentile(category, 50, :average)
end

#min(cat) ⇒ Object

Get the minimal duration of a specific category. cat The category



256
257
258
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 256

def min(cat)
  @categories[cat][:min]
end

#percentile(category, x, type = nil) ⇒ Object



195
196
197
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 195

def percentile(category, x, type = nil)
  bucket_value(percentile_index(category, x, type == :upper), type)
end

#percentile_index(category, x, inclusive = false) ⇒ Object

Returns the upper bound value that would include x% of the hits.



171
172
173
174
175
176
177
178
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 171

def percentile_index(category, x, inclusive = false)
  total_encountered = 0
  @categories[category][:buckets].each_with_index do |count, index|
    total_encountered += count
    percentage = ((total_encountered.to_f / hits(category).to_f) * 100).floor
    return index if (inclusive && percentage >= x) || (!inclusive && percentage > x)
  end
end

#percentile_indices(category, start, finish) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 180

def percentile_indices(category, start, finish)
  result = [nil, nil]
  total_encountered = 0
  @categories[category][:buckets].each_with_index do |count, index|
    total_encountered += count
    percentage = ((total_encountered.to_f / hits(category).to_f) * 100).floor
    if !result[0] && percentage > start
      result[0] = index
    elsif !result[1] && percentage >= finish
      result[1] = index
      return result
    end
  end
end

#percentile_interval(category, x) ⇒ Object

Returns a percentile interval, i.e. the lower bound and the upper bound of the values that represent the x%-interval for the bucketized dataset.

A 90% interval means that 5% of the values would have been lower than the lower bound and 5% would have been higher than the upper bound, leaving 90% of the values within the bounds. You can also provide a Range to specify the lower bound and upper bound percentages (e.g. 5..95).



209
210
211
212
213
214
215
216
217
218
219
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 209

def percentile_interval(category, x)
  case x
  when Range
    lower, upper = percentile_indices(category, x.begin, x.end)
    Range.new(bucket_lower_bound(lower), bucket_upper_bound(upper))
  when Numeric
    percentile_interval(category, Range.new((100 - x) / 2, (100 - (100 - x) / 2)))
  else
    fail 'What does it mean?'
  end
end

#prepareObject

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



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 8

def prepare
  fail "No value field set up for numeric tracker #{inspect}" unless options[:value]
  fail "No categorizer set up for numeric tracker #{inspect}" unless options[:category]

  unless options[:multiple]
    @categorizer = create_lambda(options[:category])
    @valueizer   = create_lambda(options[:value])
  end

  @number_of_buckets = options[:number_of_buckets] || 1000
  @min_bucket_value  = options[:min_bucket_value] ? options[:min_bucket_value].to_f : 0.000001
  @max_bucket_value  = options[:max_bucket_value] ? options[:max_bucket_value].to_f : 1_000_000_000

  # precalculate the bucket size
  @bucket_size = (Math.log(@max_bucket_value) - Math.log(@min_bucket_value)) / @number_of_buckets.to_f

  @categories = {}
end

#report(output) ⇒ Object

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



91
92
93
94
95
96
97
98
99
100
101
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 91

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

#report_table(output, sort, options = {}, &_block) ⇒ Object

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

* </tt>:title</tt> The title of the table
* </tt>:sort</tt> The key to sort on (:hits, :cumulative, :average, :min or :max)


62
63
64
65
66
67
68
69
70
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 62

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 { |(category, _)| rows << statistics_row(category) }
    end
  end
end

#sorted_by(by = nil) ⇒ Object

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



302
303
304
305
306
307
308
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 302

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

#statistics_header(options) ⇒ Object

Returns the column header for a statistics table to report on the statistics result



311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 311

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 },
    { title: '95 %tile',    align: :right, highlight: (options[:highlight] == :percentile_interval),  min_width: 11 }
  ]
end

#statistics_row(cat) ⇒ Object

Returns a row of statistics information for a report table, given a category



325
326
327
328
329
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 325

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)),
   display_value(percentile_interval(cat, 95).begin) + '-' + display_value(percentile_interval(cat, 95).end)]
end

#stddev(cat) ⇒ Object

Get the standard deviation of the duration of a specific category. cat The category



274
275
276
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 274

def stddev(cat)
  Math.sqrt(variance(cat))
end

#sum(cat) ⇒ Object

Get the total duration of a specific category. cat The category



250
251
252
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 250

def sum(cat)
  @categories[cat][:sum]
end

#sum_overallObject

Get the cumlative duration of a all categories.



291
292
293
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 291

def sum_overall
  @categories.reduce(0.0) { |sum, (_, cat)| sum + cat[:sum] }
end

#titleObject

Returns the title of this tracker for reports



104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 104

def title
  @title ||= begin
    if options[:title]
      options[:title]
    else
      title_builder = ''
      title_builder << "#{options[:value]} " if options[:value].is_a?(Symbol)
      title_builder << (options[:category].is_a?(Symbol) ? "per #{options[:category]}" : 'per request')
      title_builder
    end
  end
end

#to_yaml_objectObject

Returns all the categories and the tracked duration as a hash than can be exported to YAML



118
119
120
121
122
123
124
125
126
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 118

def to_yaml_object
  return nil if @categories.empty?
  @categories.each do |cat, info|
    info[:stddev] = stddev(cat)
    info[:median] = median(cat) if info[:buckets]
    info[:interval_95_percent] = percentile_interval(cat, 95) if info[:buckets]
  end
  @categories
end

#update(request) ⇒ Object

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.

Parameters:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 40

def update(request)
  if options[:multiple]
    found_categories = request.every(options[:category])
    found_values     = request.every(options[:value])
    fail '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].is_a?(Numeric)
    end
  else
    category = @categorizer.call(request)
    value    = @valueizer.call(request)
    update_statistics(category, value) if (value.is_a?(Numeric) || value.is_a?(Array)) && category
  end
end

#update_statistics(category, number) ⇒ Object

Update the 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.



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 224

def update_statistics(category, number)
  return number.map { |n| update_statistics(category, n) } if number.is_a?(Array)

  @categories[category] ||= { hits: 0, sum: 0, mean: 0.0, sum_of_squares: 0.0, min: number, max: number,
                              buckets: Array.new(@number_of_buckets, 0) }

  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]

  bucketize(category, number)
end

#variance(cat) ⇒ Object

Get the variance of the duration of a specific category. cat The category



280
281
282
283
# File 'lib/request_log_analyzer/tracker/numeric_value.rb', line 280

def variance(cat)
  return 0.0 if @categories[cat][:hits] <= 1
  (@categories[cat][:sum_of_squares] / (@categories[cat][:hits] - 1))
end