| package cdn |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import ( |
| "database/sql" |
| "errors" |
| "net/http" |
| "strconv" |
| "strings" |
| |
| "github.com/apache/trafficcontrol/lib/go-util" |
| |
| "github.com/apache/trafficcontrol/lib/go-log" |
| "github.com/apache/trafficcontrol/lib/go-tc" |
| "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api" |
| "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/monitorhlp" |
| ) |
| |
| func GetCapacity(w http.ResponseWriter, r *http.Request) { |
| inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) |
| if userErr != nil || sysErr != nil { |
| api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) |
| return |
| } |
| defer inf.Close() |
| |
| api.RespWriter(w, r, inf.Tx.Tx)(getCapacity(inf.Tx.Tx)) |
| } |
| |
| func getCapacity(tx *sql.Tx) (CapacityResp, error) { |
| monitors, err := monitorhlp.GetURLs(tx) |
| if err != nil { |
| return CapacityResp{}, errors.New("getting monitors: " + err.Error()) |
| } |
| if len(monitors) == 0 { |
| return CapacityResp{}, errors.New("no monitors found") |
| } |
| return getMonitorsCapacity(tx, monitors) |
| } |
| |
| type CapacityResp struct { |
| AvailablePercent float64 `json:"availablePercent"` |
| UnavailablePercent float64 `json:"unavailablePercent"` |
| UtilizedPercent float64 `json:"utilizedPercent"` |
| MaintenancePercent float64 `json:"maintenancePercent"` |
| } |
| |
| type CapData struct { |
| Available float64 |
| Unavailable float64 |
| Utilized float64 |
| Maintenance float64 |
| Capacity float64 |
| } |
| |
| func getMonitorsCapacity(tx *sql.Tx, monitors map[tc.CDNName][]string) (CapacityResp, error) { |
| client, err := monitorhlp.GetClient(tx) |
| if err != nil { |
| return CapacityResp{}, errors.New("getting TM client: " + err.Error()) |
| } |
| |
| thresholds, err := getEdgeProfileHealthThresholdBandwidth(tx) |
| if err != nil { |
| return CapacityResp{}, errors.New("getting profile thresholds: " + err.Error()) |
| } |
| |
| cap, err := getCapacityData(monitors, thresholds, client, tx) |
| if err != nil { |
| return CapacityResp{}, errors.New("getting capacity from monitors: " + err.Error()) |
| } else if cap.Capacity == 0 { |
| return CapacityResp{}, errors.New("capacity was zero!") // avoid divide-by-zero below. |
| } |
| |
| return CapacityResp{ |
| UtilizedPercent: (cap.Available * 100) / cap.Capacity, |
| UnavailablePercent: (cap.Unavailable * 100) / cap.Capacity, |
| MaintenancePercent: (cap.Maintenance * 100) / cap.Capacity, |
| AvailablePercent: ((cap.Capacity - cap.Unavailable - cap.Maintenance - cap.Available) * 100) / cap.Capacity, |
| }, nil |
| } |
| |
| // getCapacityData attempts to get the CDN capacity from each monitor. If one fails, it tries the next. |
| // The first monitor for which all data requests succeed is used. |
| // Only if all monitors for a CDN fail is an error returned, from the last monitor tried. |
| func getCapacityData(monitors map[tc.CDNName][]string, thresholds map[string]float64, client *http.Client, tx *sql.Tx) (CapData, error) { |
| cap := CapData{} |
| for cdn, monitorFQDNs := range monitors { |
| err := error(nil) |
| for _, monitorFQDN := range monitorFQDNs { |
| crStates := tc.CRStates{} |
| crConfig := tc.CRConfig{} |
| cacheStats := tc.Stats{} |
| if crStates, err = monitorhlp.GetCRStates(monitorFQDN, client); err != nil { |
| err = errors.New("getting CRStates for CDN '" + string(cdn) + "' monitor '" + monitorFQDN + "': " + err.Error()) |
| log.Warnln("getCapacity failed to get CRStates from cdn '" + string(cdn) + " monitor '" + monitorFQDN + "', trying next monitor: " + err.Error()) |
| continue |
| } |
| if crConfig, err = monitorhlp.GetCRConfig(monitorFQDN, client); err != nil { |
| err = errors.New("getting CRConfig for CDN '" + string(cdn) + "' monitor '" + monitorFQDN + "': " + err.Error()) |
| log.Warnln("getCapacity failed to get CRConfig from cdn '" + string(cdn) + " monitor '" + monitorFQDN + "', trying next monitor: " + err.Error()) |
| continue |
| } |
| statsToFetch := []string{tc.StatNameKBPS, tc.StatNameMaxKBPS} |
| var monitorEndpoint string |
| if cacheStats, monitorEndpoint, err = monitorhlp.GetCacheStats(monitorFQDN, client, statsToFetch); err != nil { |
| log.Warnln("getCapacity failed to get '" + monitorEndpoint + "' from cdn '" + string(cdn) + "', Error: " + err.Error() + ", trying CacheStats") |
| legacyCacheStats, monitorEndpoint, err := monitorhlp.GetLegacyCacheStats(monitorFQDN, client, statsToFetch) |
| if err != nil { |
| log.Warnln("getCapacity failed to get '" + monitorEndpoint + "' from cdn '" + string(cdn) + "', Error: " + err.Error()) |
| continue |
| } |
| cacheStats = monitorhlp.UpgradeLegacyStats(legacyCacheStats) |
| } |
| |
| cap = addCapacity(cap, cacheStats, crStates, crConfig, thresholds, tx) |
| break |
| } |
| if err != nil { |
| return CapData{}, err |
| } |
| } |
| return cap, nil |
| } |
| |
| func addCapacity(cap CapData, cacheStats tc.Stats, crStates tc.CRStates, crConfig tc.CRConfig, thresholds map[string]float64, tx *sql.Tx) CapData { |
| for cacheName, stats := range cacheStats.Caches { |
| cache, ok := crConfig.ContentServers[(cacheName)] |
| if !ok { |
| continue |
| } |
| if cache.ServerType == nil || cache.ServerStatus == nil || cache.Profile == nil { |
| log.Warnln("addCapacity got cache with nil values! Skipping!") |
| continue |
| } |
| if !strings.HasPrefix(*cache.ServerType, string(tc.CacheTypeEdge)) { |
| continue |
| } |
| kbps, maxKbps, err := getStats(stats) |
| if err != nil { |
| log.Errorf("couldn't get stats for %v. err: %v", cacheName, err.Error()) |
| continue |
| } |
| if string(*cache.ServerStatus) == string(tc.CacheStatusReported) || string(*cache.ServerStatus) == string(tc.CacheStatusOnline) { |
| if crStates.Caches[tc.CacheName(cacheName)].IsAvailable { |
| cap.Available += kbps |
| } else { |
| cap.Unavailable += kbps |
| } |
| } else if string(*cache.ServerStatus) == string(tc.CacheStatusAdminDown) { |
| cap.Maintenance += kbps |
| } else { |
| continue // don't add capacity for OFFLINE or other statuses |
| } |
| cap.Capacity += maxKbps - thresholds[*cache.Profile] |
| } |
| return cap |
| } |
| |
| func getStats(stats tc.ServerStats) (float64, float64, error) { |
| kbpsRaw, ok := stats.Stats[tc.StatNameKBPS] |
| if !ok { |
| return 0, 0, errors.New("no kbps stats") |
| } |
| maxKbpsRaw, ok := stats.Stats[tc.StatNameMaxKBPS] |
| if !ok { |
| return 0, 0, errors.New("no maxKbpsR stats") |
| } |
| if len(kbpsRaw) < 1 || |
| len(maxKbpsRaw) < 1 { |
| return 0, 0, errors.New("no kbps/maxKbps stats to return") |
| } |
| kbps, ok := util.ToNumeric(kbpsRaw[0].Val) |
| if !ok { |
| return 0, 0, errors.New("unable to convert kbps to a float") |
| } |
| maxKbps, ok := util.ToNumeric(maxKbpsRaw[0].Val) |
| if !ok { |
| return 0, 0, errors.New("unable to convert maxKbps to a float") |
| } |
| return kbps, maxKbps, nil |
| } |
| |
| func getEdgeProfileHealthThresholdBandwidth(tx *sql.Tx) (map[string]float64, error) { |
| rows, err := tx.Query(` |
| SELECT pr.name as profile, pa.name, pa.config_file, pa.value |
| FROM parameter as pa |
| JOIN profile_parameter as pp ON pp.parameter = pa.id |
| JOIN profile as pr ON pp.profile = pr.id |
| JOIN server as s ON s.profile = pr.id |
| JOIN cdn as c ON c.id = s.cdn_id |
| JOIN type as t ON s.type = t.id |
| WHERE t.name LIKE 'EDGE%' |
| AND pa.config_file = 'rascal-config.txt' |
| AND pa.name = 'health.threshold.availableBandwidthInKbps' |
| `) |
| if err != nil { |
| return nil, errors.New("querying thresholds: " + err.Error()) |
| } |
| defer rows.Close() |
| profileThresholds := map[string]float64{} |
| for rows.Next() { |
| profile := "" |
| threshStr := "" |
| if err := rows.Scan(&profile, &threshStr); err != nil { |
| return nil, errors.New("scanning thresholds: " + err.Error()) |
| } |
| threshStr = strings.TrimPrefix(threshStr, ">") |
| thresh, err := strconv.ParseFloat(threshStr, 64) |
| if err != nil { |
| return nil, errors.New("profile '" + profile + "' health.threshold.availableBandwidthInKbps is not a number") |
| } |
| profileThresholds[profile] = thresh |
| } |
| return profileThresholds, nil |
| } |