Skip to content

Commit

Permalink
new chargeback calculations
Browse files Browse the repository at this point in the history
  • Loading branch information
lpichler committed Oct 18, 2016
1 parent 719b8d2 commit f3506b7
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 35 deletions.
108 changes: 75 additions & 33 deletions app/models/chargeback.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class Chargeback < ActsAsArModel
HOURS_IN_DAY = 24
HOURS_IN_WEEK = 168

VIRTUAL_COL_USES = {
"v_derived_cpu_total_cores_used" => "cpu_usage_rate_average"
}
Expand Down Expand Up @@ -29,41 +32,74 @@ def self.build_results_for_report_chargeback(options)
rate_cols = ChargebackRate.where(:default => true).flat_map do |rate|
rate.chargeback_rate_details.map(&:metric).select { |metric| perf_cols.include?(metric.to_s) }
end

rate_cols.map! { |x| VIRTUAL_COL_USES.include?(x) ? VIRTUAL_COL_USES[x] : x }.flatten!
base_rollup = base_rollup.select(*rate_cols)

timerange = get_report_time_range(options, interval, tz)
data = {}

timerange.step_value(1.day).each_cons(2) do |query_start_time, query_end_time|
recs = base_rollup.where(:timestamp => query_start_time...query_end_time, :capture_interval_name => "hourly")
recs = where_clause(recs, options)
recs = Metric::Helper.remove_duplicate_timestamps(recs)
_log.info("Found #{recs.length} records for time range #{[query_start_time, query_end_time].inspect}")

unless recs.empty?
recs.each do |perf|
next if perf.resource.nil?
key, extra_fields = key_and_fields(perf, interval, tz)
data[key] ||= extra_fields

if perf.chargeback_fields_present?
data[key]['fixed_compute_metric'] ||= 0
data[key]['fixed_compute_metric'] = data[key]['fixed_compute_metric'] + 1
end

rates_to_apply = cb.get_rates(perf)
chargeback_rates = data[key]["chargeback_rates"].split(', ') + rates_to_apply.collect(&:description)
data[key]["chargeback_rates"] = chargeback_rates.uniq.join(', ')
calculate_costs(perf, data[key], rates_to_apply)
end
interval_duration = interval_to_duration(interval)

timerange.step_value(interval_duration).each_cons(2) do |query_start_time, query_end_time|
records = base_rollup.where(:timestamp => query_start_time...query_end_time, :capture_interval_name => "hourly")
records = where_clause(records, options)
records = Metric::Helper.remove_duplicate_timestamps(records)
next if records.empty?
_log.info("Found #{records.length} records for time range #{[query_start_time, query_end_time].inspect}")

hours_in_interval = hours_in_interval(query_start_time, query_end_time, interval)

# we are building hash with grouped calculated values
# values are grouped by resource_id and timestamp (query_start_time...query_end_time)
records.group_by(&:resource_id).each do |_, metric_rollup_records|
metric_rollup_records = metric_rollup_records.select { |x| x.resource.present? }
next if metric_rollup_records.empty?

# we need to select ChargebackRates for groups of MetricRollups records
# and rates are selected by first MetricRollup record
metric_rollup_record = metric_rollup_records.first
rates_to_apply = cb.get_rates(metric_rollup_record)

# key contains resource_id and timestamp (query_start_time...query_end_time)
# extra_fields there some extra field like resource name and
# some of them are related to specific chargeback (ChargebackVm, ChargebackContainer,...)
key, extra_fields = key_and_fields(metric_rollup_record, interval, tz)
data[key] ||= extra_fields

chargeback_rates = data[key]["chargeback_rates"].split(', ') + rates_to_apply.collect(&:description)
data[key]["chargeback_rates"] = chargeback_rates.uniq.join(', ')

# we are getting hash with metrics and costs for metrics defined for chargeback
metrics_and_costs = calculate_costs(metric_rollup_records, rates_to_apply, hours_in_interval)

data[key].merge!(metrics_and_costs)
end
end

_log.info("Calculating chargeback costs...Complete")

