bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/scollector/collectors/azureeabilling.go (about) 1 package collectors 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/mhenderson-so/azure-ea-billing" 11 12 "bosun.org/cmd/scollector/conf" 13 "bosun.org/metadata" 14 "bosun.org/opentsdb" 15 ) 16 17 var azBillConf = azureEABillingConfig{} 18 19 const ( 20 hoursInDay = 24 21 usageDesc = "Usage of Azure service. Category is concatenated meter details. Resource is concatenated resource group and resource name." 22 costDesc = "Cost of Azure service. Category is concatenated meter details. Resource is concatenated resource group and resource name." 23 priceDesc = "Azure price sheet data for Enterprise Agreement services" 24 ) 25 26 func init() { 27 registerInit(startAzureEABilling) 28 } 29 30 func startAzureEABilling(c *conf.Conf) { 31 for _, config := range c.AzureEA { 32 if config.EANumber > 0 && config.APIKey != "" { 33 azBillConf = azureEABillingConfig{ 34 AZEABillingConfig: azureeabilling.Config{ 35 EA: config.EANumber, 36 APIKey: config.APIKey, 37 }, 38 CollectorConfig: config, 39 } 40 41 collectors = append(collectors, &IntervalCollector{ 42 F: c_azureeabilling, 43 Interval: 1 * time.Hour, 44 }) 45 } 46 } 47 } 48 49 func c_azureeabilling() (opentsdb.MultiDataPoint, error) { 50 var md opentsdb.MultiDataPoint 51 52 //Get the list of available bills from the portal 53 reports, err := azBillConf.AZEABillingConfig.GetUsageReports() 54 if err != nil { 55 return nil, err 56 } 57 58 //Process the report list 59 if err = processAzureEAReports(reports, &md); err != nil { 60 return nil, err 61 } 62 63 return md, nil 64 65 } 66 67 // processAzureEAReports will go through the monthly reports provided and pull out the ones that we're going to process 68 func processAzureEAReports(reports *azureeabilling.UsageReports, md *opentsdb.MultiDataPoint) error { 69 baseTime := time.Now() 70 thisMonth := baseTime.Format("2006-01") 71 lastMonth := time.Date(baseTime.Year(), baseTime.Month()-1, 1, 0, 0, 0, 0, time.UTC).Format("2006-01") 72 for _, r := range reports.AvailableMonths { 73 //There's potentially a lot of reports. We only want to process this months + last months report 74 if !(thisMonth == r.Month || lastMonth == r.Month) { 75 continue 76 } 77 78 csv := azBillConf.AZEABillingConfig.GetMonthReportsCSV(r, azureeabilling.DownloadForStructs) 79 structs, err := csv.ConvertToStructs() 80 81 if err != nil { 82 return err 83 } 84 for _, p := range structs.PriceSheetReport { 85 err := processAzureEAPriceSheetRow(p, md) 86 if err != nil { 87 return err 88 } 89 } 90 for _, d := range structs.DetailReport { 91 err := processAzureEADetailRow(d, md) 92 if err != nil { 93 return err 94 } 95 } 96 } 97 98 return nil 99 } 100 101 // processAzureEAPriceSheetRow will take the price sheet info and log it, so we can track price changes over time 102 func processAzureEAPriceSheetRow(p *azureeabilling.PriceSheetRow, md *opentsdb.MultiDataPoint) error { 103 fullProdName := fmt.Sprintf("%s-%s", p.Service, p.UnitOfMeasure) 104 priceString := convertAzurePriceToString(p.UnitPrice) 105 tags := opentsdb.TagSet{ 106 "partnumber": p.PartNumber, 107 "service": fullProdName, 108 } 109 Add(md, "azure.ea.pricesheet", priceString, tags, metadata.Gauge, metadata.Count, priceDesc) 110 return nil 111 } 112 113 // processAzureEADetailRow will take the actual usage data for the provided month 114 func processAzureEADetailRow(p *azureeabilling.DetailRow, md *opentsdb.MultiDataPoint) error { 115 //Don't process todays records as they are subject to change 116 nowYear, nowMonth, nowDay := time.Now().Date() 117 recordMonth := int(nowMonth) 118 if nowYear == p.Year && recordMonth == p.Month && nowDay == p.Day { 119 return nil 120 } 121 122 resourcePaths := strings.Split(strings.ToLower(p.InstanceID), "/") 123 var resourceString string 124 125 if len(resourcePaths) < 8 { 126 resourceString = strings.ToLower(p.InstanceID) 127 } else { 128 resourceIDs := resourcePaths[8:] 129 resourceString = strings.Join(resourceIDs, "-") 130 } 131 132 if p.ResourceGroup != "" { 133 resourceString = fmt.Sprintf("%s-%s", strings.ToLower(p.ResourceGroup), resourceString) 134 } 135 136 tags := opentsdb.TagSet{ 137 "category": p.MeterCategory, 138 "subcategory": fmt.Sprintf("%s-%s", strings.ToLower(p.MeterSubCategory), strings.ToLower(p.MeterName)), 139 } 140 141 resourceString, err := opentsdb.Clean(resourceString) 142 if err != nil && resourceString != "" { 143 tags["resource"] = resourceString 144 } 145 146 //Only log resource group details if they are enabled in the config 147 if azBillConf.CollectorConfig.LogResourceDetails { 148 resourcLocation, _ := opentsdb.Clean(p.ResourceLocation) 149 resouceGroup, _ := opentsdb.Clean(p.ResourceGroup) 150 if resouceGroup != "" { 151 tags["resoucegroup"] = strings.ToLower(resouceGroup) 152 } 153 if resourcLocation != "" { 154 tags["resourcelocation"] = strings.ToLower(resourcLocation) 155 } 156 } 157 158 //Only log extra Azure tags if enabled in the config 159 if azBillConf.CollectorConfig.LogExtraTags { 160 if p.Tags != "" { 161 customTags := make(map[string]string) 162 json.Unmarshal([]byte(p.Tags), &customTags) 163 for t, v := range customTags { 164 if t[:6] == "hidden" { 165 continue 166 } 167 value, _ := opentsdb.Clean(v) 168 if value == "" { 169 continue 170 } 171 tags[strings.ToLower(t)] = strings.ToLower(value) 172 } 173 } 174 } 175 176 //Only log billing details if they are enabled in the config 177 if azBillConf.CollectorConfig.LogBillingDetails { 178 if p.CostCenter != "" { 179 tags["costcenter"] = strings.ToLower(p.CostCenter) 180 } 181 cleanAccountName, _ := opentsdb.Clean(p.AccountName) 182 tags["accountname"] = strings.ToLower(cleanAccountName) 183 tags["subscription"] = strings.ToLower(p.SubscriptionName) 184 } 185 186 recordDate := time.Date(p.Year, time.Month(p.Month), p.Day, 0, 0, 0, 0, time.UTC) 187 188 //Because we need to log this hourly and we only have daily data, divide the daily cost into hourly costs 189 qtyPerHour := p.ConsumedQuantity / hoursInDay 190 191 //ExtendedCost is stored only in a string, because it's a variable number of decimal places. Which means we can't reliably store it in an int, and storing in a float reduces precision. 192 //This way we're choosing ourselves to drop the precision, which adds up to around 10-20c under initial testing. 193 costPerDay, err := strconv.ParseFloat(p.ExtendedCostRaw, 64) 194 if err != nil { 195 return err 196 } 197 costPerHour := costPerDay / hoursInDay 198 199 //Get 24 records for 24 hours in a day 200 for i := 0; i < hoursInDay; i++ { 201 recordTime := recordDate.Add(time.Duration(i) * time.Hour) 202 AddTS(md, "azure.ea.usage", recordTime.Unix(), qtyPerHour, tags, metadata.Gauge, metadata.Count, usageDesc) 203 AddTS(md, "azure.ea.cost", recordTime.Unix(), costPerHour, tags, metadata.Gauge, metadata.Count, costDesc) 204 } 205 206 return nil 207 } 208 209 //The cost is stored in cents, and we want to translate the cent cost into dollars and cents, but in a string 210 //which will not lose precision and is close enough for government work. 211 func convertAzurePriceToString(costInCents int) string { 212 priceString := strconv.Itoa(costInCents) 213 priceLen := len(priceString) 214 if priceLen == 1 { 215 priceString = fmt.Sprintf("0.0%s", priceString) 216 } 217 if priceLen == 2 { 218 priceString = fmt.Sprintf("0.%s", priceString) 219 } 220 if priceLen >= 3 { 221 priceString = fmt.Sprintf("%s.%s", priceString[0:priceLen-2], priceString[priceLen-2:]) 222 } 223 224 return priceString 225 } 226 227 type azureEABillingConfig struct { 228 CollectorConfig conf.AzureEA 229 AZEABillingConfig azureeabilling.Config 230 }