# frozen_string_literal: true module Core # Utility module to collect statistics module Stats @mutex = Mutex.new @buffer = {} # accumulate count, sum, and sum of squares to support calculation of mean and population standard deviation # note: use the first value as a zero offset to reduce floating point precision errors class Accumulator attr_reader :count, :max, :min def initialize @count = 0 @sum = 0 @sumsqr = 0 end def add_value(value) @offset ||= value @max = value if @max.nil? || value > @max @min = value if @min.nil? || value < @max offset_val = value - @offset @sum += offset_val @sumsqr += offset_val**2 @count += 1 end def mean @offset + @sum / @count end def stddev Math.sqrt((@sumsqr - @sum**2 / @count) / @count) end end # collect a numeric data point # first n-1 args are used as hash keys, the last arg is the data value def self.collect(*args) raise ArgumentError unless args.length >= 2 value = args.pop @mutex.synchronize do # find/create the appropriate object within the buffer obj = args.inject(@buffer) { |buf, key| buf[key.to_s] ||= {} } # add the data value to the object's accumulator (obj[:_data] ||= Accumulator.new).add_value(value) end end # traverse the buffer and yield with each private_class_method def self.traverse_data(hash) stack = [] # add top level keys to the array in order hash.keys.sort.map { |k| stack.push [[k], @buffer[k]] unless k == :_data } until stack.empty? keylist, obj = stack.shift yield keylist, obj[:_data] if obj.key? :_data # add next level of keys in reverse order to the front of the list obj.keys.sort.reverse.map { |k| stack.unshift [keylist.dup << k, obj[k]] unless k == :_data } end end def self.log_and_reset msgs = ["Server: #{Server::ID}", "Commit: #{Core::Version::ID}", "Tag: #{Core::Version::TAG}"] # app can define a #custom_messages method to customize reporting msgs.concat(Array(custom_messages)) if respond_to? :custom_messages @mutex.synchronize do traverse_data(@buffer) do |keylist, data| count = data.count min = data.min mean = data.mean.round(2) max = data.max stddev = data.stddev.round(2) msgs << "#{keylist.join('|')}: count: #{count} min: #{min} mean: #{mean} max: #{max} stddev: #{stddev}" end @buffer = {} end msgs.each { |msg| log.info('stats') { msg } } end end end