bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/scollector/collectors/fastly.go (about) 1 package collectors 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "net/url" 9 "reflect" 10 "regexp" 11 "strconv" 12 "time" 13 14 "bosun.org/cmd/scollector/conf" 15 "bosun.org/metadata" 16 "bosun.org/opentsdb" 17 "bosun.org/slog" 18 "github.com/bosun-monitor/statusio" 19 ) 20 21 func init() { 22 registerInit(func(c *conf.Conf) { 23 for _, f := range c.Fastly { 24 client := newFastlyClient(f.Key) 25 collectors = append(collectors, &IntervalCollector{ 26 F: func() (opentsdb.MultiDataPoint, error) { 27 return c_fastly(client) 28 }, 29 name: "c_fastly", 30 Interval: time.Minute * 1, 31 }) 32 collectors = append(collectors, &IntervalCollector{ 33 F: func() (opentsdb.MultiDataPoint, error) { 34 return c_fastly_billing(client) 35 }, 36 name: "c_fastly_billing", 37 Interval: time.Minute * 5, 38 }) 39 if f.StatusBaseAddr != "" { 40 collectors = append(collectors, &IntervalCollector{ 41 F: func() (opentsdb.MultiDataPoint, error) { 42 return c_fastly_status(f.StatusBaseAddr) 43 }, 44 name: "c_fastly_status", 45 Interval: time.Minute * 1, 46 }) 47 } 48 } 49 }) 50 } 51 52 const ( 53 fastlyBillingPrefix = "fastly.billing." 54 fastlyBillingBandwidthDesc = "The total amount of bandwidth used this month." 55 fastlyBillingBandwidthCostDesc = "The cost of the bandwidth used this month in USD." 56 fastlyBillingRequestsDesc = "The total number of requests used this month." 57 fastlyBillingRequestsCostDesc = "The cost of the requests used this month." 58 fastlyBillingIncurredCostDesc = "The total cost of bandwidth and requests used this month." 59 fastlyBillingOverageDesc = "How much over the plan minimum has been incurred this month." 60 fastlyBillingExtrasCostDesc = "The total cost of all extras this month." 61 fastlyBillingBeforeDiscountDesc = "The total incurred cost plus extras cost this month." 62 fastlyBillingDiscountDesc = "The calculated discount rate this month." 63 fastlyBillingCostDesc = "The final amount to be paid this month." 64 65 fastlyStatusPrefix = "fastly.status." 66 fastlyComponentStatusDesc = "The current status of the %v. 0: Operational, 1: Degraded Performance, 2: Partial Outage, 3: Major Outage." // see iota for statusio.ComponentStatus 67 fastlyScheduledMaintDesc = "The number of currently scheduled maintenances. Does not include maintenance that is current active" 68 fastlyActiveScheduledMaintDesc = "The number of currently scheduled maintenances currently in progress. Includes the 'in_progress' and 'verifying'" 69 fastlyActiveIncidentDesc = "The number of currently active incidents. Includes the 'investingating', 'identified', and 'monitoring' states." 70 ) 71 72 var ( 73 fastlyStatusPopRegex = regexp.MustCompile(`(.*)\(([A-Z]{3})\)`) // i.e. Miami (MIA) 74 ) 75 76 func c_fastly_status(baseAddr string) (opentsdb.MultiDataPoint, error) { 77 var md opentsdb.MultiDataPoint 78 c := statusio.NewClient(baseAddr) 79 summary, err := c.GetSummary() 80 if err != nil { 81 return md, err 82 } 83 84 // Process Components (Pops, Support Systems) 85 for _, comp := range summary.Components { 86 match := fastlyStatusPopRegex.FindAllStringSubmatch(comp.Name, 1) 87 if len(match) != 0 && len(match[0]) == 3 { // We have a pop 88 //name := match[0][1] 89 code := match[0][2] 90 tagSet := opentsdb.TagSet{"code": code} 91 Add(&md, fastlyStatusPrefix+"pop", int(comp.Status), tagSet, metadata.Gauge, metadata.StatusCode, fmt.Sprintf(fastlyComponentStatusDesc, "pop")) 92 continue 93 } 94 // Must be service component 95 tagSet := opentsdb.TagSet{"service": comp.Name} 96 Add(&md, fastlyStatusPrefix+"service", int(comp.Status), tagSet, metadata.Gauge, metadata.StatusCode, fmt.Sprintf(fastlyComponentStatusDesc, "service")) 97 } 98 99 // Scheduled Maintenance 100 scheduledMaintByImpact := make(map[statusio.StatusIndicator]int) 101 activeScheduledMaintByImpact := make(map[statusio.StatusIndicator]int) 102 // Make Maps 103 for _, si := range statusio.StatusIndicatorValues { 104 scheduledMaintByImpact[si] = 0 105 activeScheduledMaintByImpact[si] = 0 106 } 107 // Group by scheduled vs inprogress/verifying 108 for _, maint := range summary.ScheduledMaintenances { 109 switch maint.Status { 110 case statusio.Scheduled: 111 scheduledMaintByImpact[maint.Impact]++ 112 case statusio.InProgress, statusio.Verifying: 113 activeScheduledMaintByImpact[maint.Impact]++ 114 } 115 } 116 for impact, count := range scheduledMaintByImpact { 117 tagSet := opentsdb.TagSet{"impact": fmt.Sprint(impact)} 118 Add(&md, fastlyStatusPrefix+"scheduled_maint_count", count, tagSet, metadata.Gauge, metadata.Count, fastlyScheduledMaintDesc) 119 } 120 for impact, count := range activeScheduledMaintByImpact { 121 tagSet := opentsdb.TagSet{"impact": fmt.Sprint(impact)} 122 Add(&md, fastlyStatusPrefix+"in_progress_maint_count", count, tagSet, metadata.Gauge, metadata.Count, fastlyActiveScheduledMaintDesc) 123 } 124 125 // Incidents 126 // Make Map 127 incidentsByImpact := make(map[statusio.StatusIndicator]int) 128 for _, si := range statusio.StatusIndicatorValues { 129 incidentsByImpact[si] = 0 130 } 131 for _, incident := range summary.Incidents { 132 switch incident.Status { 133 case statusio.Investigating, statusio.Identified, statusio.Monitoring: 134 incidentsByImpact[incident.Impact]++ 135 default: 136 continue 137 } 138 } 139 for impact, count := range incidentsByImpact { 140 tagSet := opentsdb.TagSet{"impact": fmt.Sprint(impact)} 141 Add(&md, fastlyStatusPrefix+"active_incident_count", count, tagSet, metadata.Gauge, metadata.Incident, fastlyActiveIncidentDesc) 142 } 143 144 return md, nil 145 } 146 147 func c_fastly_billing(c fastlyClient) (opentsdb.MultiDataPoint, error) { 148 var md opentsdb.MultiDataPoint 149 now := time.Now().UTC() 150 year := now.Format("2006") 151 month := now.Format("01") 152 b, err := c.GetBilling(year, month) 153 if err != nil { 154 return md, err 155 } 156 Add(&md, fastlyBillingPrefix+"bandwidth", b.Total.Bandwidth, nil, metadata.Gauge, metadata.Unit(b.Total.BandwidthUnits), fastlyBillingBandwidthDesc) 157 Add(&md, fastlyBillingPrefix+"bandwidth_cost", b.Total.BandwidthCost, nil, metadata.Gauge, metadata.USD, fastlyBillingBandwidthCostDesc) 158 Add(&md, fastlyBillingPrefix+"requests", b.Total.Requests, nil, metadata.Gauge, metadata.Request, fastlyBillingRequestsDesc) 159 Add(&md, fastlyBillingPrefix+"requests_cost", b.Total.RequestsCost, nil, metadata.Gauge, metadata.USD, fastlyBillingRequestsCostDesc) 160 Add(&md, fastlyBillingPrefix+"incurred_cost", b.Total.IncurredCost, nil, metadata.Gauge, metadata.USD, fastlyBillingIncurredCostDesc) 161 Add(&md, fastlyBillingPrefix+"overage", b.Total.Overage, nil, metadata.Gauge, metadata.Unit("unknown"), fastlyBillingOverageDesc) 162 Add(&md, fastlyBillingPrefix+"extras_cost", b.Total.ExtrasCost, nil, metadata.Gauge, metadata.USD, fastlyBillingExtrasCostDesc) 163 Add(&md, fastlyBillingPrefix+"cost_before_discount", b.Total.CostBeforeDiscount, nil, metadata.Gauge, metadata.USD, fastlyBillingBeforeDiscountDesc) 164 Add(&md, fastlyBillingPrefix+"discount", b.Total.Discount, nil, metadata.Gauge, metadata.Pct, fastlyBillingDiscountDesc) 165 Add(&md, fastlyBillingPrefix+"cost", b.Total.Cost, nil, metadata.Gauge, metadata.USD, fastlyBillingCostDesc) 166 167 return md, nil 168 } 169 170 func c_fastly(c fastlyClient) (opentsdb.MultiDataPoint, error) { 171 var md opentsdb.MultiDataPoint 172 to := time.Now().UTC().Truncate(time.Minute) 173 from := to.Add(-15 * time.Minute) // "Minutely data will be delayed by roughly 10 to 15 minutes from the current time -- Fastly Docs" 174 175 // Aggregate 176 statsCollection, err := c.GetAggregateStats(from, to) 177 if err != nil { 178 return md, err 179 } 180 for _, stats := range statsCollection { 181 fastlyReflectAdd(&md, "fastly", "", stats, stats.StartTime, nil) 182 } 183 184 // By Service 185 services, err := c.GetServices() 186 if err != nil { 187 return md, err 188 } 189 for _, service := range services { 190 statsCollection, err := c.GetServiceStats(from, to, service.Id) 191 if err != nil { 192 slog.Errorf("couldn't get stats for service %v with id %v: %v", service.Name, service.Id, err) 193 continue 194 } 195 for _, stats := range statsCollection { 196 fastlyReflectAdd(&md, "fastly", "_by_service", stats, stats.StartTime, service.TagSet()) 197 } 198 } 199 200 // By Region 201 regions, err := c.GetRegions() 202 if err != nil { 203 return md, err 204 } 205 for _, region := range regions { 206 statsCollection, err := c.GetRegionStats(from, to, region) 207 if err != nil { 208 slog.Errorf("couldn't get stats for region %v: %v", region, err) 209 continue 210 } 211 for _, stats := range statsCollection { 212 fastlyReflectAdd(&md, "fastly", "_by_region", stats, stats.StartTime, region.TagSet()) 213 } 214 } 215 return md, nil 216 } 217 218 type fastlyClient struct { 219 key string 220 client *http.Client 221 } 222 223 func newFastlyClient(key string) fastlyClient { 224 return fastlyClient{key, &http.Client{}} 225 } 226 227 func (f *fastlyClient) request(path string, values url.Values, s interface{}) error { 228 u := &url.URL{ 229 Scheme: "https", 230 Host: "api.fastly.com", 231 Path: path, 232 RawQuery: values.Encode(), 233 } 234 req, err := http.NewRequest("GET", u.String(), nil) 235 if err != nil { 236 slog.Error(err) 237 return err 238 } 239 req.Header.Set("Accept", "application/json") 240 req.Header.Set("Fastly-Key", f.key) 241 resp, err := f.client.Do(req) 242 if err != nil { 243 return err 244 } 245 defer resp.Body.Close() 246 if resp.StatusCode != 200 { 247 b, _ := ioutil.ReadAll(resp.Body) 248 return fmt.Errorf("%v: %v: %v", req.URL, resp.Status, string(b)) 249 } 250 d := json.NewDecoder(resp.Body) 251 if err := d.Decode(&s); err != nil { 252 return err 253 } 254 return nil 255 } 256 257 func (f *fastlyClient) GetServices() ([]fastlyService, error) { 258 var services []fastlyService 259 err := f.request("service", url.Values{}, &services) 260 return services, err 261 } 262 263 type fastlyService struct { 264 Name string `json:"name"` 265 Id string `json:"id"` 266 } 267 268 func (f *fastlyService) TagSet() opentsdb.TagSet { 269 return opentsdb.TagSet{"service": f.Name} 270 } 271 272 type fastlyRegion string 273 274 func (fr fastlyRegion) TagSet() opentsdb.TagSet { 275 return opentsdb.TagSet{"region": string(fr)} 276 } 277 278 func (f *fastlyClient) GetRegions() ([]fastlyRegion, error) { 279 r := struct { 280 Data []fastlyRegion `json:"data"` 281 }{ 282 []fastlyRegion{}, 283 } 284 err := f.request("stats/regions", url.Values{}, &r) 285 return r.Data, err 286 } 287 288 type fastlyBilling struct { 289 // There are other breakdowns in the API response for by service and by regions. So this 290 // can be expanded in the future if we wish 291 Total struct { 292 Bandwidth float64 `json:"bandwidth"` 293 BandwidthCost float64 `json:"bandwidth_cost"` 294 BandwidthUnits string `json:"bandwidth_units"` 295 Cost float64 `json:"cost"` 296 CostBeforeDiscount float64 `json:"cost_before_discount"` 297 Discount float64 `json:"discount"` 298 Extras []interface{} `json:"extras"` 299 ExtrasCost float64 `json:"extras_cost"` 300 Overage float64 `json:"overage"` 301 IncurredCost float64 `json:"incurred_cost"` 302 PlanCode string `json:"plan_code"` 303 PlanMinimum float64 `json:"plan_minimum"` 304 PlanName string `json:"plan_name"` 305 Requests float64 `json:"requests"` 306 RequestsCost float64 `json:"requests_cost"` 307 Terms string `json:"terms"` 308 } `json:"total"` 309 } 310 311 func (f *fastlyClient) GetBilling(year, month string) (fastlyBilling, error) { 312 var b fastlyBilling 313 err := f.request(fmt.Sprintf("billing/year/%v/month/%v", year, month), nil, &b) 314 return b, err 315 } 316 317 func (f *fastlyClient) GetAggregateStats(from, to time.Time) ([]fastlyStats, error) { 318 v := url.Values{} 319 v.Add("from", fmt.Sprintf("%v", from.Unix())) 320 v.Add("to", fmt.Sprintf("%v", to.Unix())) 321 v.Add("by", "minute") 322 r := struct { 323 Data []fastlyStats `json:"data"` 324 }{ 325 []fastlyStats{}, 326 } 327 err := f.request("stats/aggregate", v, &r) 328 return r.Data, err 329 } 330 331 func (f *fastlyClient) GetServiceStats(from, to time.Time, serviceId string) ([]fastlyStats, error) { 332 v := url.Values{} 333 v.Add("from", fmt.Sprintf("%v", from.Unix())) 334 v.Add("to", fmt.Sprintf("%v", to.Unix())) 335 v.Add("by", "minute") 336 r := struct { 337 Data []fastlyStats `json:"data"` 338 }{ 339 []fastlyStats{}, 340 } 341 err := f.request(fmt.Sprintf("stats/service/%v", serviceId), v, &r) 342 return r.Data, err 343 } 344 345 func (f *fastlyClient) GetRegionStats(from, to time.Time, region fastlyRegion) ([]fastlyStats, error) { 346 v := url.Values{} 347 v.Add("from", fmt.Sprintf("%v", from.Unix())) 348 v.Add("to", fmt.Sprintf("%v", to.Unix())) 349 v.Add("by", "minute") 350 v.Add("region", string(region)) 351 r := struct { 352 Data []fastlyStats `json:"data"` 353 }{ 354 []fastlyStats{}, 355 } 356 err := f.request("stats/aggregate", v, &r) 357 return r.Data, err 358 } 359 360 // Stats without a description are not sent as it means I wasn't able to find documentation on them 361 type fastlyStats struct { 362 AttackBlock int64 `json:"attack_block"` 363 AttackBodySize int64 `json:"attack_body_size"` 364 AttackHeaderSize int64 `json:"attack_header_size"` 365 AttackSynth int64 `json:"attack_synth"` 366 Bandwidth int64 `json:"bandwidth" div:"true" rate:"gauge" unit:"bytes per second" desc:"The total bytes delivered per second (body_size + header_size)."` 367 Blacklist int64 `json:"blacklist"` 368 BodySize int64 `json:"body_size" div:"true" rate:"gauge" unit:"bytes per second" desc:"The total bytes delivered per second for the bodies."` 369 Errors int64 `json:"errors" div:"true" rate:"gauge" unit:"errors per second" desc:"The number of cache errors per second."` 370 HeaderSize int64 `json:"header_size" div:"true" rate:"gauge" unit:"bytes per second" desc:"The total bytes delivered per second for headers."` 371 HitRatio float64 `json:"hit_ratio" rate:"gauge" unit:"ratio" desc:"The ratio of cache hits to cache misses (between 0-1)."` 372 Hits int64 `json:"hits" div:"true" rate:"gauge" unit:"hits per second" desc:"The number of cache hits per second."` 373 HitsTime float64 `json:"hits_time" div:"true" rate:"gauge" unit:"seconds per second" desc:"The amount of time spent processing cache hits."` 374 HTTP2 int64 `json:"http2"` 375 Imgopto int64 `json:"imgopto"` 376 Ipv6 int64 `json:"ipv6"` 377 Log int64 `json:"log"` 378 MissTime float64 `json:"miss_time" div:"true" rate:"gauge" unit:"seconds per second" desc:"The amount of time spent processing cache misses"` 379 OrigReqBodySize int64 `json:"orig_req_body_size"` 380 OrigReqHeaderSize int64 `json:"orig_req_header_size"` 381 OrigRespBodySize int64 `json:"orig_resp_body_size"` 382 OrigRespHeaderSize int64 `json:"orig_resp_header_size"` 383 Otfp int64 `json:"otfp"` 384 Pass int64 `json:"pass" div:"true" rate:"gauge" unit:"hits per second" desc:"The number of requests that passed through the CDN without being cached"` 385 Pci int64 `json:"pci"` 386 Requests int64 `json:"requests" div:"true" rate:"gauge" unit:"requests per second" desc:"The number of requests processed"` 387 Shield int64 `json:"shield"` 388 StartTime int64 `json:"start_time" exclude:""` 389 Status1xx int64 `json:"status_1xx" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 1xx response code (Informational)."` 390 Status200 int64 `json:"status_200" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 200 response code (Success)."` 391 Status204 int64 `json:"status_204" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 204 response code (No Content)."` 392 Status2xx int64 `json:"status_2xx" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 2xx response code (Success)."` 393 Status301 int64 `json:"status_301" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 301 response code (Moved Permanently)."` 394 Status302 int64 `json:"status_302" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 302 response code (Found)."` 395 Status304 int64 `json:"status_304" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 304 response code."` 396 Status3xx int64 `json:"status_3xx" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 3xx response code (Redirection)."` 397 Status4xx int64 `json:"status_4xx" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 4xx response code (Client Error)."` 398 Status503 int64 `json:"status_503" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 503 response code. (Service Unavailable)"` 399 Status5xx int64 `json:"status_5xx" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a xxx response code."` 400 Synth int64 `json:"synth" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of synthetic responses sent from Varnish. This is typically used to send edge-generated error pages."` 401 TLS int64 `json:"tls"` 402 Uncacheable int64 `json:"uncacheable" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http requests that were uncacheable."` 403 Video int64 `json:"video"` 404 } 405 406 const fastlyDivDesc = "This metric is collected per minute, but we divide it by 60 seconds in order to normalize the rate to per second instead of per minute." 407 408 func fastlyReflectAdd(md *opentsdb.MultiDataPoint, prefix, suffix string, st interface{}, timeStamp int64, ts opentsdb.TagSet) { 409 t := reflect.TypeOf(st) 410 valueOf := reflect.ValueOf(st) 411 for i := 0; i < t.NumField(); i++ { 412 field := t.Field(i) 413 value := valueOf.Field(i).Interface() 414 var ( 415 jsonTag = field.Tag.Get("json") 416 metricTag = field.Tag.Get("metric") 417 rateTag = field.Tag.Get("rate") 418 unitTag = field.Tag.Get("unit") 419 divTag = field.Tag.Get("div") 420 descTag = field.Tag.Get("desc") 421 exclude = field.Tag.Get("exclude") != "" 422 ) 423 if exclude || descTag == "" { 424 continue 425 } 426 metricName := jsonTag 427 if metricTag != "" { 428 metricName = metricTag 429 } 430 if metricName == "" { 431 slog.Errorf("Unable to determine metric name for field %s. Skipping.", field.Name) 432 continue 433 } 434 shouldDiv := divTag != "" 435 if shouldDiv { 436 descTag = fmt.Sprintf("%v %v", descTag, fastlyDivDesc) 437 } 438 fullMetric := fmt.Sprintf("%v.%v%v", prefix, metricName, suffix) 439 switch value := value.(type) { 440 case int64, float64: 441 var v float64 442 if f, found := value.(float64); found { 443 v = f 444 } else { 445 v = float64(value.(int64)) 446 } 447 if shouldDiv { 448 v /= 60.0 449 } 450 AddTS(md, fullMetric, timeStamp, v, ts, metadata.RateType(rateTag), metadata.Unit(unitTag), descTag) 451 case string: 452 // Floats in strings, I know not why, precision perhaps? 453 // err ignored since we expect non number strings in the struct 454 if f, err := strconv.ParseFloat(value, 64); err != nil { 455 if shouldDiv { 456 f /= 60.0 457 } 458 AddTS(md, fullMetric, timeStamp, f, ts, metadata.RateType(rateTag), metadata.Unit(unitTag), descTag) 459 } 460 default: 461 // Pass since there is no need to recurse 462 } 463 } 464 }