func (handler Handler) Handle(id string, r io.Reader, err error, pollId uint64, pollFinished chan<- uint64) { log.Debugf("poll %v %v handle start\n", pollId, time.Now()) result := Result{ Id: enum.CacheName(id), Available: false, Errors: []error{}, Time: time.Now(), // TODO change this to be computed the instant we get the result back, to minimise inaccuracy PollID: pollId, PollFinished: pollFinished, } if err != nil { log.Errorf("%v handler given error '%v'\n", id, err) // error here, in case the thing that called Handle didn't error result.Errors = append(result.Errors, err) handler.ResultChannel <- result return } if r == nil { log.Errorf("%v handle reader nil\n", id) result.Errors = append(result.Errors, fmt.Errorf("handler got nil reader")) handler.ResultChannel <- result return } result.PrecomputedData.Reporting = true if err := json.NewDecoder(r).Decode(&result.Astats); err != nil { log.Errorf("%s procnetdev decode error '%v'\n", id, err) result.Errors = append(result.Errors, err) handler.ResultChannel <- result return } if result.Astats.System.ProcNetDev == "" { log.Warnf("addkbps %s procnetdev empty\n", id) } if result.Astats.System.InfSpeed == 0 { log.Warnf("addkbps %s inf.speed empty\n", id) } log.Debugf("poll %v %v handle decode end\n", pollId, time.Now()) if err != nil { result.Errors = append(result.Errors, err) log.Errorf("addkbps handle %s error '%v'\n", id, err) } else { result.Available = true } if handler.Precompute() { log.Debugf("poll %v %v handle precompute start\n", pollId, time.Now()) result = handler.precompute(result) log.Debugf("poll %v %v handle precompute end\n", pollId, time.Now()) } log.Debugf("poll %v %v handle write start\n", pollId, time.Now()) handler.ResultChannel <- result log.Debugf("poll %v %v handle end\n", pollId, time.Now()) }
func (p FilePoller) Poll() { // initial read before watching for changes contents, err := ioutil.ReadFile(p.File) if err != nil { log.Errorf("reading %s: %s\n", p.File, err) os.Exit(1) // TODO: this is a little drastic -jse } else { p.ResultChannel <- contents } watcher, _ := fsnotify.NewWatcher() watcher.Add(p.File) for { select { case event := <-watcher.Events: if event.Op&fsnotify.Write == fsnotify.Write { contents, err := ioutil.ReadFile(p.File) if err != nil { log.Errorf("opening %s: %s\n", p.File, err) } else { p.ResultChannel <- contents } } case err := <-watcher.Errors: log.Errorln(time.Now(), "error:", err) } } }
// addLastDSStatTotals takes a LastDSStat with only raw `Caches` data, and calculates and sets the `CacheGroups`, `Type`, and `Total` data, and returns the augmented structure. func addLastDSStatTotals(lastStat LastDSStat, cachesReporting map[enum.CacheName]bool, serverCachegroups map[enum.CacheName]enum.CacheGroupName, serverTypes map[enum.CacheName]enum.CacheType) LastDSStat { cacheGroups := map[enum.CacheGroupName]LastStatsData{} cacheTypes := map[enum.CacheType]LastStatsData{} total := LastStatsData{} for cacheName, cacheStats := range lastStat.Caches { if !cachesReporting[cacheName] { continue } if cacheGroup, ok := serverCachegroups[cacheName]; ok { cacheGroups[cacheGroup] = cacheGroups[cacheGroup].Sum(cacheStats) } else { log.Errorf("while computing delivery service data, cache %v not in cachegroups\n", cacheName) } if cacheType, ok := serverTypes[cacheName]; ok { cacheTypes[cacheType] = cacheTypes[cacheType].Sum(cacheStats) } else { log.Errorf("while computing delivery service data, cache %v not in types\n", cacheName) } total = total.Sum(cacheStats) } lastStat.CacheGroups = cacheGroups lastStat.Type = cacheTypes lastStat.Total = total return lastStat }
func CreateStats(statHistory map[enum.CacheName][]cache.Result, toData todata.TOData, crStates peer.Crstates, lastStats LastStats, now time.Time) (Stats, LastStats, error) { start := time.Now() dsStats := NewStats() for deliveryService, _ := range toData.DeliveryServiceServers { if deliveryService == "" { log.Errorf("EMPTY CreateStats deliveryService") continue } dsStats.DeliveryService[enum.DeliveryServiceName(deliveryService)] = *dsdata.NewStat() } dsStats = setStaticData(dsStats, toData.DeliveryServiceServers) var err error dsStats, err = addAvailableData(dsStats, crStates, toData.ServerCachegroups, toData.ServerDeliveryServices, toData.ServerTypes, statHistory) // TODO move after stat summarisation if err != nil { return dsStats, lastStats, fmt.Errorf("Error getting Cache availability data: %v", err) } for server, history := range statHistory { if len(history) < 1 { continue // TODO warn? } cachegroup, ok := toData.ServerCachegroups[server] if !ok { log.Warnf("server %s has no cachegroup, skipping\n", server) continue } serverType, ok := toData.ServerTypes[enum.CacheName(server)] if !ok { log.Warnf("server %s not in CRConfig, skipping\n", server) continue } result := history[len(history)-1] // TODO check result.PrecomputedData.Errors for ds, resultStat := range result.PrecomputedData.DeliveryServiceStats { if ds == "" { log.Errorf("EMPTY precomputed delivery service") continue } if _, ok := dsStats.DeliveryService[ds]; !ok { dsStats.DeliveryService[ds] = resultStat continue } httpDsStat := dsStats.DeliveryService[ds] httpDsStat.TotalStats = httpDsStat.TotalStats.Sum(resultStat.TotalStats) httpDsStat.CacheGroups[cachegroup] = httpDsStat.CacheGroups[cachegroup].Sum(resultStat.CacheGroups[cachegroup]) httpDsStat.Types[serverType] = httpDsStat.Types[serverType].Sum(resultStat.Types[serverType]) httpDsStat.Caches[server] = httpDsStat.Caches[server].Sum(resultStat.Caches[server]) httpDsStat.CachesTimeReceived[server] = resultStat.CachesTimeReceived[server] httpDsStat.CommonStats = dsStats.DeliveryService[ds].CommonStats dsStats.DeliveryService[ds] = httpDsStat // TODO determine if necessary } } perSecStats, lastStats := addPerSecStats(statHistory, dsStats, lastStats, now, toData.ServerCachegroups, toData.ServerTypes) log.Infof("CreateStats took %v\n", time.Since(start)) return perSecStats, lastStats, nil }
// addDSPerSecStats calculates and adds the per-second delivery service stats to both the Stats and LastStats structures, and returns the augmented structures. func addDSPerSecStats(dsName enum.DeliveryServiceName, stat dsdata.Stat, lastStats LastStats, dsStats Stats, dsStatsTime time.Time, serverCachegroups map[enum.CacheName]enum.CacheGroupName, serverTypes map[enum.CacheName]enum.CacheType) (Stats, LastStats) { err := error(nil) lastStat, lastStatExists := lastStats.DeliveryServices[dsName] if !lastStatExists { lastStat = newLastDSStat() } for cacheName, cacheStats := range stat.Caches { lastStat.Caches[cacheName], err = addLastStats(lastStat.Caches[cacheName], cacheStats, dsStatsTime) if err != nil { log.Errorf("debugq %v Error adding kbps for cache %v: %v", cacheName, err) continue } cacheStats.Kbps.Value = lastStat.Caches[cacheName].Bytes.PerSec / BytesPerKilobit stat.Caches[cacheName] = cacheStats } lastStat = addLastDSStatTotals(lastStat, stat.CommonStats.CachesReporting, serverCachegroups, serverTypes) for cacheGroup, cacheGroupStat := range lastStat.CacheGroups { stat.CacheGroups[cacheGroup] = addLastStatsToStatCacheStats(stat.CacheGroups[cacheGroup], cacheGroupStat) } for cacheType, cacheTypeStat := range lastStat.Type { stat.Types[cacheType] = addLastStatsToStatCacheStats(stat.Types[cacheType], cacheTypeStat) } stat.TotalStats = addLastStatsToStatCacheStats(stat.TotalStats, lastStat.Total) lastStats.DeliveryServices[dsName] = lastStat dsStats.DeliveryService[dsName] = stat return dsStats, lastStats }
func (p MonitorConfigPoller) Poll() { tick := time.NewTicker(p.Interval) for { select { case opsConfig := <-p.OpsConfigChannel: log.Infof("MonitorConfigPoller: received new opsConfig: %v\n", opsConfig) p.OpsConfig = opsConfig case session := <-p.SessionChannel: log.Infof("MonitorConfigPoller: received new session: %v\n", session) p.Session = session case <-tick.C: if p.Session != nil && p.OpsConfig.CdnName != "" { monitorConfig, err := p.Session.TrafficMonitorConfigMap(p.OpsConfig.CdnName) if err != nil { log.Errorf("MonitorConfigPoller: %s\n %v\n", err, monitorConfig) } else { log.Infoln("MonitorConfigPoller: fetched monitorConfig") p.ConfigChannel <- *monitorConfig } } else { log.Warnln("MonitorConfigPoller: skipping this iteration, Session is nil") } } } }
// processStatResults processes the given results, creating and setting DSStats, LastStats, and other stats. Note this is NOT threadsafe, and MUST NOT be called from multiple threads. func processStatResults( results []cache.Result, statHistoryThreadsafe StatHistoryThreadsafe, combinedStates peer.Crstates, lastStats LastStatsThreadsafe, toData todata.TOData, errorCount UintThreadsafe, dsStats DSStatsThreadsafe, lastStatEndTimes map[enum.CacheName]time.Time, lastStatDurationsThreadsafe DurationMapThreadsafe, unpolledCaches UnpolledCachesThreadsafe, ) { statHistory := statHistoryThreadsafe.Get().Copy() maxStats := statHistoryThreadsafe.Max() for _, result := range results { // TODO determine if we want to add results with errors, or just print the errors now and don't add them. statHistory[enum.CacheName(result.Id)] = pruneHistory(append(statHistory[enum.CacheName(result.Id)], result), maxStats) } statHistoryThreadsafe.Set(statHistory) for _, result := range results { log.Debugf("poll %v %v CreateStats start\n", result.PollID, time.Now()) } newDsStats, newLastStats, err := ds.CreateStats(statHistory, toData, combinedStates, lastStats.Get().Copy(), time.Now()) for _, result := range results { log.Debugf("poll %v %v CreateStats end\n", result.PollID, time.Now()) } if err != nil { errorCount.Inc() log.Errorf("getting deliveryservice: %v\n", err) } else { dsStats.Set(newDsStats) lastStats.Set(newLastStats) } endTime := time.Now() lastStatDurations := lastStatDurationsThreadsafe.Get().Copy() for _, result := range results { if lastStatStart, ok := lastStatEndTimes[enum.CacheName(result.Id)]; ok { d := time.Since(lastStatStart) lastStatDurations[enum.CacheName(result.Id)] = d } lastStatEndTimes[enum.CacheName(result.Id)] = endTime // log.Debugf("poll %v %v statfinish\n", result.PollID, endTime) result.PollFinished <- result.PollID } lastStatDurationsThreadsafe.Set(lastStatDurations) unpolledCaches.SetPolled(results, lastStats) }
func (handler OpsConfigFileHandler) Listen() { for { result := <-handler.ResultChannel var toc OpsConfig err := json.Unmarshal(result.([]byte), &toc) if err != nil { log.Errorf("unmarshalling JSON: %s\n", err) } else { handler.OpsConfigChannel <- toc } } }
// precompute does the calculations which are possible with only this one cache result. func (handler Handler) precompute(result Result) Result { todata := handler.ToData.Get() stats := map[enum.DeliveryServiceName]dsdata.Stat{} var err error if result.PrecomputedData.OutBytes, err = outBytes(result.Astats.System.ProcNetDev, result.Astats.System.InfName, handler.MultipleSpaceRegex); err != nil { result.PrecomputedData.OutBytes = 0 log.Errorf("addkbps %s handle precomputing outbytes '%v'\n", result.Id, err) } kbpsInMbps := int64(1000) result.PrecomputedData.MaxKbps = int64(result.Astats.System.InfSpeed) * kbpsInMbps for stat, value := range result.Astats.Ats { var err error stats, err = processStat(result.Id, stats, todata, stat, value, result.Time) if err != nil && err != dsdata.ErrNotProcessedStat { log.Errorf("precomputing cache %v stat %v value %v error %v", result.Id, stat, value, err) result.PrecomputedData.Errors = append(result.PrecomputedData.Errors, err) } } result.PrecomputedData.DeliveryServiceStats = stats return result }
// addCachePerSecStats calculates the cache per-second stats, adds them to LastStats, and returns the augmented object. func addCachePerSecStats(cacheName enum.CacheName, results []cache.Result, lastStats LastStats) LastStats { outBytes, outBytesTime, err := latestBytes(results) // it's ok if `latestBytes` returns 0s with an error, `addLastStat` will refrain from setting it (unless the previous calculation was nonzero, in which case it will error appropriately). if err != nil { log.Warnf("while computing delivery service data for cache %v: %v\n", cacheName, err) } lastStat := lastStats.Caches[cacheName] // if lastStats.Caches[cacheName] doesn't exist, it will be zero-constructed, and `addLastStat` will refrain from setting the PerSec for zero LastStats lastStat.Bytes, err = addLastStat(lastStat.Bytes, outBytes, outBytesTime) if err != nil { log.Errorf("while computing delivery service data for cache %v: %v\n", cacheName, err) return lastStats } lastStats.Caches[cacheName] = lastStat return lastStats }
func addAvailableData(dsStats Stats, crStates peer.Crstates, serverCachegroups map[enum.CacheName]enum.CacheGroupName, serverDs map[enum.CacheName][]enum.DeliveryServiceName, serverTypes map[enum.CacheName]enum.CacheType, statHistory map[enum.CacheName][]cache.Result) (Stats, error) { for cache, available := range crStates.Caches { cacheGroup, ok := serverCachegroups[cache] if !ok { log.Warnf("CreateStats not adding availability data for '%s': not found in Cachegroups\n", cache) continue } deliveryServices, ok := serverDs[cache] if !ok { log.Warnf("CreateStats not adding availability data for '%s': not found in DeliveryServices\n", cache) continue } cacheType, ok := serverTypes[enum.CacheName(cache)] if !ok { log.Warnf("CreateStats not adding availability data for '%s': not found in Server Types\n", cache) continue } for _, deliveryService := range deliveryServices { if deliveryService == "" { log.Errorf("EMPTY addAvailableData DS") // various bugs in other functions can cause this - this will help identify and debug them. continue } stat, ok := dsStats.DeliveryService[enum.DeliveryServiceName(deliveryService)] if !ok { log.Warnf("CreateStats not adding availability data for '%s': not found in Stats\n", cache) continue // TODO log warning? Error? } if available.IsAvailable { // c.IsAvailable.Value stat.CommonStats.IsAvailable.Value = true stat.CommonStats.CachesAvailableNum.Value++ cacheGroupStats := stat.CacheGroups[enum.CacheGroupName(cacheGroup)] cacheGroupStats.IsAvailable.Value = true stat.CacheGroups[enum.CacheGroupName(cacheGroup)] = cacheGroupStats stat.TotalStats.IsAvailable.Value = true typeStats := stat.Types[cacheType] typeStats.IsAvailable.Value = true stat.Types[cacheType] = typeStats } // TODO fix nested ifs if results, ok := statHistory[enum.CacheName(cache)]; ok { if len(results) < 1 { log.Warnf("no results %v %v\n", cache, deliveryService) } else { result := results[0] if result.PrecomputedData.Reporting { stat.CommonStats.CachesReporting[enum.CacheName(cache)] = true } else { log.Debugf("no reporting %v %v\n", cache, deliveryService) } } } else { log.Debugf("no result for %v %v\n", cache, deliveryService) } dsStats.DeliveryService[enum.DeliveryServiceName(deliveryService)] = stat // TODO Necessary? Remove? } } return dsStats, nil }
// Note the OpsConfigManager is in charge of the httpServer, because ops config changes trigger server changes. If other things needed to trigger server restarts, the server could be put in its own goroutine with signal channels func StartOpsConfigManager( opsConfigFile string, toSession towrap.ITrafficOpsSession, toData todata.TODataThreadsafe, opsConfigChangeSubscribers []chan<- handler.OpsConfig, toChangeSubscribers []chan<- towrap.ITrafficOpsSession, localStates peer.CRStatesThreadsafe, peerStates peer.CRStatesPeersThreadsafe, combinedStates peer.CRStatesThreadsafe, statHistory StatHistoryThreadsafe, lastStats LastStatsThreadsafe, dsStats DSStatsThreadsafe, events EventsThreadsafe, staticAppData StaticAppData, healthPollInterval time.Duration, lastHealthDurations DurationMapThreadsafe, fetchCount UintThreadsafe, healthIteration UintThreadsafe, errorCount UintThreadsafe, localCacheStatus CacheAvailableStatusThreadsafe, unpolledCaches UnpolledCachesThreadsafe, ) OpsConfigThreadsafe { opsConfigFileChannel := make(chan interface{}) opsConfigFilePoller := poller.FilePoller{ File: opsConfigFile, ResultChannel: opsConfigFileChannel, } opsConfigChannel := make(chan handler.OpsConfig) opsConfigFileHandler := handler.OpsConfigFileHandler{ ResultChannel: opsConfigFilePoller.ResultChannel, OpsConfigChannel: opsConfigChannel, } go opsConfigFileHandler.Listen() go opsConfigFilePoller.Poll() opsConfig := NewOpsConfigThreadsafe() // TODO remove change subscribers, give Threadsafes directly to the things that need them. If they only set vars, and don't actually do work on change. go func() { httpServer := http_server.Server{} for { select { case newOpsConfig := <-opsConfigChannel: var err error opsConfig.Set(newOpsConfig) listenAddress := ":80" // default if newOpsConfig.HttpListener != "" { listenAddress = newOpsConfig.HttpListener } handleErr := func(err error) { errorCount.Inc() log.Errorf("OpsConfigManager: %v\n", err) } err = httpServer.Run(func(req http_server.DataRequest) ([]byte, int) { return DataRequest( req, opsConfig, toSession, localStates, peerStates, combinedStates, statHistory, dsStats, events, staticAppData, healthPollInterval, lastHealthDurations, fetchCount, healthIteration, errorCount, toData, localCacheStatus, lastStats, unpolledCaches, ) }, listenAddress) if err != nil { handleErr(fmt.Errorf("MonitorConfigPoller: error creating HTTP server: %s\n", err)) continue } realToSession, err := to.Login(newOpsConfig.Url, newOpsConfig.Username, newOpsConfig.Password, newOpsConfig.Insecure) if err != nil { handleErr(fmt.Errorf("MonitorConfigPoller: error instantiating Session with traffic_ops: %s\n", err)) continue } toSession.Set(realToSession) if err := toData.Fetch(toSession, newOpsConfig.CdnName); err != nil { handleErr(fmt.Errorf("Error getting Traffic Ops data: %v\n", err)) continue } // These must be in a goroutine, because the monitorConfigPoller tick sends to a channel this select listens for. Thus, if we block on sends to the monitorConfigPoller, we have a livelock race condition. // More generically, we're using goroutines as an infinite chan buffer, to avoid potential livelocks for _, subscriber := range opsConfigChangeSubscribers { go func() { subscriber <- newOpsConfig // this is needed for cdnName }() } for _, subscriber := range toChangeSubscribers { go func() { subscriber <- toSession }() } } } }() return opsConfig }
// DataRequest takes an `http_server.DataRequest`, and the monitored data objects, and returns the appropriate response, and the status code. func DataRequest( req http_server.DataRequest, opsConfig OpsConfigThreadsafe, toSession towrap.ITrafficOpsSession, localStates peer.CRStatesThreadsafe, peerStates peer.CRStatesPeersThreadsafe, combinedStates peer.CRStatesThreadsafe, statHistory StatHistoryThreadsafe, dsStats DSStatsThreadsafe, events EventsThreadsafe, staticAppData StaticAppData, healthPollInterval time.Duration, lastHealthDurations DurationMapThreadsafe, fetchCount UintThreadsafe, healthIteration UintThreadsafe, errorCount UintThreadsafe, toData todata.TODataThreadsafe, localCacheStatus CacheAvailableStatusThreadsafe, lastStats LastStatsThreadsafe, unpolledCaches UnpolledCachesThreadsafe, ) (body []byte, responseCode int) { // handleErr takes an error, and the request type it came from, and logs. It is ok to call with a nil error, in which case this is a no-op. handleErr := func(err error) { if err == nil { return } errorCount.Inc() log.Errorf("Request Error: %v\n", fmt.Errorf(req.Type.String()+": %v", err)) } // commonReturn takes the body, err, and the data request Type which has been processed. It logs and deals with any error, and returns the appropriate bytes and response code for the `http_server`. commonReturn := func(body []byte, err error) ([]byte, int) { if err == nil { return body, http.StatusOK } handleErr(err) return nil, http.StatusInternalServerError } if unpolledCaches.Any() { handleErr(fmt.Errorf("service still starting, some caches unpolled")) return []byte("Service Unavailable"), http.StatusServiceUnavailable } var err error switch req.Type { case http_server.TRConfig: cdnName := opsConfig.Get().CdnName if toSession == nil { return commonReturn(nil, fmt.Errorf("Unable to connect to Traffic Ops")) } if cdnName == "" { return commonReturn(nil, fmt.Errorf("No CDN Configured")) } return commonReturn(body, err) case http_server.TRStateDerived: body, err = peer.CrstatesMarshall(combinedStates.Get()) return commonReturn(body, err) case http_server.TRStateSelf: body, err = peer.CrstatesMarshall(localStates.Get()) return commonReturn(body, err) case http_server.CacheStats: filter, err := NewCacheStatFilter(req.Parameters, toData.Get().ServerTypes) if err != nil { handleErr(err) return []byte(err.Error()), http.StatusBadRequest } body, err = cache.StatsMarshall(statHistory.Get(), filter, req.Parameters) return commonReturn(body, err) case http_server.DSStats: filter, err := NewDSStatFilter(req.Parameters, toData.Get().DeliveryServiceTypes) if err != nil { handleErr(err) return []byte(err.Error()), http.StatusBadRequest } body, err = json.Marshal(dsStats.Get().JSON(filter, req.Parameters)) // TODO marshall beforehand, for performance? (test to see how often requests are made) return commonReturn(body, err) case http_server.EventLog: body, err = json.Marshal(JSONEvents{Events: events.Get()}) return commonReturn(body, err) case http_server.PeerStates: filter, err := NewPeerStateFilter(req.Parameters, toData.Get().ServerTypes) if err != nil { handleErr(err) return []byte(err.Error()), http.StatusBadRequest } body, err = json.Marshal(createApiPeerStates(peerStates.Get(), filter, req.Parameters)) return commonReturn(body, err) case http_server.StatSummary: return nil, http.StatusNotImplemented case http_server.Stats: body, err = getStats(staticAppData, healthPollInterval, lastHealthDurations.Get(), fetchCount.Get(), healthIteration.Get(), errorCount.Get()) return commonReturn(body, err) case http_server.ConfigDoc: opsConfigCopy := opsConfig.Get() // if the password is blank, leave it blank, so callers can see it's missing. if opsConfigCopy.Password != "" { opsConfigCopy.Password = "******" } body, err = json.Marshal(opsConfigCopy) return commonReturn(body, err) case http_server.APICacheCount: // TODO determine if this should use peerStates return []byte(strconv.Itoa(len(localStates.Get().Caches))), http.StatusOK case http_server.APICacheAvailableCount: return []byte(strconv.Itoa(cacheAvailableCount(localStates.Get().Caches))), http.StatusOK case http_server.APICacheDownCount: return []byte(strconv.Itoa(cacheDownCount(localStates.Get().Caches))), http.StatusOK case http_server.APIVersion: s := "traffic_monitor-" + staticAppData.Version + "." if len(staticAppData.GitRevision) > 6 { s += staticAppData.GitRevision[:6] } else { s += staticAppData.GitRevision } return []byte(s), http.StatusOK case http_server.APITrafficOpsURI: return []byte(opsConfig.Get().Url), http.StatusOK case http_server.APICacheStates: body, err = json.Marshal(createCacheStatuses(toData.Get().ServerTypes, statHistory.Get(), lastHealthDurations.Get(), localStates.Get().Caches, lastStats.Get(), localCacheStatus)) return commonReturn(body, err) case http_server.APIBandwidthKbps: serverTypes := toData.Get().ServerTypes kbpsStats := lastStats.Get() sum := float64(0.0) for cache, data := range kbpsStats.Caches { if serverTypes[cache] != enum.CacheTypeEdge { continue } sum += data.Bytes.PerSec / ds.BytesPerKilobit } return []byte(fmt.Sprintf("%f", sum)), http.StatusOK case http_server.APIBandwidthCapacityKbps: statHistory := statHistory.Get() cap := int64(0) for _, results := range statHistory { if len(results) == 0 { continue } cap += results[0].MaxKbps } return []byte(fmt.Sprintf("%d", cap)), http.StatusOK default: return commonReturn(nil, fmt.Errorf("Unknown Request Type")) } }