2015-11-16 13:07:02 -08:00
package server
2015-11-16 13:25:25 -08:00
import (
"bytes"
"encoding/json"
2020-01-31 21:35:36 -08:00
"fmt"
"io"
2015-11-16 13:25:25 -08:00
"net/http"
2015-11-16 21:15:29 -08:00
"runtime"
2015-11-17 11:11:14 -08:00
"sync"
2015-11-16 21:15:29 -08:00
"time"
linuxproc "github.com/c9s/goprocinfo/linux"
2015-11-16 13:25:25 -08:00
)
2015-11-16 13:07:02 -08:00
type StatsData struct {
2017-09-15 15:55:42 -07:00
updateMu sync . Mutex
2015-11-16 22:56:40 -08:00
StatsDataVersion int
2015-11-16 22:46:19 -08:00
StartTime time . Time
2015-11-16 23:15:38 -08:00
Uptime string
2015-11-16 22:56:40 -08:00
BuildTime string
BuildHash string
2015-11-16 21:57:18 -08:00
CachedStatsLastUpdate time . Time
2015-11-16 20:36:50 -08:00
2016-06-02 08:16:16 -07:00
Health struct {
IRC bool
Backend map [ string ] time . Time
}
2015-11-16 21:15:29 -08:00
CurrentClientCount uint64
2017-09-15 13:41:18 -07:00
LiveClientCount uint64
2015-11-16 21:15:29 -08:00
2015-11-16 21:57:18 -08:00
PubSubChannelCount int
2017-09-25 15:24:58 -07:00
ResponseCacheItems int
2015-11-16 21:57:18 -08:00
2020-01-31 21:35:36 -08:00
MemPerClientBytes uint64
SysMemTotalKB uint64
SysMemFreeKB uint64
MemoryInUseKB uint64
MemoryRSSKB uint64
CpuUsagePct float64
2015-11-16 21:57:18 -08:00
2015-11-16 21:15:29 -08:00
ClientConnectsTotal uint64
ClientDisconnectsTotal uint64
2016-04-20 14:11:12 -07:00
ClientVersions map [ string ] uint64
2015-11-16 21:15:29 -08:00
DisconnectCodes map [ string ] uint64
2015-11-16 20:35:03 -08:00
2015-11-16 21:15:29 -08:00
CommandsIssuedTotal uint64
CommandsIssuedMap map [ Command ] uint64
2015-11-16 13:07:02 -08:00
2015-11-16 21:15:29 -08:00
MessagesSent uint64
2015-11-16 13:07:02 -08:00
2015-11-16 21:57:18 -08:00
EmotesReportedTotal uint64
2015-11-16 13:25:25 -08:00
2015-11-21 12:10:01 -08:00
BackendVerifyFails uint64
2015-11-16 20:36:50 -08:00
// DisconnectReasons is at the bottom because it has indeterminate size
2015-11-16 21:15:29 -08:00
DisconnectReasons map [ string ] uint64
2015-11-16 13:07:02 -08:00
}
2015-11-16 21:15:29 -08:00
// Statistics is several variables that get incremented during normal operation of the server.
// Its structure should be versioned as it is exposed via JSON.
2015-11-16 21:57:18 -08:00
//
// Note as to threaded access - this is soft/fun data and not critical to data integrity.
2015-12-23 21:56:56 -08:00
// Fix anything that -race turns up, but otherwise it's not too much of a problem.
2015-11-16 21:15:29 -08:00
var Statistics = newStatsData ( )
2015-12-23 21:56:56 -08:00
// CommandCounter is a channel for race-free counting of command usage.
2015-12-16 11:58:48 -08:00
var CommandCounter = make ( chan Command , 10 )
2015-12-23 21:56:56 -08:00
// commandCounter receives from the CommandCounter channel and uses the value to increment the values in Statistics.
// is_init_func
2015-12-16 11:58:48 -08:00
func commandCounter ( ) {
for cmd := range CommandCounter {
Statistics . CommandsIssuedTotal ++
Statistics . CommandsIssuedMap [ cmd ] ++
}
}
2016-01-17 18:01:21 -08:00
// StatsDataVersion is the version of the StatsData struct.
2017-09-25 14:28:21 -07:00
const StatsDataVersion = 8
2015-11-16 21:15:29 -08:00
const pageSize = 4096
var cpuUsage struct {
UserTime uint64
2015-11-16 23:15:38 -08:00
SysTime uint64
2015-11-16 21:15:29 -08:00
}
2015-11-16 13:25:25 -08:00
2015-11-16 13:07:02 -08:00
func newStatsData ( ) * StatsData {
return & StatsData {
2015-11-16 22:46:19 -08:00
StartTime : time . Now ( ) ,
2015-11-16 21:15:29 -08:00
CommandsIssuedMap : make ( map [ Command ] uint64 ) ,
DisconnectCodes : make ( map [ string ] uint64 ) ,
DisconnectReasons : make ( map [ string ] uint64 ) ,
2016-04-20 14:11:12 -07:00
ClientVersions : make ( map [ string ] uint64 ) ,
2015-11-16 22:56:40 -08:00
StatsDataVersion : StatsDataVersion ,
2016-06-02 08:59:40 -07:00
Health : struct {
IRC bool
Backend map [ string ] time . Time
} {
Backend : make ( map [ string ] time . Time ) ,
} ,
2015-11-16 13:07:02 -08:00
}
}
2015-11-17 11:01:42 -08:00
// SetBuildStamp should be called from the main package to identify the git build hash and build time.
2015-11-16 22:56:40 -08:00
func SetBuildStamp ( buildTime , buildHash string ) {
Statistics . BuildTime = buildTime
Statistics . BuildHash = buildHash
}
2015-11-16 21:15:29 -08:00
func updateStatsIfNeeded ( ) {
2017-09-15 15:55:42 -07:00
Statistics . updateMu . Lock ( )
defer Statistics . updateMu . Unlock ( )
2015-11-16 21:15:29 -08:00
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 )
2015-11-17 12:16:46 -08:00
Statistics . MemoryInUseKB = m . Alloc / 1024
2015-11-16 21:15:29 -08:00
}
{
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
2015-11-16 23:15:38 -08:00
Statistics . CpuUsagePct = 100 * float64 ( userTicks + sysTicks ) / ( timeDiff . Seconds ( ) * float64 ( ticksPerSecond ) )
2015-11-17 12:16:46 -08:00
Statistics . MemoryRSSKB = uint64 ( pstat . Rss * pageSize / 1024 )
2015-12-23 21:55:15 -08:00
Statistics . MemPerClientBytes = ( Statistics . MemoryRSSKB * 1024 ) / ( Statistics . CurrentClientCount + 1 )
2015-11-16 21:15:29 -08:00
}
2015-11-17 11:01:42 -08:00
updateSysMem ( )
2015-11-16 21:15:29 -08:00
}
2015-11-16 21:57:18 -08:00
{
ChatSubscriptionLock . RLock ( )
Statistics . PubSubChannelCount = len ( ChatSubscriptionInfo )
ChatSubscriptionLock . RUnlock ( )
2015-11-18 09:07:34 -08:00
GlobalSubscriptionLock . RLock ( )
2016-04-20 14:11:12 -07:00
2017-09-15 13:07:41 -07:00
Statistics . LiveClientCount = uint64 ( len ( GlobalSubscriptionInfo ) )
2016-04-20 14:11:12 -07:00
versions := make ( map [ string ] uint64 )
for _ , v := range GlobalSubscriptionInfo {
versions [ v . VersionString ] ++
}
Statistics . ClientVersions = versions
2015-11-18 09:07:34 -08:00
GlobalSubscriptionLock . RUnlock ( )
2015-11-16 21:57:18 -08:00
}
2015-11-16 22:46:19 -08:00
{
2015-11-16 23:15:38 -08:00
Statistics . Uptime = nowUpdate . Sub ( Statistics . StartTime ) . String ( )
2017-09-25 15:24:58 -07:00
Statistics . ResponseCacheItems = Backend . responseCache . ItemCount ( )
2015-11-16 22:46:19 -08:00
}
2016-06-02 08:16:16 -07:00
{
Statistics . Health . IRC = authIrcConnection . Connected ( )
2016-06-02 08:59:40 -07:00
Backend . lastSuccessLock . Lock ( )
for k , v := range Backend . lastSuccess {
Statistics . Health . Backend [ k ] = v
}
Backend . lastSuccessLock . Unlock ( )
2016-06-02 08:16:16 -07:00
}
2015-11-16 21:15:29 -08:00
}
2015-11-16 13:25:25 -08:00
2015-11-17 11:01:42 -08:00
var sysMemLastUpdate time . Time
var sysMemUpdateLock sync . Mutex
2015-12-23 21:56:56 -08:00
// updateSysMem reads the system's available RAM.
2015-11-17 11:01:42 -08:00
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 {
2015-11-17 11:11:14 -08:00
Statistics . SysMemTotalKB = memInfo . MemTotal
Statistics . SysMemFreeKB = memInfo . MemAvailable
2016-01-15 20:54:30 -08:00
}
2015-12-23 21:57:33 -08:00
2016-01-15 20:54:30 -08:00
{
writeHLL ( )
2015-11-17 11:01:42 -08:00
}
}
2015-12-23 21:56:56 -08:00
// HTTPShowStatistics handles the /stats endpoint. It writes out the Statistics object as indented JSON.
2017-09-15 16:30:11 -07:00
func HTTPShowStatistics ( w http . ResponseWriter , _ * http . Request ) {
2015-11-16 13:25:25 -08:00
w . Header ( ) . Set ( "Content-Type" , "application/json" )
2015-11-16 21:15:29 -08:00
updateStatsIfNeeded ( )
2015-11-16 13:25:25 -08:00
jsonBytes , _ := json . Marshal ( Statistics )
outBuf := bytes . NewBuffer ( nil )
json . Indent ( outBuf , jsonBytes , "" , "\t" )
outBuf . WriteTo ( w )
}
2020-01-31 21:35:36 -08:00
func HTTPShowStatisticsPrometheus ( w http . ResponseWriter , _ * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "text/plain; version=0.0.4; charset=utf-8" )
updateStatsIfNeeded ( )
Statistics . RenderPrometheus ( w )
}
const (
mGauge = "gauge"
mCounter = "counter"
)
func writeManualMetric ( w io . Writer , name string , value float64 , help , typ string ) {
fmt . Fprintf ( w , "# HELP %s %s\n# TYPE %s %s\n%s %v\n" , name , help , name , typ , name , value )
}
func writeLabelledStart ( w io . Writer , name string , help , typ string ) {
fmt . Fprintf ( w , "# HELP %s %s\n# TYPE %s %s\n" , name , help , name , typ )
}
func writeLabelledValue ( w io . Writer , name string , value float64 , labelKey , labelValue string ) {
fmt . Fprintf ( w , "%s{%s=%q} %v\n" , name , labelKey , labelValue , value )
}
func boolToFloat64 ( v bool ) float64 {
if v {
return 1
}
return 0
}
func ( s * StatsData ) RenderPrometheus ( w io . Writer ) {
writeManualMetric ( w , "process_start_time_seconds" , float64 ( s . StartTime . Unix ( ) ) , "Start time of the process since unix epoch in seconds." , mGauge )
// build stamp
writeLabelledStart ( w , "frankerfacez_build_hash" , "Compilation git version hash of the socket server." , mGauge )
writeLabelledValue ( w , "frankerfacez_build_hash" , 1 , "hash" , s . BuildHash )
writeLabelledStart ( w , "frankerfacez_build_time" , "Compilation timestamp of the socket server as a string." , mGauge )
writeLabelledValue ( w , "frankerfacez_build_time" , 1 , "time" , s . BuildTime )
writeManualMetric ( w , "frankerfacez_health_irc" , boolToFloat64 ( s . Health . IRC ) , "State of the Twitch IRC health checks." , mGauge )
// todo: backend last seen
// connections
writeManualMetric ( w , "frankerfacez_clients_current" , float64 ( s . CurrentClientCount ) , "Number of current websocket connections to the socket server." , mGauge )
writeManualMetric ( w , "frankerfacez_clients_live" , float64 ( s . LiveClientCount ) , "Number of live (completed handshake) websocket connections to the socket server." , mGauge )
writeManualMetric ( w , "frankerfacez_clients_connects_total" , float64 ( s . ClientConnectsTotal ) , "Number of connections initiated to the socket server." , mCounter )
writeManualMetric ( w , "frankerfacez_clients_disconnects_total" , float64 ( s . ClientConnectsTotal ) , "Number of connections to the socket server that have ended." , mCounter )
writeManualMetric ( w , "frankerfacez_pubsub_channels" , float64 ( s . PubSubChannelCount ) , "Number of publish/subscribe channels the socket server knows about." , mGauge )
writeManualMetric ( w , "frankerfacez_pubsub_response_cache_items" , float64 ( s . ResponseCacheItems ) , "Number of entries in the command response cache." , mGauge )
// memory stats
writeManualMetric ( w , "frankerfacez_memory_per_client_bytes" , float64 ( s . MemPerClientBytes ) , "Average number of bytes needed to serve a connection." , mGauge )
writeManualMetric ( w , "go_memstats_alloc_bytes" , float64 ( s . MemoryInUseKB * 1024 ) , "Number of bytes allocated and still in use." , mGauge )
writeManualMetric ( w , "process_resident_memory_bytes" , float64 ( s . MemoryRSSKB * 1024 ) , "Resident memory size in bytes." , mGauge )
writeManualMetric ( w , "system_total_memory_bytes" , float64 ( s . SysMemTotalKB * 1024 ) , "Total amount of memory available on the system." , mGauge )
writeManualMetric ( w , "system_free_memory_bytes" , float64 ( s . SysMemFreeKB * 1024 ) , "Total amount of free memory on the system." , mGauge )
writeManualMetric ( w , "process_cpu_recent_ratio" , s . CpuUsagePct / 100 , "Percentage of CPU time used since the last measurement." , mGauge )
writeLabelledStart ( w , "frankerfacez_client_versions" , "Reported version of connected clients." , mGauge )
for version , count := range s . ClientVersions {
writeLabelledValue ( w , "frankerfacez_client_versions" , float64 ( count ) , "version" , version )
}
writeLabelledStart ( w , "frankerfacez_commands_issued" , "Number of times each command has been issued." , mCounter )
for command , count := range s . CommandsIssuedMap {
writeLabelledValue ( w , "frankerfacez_commands_issued" , float64 ( count ) , "command" , string ( command ) )
}
writeManualMetric ( w , "frankerfacez_commands_issued_total" , float64 ( s . CommandsIssuedTotal ) , "Number of times each command has been issued." , mCounter )
writeManualMetric ( w , "frankerfacez_messages_sent_total" , float64 ( s . MessagesSent ) , "Number of times the server has sent a message over the socket." , mCounter )
writeManualMetric ( w , "frankerfacez_backend_verify_fail_total" , float64 ( s . BackendVerifyFails ) , "Number of times the server has failed to securely receive a message from the backend." , mCounter )
}