[data.map { |r| new(r.last) }]
end

def self.hours_in_interval(query_start_time, query_end_time, interval)
return HOURS_IN_DAY if interval == "daily"
return HOURS_IN_WEEK if interval == "weekly"

(query_end_time - query_start_time) / 1.hour
end

def self.interval_to_duration(interval)
case interval
when "daily"
1.day
when "weekly"
1.week
when "monthly"
1.month
end
end

def self.key_and_fields(metric_rollup_record, interval, tz)
ts_key = get_group_key_ts(metric_rollup_record, interval, tz)

Expand Down Expand Up @@ -123,25 +159,31 @@ def get_rates(perf)
@rates[key] = ChargebackRate.get_assigned_for_target(perf.resource, :tag_list => tag_list, :parents => parents)
end

def self.calculate_costs(perf, h, rates)
# This expects perf interval to be hourly. That will be the most granular interval available for chargeback.
unless perf.capture_interval_name == "hourly"
raise _("expected 'hourly' performance interval but got '%{interval}") % {:interval => perf.capture_interval_name}
end
def self.calculate_costs(metric_rollup_records, rates, hours_in_interval)
calculated_costs = {}

chargeback_fields_present = metric_rollup_records.count(&:chargeback_fields_present?)
calculated_costs['fixed_compute_metric'] = chargeback_fields_present if chargeback_fields_present

rates.each do |rate|
rate.chargeback_rate_details.each do |r|
rec = r.metric && perf.respond_to?(r.metric) ? perf : perf.resource
metric = r.metric.nil? ? 0 : rec.send(r.metric) || 0
cost = r.group == 'fixed' && !perf.chargeback_fields_present? ? 0 : r.cost(metric)
if !chargeback_fields_present && r.fixed?
cost = 0
else
metric_value = r.metric_value_by(metric_rollup_records)
cost = r.cost(metric_value) * hours_in_interval
end

reportable_metric_and_cost_fields(r.rate_name, r.group, metric, cost).each do |k, val|
# add values to hash and sum
reportable_metric_and_cost_fields(r.rate_name, r.group, metric_value, cost).each do |k, val|
next unless attribute_names.include?(k)
h[k] ||= 0
h[k] += val
calculated_costs[k] ||= 0
calculated_costs[k] += val
end
end
end

calculated_costs
end

def self.reportable_metric_and_cost_fields(rate_name, rate_group, metric, cost)
Expand Down
38 changes: 36 additions & 2 deletions app/models/chargeback_rate_detail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,37 @@ class ChargebackRateDetail < ApplicationRecord

FORM_ATTRIBUTES = %i(description per_time per_unit metric group source metric).freeze

def max_of_metric_from(metric_rollup_records)
metric_rollup_records.map(&metric.to_sym).max
end

def avg_of_metric_from(metric_rollup_records)
record_count = metric_rollup_records.count
metric_sum = metric_rollup_records.sum(&metric.to_sym)
metric_sum / record_count
end

def metric_value_by(metric_rollup_records)
return 1.0 if fixed?

metric_rollups_without_nils = metric_rollup_records.select { |x| x.send(metric.to_sym).present? }
return 0 if metric_rollups_without_nils.empty?
return max_of_metric_from(metric_rollups_without_nils) if allocated?
return avg_of_metric_from(metric_rollups_without_nils) if used?
end

def used?
source == "used"
end

def allocated?
source == "allocated"
end

def fixed?
group == "fixed"
end

# Set the rates according to the tiers
def find_rate(value)
fixed_rate = 0.0
Expand All @@ -36,9 +67,12 @@ def find_rate(value)

def cost(value)
return 0.0 unless self.enabled?
value = 1 if group == 'fixed'

value = 1.0 if fixed?

(fixed_rate, variable_rate) = find_rate(value)
hourly(fixed_rate) + hourly(variable_rate) * value

hourly(fixed_rate) + hourly(variable_rate) * value
end

def hourly(rate)
Expand Down

0 comments on commit f3506b7

Please sign in to comment.