|
|
@@ -2,15 +2,11 @@ package costmodel
|
|
|
|
|
|
import (
|
|
|
"fmt"
|
|
|
- "math"
|
|
|
"net/http"
|
|
|
- "sort"
|
|
|
- "strconv"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
- "github.com/opencost/opencost/pkg/cloud/provider"
|
|
|
"github.com/opencost/opencost/pkg/errors"
|
|
|
|
|
|
"github.com/opencost/opencost/core/pkg/log"
|
|
|
@@ -18,9 +14,7 @@ import (
|
|
|
"github.com/opencost/opencost/core/pkg/util"
|
|
|
"github.com/opencost/opencost/core/pkg/util/httputil"
|
|
|
"github.com/opencost/opencost/core/pkg/util/json"
|
|
|
- "github.com/opencost/opencost/core/pkg/util/promutil"
|
|
|
"github.com/opencost/opencost/core/pkg/util/timeutil"
|
|
|
- "github.com/opencost/opencost/pkg/cloud/models"
|
|
|
"github.com/opencost/opencost/pkg/env"
|
|
|
)
|
|
|
|
|
|
@@ -35,614 +29,6 @@ const (
|
|
|
UnallocatedSubfield = "__unallocated__"
|
|
|
)
|
|
|
|
|
|
-// Aggregation describes aggregated cost data, containing cumulative cost and
|
|
|
-// allocation data per resource, vectors of rate data per resource, efficiency
|
|
|
-// data, and metadata describing the type of aggregation operation.
|
|
|
-type Aggregation struct {
|
|
|
- Aggregator string `json:"aggregation"`
|
|
|
- Subfields []string `json:"subfields,omitempty"`
|
|
|
- Environment string `json:"environment"`
|
|
|
- Cluster string `json:"cluster,omitempty"`
|
|
|
- Properties *opencost.AllocationProperties `json:"-"`
|
|
|
- Start time.Time `json:"-"`
|
|
|
- End time.Time `json:"-"`
|
|
|
- CPUAllocationHourlyAverage float64 `json:"cpuAllocationAverage"`
|
|
|
- CPUAllocationVectors []*util.Vector `json:"-"`
|
|
|
- CPUAllocationTotal float64 `json:"-"`
|
|
|
- CPUCost float64 `json:"cpuCost"`
|
|
|
- CPUCostVector []*util.Vector `json:"cpuCostVector,omitempty"`
|
|
|
- CPUEfficiency float64 `json:"cpuEfficiency"`
|
|
|
- CPURequestedVectors []*util.Vector `json:"-"`
|
|
|
- CPUUsedVectors []*util.Vector `json:"-"`
|
|
|
- Efficiency float64 `json:"efficiency"`
|
|
|
- GPUAllocationHourlyAverage float64 `json:"gpuAllocationAverage"`
|
|
|
- GPUAllocationVectors []*util.Vector `json:"-"`
|
|
|
- GPUCost float64 `json:"gpuCost"`
|
|
|
- GPUCostVector []*util.Vector `json:"gpuCostVector,omitempty"`
|
|
|
- GPUAllocationTotal float64 `json:"-"`
|
|
|
- RAMAllocationHourlyAverage float64 `json:"ramAllocationAverage"`
|
|
|
- RAMAllocationVectors []*util.Vector `json:"-"`
|
|
|
- RAMAllocationTotal float64 `json:"-"`
|
|
|
- RAMCost float64 `json:"ramCost"`
|
|
|
- RAMCostVector []*util.Vector `json:"ramCostVector,omitempty"`
|
|
|
- RAMEfficiency float64 `json:"ramEfficiency"`
|
|
|
- RAMRequestedVectors []*util.Vector `json:"-"`
|
|
|
- RAMUsedVectors []*util.Vector `json:"-"`
|
|
|
- PVAllocationHourlyAverage float64 `json:"pvAllocationAverage"`
|
|
|
- PVAllocationVectors []*util.Vector `json:"-"`
|
|
|
- PVAllocationTotal float64 `json:"-"`
|
|
|
- PVCost float64 `json:"pvCost"`
|
|
|
- PVCostVector []*util.Vector `json:"pvCostVector,omitempty"`
|
|
|
- NetworkCost float64 `json:"networkCost"`
|
|
|
- NetworkCostVector []*util.Vector `json:"networkCostVector,omitempty"`
|
|
|
- SharedCost float64 `json:"sharedCost"`
|
|
|
- TotalCost float64 `json:"totalCost"`
|
|
|
- TotalCostVector []*util.Vector `json:"totalCostVector,omitempty"`
|
|
|
-}
|
|
|
-
|
|
|
-// TotalHours determines the amount of hours the Aggregation covers, as a
|
|
|
-// function of the cost vectors and the resolution of those vectors' data
|
|
|
-func (a *Aggregation) TotalHours(resolutionHours float64) float64 {
|
|
|
- length := 1
|
|
|
-
|
|
|
- if length < len(a.CPUCostVector) {
|
|
|
- length = len(a.CPUCostVector)
|
|
|
- }
|
|
|
- if length < len(a.RAMCostVector) {
|
|
|
- length = len(a.RAMCostVector)
|
|
|
- }
|
|
|
- if length < len(a.PVCostVector) {
|
|
|
- length = len(a.PVCostVector)
|
|
|
- }
|
|
|
- if length < len(a.GPUCostVector) {
|
|
|
- length = len(a.GPUCostVector)
|
|
|
- }
|
|
|
- if length < len(a.NetworkCostVector) {
|
|
|
- length = len(a.NetworkCostVector)
|
|
|
- }
|
|
|
-
|
|
|
- return float64(length) * resolutionHours
|
|
|
-}
|
|
|
-
|
|
|
-// RateCoefficient computes the coefficient by which the total cost needs to be
|
|
|
-// multiplied in order to convert totals costs into per-rate costs.
|
|
|
-func (a *Aggregation) RateCoefficient(rateStr string, resolutionHours float64) float64 {
|
|
|
- // monthly rate = (730.0)*(total cost)/(total hours)
|
|
|
- // daily rate = (24.0)*(total cost)/(total hours)
|
|
|
- // hourly rate = (1.0)*(total cost)/(total hours)
|
|
|
-
|
|
|
- // default to hourly rate
|
|
|
- coeff := 1.0
|
|
|
- switch rateStr {
|
|
|
- case "daily":
|
|
|
- coeff = timeutil.HoursPerDay
|
|
|
- case "monthly":
|
|
|
- coeff = timeutil.HoursPerMonth
|
|
|
- }
|
|
|
-
|
|
|
- return coeff / a.TotalHours(resolutionHours)
|
|
|
-}
|
|
|
-
|
|
|
-type SharedResourceInfo struct {
|
|
|
- ShareResources bool
|
|
|
- SharedNamespace map[string]bool
|
|
|
- LabelSelectors map[string]map[string]bool
|
|
|
-}
|
|
|
-
|
|
|
-type SharedCostInfo struct {
|
|
|
- Name string
|
|
|
- Cost float64
|
|
|
- ShareType string
|
|
|
-}
|
|
|
-
|
|
|
-func (s *SharedResourceInfo) IsSharedResource(costDatum *CostData) bool {
|
|
|
- // exists in a shared namespace
|
|
|
- if _, ok := s.SharedNamespace[costDatum.Namespace]; ok {
|
|
|
- return true
|
|
|
- }
|
|
|
- // has at least one shared label (OR, not AND in the case of multiple labels)
|
|
|
- for labelName, labelValues := range s.LabelSelectors {
|
|
|
- if val, ok := costDatum.Labels[labelName]; ok && labelValues[val] {
|
|
|
- return true
|
|
|
- }
|
|
|
- }
|
|
|
- return false
|
|
|
-}
|
|
|
-
|
|
|
-func NewSharedResourceInfo(shareResources bool, sharedNamespaces []string, labelNames []string, labelValues []string) *SharedResourceInfo {
|
|
|
- sr := &SharedResourceInfo{
|
|
|
- ShareResources: shareResources,
|
|
|
- SharedNamespace: make(map[string]bool),
|
|
|
- LabelSelectors: make(map[string]map[string]bool),
|
|
|
- }
|
|
|
-
|
|
|
- for _, ns := range sharedNamespaces {
|
|
|
- sr.SharedNamespace[strings.Trim(ns, " ")] = true
|
|
|
- }
|
|
|
-
|
|
|
- // Creating a map of label name to label value, but only if
|
|
|
- // the cardinality matches
|
|
|
- if len(labelNames) == len(labelValues) {
|
|
|
- for i := range labelNames {
|
|
|
- cleanedLname := promutil.SanitizeLabelName(strings.Trim(labelNames[i], " "))
|
|
|
- if values, ok := sr.LabelSelectors[cleanedLname]; ok {
|
|
|
- values[strings.Trim(labelValues[i], " ")] = true
|
|
|
- } else {
|
|
|
- sr.LabelSelectors[cleanedLname] = map[string]bool{strings.Trim(labelValues[i], " "): true}
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return sr
|
|
|
-}
|
|
|
-
|
|
|
-func GetTotalContainerCost(costData map[string]*CostData, rate string, cp models.Provider, discount float64, customDiscount float64, idleCoefficients map[string]float64) float64 {
|
|
|
- totalContainerCost := 0.0
|
|
|
- for _, costDatum := range costData {
|
|
|
- clusterID := costDatum.ClusterID
|
|
|
- cpuv, ramv, gpuv, pvvs, netv := getPriceVectors(cp, costDatum, discount, customDiscount, idleCoefficients[clusterID])
|
|
|
- totalContainerCost += totalVectors(cpuv)
|
|
|
- totalContainerCost += totalVectors(ramv)
|
|
|
- totalContainerCost += totalVectors(gpuv)
|
|
|
- for _, pv := range pvvs {
|
|
|
- totalContainerCost += totalVectors(pv)
|
|
|
- }
|
|
|
- totalContainerCost += totalVectors(netv)
|
|
|
- }
|
|
|
- return totalContainerCost
|
|
|
-}
|
|
|
-
|
|
|
-func (a *Accesses) ComputeIdleCoefficient(costData map[string]*CostData, discount float64, customDiscount float64, window, offset time.Duration) (map[string]float64, error) {
|
|
|
- coefficients := make(map[string]float64)
|
|
|
-
|
|
|
- profileName := "ComputeIdleCoefficient: ComputeClusterCosts"
|
|
|
- profileStart := time.Now()
|
|
|
-
|
|
|
- var clusterCosts map[string]*ClusterCosts
|
|
|
- var err error
|
|
|
- fmtWindow, fmtOffset := timeutil.DurationOffsetStrings(window, offset)
|
|
|
- key := fmt.Sprintf("%s:%s", fmtWindow, fmtOffset)
|
|
|
- if data, valid := a.ClusterCostsCache.Get(key); valid {
|
|
|
- clusterCosts = data.(map[string]*ClusterCosts)
|
|
|
- } else {
|
|
|
- clusterCosts, err = a.ComputeClusterCosts(a.DataSource, a.CloudProvider, window, offset, false)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- measureTime(profileStart, profileThreshold, profileName)
|
|
|
-
|
|
|
- for cid, costs := range clusterCosts {
|
|
|
- if costs.CPUCumulative == 0 && costs.RAMCumulative == 0 && costs.StorageCumulative == 0 {
|
|
|
- log.Warnf("No ClusterCosts data for cluster '%s'. Is it emitting data?", cid)
|
|
|
- coefficients[cid] = 1.0
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- if costs.TotalCumulative == 0 {
|
|
|
- return nil, fmt.Errorf("TotalCumulative cluster cost for cluster '%s' returned 0 over window '%s' offset '%s'", cid, fmtWindow, fmtOffset)
|
|
|
- }
|
|
|
-
|
|
|
- totalContainerCost := 0.0
|
|
|
- for _, costDatum := range costData {
|
|
|
- if costDatum.ClusterID == cid {
|
|
|
- cpuv, ramv, gpuv, pvvs, _ := getPriceVectors(a.CloudProvider, costDatum, discount, customDiscount, 1)
|
|
|
- totalContainerCost += totalVectors(cpuv)
|
|
|
- totalContainerCost += totalVectors(ramv)
|
|
|
- totalContainerCost += totalVectors(gpuv)
|
|
|
- for _, pv := range pvvs {
|
|
|
- totalContainerCost += totalVectors(pv)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- coeff := totalContainerCost / costs.TotalCumulative
|
|
|
- coefficients[cid] = coeff
|
|
|
- }
|
|
|
-
|
|
|
- return coefficients, nil
|
|
|
-}
|
|
|
-
|
|
|
-// AggregationOptions provides optional parameters to AggregateCostData, allowing callers to perform more complex operations
|
|
|
-type AggregationOptions struct {
|
|
|
- Discount float64 // percent by which to discount CPU, RAM, and GPU cost
|
|
|
- CustomDiscount float64 // additional custom discount applied to all prices
|
|
|
- IdleCoefficients map[string]float64 // scales costs by amount of idle resources on a per-cluster basis
|
|
|
- IncludeEfficiency bool // set to true to receive efficiency/usage data
|
|
|
- IncludeTimeSeries bool // set to true to receive time series data
|
|
|
- Rate string // set to "hourly", "daily", or "monthly" to receive cost rate, rather than cumulative cost
|
|
|
- ResolutionHours float64
|
|
|
- SharedResourceInfo *SharedResourceInfo
|
|
|
- SharedCosts map[string]*SharedCostInfo
|
|
|
- FilteredContainerCount int
|
|
|
- FilteredEnvironments map[string]int
|
|
|
- SharedSplit string
|
|
|
- TotalContainerCost float64
|
|
|
-}
|
|
|
-
|
|
|
-// Returns the blended discounts applied to the node as a result of global discounts and reserved instance
|
|
|
-// discounts
|
|
|
-func getDiscounts(costDatum *CostData, cpuCost float64, ramCost float64, discount float64) (float64, float64) {
|
|
|
- if costDatum.NodeData == nil {
|
|
|
- return discount, discount
|
|
|
- }
|
|
|
- if costDatum.NodeData.IsSpot() {
|
|
|
- return 0, 0
|
|
|
- }
|
|
|
-
|
|
|
- reserved := costDatum.NodeData.Reserved
|
|
|
-
|
|
|
- // blended discounts
|
|
|
- blendedCPUDiscount := discount
|
|
|
- blendedRAMDiscount := discount
|
|
|
-
|
|
|
- if reserved != nil && reserved.CPUCost > 0 && reserved.RAMCost > 0 {
|
|
|
- reservedCPUDiscount := 0.0
|
|
|
- if cpuCost == 0 {
|
|
|
- log.Warnf("No cpu cost found for cluster '%s' node '%s'", costDatum.ClusterID, costDatum.NodeName)
|
|
|
- } else {
|
|
|
- reservedCPUDiscount = 1.0 - (reserved.CPUCost / cpuCost)
|
|
|
- }
|
|
|
- reservedRAMDiscount := 0.0
|
|
|
- if ramCost == 0 {
|
|
|
- log.Warnf("No ram cost found for cluster '%s' node '%s'", costDatum.ClusterID, costDatum.NodeName)
|
|
|
- } else {
|
|
|
- reservedRAMDiscount = 1.0 - (reserved.RAMCost / ramCost)
|
|
|
- }
|
|
|
-
|
|
|
- // AWS passes the # of reserved CPU and RAM as -1 to represent "All"
|
|
|
- if reserved.ReservedCPU < 0 && reserved.ReservedRAM < 0 {
|
|
|
- blendedCPUDiscount = reservedCPUDiscount
|
|
|
- blendedRAMDiscount = reservedRAMDiscount
|
|
|
- } else {
|
|
|
- nodeCPU, ierr := strconv.ParseInt(costDatum.NodeData.VCPU, 10, 64)
|
|
|
- nodeRAM, ferr := strconv.ParseFloat(costDatum.NodeData.RAMBytes, 64)
|
|
|
- if ierr == nil && ferr == nil {
|
|
|
- nodeRAMGB := nodeRAM / 1024 / 1024 / 1024
|
|
|
- reservedRAMGB := float64(reserved.ReservedRAM) / 1024 / 1024 / 1024
|
|
|
- nonReservedCPU := nodeCPU - reserved.ReservedCPU
|
|
|
- nonReservedRAM := nodeRAMGB - reservedRAMGB
|
|
|
-
|
|
|
- if nonReservedCPU == 0 {
|
|
|
- blendedCPUDiscount = reservedCPUDiscount
|
|
|
- } else {
|
|
|
- if nodeCPU == 0 {
|
|
|
- log.Warnf("No ram found for cluster '%s' node '%s'", costDatum.ClusterID, costDatum.NodeName)
|
|
|
- } else {
|
|
|
- blendedCPUDiscount = (float64(reserved.ReservedCPU) * reservedCPUDiscount) + (float64(nonReservedCPU)*discount)/float64(nodeCPU)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if nonReservedRAM == 0 {
|
|
|
- blendedRAMDiscount = reservedRAMDiscount
|
|
|
- } else {
|
|
|
- if nodeRAMGB == 0 {
|
|
|
- log.Warnf("No ram found for cluster '%s' node '%s'", costDatum.ClusterID, costDatum.NodeName)
|
|
|
- } else {
|
|
|
- blendedRAMDiscount = (reservedRAMGB * reservedRAMDiscount) + (nonReservedRAM*discount)/nodeRAMGB
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return blendedCPUDiscount, blendedRAMDiscount
|
|
|
-}
|
|
|
-
|
|
|
-func parseVectorPricing(cfg *models.CustomPricing, cpuCostStr, ramCostStr, gpuCostStr, pvCostStr string) (float64, float64, float64, float64, bool) {
|
|
|
- usesCustom := false
|
|
|
- cpuCost, err := strconv.ParseFloat(cpuCostStr, 64)
|
|
|
- if err != nil || math.IsNaN(cpuCost) || math.IsInf(cpuCost, 0) || cpuCost == 0 {
|
|
|
- cpuCost, err = strconv.ParseFloat(cfg.CPU, 64)
|
|
|
- usesCustom = true
|
|
|
- if err != nil || math.IsNaN(cpuCost) || math.IsInf(cpuCost, 0) {
|
|
|
- cpuCost = 0
|
|
|
- }
|
|
|
- }
|
|
|
- ramCost, err := strconv.ParseFloat(ramCostStr, 64)
|
|
|
- if err != nil || math.IsNaN(ramCost) || math.IsInf(ramCost, 0) || ramCost == 0 {
|
|
|
- ramCost, err = strconv.ParseFloat(cfg.RAM, 64)
|
|
|
- usesCustom = true
|
|
|
- if err != nil || math.IsNaN(ramCost) || math.IsInf(ramCost, 0) {
|
|
|
- ramCost = 0
|
|
|
- }
|
|
|
- }
|
|
|
- gpuCost, err := strconv.ParseFloat(gpuCostStr, 64)
|
|
|
- if err != nil || math.IsNaN(gpuCost) || math.IsInf(gpuCost, 0) {
|
|
|
- gpuCost, err = strconv.ParseFloat(cfg.GPU, 64)
|
|
|
- if err != nil || math.IsNaN(gpuCost) || math.IsInf(gpuCost, 0) {
|
|
|
- gpuCost = 0
|
|
|
- }
|
|
|
- }
|
|
|
- pvCost, err := strconv.ParseFloat(pvCostStr, 64)
|
|
|
- if err != nil || math.IsNaN(cpuCost) || math.IsInf(cpuCost, 0) {
|
|
|
- pvCost, err = strconv.ParseFloat(cfg.Storage, 64)
|
|
|
- if err != nil || math.IsNaN(pvCost) || math.IsInf(pvCost, 0) {
|
|
|
- pvCost = 0
|
|
|
- }
|
|
|
- }
|
|
|
- return cpuCost, ramCost, gpuCost, pvCost, usesCustom
|
|
|
-}
|
|
|
-
|
|
|
-func getPriceVectors(cp models.Provider, costDatum *CostData, discount float64, customDiscount float64, idleCoefficient float64) ([]*util.Vector, []*util.Vector, []*util.Vector, [][]*util.Vector, []*util.Vector) {
|
|
|
-
|
|
|
- var cpuCost float64
|
|
|
- var ramCost float64
|
|
|
- var gpuCost float64
|
|
|
- var pvCost float64
|
|
|
- var usesCustom bool
|
|
|
-
|
|
|
- // If custom pricing is enabled and can be retrieved, replace
|
|
|
- // default cost values with custom values
|
|
|
- customPricing, err := cp.GetConfig()
|
|
|
- if err != nil {
|
|
|
- log.Errorf("failed to load custom pricing: %s", err)
|
|
|
- }
|
|
|
- if provider.CustomPricesEnabled(cp) && err == nil {
|
|
|
- var cpuCostStr string
|
|
|
- var ramCostStr string
|
|
|
- var gpuCostStr string
|
|
|
- var pvCostStr string
|
|
|
- if costDatum.NodeData.IsSpot() {
|
|
|
- cpuCostStr = customPricing.SpotCPU
|
|
|
- ramCostStr = customPricing.SpotRAM
|
|
|
- gpuCostStr = customPricing.SpotGPU
|
|
|
- } else {
|
|
|
- cpuCostStr = customPricing.CPU
|
|
|
- ramCostStr = customPricing.RAM
|
|
|
- gpuCostStr = customPricing.GPU
|
|
|
- }
|
|
|
- pvCostStr = customPricing.Storage
|
|
|
- cpuCost, ramCost, gpuCost, pvCost, usesCustom = parseVectorPricing(customPricing, cpuCostStr, ramCostStr, gpuCostStr, pvCostStr)
|
|
|
- } else if costDatum.NodeData == nil && err == nil {
|
|
|
- cpuCostStr := customPricing.CPU
|
|
|
- ramCostStr := customPricing.RAM
|
|
|
- gpuCostStr := customPricing.GPU
|
|
|
- pvCostStr := customPricing.Storage
|
|
|
- cpuCost, ramCost, gpuCost, pvCost, usesCustom = parseVectorPricing(customPricing, cpuCostStr, ramCostStr, gpuCostStr, pvCostStr)
|
|
|
- } else {
|
|
|
- cpuCostStr := costDatum.NodeData.VCPUCost
|
|
|
- ramCostStr := costDatum.NodeData.RAMCost
|
|
|
- gpuCostStr := costDatum.NodeData.GPUCost
|
|
|
- pvCostStr := costDatum.NodeData.StorageCost
|
|
|
- cpuCost, ramCost, gpuCost, pvCost, usesCustom = parseVectorPricing(customPricing, cpuCostStr, ramCostStr, gpuCostStr, pvCostStr)
|
|
|
- }
|
|
|
-
|
|
|
- if usesCustom {
|
|
|
- log.DedupedWarningf(5, "No pricing data found for node `%s` , using custom pricing", costDatum.NodeName)
|
|
|
- }
|
|
|
-
|
|
|
- cpuDiscount, ramDiscount := getDiscounts(costDatum, cpuCost, ramCost, discount)
|
|
|
-
|
|
|
- log.Debugf("Node Name: %s", costDatum.NodeName)
|
|
|
- log.Debugf("Blended CPU Discount: %f", cpuDiscount)
|
|
|
- log.Debugf("Blended RAM Discount: %f", ramDiscount)
|
|
|
-
|
|
|
- // TODO should we try to apply the rate coefficient here or leave it as a totals-only metric?
|
|
|
- rateCoeff := 1.0
|
|
|
-
|
|
|
- if idleCoefficient == 0 {
|
|
|
- idleCoefficient = 1.0
|
|
|
- }
|
|
|
-
|
|
|
- cpuv := make([]*util.Vector, 0, len(costDatum.CPUAllocation))
|
|
|
- for _, val := range costDatum.CPUAllocation {
|
|
|
- cpuv = append(cpuv, &util.Vector{
|
|
|
- Timestamp: math.Round(val.Timestamp/10) * 10,
|
|
|
- Value: (val.Value * cpuCost * (1 - cpuDiscount) * (1 - customDiscount) / idleCoefficient) * rateCoeff,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- ramv := make([]*util.Vector, 0, len(costDatum.RAMAllocation))
|
|
|
- for _, val := range costDatum.RAMAllocation {
|
|
|
- ramv = append(ramv, &util.Vector{
|
|
|
- Timestamp: math.Round(val.Timestamp/10) * 10,
|
|
|
- Value: ((val.Value / 1024 / 1024 / 1024) * ramCost * (1 - ramDiscount) * (1 - customDiscount) / idleCoefficient) * rateCoeff,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- gpuv := make([]*util.Vector, 0, len(costDatum.GPUReq))
|
|
|
- for _, val := range costDatum.GPUReq {
|
|
|
- gpuv = append(gpuv, &util.Vector{
|
|
|
- Timestamp: math.Round(val.Timestamp/10) * 10,
|
|
|
- Value: (val.Value * gpuCost * (1 - discount) * (1 - customDiscount) / idleCoefficient) * rateCoeff,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- pvvs := make([][]*util.Vector, 0, len(costDatum.PVCData))
|
|
|
- for _, pvcData := range costDatum.PVCData {
|
|
|
- pvv := make([]*util.Vector, 0, len(pvcData.Values))
|
|
|
- if pvcData.Volume != nil {
|
|
|
- cost, _ := strconv.ParseFloat(pvcData.Volume.Cost, 64)
|
|
|
-
|
|
|
- // override with custom pricing if enabled
|
|
|
- if provider.CustomPricesEnabled(cp) {
|
|
|
- cost = pvCost
|
|
|
- }
|
|
|
-
|
|
|
- for _, val := range pvcData.Values {
|
|
|
- pvv = append(pvv, &util.Vector{
|
|
|
- Timestamp: math.Round(val.Timestamp/10) * 10,
|
|
|
- Value: ((val.Value / 1024 / 1024 / 1024) * cost * (1 - customDiscount) / idleCoefficient) * rateCoeff,
|
|
|
- })
|
|
|
- }
|
|
|
- pvvs = append(pvvs, pvv)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- netv := make([]*util.Vector, 0, len(costDatum.NetworkData))
|
|
|
- for _, val := range costDatum.NetworkData {
|
|
|
- netv = append(netv, &util.Vector{
|
|
|
- Timestamp: math.Round(val.Timestamp/10) * 10,
|
|
|
- Value: val.Value,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- return cpuv, ramv, gpuv, pvvs, netv
|
|
|
-}
|
|
|
-
|
|
|
-func totalVectors(vectors []*util.Vector) float64 {
|
|
|
- total := 0.0
|
|
|
- for _, vector := range vectors {
|
|
|
- total += vector.Value
|
|
|
- }
|
|
|
- return total
|
|
|
-}
|
|
|
-
|
|
|
-// EmptyDataError describes an error caused by empty cost data for some
|
|
|
-// defined interval
|
|
|
-type EmptyDataError struct {
|
|
|
- err error
|
|
|
- window opencost.Window
|
|
|
-}
|
|
|
-
|
|
|
-// Error implements the error interface
|
|
|
-func (ede *EmptyDataError) Error() string {
|
|
|
- err := fmt.Sprintf("empty data for range: %s", ede.window)
|
|
|
- if ede.err != nil {
|
|
|
- err += fmt.Sprintf(": %s", ede.err)
|
|
|
- }
|
|
|
- return err
|
|
|
-}
|
|
|
-
|
|
|
-// ScaleHourlyCostData converts per-hour cost data to per-resolution data. If the target resolution is higher (i.e. < 1.0h)
|
|
|
-// then we can do simple multiplication by the fraction-of-an-hour and retain accuracy. If the target resolution is
|
|
|
-// lower (i.e. > 1.0h) then we sum groups of hourly data by resolution to maintain fidelity.
|
|
|
-// e.g. (100 hours of per-hour hourly data, resolutionHours=10) => 10 data points, grouped and summed by 10-hour window
|
|
|
-// e.g. (20 minutes of per-minute hourly data, resolutionHours=1/60) => 20 data points, scaled down by a factor of 60
|
|
|
-func ScaleHourlyCostData(data map[string]*CostData, resolutionHours float64) map[string]*CostData {
|
|
|
- scaled := map[string]*CostData{}
|
|
|
-
|
|
|
- for key, datum := range data {
|
|
|
- datum.RAMReq = scaleVectorSeries(datum.RAMReq, resolutionHours)
|
|
|
- datum.RAMUsed = scaleVectorSeries(datum.RAMUsed, resolutionHours)
|
|
|
- datum.RAMAllocation = scaleVectorSeries(datum.RAMAllocation, resolutionHours)
|
|
|
- datum.CPUReq = scaleVectorSeries(datum.CPUReq, resolutionHours)
|
|
|
- datum.CPUUsed = scaleVectorSeries(datum.CPUUsed, resolutionHours)
|
|
|
- datum.CPUAllocation = scaleVectorSeries(datum.CPUAllocation, resolutionHours)
|
|
|
- datum.GPUReq = scaleVectorSeries(datum.GPUReq, resolutionHours)
|
|
|
- datum.NetworkData = scaleVectorSeries(datum.NetworkData, resolutionHours)
|
|
|
-
|
|
|
- for _, pvcDatum := range datum.PVCData {
|
|
|
- pvcDatum.Values = scaleVectorSeries(pvcDatum.Values, resolutionHours)
|
|
|
- }
|
|
|
-
|
|
|
- scaled[key] = datum
|
|
|
- }
|
|
|
-
|
|
|
- return scaled
|
|
|
-}
|
|
|
-
|
|
|
-func scaleVectorSeries(vs []*util.Vector, resolutionHours float64) []*util.Vector {
|
|
|
- // if scaling to a lower resolution, compress the hourly data for maximum accuracy
|
|
|
- if resolutionHours > 1.0 {
|
|
|
- return compressVectorSeries(vs, resolutionHours)
|
|
|
- }
|
|
|
-
|
|
|
- // if scaling to a higher resolution, simply scale each value down by the fraction of an hour
|
|
|
- for _, v := range vs {
|
|
|
- v.Value *= resolutionHours
|
|
|
- }
|
|
|
- return vs
|
|
|
-}
|
|
|
-
|
|
|
-func compressVectorSeries(vs []*util.Vector, resolutionHours float64) []*util.Vector {
|
|
|
- if len(vs) == 0 {
|
|
|
- return vs
|
|
|
- }
|
|
|
-
|
|
|
- compressed := []*util.Vector{}
|
|
|
-
|
|
|
- threshold := float64(60 * 60 * resolutionHours)
|
|
|
- var acc *util.Vector
|
|
|
-
|
|
|
- for i, v := range vs {
|
|
|
- if acc == nil {
|
|
|
- // start a new accumulation from current datum
|
|
|
- acc = &util.Vector{
|
|
|
- Value: vs[i].Value,
|
|
|
- Timestamp: vs[i].Timestamp,
|
|
|
- }
|
|
|
- continue
|
|
|
- }
|
|
|
- if v.Timestamp-acc.Timestamp < threshold {
|
|
|
- // v should be accumulated in current datum
|
|
|
- acc.Value += v.Value
|
|
|
- } else {
|
|
|
- // v falls outside current datum's threshold; append and start a new one
|
|
|
- compressed = append(compressed, acc)
|
|
|
- acc = &util.Vector{
|
|
|
- Value: vs[i].Value,
|
|
|
- Timestamp: vs[i].Timestamp,
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- // append any remaining, incomplete accumulation
|
|
|
- if acc != nil {
|
|
|
- compressed = append(compressed, acc)
|
|
|
- }
|
|
|
-
|
|
|
- return compressed
|
|
|
-}
|
|
|
-
|
|
|
-// ScaleAggregationTimeSeries reverses the scaling done by ScaleHourlyCostData, returning
|
|
|
-// the aggregation's time series to hourly data.
|
|
|
-func ScaleAggregationTimeSeries(aggregation *Aggregation, resolutionHours float64) {
|
|
|
- for _, v := range aggregation.CPUCostVector {
|
|
|
- v.Value /= resolutionHours
|
|
|
- }
|
|
|
-
|
|
|
- for _, v := range aggregation.GPUCostVector {
|
|
|
- v.Value /= resolutionHours
|
|
|
- }
|
|
|
-
|
|
|
- for _, v := range aggregation.RAMCostVector {
|
|
|
- v.Value /= resolutionHours
|
|
|
- }
|
|
|
-
|
|
|
- for _, v := range aggregation.PVCostVector {
|
|
|
- v.Value /= resolutionHours
|
|
|
- }
|
|
|
-
|
|
|
- for _, v := range aggregation.NetworkCostVector {
|
|
|
- v.Value /= resolutionHours
|
|
|
- }
|
|
|
-
|
|
|
- for _, v := range aggregation.TotalCostVector {
|
|
|
- v.Value /= resolutionHours
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// String returns a string representation of the encapsulated shared resources, which
|
|
|
-// can be used to uniquely identify a set of shared resources. Sorting sets of shared
|
|
|
-// resources ensures that strings representing permutations of the same combination match.
|
|
|
-func (s *SharedResourceInfo) String() string {
|
|
|
- if s == nil {
|
|
|
- return ""
|
|
|
- }
|
|
|
-
|
|
|
- nss := []string{}
|
|
|
- for ns := range s.SharedNamespace {
|
|
|
- nss = append(nss, ns)
|
|
|
- }
|
|
|
- sort.Strings(nss)
|
|
|
- nsStr := strings.Join(nss, ",")
|
|
|
-
|
|
|
- labels := []string{}
|
|
|
- for lbl, vals := range s.LabelSelectors {
|
|
|
- for val := range vals {
|
|
|
- if lbl != "" && val != "" {
|
|
|
- labels = append(labels, fmt.Sprintf("%s=%s", lbl, val))
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- sort.Strings(labels)
|
|
|
- labelStr := strings.Join(labels, ",")
|
|
|
-
|
|
|
- return fmt.Sprintf("%s:%s", nsStr, labelStr)
|
|
|
-}
|
|
|
-
|
|
|
// ParseAggregationProperties attempts to parse and return aggregation properties
|
|
|
// encoded under the given key. If none exist, or if parsing fails, an error
|
|
|
// is returned with empty AllocationProperties.
|