class Ezmetrics::Storage

Constants

AGGREGATION_FUNCTIONS
METRICS
PARTITION_UNITS

Attributes

flat[R]
interval_metrics[R]
interval_seconds[R]
options[R]
partitioned_metrics[R]
redis[R]
requests[R]
safe_payload[R]
schema[R]
storage_key[R]
this_second_metrics[R]

Public Class Methods

new(interval_seconds=60) click to toggle source
# File lib/ezmetrics/storage.rb, line 6
def initialize(interval_seconds=60)
  @interval_seconds = interval_seconds.to_i
  @redis            = Redis.new(driver: :hiredis)
  @schema           = redis_schema
end

Public Instance Methods

flatten() click to toggle source
# File lib/ezmetrics/storage.rb, line 80
def flatten
  @flat = true
  self
end
log(payload={duration: 0.0, views: 0.0, db: 0.0, queries: 0, status: 200, store_each_value: false}) click to toggle source
# File lib/ezmetrics/storage.rb, line 12
def log(payload={duration: 0.0, views: 0.0, db: 0.0, queries: 0, status: 200, store_each_value: false})
  @safe_payload = {
    duration:         payload[:duration].to_f,
    views:            payload[:views].to_f,
    db:               payload[:db].to_f,
    queries:          payload[:queries].to_i,
    status:           payload[:status].to_i,
    store_each_value: payload[:store_each_value].to_s == "true"
  }

  this_second          = Time.now.to_i
  status_group         = "#{payload[:status].to_s[0]}xx"
  @this_second_metrics = redis.get(this_second)

  if this_second_metrics
    @this_second_metrics = Oj.load(this_second_metrics)

    METRICS.each do |metrics_type|
      update_sum(metrics_type)
      update_max(metrics_type)
      store_value(metrics_type) if safe_payload[:store_each_value]
    end

    this_second_metrics[schema["all"]]        += 1
    this_second_metrics[schema[status_group]] += 1
  else
    @this_second_metrics = {
      "second"       => this_second,
      "duration_sum" => safe_payload[:duration],
      "duration_max" => safe_payload[:duration],
      "views_sum"    => safe_payload[:views],
      "views_max"    => safe_payload[:views],
      "db_sum"       => safe_payload[:db],
      "db_max"       => safe_payload[:db],
      "queries_sum"  => safe_payload[:queries],
      "queries_max"  => safe_payload[:queries],
      "2xx"          => 0,
      "3xx"          => 0,
      "4xx"          => 0,
      "5xx"          => 0,
      "all"          => 1
    }

    if safe_payload[:store_each_value]
      this_second_metrics.merge!(
        "duration_values" => [safe_payload[:duration]],
        "views_values"    => [safe_payload[:views]],
        "db_values"       => [safe_payload[:db]],
        "queries_values"  => [safe_payload[:queries]]
      )
    end

    this_second_metrics[status_group] = 1

    @this_second_metrics = this_second_metrics.values
  end

  redis.setex(this_second, interval_seconds, Oj.dump(this_second_metrics))
  true
rescue => error
  formatted_error(error)
end
partition_by(time_unit=:minute) click to toggle source
# File lib/ezmetrics/storage.rb, line 85
def partition_by(time_unit=:minute)
  time_unit = PARTITION_UNITS.include?(time_unit) ? time_unit : :minute
  @partitioned_metrics = interval_metrics.group_by { |array| second_to_partition_unit(time_unit, array[schema["second"]]) }
  self
end
show(options=nil) click to toggle source
# File lib/ezmetrics/storage.rb, line 75
def show(options=nil)
  @options = options || default_options
  partitioned_metrics ? aggregate_partitioned_data : aggregate_data
end

Private Instance Methods

aggregate(metrics, aggregation_function) click to toggle source
# File lib/ezmetrics/storage.rb, line 169
def aggregate(metrics, aggregation_function)
  return avg("#{metrics}_sum") if aggregation_function == :avg
  return max("#{metrics}_max") if aggregation_function == :max

  if aggregation_function == :percentile_distribution
    sorted_values = send("sorted_#{metrics}_values")
    return (1..99).to_a.inject({}) do |result, number|
      result[number] = percentile(sorted_values, number)
      result
    end
  end

  percentile = aggregation_function.match(/percentile_(?<value>\d+)/)

  if percentile && percentile["value"].to_i.between?(1, 99)
    sorted_values = send("sorted_#{metrics}_values")
    percentile(sorted_values, percentile["value"].to_i)
  end
end
aggregate_data() click to toggle source
# File lib/ezmetrics/storage.rb, line 96
def aggregate_data
  return {} unless interval_metrics.any?
  @requests = interval_metrics.sum { |array| array[schema["all"]] }
  build_result
rescue
  {}
end
aggregate_partitioned_data() click to toggle source
# File lib/ezmetrics/storage.rb, line 104
def aggregate_partitioned_data
  partitioned_metrics.map do |partition, metrics|
    @interval_metrics = metrics
    @requests = interval_metrics.sum { |array| array[schema["all"]] }
    METRICS.each { |metrics_type| instance_variable_set("@sorted_#{metrics_type}_values", nil) }
    flat ? { timestamp: partition, **build_result } : { timestamp: partition, data: build_result }
  end
rescue
  self
