package dbManager import ( "encoding/json" "errors" "fmt" "math" "reflect" "strconv" "strings" "time" "github.com/go-openapi/strfmt" "github.com/prometheus/client_golang/prometheus" datamodels "gitlab.com/cyclops-utilities/datamodels" "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/cdr/models" "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/cdr/server/cacheManager" cusModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/customerdb/models" pmModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/plan-manager/models" udrModels "github.com/Cyclops-Labs/cyclops-4-hpc.git/services/udr/models" l "gitlab.com/cyclops-utilities/logging" "gorm.io/driver/postgres" "gorm.io/gorm" ) const ( scaler = 1e9 statusDuplicated = iota statusFail statusMissing statusOK ) var ( totalTime float64 totalCount int64 ) // DbParameter is the struct defined to group and contain all the methods // that interact with the database. // On it there is the following parameters: // - Cache: CacheManager pointer for the cache mechanism. // - connStr: strings with the connection information to the database. // - Db: a gorm.DB pointer to the db to invoke all the db methods. type DbParameter struct { Cache *cacheManager.CacheManager connStr string Db *gorm.DB Metrics map[string]*prometheus.GaugeVec Pipe chan interface{} } // New is the function to create the struct DbParameter. // Parameters: // - dbConn: strings with the connection information to the database. // - tables: array of interfaces that will contains the models to migrate // to the database on initialization. // Returns: // - DbParameter: struct to interact with dbManager functionalities. func New(dbConn string, tables ...interface{}) *DbParameter { l.Trace.Printf("[DB] Gerenating new DBParameter.\n") var ( dp DbParameter err error ) dp.connStr = dbConn dp.Db, err = gorm.Open(postgres.Open(dbConn), &gorm.Config{}) if err != nil { l.Error.Printf("[DB] Error opening connection. Error: %v\n", err) } l.Trace.Printf("[DB] Migrating tables.\n") //Database migration, it handles everything dp.Db.AutoMigrate(tables...) //l.Trace.Printf("[DB] Generating hypertables.\n") // Hypertables creation for timescaledb in case of needed //dp.Db.Exec("SELECT create_hypertable('" + dp.Db.NewScope(&models.TABLE).TableName() + "', 'TIMESCALE-ROW-INDEX');") return &dp } // AddRecord job is to save in the system the provided CDR report to be easy to // retrieve later with the usage endpoints. // Parameters: // - cdr: the CDR report model to be added to the system. // Returns: // - e: error in case of duplication or failure in the task. func (d *DbParameter) AddRecord(cdr models.CReport) (e error) { l.Trace.Printf("[DB] Attempting to add a new cost report to the system.\n") if r := d.Db.Where(&cdr).First(&models.CReport{}).Error; errors.Is(r, gorm.ErrRecordNotFound) { e = d.Db.Create(&cdr).Error if e != nil { l.Trace.Printf("[DB] Something went wrong when adding a new usage report to the system. Error: %v\n", e) } else { d.Metrics["count"].With(prometheus.Labels{"type": "CDR Reports added"}).Inc() l.Trace.Printf("[DB] New cost report added to the system.\n") } } else { l.Warning.Printf("[DB] Cost report already in the system, skipping creation...\n") } l.Trace.Printf("[DB] Attempting to add CDR Records from reported account [ %v ] to the system.\n", cdr.AccountID) for i := range cdr.Usage { var cdrrecord, cdrUpdate models.CDRRecord cdrrecord.AccountID = cdr.AccountID cdrrecord.Metadata = cdr.Usage[i].Metadata cdrrecord.ResourceID = cdr.Usage[i].ResourceID cdrrecord.ResourceName = cdr.Usage[i].ResourceName cdrrecord.ResourceType = cdr.Usage[i].ResourceType cdrrecord.TimeFrom = cdr.TimeFrom cdrrecord.TimeTo = cdr.TimeTo cdrrecord.Unit = cdr.Usage[i].Unit if r := d.Db.Where(&cdrrecord).First(&cdrUpdate).Error; errors.Is(r, gorm.ErrRecordNotFound) { cdrrecord.Cost = cdr.Usage[i].Cost cdrrecord.UsageBreakup = cdr.Usage[i].UsageBreakup if e = d.Db.Create(&cdrrecord).Error; e != nil { l.Warning.Printf("[DB] Something went wrong when adding a new CDR Record to the system. Error: %v\n", e) } else { d.Metrics["count"].With(prometheus.Labels{"type": "CDR Records added"}).Inc() l.Trace.Printf("[DB] New CDR Record added in the system.\n") } } else { l.Trace.Printf("[DB] CDR record already in the system, updating...\n") cdrrecord.Cost = cdr.Usage[i].Cost cdrrecord.UsageBreakup = cdr.Usage[i].UsageBreakup if e = d.Db.Model(&cdrUpdate).Updates(&cdrrecord).Error; e == nil { d.Metrics["count"].With(prometheus.Labels{"type": "CDR Records updated"}).Inc() l.Trace.Printf("[DB] CDR Record updated in the system.\n") } else { l.Trace.Printf("[DB] Something went wrong when updating the existing CDR Record to the system. Error: %v\n", e) } } } return } // GetReport job is to retrieve the compacted usage records from the system for // the provided account in the requested time-window with the posibility of // filter by metric. // Parameters: // - id: string containing the id of the account requested. // - metric: a string with the metric to filter the records. // - from: a datatime reference for the initial border of the time-window. // - to: a datatime reference for the final border of the time-window. // Returns: // - a slice of references with the usage contained in the system within the // requested time-window. func (d *DbParameter) GetReport(id string, metric string, from strfmt.DateTime, to strfmt.DateTime) []*models.CDRReport { l.Trace.Printf("[DB] Attempting to retrieve the compacted usage records.\n") var r []*models.CDRReport var u models.CDRRecord window := d.getWindow(from, to) if id != "" { u.AccountID = id } if metric != "" { u.ResourceType = metric } if e := d.Db.Where(window).Where(&u).Find(&models.CDRRecord{}).Scan(&r).Error; e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the compacted usage records from the system. Error: %v\n", e) } else { l.Debug.Printf("[DB] [ %v ] compacted usage records retrieved from the system.\n", len(r)) } return r } // GetUsage job is to retrieve the usage report of the system or the specified // account within the requested time-window with the posibility to filter by // metric. // Parameters: // - id: string containing the id of the account requested. // - metric: a string with the metric to filter the records. // - from: a datatime reference for the initial border of the time-window. // - to: a datatime reference for the final border of the time-window. // Returns: // - a slice of references with the usage report contained in the system within // the requested time-window. // - error in case of duplication or failure in the task. func (d *DbParameter) GetUsage(id, metric string, from, to strfmt.DateTime) ([]*models.CReport, error) { l.Trace.Printf("[DB] Attempting to retrieve the usage report from the system.\n") var r []*models.CReport var r0 models.CReport var e error window := d.getWindow(from, to) if id != "" { r0.AccountID = id } e = d.Db.Where(window).Where(&r0).Find(&r).Error if e == nil { for idx := range r { r[idx].Usage = d.GetReport(r[idx].AccountID, metric, r[idx].TimeFrom, r[idx].TimeTo) } l.Debug.Printf("[DB] [ %v ] Usage reports retrieved from the system.\n", len(r)) } else { l.Warning.Printf("[DB] Something went wrong while retrieving the usage reports from the system. Error: %v\n", e) } return r, e } // GetUsages job is to retrieve the usage report of the system or the specified // account accounts within the requested time-window with the posibility to // filter by metric. // Parameters: // - ids: string containing the list of ids of the accounts requested. // - metric: a string with the metric to filter the records. // - from: a datatime reference for the initial border of the time-window. // - to: a datatime reference for the final border of the time-window. // Returns: // - a slice of references with the usage report contained in the system within // the requested time-window. // - error in case of duplication or failure in the task. func (d *DbParameter) GetUsages(ids, metric string, from, to strfmt.DateTime) ([]*models.CReport, error) { l.Trace.Printf("[DB] Attempting to retrieve the usage report from the system.\n") var total, r []*models.CReport var r0 models.CReport var e error window := d.getWindow(from, to) list := strings.Split(ids, ",") for _, acc := range list { r0.AccountID = acc if e = d.Db.Where(window).Where(&r0).Find(&r).Error; e == nil { for idx := range r { r[idx].Usage = d.GetReport(r[idx].AccountID, metric, r[idx].TimeFrom, r[idx].TimeTo) } l.Debug.Printf("[DB] [ %v ] Usage reports retrieved from the system.\n", len(r)) } else { l.Warning.Printf("[DB] Something went wrong while retrieving the usage reports from the system. Error: %v\n", e) } total = append(total, r...) } return total, e } // GetUsageSummary job is to retrieve the usage report of the system of the specified // account within the requested time-window and process it into an easy to use // estructure for the UI. // Parameters: // - id: string containing the id of the account requested. // - from: a datatime reference for the initial border of the time-window. // - to: a datatime reference for the final border of the time-window. // Returns: // - a references with the usage report contained in the system within // the requested time-window. // - error in case of duplication or failure in the task. func (d *DbParameter) GetUsageSummary(id string, from, to strfmt.DateTime) (*models.UISummary, error) { l.Trace.Printf("[DB] Attempting to retrieve the usage report from the system.\n") var usages []*models.CDRReport var report models.UISummary interval := float64(28800) //UDR are created every 8h and the usage is provided in GB*s... rounder := float64(((time.Time)(to)).Sub((time.Time)(from)).Seconds()) / interval report.AccountID = id report.TimeFrom = from report.TimeTo = to // Get the reseller data r, err := d.Cache.Get(id, "reseller", "") if err != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the reseller info for id [ %v ]. Error: %v\n", id, err) return nil, err } reseller := r.(cusModels.Reseller) // Get all the usages linked to the reseller for _, customer := range reseller.Customers { for _, product := range customer.Products { u := d.GetReport(product.ProductID, "", from, to) usages = append(usages, u...) } } // Scan the usages and make the numbers resourcesMap := make(datamodels.JSONdb) for i := range usages { masterKey := strings.ToTitle(usages[i].ResourceType) if _, exists := resourcesMap[masterKey]; !exists { resourcesMap[masterKey] = make(datamodels.JSONdb) } // First the total key := "TotalCount" if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key]; !exists { resourcesMap[masterKey].(datamodels.JSONdb)[key] = float64(0) } resourcesMap[masterKey].(datamodels.JSONdb)[key] = resourcesMap[masterKey].(datamodels.JSONdb)[key].(float64) + float64(1/rounder) // Second the regions if region, exists := usages[i].Metadata["region"]; exists { key := "Regions" if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key]; !exists { resourcesMap[masterKey].(datamodels.JSONdb)[key] = make(datamodels.JSONdb) } regionKey := region.(string) if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[regionKey]; !exists { resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[regionKey] = float64(0) } resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[regionKey] = resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[regionKey].(float64) + float64(1/rounder) } // Third the flavors in case of if flavor, exists := usages[i].Metadata["flavorname"]; exists { key := "Flavors" if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key]; !exists { resourcesMap[masterKey].(datamodels.JSONdb)[key] = make(datamodels.JSONdb) } flavorKey := flavor.(string) if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[flavorKey]; !exists { resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[flavorKey] = float64(0) } resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[flavorKey] = resourcesMap[masterKey].(datamodels.JSONdb)[key].(datamodels.JSONdb)[flavorKey].(float64) + float64(1/rounder) } // Fourth the sizes in case of if size, exists := usages[i].Metadata["size"]; exists { sizeF, err := strconv.ParseFloat(size.(string), 64) if err != nil { l.Warning.Printf("[DB] Failed to turn the size [ %v ] into a float64 value. Error: %v\n", size, err) } else { key := "TotalAmount" if _, exists := resourcesMap[key]; !exists { resourcesMap[masterKey].(datamodels.JSONdb)[key] = float64(0) } resourcesMap[masterKey].(datamodels.JSONdb)[key] = resourcesMap[masterKey].(datamodels.JSONdb)[key].(float64) + float64(sizeF/rounder) } } // UDR usages if size, exists := usages[i].UsageBreakup["used"]; exists { var sizeF float64 key := "TotalAmount" // size to float64 if k := reflect.ValueOf(size); k.Kind() == reflect.Float64 { sizeF = size.(float64) / interval } else { v, err := size.(json.Number).Float64() if err != nil { l.Warning.Printf("[DB] Failed to turn the size [ %v ] into a float64 value. Error: %v\n", size, err) } else { sizeF = float64(v) / interval } } if _, exists := resourcesMap[masterKey].(datamodels.JSONdb)[key]; !exists { resourcesMap[masterKey].(datamodels.JSONdb)[key] = float64(0) } resourcesMap[masterKey].(datamodels.JSONdb)[key] = resourcesMap[masterKey].(datamodels.JSONdb)[key].(float64) + float64(sizeF/rounder) } } // Reshape of the JSON to easy showing in the front.. for key, value := range resourcesMap { metricData := value.(datamodels.JSONdb) if _, exists := metricData["Regions"]; exists { var regions []datamodels.JSONdb for k, v := range metricData["Regions"].(datamodels.JSONdb) { region := make(datamodels.JSONdb) region["name"] = k region["amount"] = v regions = append(regions, region) } resourcesMap[key].(datamodels.JSONdb)["Regions"] = regions } if _, exists := metricData["Flavors"]; exists { var regions []datamodels.JSONdb for k, v := range metricData["Flavors"].(datamodels.JSONdb) { region := make(datamodels.JSONdb) region["name"] = k region["amount"] = v regions = append(regions, region) } resourcesMap[key].(datamodels.JSONdb)["Flavors"] = regions } } // Create the summarey report report.UsageBreakup = resourcesMap return &report, nil } // getWindow job is to select the timeframe for the usage retrievals according // to the data providad from= '%v'", d.Db.NamingStrategy.ColumnName("", "TimeFrom"), from) } else { s = fmt.Sprintf("%v >= '%v' AND %v <= '%v'", d.Db.NamingStrategy.ColumnName("", "TimeFrom"), from, d.Db.NamingStrategy.ColumnName("", "TimeTo"), to) } } else { if !((time.Time)(to)).IsZero() { s = fmt.Sprintf("%v <= '%v'", d.Db.NamingStrategy.ColumnName("", "TimeTo"), to) } } return s } // ProcessUDR job is to process the UDR Reports and turn then into CDR Reports // and store them in the system. // Parameters: // - report: UDR Report model reference to be processed. // - token: an optional keycloak token in case it's provided. // Returns: // - e: error in case of duplication or failure in the task. func (d *DbParameter) ProcessUDR(report udrModels.UReport, token string) (e error) { l.Trace.Printf("[DB] Attempting to process and transform the UDR report into a CDR report and save it to the system.\n") // First we clean all the records so we always have a clean UDR generation templateRecord := models.CDRRecord{ AccountID: report.AccountID, TimeFrom: report.TimeFrom, TimeTo: report.TimeTo, } l.Info.Printf("[DB] Clean up of previous CDRRecords started for period [ %v ] - [ %v ].\n", report.TimeFrom, report.TimeTo) if e := d.Db.Where(&templateRecord).Delete(&models.CDRRecord{}).Error; e != nil { l.Warning.Printf("[DB] The clean up of previous CDRecords failed. Error: %v\n", e) } templateReport := models.CReport{ AccountID: report.AccountID, TimeFrom: report.TimeFrom, TimeTo: report.TimeTo, } l.Info.Printf("[DB] Clean up of previous CReports started for period [ %v ] - [ %v ].\n", report.TimeFrom, report.TimeTo) if e := d.Db.Where(&templateReport).Delete(&models.CReport{}).Error; e != nil { l.Warning.Printf("[DB] The clean up of previous CReports failed. Error: %v\n", e) } // First we get the metric and accounts in the system. var planID string var cdr models.CReport var usages []*models.CDRReport now := time.Now().UnixNano() x, e := d.Cache.Get(report.AccountID, "product", token) if e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the product info for id [ %v ]. Error: %v\n", report.AccountID, e) return } product := x.(cusModels.Product) if product.PlanID != "" { planID = product.PlanID } else if product.Type != "" { planID = product.Type } else { c, e := d.Cache.Get(product.CustomerID, "customer", token) if e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the customer info for id [ %v ]. Error: %v\n", product.CustomerID, e) return e } customer := c.(cusModels.Customer) if customer.PlanID != "" { planID = customer.PlanID } else { r, e := d.Cache.Get(customer.ResellerID, "reseller", token) if e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the reseller info for id [ %v ]. Error: %v\n", customer.ResellerID, e) return e } reseller := r.(cusModels.Reseller) planID = reseller.PlanID } } PlanDefault: p, e := d.Cache.Get(planID, "plan", token) if e != nil { if planID == "DEFAULT" { l.Warning.Printf("[DB] Something went wrong while retrieving the default plan id [ %v ]. Error: %v\n", planID, e) return } l.Warning.Printf("[DB] Something went wrong while retrieving the plan id [ %v ]. Re-trying with default plan. Error: %v\n", planID, e) planID = "DEFAULT" goto PlanDefault } plan := p.(pmModels.Plan) savedPlan := p.(pmModels.Plan) // In case the plan is not valid we return to the deault plan (id 0) which is valid ad vitam if (time.Now()).After((time.Time)(*plan.OfferedEndDate)) || (time.Now()).Before((time.Time)(*plan.OfferedStartDate)) { l.Warning.Printf("[DB] The plan [ %v ] is only valid between [ %v ] and [ %v ]. Falling back to default plan.\n", plan.ID, *plan.OfferedStartDate, *plan.OfferedEndDate) planID = "DEFAULT" goto PlanDefault } s, e := d.Cache.Get("ALL", "sku", token) if e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the skus list. Error: %v\n", e) return } skus := make(map[string]string) for _, k := range s.([]*pmModels.Sku) { skus[*k.Name] = k.ID } // having the customer and the plan... // iter the report for _, u := range report.Usage { costBreakup := make(datamodels.JSONdb) var costSkuTotal []datamodels.JSONdb var cycles []*pmModels.Cycle var bundle pmModels.SkuBundle var skuDiscount, skuPrice, usage, multiplier, amount, quantity float64 // get cycle of resourceType c, e := d.Cache.Get(u.ResourceType, "cycle", token) if e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the states for the life cycle linked to the ResourceType [ %v ]. Error: %v\n", u.ResourceType, e) return e } cycles = c.([]*pmModels.Cycle) // if server, get the skubundle associated if _, exist := cycles[0].SkuList[u.ResourceType]; !exist { if id, exists := u.Metadata["flavorid"]; exists { b, e := d.Cache.Get(id, "bundle", token) if e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the skuBundle id [ %v ]. Error: %v\n", u.Metadata["flavorid"], e) return e } bundle = b.(pmModels.SkuBundle) // Usecase: flavor is a generic one but then the image is a windows base one, so the license is not included by default if id, exists := u.Metadata["imagename"]; exists && strings.Contains(strings.ToLower(id.(string)), "windows") { _, license := bundle.SkuPrices["license"] vcpu, exists := bundle.SkuPrices["vcpu"] if !license && exists { var previous pmModels.SkuBundle previous.ID = bundle.ID previous.Name = bundle.Name previous.SkuPrices = make(datamodels.JSONdb) for k, v := range bundle.SkuPrices { previous.SkuPrices[k] = v } previous.SkuPrices["license"] = vcpu bundle = previous } } } else { l.Warning.Printf("[DB] Something is wrong. Error: The flavorid is supposed to exist in the metadata.\n") continue } } // METADA OVERRIDERS if override, exists := u.Metadata["PlanOverride"]; exists && override.(bool) { overridePlan, e := d.Cache.Get("DEFAULT", "plan", token) if e != nil { l.Warning.Printf("[DB] Something went wrong while retrieving the Overriding plan. Error: %v\n", e) } else { plan = overridePlan.(pmModels.Plan) } } else { plan = savedPlan } // loop cycle for i := range cycles { // if usageBreakup(cycle.state) !exists, continue if stateUse, exists := u.UsageBreakup[*(cycles[i].State)]; !exists { continue // else, get the usage value } else { // usage to float64 if k := reflect.ValueOf(stateUse); k.Kind() == reflect.Float64 { usage = stateUse.(float64) } else { v, _ := stateUse.(json.Number).Int64() usage = float64(v) } } // and loop actual.cycle.skus for sku, value := range cycles[i].SkuList { // get the skuDiscount and skuPrice associated if id, exists := skus[sku]; exists { for j := range plan.SkuPrices { if *plan.SkuPrices[j].SkuID == id { skuDiscount = float64(plan.SkuPrices[j].Discount) skuPrice = float64(*plan.SkuPrices[j].UnitPrice) break } } } else { l.Warning.Printf("[DB] Something is wrong. Error: [ %v ] is supposed to exist in the system as a sku.\n", u.ResourceType) continue } // multiplier to float64 if k := reflect.ValueOf(value); k.Kind() == reflect.Float64 { multiplier = value.(float64) } else { v, _ := value.(json.Number).Int64() multiplier = float64(v) } // if resType not server then use actual.cycle.skus as multiplier for usage if *cycles[i].ResourceType == sku { // Size of buckets to be computed if sz, exists := u.Metadata["size"]; exists { size, e := strconv.ParseInt(sz.(string), 10, 0) if e != nil { l.Warning.Printf("[DB] Something is wrong with the size [ %v ] of the metadata. Error: %v.\n", sz, u.ResourceType) amount = usage * multiplier } else { amount = usage * multiplier * float64(size) } } else { amount = usage * multiplier } // else, resType is server } else { // check if in bundle for amounts, get bundle value times the usage and use actual.cycle.skus as an extra multiplier if q, exists := bundle.SkuPrices[sku]; !exists { continue } else { // quantity to float64 if k := reflect.ValueOf(q); k.Kind() == reflect.Float64 { quantity = q.(float64) } else { v, _ := q.(json.Number).Int64() quantity = float64(v) } amount = usage * multiplier * quantity } } // create the cost elemt costSku := make(datamodels.JSONdb) costSku["sku"] = sku costSku["sku-state"] = *cycles[i].State // get cost costSku["sku-cost"] = amount * skuPrice // get discount as % of cost costSku["sku-discount"] = costSku["sku-cost"].(float64) * skuDiscount // get the net cost as cost-discount costSku["sku-net"] = costSku["sku-cost"].(float64) - costSku["sku-discount"].(float64) // Add the cost to the collection costSkuTotal = append(costSkuTotal, costSku) } } // place the costbreakup in place costBreakup["costBreakup"] = costSkuTotal if costBreakup["costBreakup"] == nil { l.Warning.Printf("[DB] Something went wrong in the cost breakup generation. Skipping this element...\n") continue } // Let's create the total cost costBreakup["totalFromSku"] = float64(0) for _, c := range costBreakup["costBreakup"].([]datamodels.JSONdb) { costBreakup["totalFromSku"] = costBreakup["totalFromSku"].(float64) + c["sku-net"].(float64) } costBreakup["appliedDiscount"] = costBreakup["totalFromSku"].(float64) * float64(plan.Discount) costBreakup["netTotal"] = costBreakup["totalFromSku"].(float64) - costBreakup["appliedDiscount"].(float64) var use models.CDRReport use.Cost = costBreakup use.Metadata = u.Metadata use.ResourceID = u.ResourceID use.ResourceName = u.ResourceName use.ResourceType = u.ResourceType use.Unit = u.Unit use.UsageBreakup = u.UsageBreakup usages = append(usages, &use) } cdr.AccountID = report.AccountID cdr.TimeFrom = report.TimeFrom cdr.TimeTo = report.TimeTo cdr.Usage = usages if e = d.AddRecord(cdr); e != nil { l.Warning.Printf("[DB] Something went wrong while saving the CDR record in the system. Error: %v\n", e) return } l.Trace.Printf("[DB] UDR processed and transformed into CDR and saved in the system successfully .\n") d.Pipe <- cdr l.Trace.Printf("[DB] CDR transmited to the Credit Manager successfully .\n") totalTime += float64(time.Now().UnixNano() - now) totalCount++ d.Metrics["count"].With(prometheus.Labels{"type": "Total UDRs transformed to CDRs"}).Inc() d.Metrics["time"].With(prometheus.Labels{"type": "CDRs average generation time"}).Set(totalTime / float64(totalCount) / float64(time.Millisecond)) return } // getNiceFloat job is to turn the ugly float64 provided into a "nice looking" // one according to the scale set, so we move from a floating coma number into // a fixed coma number. // Parameters: // - i: the ugly floating coma number. // Returns: // - o: the "nice looking" fixed coma float. func (d *DbParameter) getNiceFloat(i float64) (o float64) { min := float64(float64(1) / float64(scaler)) if diff := math.Abs(i - min); diff < min { o = float64(0) } else { o = float64(math.Round(i*scaler) / scaler) } return }