package server import ( "bytes" "encoding/json" "net/http" "runtime" "sync" "time" linuxproc "github.com/c9s/goprocinfo/linux" ) type StatsData struct { updateMu sync.Mutex StatsDataVersion int StartTime time.Time Uptime string BuildTime string BuildHash string CachedStatsLastUpdate time.Time Health struct { IRC bool Backend map[string]time.Time } CurrentClientCount uint64 LiveClientCount uint64 PubSubChannelCount int SysMemTotalKB uint64 SysMemFreeKB uint64 MemoryInUseKB uint64 MemoryRSSKB uint64 ResponseCacheItems int MemPerClientBytes uint64 CpuUsagePct float64 ClientConnectsTotal uint64 ClientDisconnectsTotal uint64 ClientVersions map[string]uint64 DisconnectCodes map[string]uint64 CommandsIssuedTotal uint64 CommandsIssuedMap map[Command]uint64 MessagesSent uint64 EmotesReportedTotal uint64 BackendVerifyFails uint64 // DisconnectReasons is at the bottom because it has indeterminate size DisconnectReasons map[string]uint64 } // Statistics is several variables that get incremented during normal operation of the server. // Its structure should be versioned as it is exposed via JSON. // // Note as to threaded access - this is soft/fun data and not critical to data integrity. // Fix anything that -race turns up, but otherwise it's not too much of a problem. var Statistics = newStatsData() // CommandCounter is a channel for race-free counting of command usage. var CommandCounter = make(chan Command, 10) // commandCounter receives from the CommandCounter channel and uses the value to increment the values in Statistics. // is_init_func func commandCounter() { for cmd := range CommandCounter { Statistics.CommandsIssuedTotal++ Statistics.CommandsIssuedMap[cmd]++ } } // StatsDataVersion is the version of the StatsData struct. const StatsDataVersion = 8 const pageSize = 4096 var cpuUsage struct { UserTime uint64 SysTime uint64 } func newStatsData() *StatsData { return &StatsData{ StartTime: time.Now(), CommandsIssuedMap: make(map[Command]uint64), DisconnectCodes: make(map[string]uint64), DisconnectReasons: make(map[string]uint64), ClientVersions: make(map[string]uint64), StatsDataVersion: StatsDataVersion, Health: struct { IRC bool Backend map[string]time.Time }{ Backend: make(map[string]time.Time), }, } } // SetBuildStamp should be called from the main package to identify the git build hash and build time. func SetBuildStamp(buildTime, buildHash string) { Statistics.BuildTime = buildTime Statistics.BuildHash = buildHash } func updateStatsIfNeeded() { Statistics.updateMu.Lock() defer Statistics.updateMu.Unlock() if time.Now().Add(-2 * time.Second).After(Statistics.CachedStatsLastUpdate) { updatePeriodicStats() } } func updatePeriodicStats() { nowUpdate := time.Now() timeDiff := nowUpdate.Sub(Statistics.CachedStatsLastUpdate) Statistics.CachedStatsLastUpdate = nowUpdate { m := runtime.MemStats{} runtime.ReadMemStats(&m) Statistics.MemoryInUseKB = m.Alloc / 1024 } { pstat, err := linuxproc.ReadProcessStat("/proc/self/stat") if err == nil { userTicks := pstat.Utime - cpuUsage.UserTime sysTicks := pstat.Stime - cpuUsage.SysTime cpuUsage.UserTime = pstat.Utime cpuUsage.SysTime = pstat.Stime Statistics.CpuUsagePct = 100 * float64(userTicks+sysTicks) / (timeDiff.Seconds() * float64(ticksPerSecond)) Statistics.MemoryRSSKB = uint64(pstat.Rss * pageSize / 1024) Statistics.MemPerClientBytes = (Statistics.MemoryRSSKB * 1024) / (Statistics.CurrentClientCount + 1) } updateSysMem() } { ChatSubscriptionLock.RLock() Statistics.PubSubChannelCount = len(ChatSubscriptionInfo) ChatSubscriptionLock.RUnlock() GlobalSubscriptionLock.RLock() Statistics.LiveClientCount = uint64(len(GlobalSubscriptionInfo)) versions := make(map[string]uint64) for _, v := range GlobalSubscriptionInfo { versions[v.VersionString]++ } Statistics.ClientVersions = versions GlobalSubscriptionLock.RUnlock() } { Statistics.Uptime = nowUpdate.Sub(Statistics.StartTime).String() Statistics.ResponseCacheItems = Backend.responseCache.ItemCount() } { Statistics.Health.IRC = authIrcConnection.Connected() Backend.lastSuccessLock.Lock() for k, v := range Backend.lastSuccess { Statistics.Health.Backend[k] = v } Backend.lastSuccessLock.Unlock() } } var sysMemLastUpdate time.Time var sysMemUpdateLock sync.Mutex // updateSysMem reads the system's available RAM. func updateSysMem() { if time.Now().Add(-2 * time.Second).After(sysMemLastUpdate) { sysMemUpdateLock.Lock() defer sysMemUpdateLock.Unlock() if !time.Now().Add(-2 * time.Second).After(sysMemLastUpdate) { return } } else { return } sysMemLastUpdate = time.Now() memInfo, err := linuxproc.ReadMemInfo("/proc/meminfo") if err == nil { Statistics.SysMemTotalKB = memInfo.MemTotal Statistics.SysMemFreeKB = memInfo.MemAvailable } { writeHLL() } } // HTTPShowStatistics handles the /stats endpoint. It writes out the Statistics object as indented JSON. func HTTPShowStatistics(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") updateStatsIfNeeded() jsonBytes, _ := json.Marshal(Statistics) outBuf := bytes.NewBuffer(nil) json.Indent(outBuf, jsonBytes, "", "\t") outBuf.WriteTo(w) }