end
append_metrics_to_result(result, metrics, aggregation_function, aggregated_metrics) click to toggle source
# File lib/ezmetrics/storage.rb, line 146
def append_metrics_to_result(result, metrics, aggregation_function, aggregated_metrics)
  return result[:"#{metrics}_#{aggregation_function}"] = aggregated_metrics if flat

  result[metrics] ||= {}
  result[metrics][aggregation_function] = aggregated_metrics
end
append_requests_to_result(result, aggregated_requests) click to toggle source
# File lib/ezmetrics/storage.rb, line 137
def append_requests_to_result(result, aggregated_requests)
  return result[:requests] = aggregated_requests unless flat

  result[:requests_all] = aggregated_requests[:all]
  aggregated_requests[:grouped].each do |group, counter|
    result[:"requests_#{group}"] = counter
  end
end
avg(metrics) click to toggle source
# File lib/ezmetrics/storage.rb, line 233
def avg(metrics)
  (interval_metrics.sum { |array| array[schema[metrics]] }.to_f / requests).round
end
build_result() click to toggle source
# File lib/ezmetrics/storage.rb, line 115
def build_result
  result = {}

  if options[:requests]
    append_requests_to_result(result, { all: requests, grouped: count_all_status_groups })
  end

  options.each do |metrics, aggregation_functions|
    next unless METRICS.include?(metrics)
    aggregation_functions = [aggregation_functions] unless aggregation_functions.is_a?(Array)
    next unless aggregation_functions.any?

    aggregation_functions.each do |aggregation_function|
      aggregated_metrics = aggregate(metrics, aggregation_function)
      append_metrics_to_result(result, metrics, aggregation_function, aggregated_metrics)
    end
  end
  result
ensure
  result
end
count_all_status_groups() click to toggle source
# File lib/ezmetrics/storage.rb, line 262
def count_all_status_groups
  interval_metrics.inject({ "2xx" => 0, "3xx" => 0, "4xx" => 0, "5xx" => 0 }) do |result, array|
    result["2xx"] += array[schema["2xx"]]
    result["3xx"] += array[schema["3xx"]]
    result["4xx"] += array[schema["4xx"]]
    result["5xx"] += array[schema["5xx"]]
    result
  end
end
default_options() click to toggle source
# File lib/ezmetrics/storage.rb, line 272
def default_options
  {
    duration: AGGREGATION_FUNCTIONS,
    views:    AGGREGATION_FUNCTIONS,
    db:       AGGREGATION_FUNCTIONS,
    queries:  AGGREGATION_FUNCTIONS,
    requests: true
  }
end
formatted_error(error) click to toggle source
# File lib/ezmetrics/storage.rb, line 282
def formatted_error(error)
  {
    error:     error.class.name,
    message:   error.message,
    backtrace: error.backtrace.reject { |line| line.match(/ruby|gems/) }
  }
end
max(metrics) click to toggle source
# File lib/ezmetrics/storage.rb, line 237
def max(metrics)
  interval_metrics.max { |array| array[schema[metrics]] }[schema[metrics]].round
end
percentile(sorted_array, pcnt) click to toggle source
# File lib/ezmetrics/storage.rb, line 241
def percentile(sorted_array, pcnt)
  array_length = sorted_array.length

  return "not enough data (requests: #{array_length}, required: #{pcnt})" if array_length < pcnt

  rank  = (pcnt.to_f / 100) * (array_length + 1)
  whole = rank.truncate

  # if has fractional part
  if whole != rank
    s0 = sorted_array[whole - 1]
    s1 = sorted_array[whole]

    f = (rank - rank.truncate).abs

    return ((f * (s1 - s0)) + s0)&.round
  else
    return (sorted_array[whole - 1])&.round
  end
end
redis_schema() click to toggle source
# File lib/ezmetrics/storage.rb, line 197
def redis_schema
  [
    "second",
    "duration_sum",
    "duration_max",
    "views_sum",
    "views_max",
    "db_sum",
    "db_max",
    "queries_sum",
    "queries_max",
    "2xx",
    "3xx",
    "4xx",
    "5xx",
    "all",
    "duration_values",
    "views_values",
    "db_values",
    "queries_values"
  ].each_with_index.inject({}){ |result, pair| result[pair[0]] = pair[1] ; result }
end
second_to_partition_unit(time_unit, second) click to toggle source
# File lib/ezmetrics/storage.rb, line 153
def second_to_partition_unit(time_unit, second)
  return second if time_unit == :second
  time = Time.at(second)
  return (time - time.sec - time.min * 60 - time.hour * 3600).to_i if time_unit == :day
  return (time - time.sec - time.min * 60).to_i                    if time_unit == :hour
  (time - time.sec).to_i
end
store_value(metrics) click to toggle source
# File lib/ezmetrics/storage.rb, line 224
def store_value(metrics)
  this_second_metrics[schema["#{metrics}_values"]] << safe_payload[metrics]
end
update_max(metrics) click to toggle source
# File lib/ezmetrics/storage.rb, line 228
def update_max(metrics)
  max_value = [safe_payload[metrics], this_second_metrics[schema["#{metrics}_max"]]].max
  this_second_metrics[schema["#{metrics}_max"]] = max_value
end
update_sum(metrics) click to toggle source
# File lib/ezmetrics/storage.rb, line 220
def update_sum(metrics)
  this_second_metrics[schema["#{metrics}_sum"]] += safe_payload[metrics]
end