From ee09a39de5ee1d885033beb93d5f576f51b01507 Mon Sep 17 00:00:00 2001 From: oofdog <46097282+oofdog@users.noreply.github.com> Date: Wed, 3 Mar 2021 14:05:14 -0500 Subject: [PATCH] Add CSGO SRCDS input plugin (#8525) --- docs/LICENSE_OF_DEPENDENCIES.md | 1 + go.mod | 1 + go.sum | 2 + plugins/inputs/all/all.go | 1 + plugins/inputs/csgo/README.md | 37 ++++++ plugins/inputs/csgo/csgo.go | 192 +++++++++++++++++++++++++++++++ plugins/inputs/csgo/csgo_test.go | 54 +++++++++ 7 files changed, 288 insertions(+) create mode 100644 plugins/inputs/csgo/README.md create mode 100644 plugins/inputs/csgo/csgo.go create mode 100644 plugins/inputs/csgo/csgo_test.go diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md index 657c63276..ee4cbb665 100644 --- a/docs/LICENSE_OF_DEPENDENCIES.md +++ b/docs/LICENSE_OF_DEPENDENCIES.md @@ -99,6 +99,7 @@ following works: - github.com/influxdata/wlog [MIT License](https://github.com/influxdata/wlog/blob/master/LICENSE) - github.com/jackc/pgx [MIT License](https://github.com/jackc/pgx/blob/master/LICENSE) - github.com/jaegertracing/jaeger [Apache License 2.0](https://github.com/jaegertracing/jaeger/blob/master/LICENSE) +- github.com/james4k/rcon [MIT License](https://github.com/james4k/rcon/blob/master/LICENSE) - github.com/jcmturner/gofork [BSD 3-Clause "New" or "Revised" License](https://github.com/jcmturner/gofork/blob/master/LICENSE) - github.com/jmespath/go-jmespath [Apache License 2.0](https://github.com/jmespath/go-jmespath/blob/master/LICENSE) - github.com/jpillora/backoff [MIT License](https://github.com/jpillora/backoff/blob/master/LICENSE) diff --git a/go.mod b/go.mod index fce100462..d8e19b95a 100644 --- a/go.mod +++ b/go.mod @@ -85,6 +85,7 @@ require ( github.com/influxdata/wlog v0.0.0-20160411224016-7c63b0a71ef8 github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect github.com/jackc/pgx v3.6.0+incompatible + github.com/james4k/rcon v0.0.0-20120923215419-8fbb8268b60a github.com/kardianos/service v1.0.0 github.com/karrick/godirwalk v1.16.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 diff --git a/go.sum b/go.sum index 6c48bcd5c..7085775b0 100644 --- a/go.sum +++ b/go.sum @@ -406,6 +406,8 @@ github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGU github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= github.com/jackc/pgx v3.6.0+incompatible h1:bJeo4JdVbDAW8KB2m8XkFeo8CPipREoG37BwEoKGz+Q= github.com/jackc/pgx v3.6.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/james4k/rcon v0.0.0-20120923215419-8fbb8268b60a h1:JxcWget6X/VfBMKxPIc28Jel37LGREut2fpV+ObkwJ0= +github.com/james4k/rcon v0.0.0-20120923215419-8fbb8268b60a/go.mod h1:1qNVsDcmNQDsAXYfUuF/Z0rtK5eT8x9D6Pi7S3PjXAg= github.com/jaegertracing/jaeger v1.15.1 h1:7QzNAXq+4ko9GtCjozDNAp2uonoABu+B2Rk94hjQcp4= github.com/jaegertracing/jaeger v1.15.1/go.mod h1:LUWPSnzNPGRubM8pk0inANGitpiMOOxihXx0+53llXI= github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index dd3474d25..595be84ca 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -29,6 +29,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/couchbase" _ "github.com/influxdata/telegraf/plugins/inputs/couchdb" _ "github.com/influxdata/telegraf/plugins/inputs/cpu" + _ "github.com/influxdata/telegraf/plugins/inputs/csgo" _ "github.com/influxdata/telegraf/plugins/inputs/dcos" _ "github.com/influxdata/telegraf/plugins/inputs/directory_monitor" _ "github.com/influxdata/telegraf/plugins/inputs/disk" diff --git a/plugins/inputs/csgo/README.md b/plugins/inputs/csgo/README.md new file mode 100644 index 000000000..ad8003006 --- /dev/null +++ b/plugins/inputs/csgo/README.md @@ -0,0 +1,37 @@ +# CSGO Input Plugin + +The `csgo` plugin gather metrics from CSGO servers. + +#### Configuration +```toml +[[inputs.csgo]] + ## Specify servers using the following format: + ## servers = [ + ## ["ip1:port1", "rcon_password1"], + ## ["ip2:port2", "rcon_password2"], + ## ] + # + ## If no servers are specified, no data will be collected + servers = [] +``` + +### Metrics + +The plugin retrieves the output of the `stats` command that is executed via rcon. + +If no servers are specified, no data will be collected + +- csgo + - tags: + - host + - fields: + - cpu (float) + - net_in (float) + - net_out (float) + - uptime_minutes (float) + - maps (float) + - fps (float) + - players (float) + - sv_ms (float) + - variance_ms (float) + - tick_ms (float) diff --git a/plugins/inputs/csgo/csgo.go b/plugins/inputs/csgo/csgo.go new file mode 100644 index 000000000..fe8296266 --- /dev/null +++ b/plugins/inputs/csgo/csgo.go @@ -0,0 +1,192 @@ +package csgo + +import ( + "encoding/json" + "errors" + "strconv" + "strings" + "sync" + "time" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" + "github.com/james4k/rcon" +) + +type statsData struct { + CPU float64 `json:"cpu"` + NetIn float64 `json:"net_in"` + NetOut float64 `json:"net_out"` + UptimeMinutes float64 `json:"uptime_minutes"` + Maps float64 `json:"maps"` + FPS float64 `json:"fps"` + Players float64 `json:"players"` + Sim float64 `json:"sv_ms"` + Variance float64 `json:"variance_ms"` + Tick float64 `json:"tick_ms"` +} + +type CSGO struct { + Servers [][]string `toml:"servers"` +} + +func (_ *CSGO) Description() string { + return "Fetch metrics from a CSGO SRCDS" +} + +var sampleConfig = ` + ## Specify servers using the following format: + ## servers = [ + ## ["ip1:port1", "rcon_password1"], + ## ["ip2:port2", "rcon_password2"], + ## ] + # + ## If no servers are specified, no data will be collected + servers = [] +` + +func (_ *CSGO) SampleConfig() string { + return sampleConfig +} + +func (s *CSGO) Gather(acc telegraf.Accumulator) error { + var wg sync.WaitGroup + + // Loop through each server and collect metrics + for _, server := range s.Servers { + wg.Add(1) + go func(ss []string) { + defer wg.Done() + acc.AddError(s.gatherServer(ss, requestServer, acc)) + }(server) + } + + wg.Wait() + return nil +} + +func init() { + inputs.Add("csgo", func() telegraf.Input { + return &CSGO{} + }) +} + +func (s *CSGO) gatherServer( + server []string, + request func(string, string) (string, error), + acc telegraf.Accumulator) error { + + if len(server) != 2 { + return errors.New("incorrect server config") + } + + url, rconPw := server[0], server[1] + resp, err := request(url, rconPw) + if err != nil { + return err + } + + rows := strings.Split(resp, "\n") + if len(rows) < 2 { + return errors.New("bad response") + } + + fields := strings.Fields(rows[1]) + if len(fields) != 10 { + return errors.New("bad response") + } + + cpu, err := strconv.ParseFloat(fields[0], 32) + if err != nil { + return err + } + netIn, err := strconv.ParseFloat(fields[1], 64) + if err != nil { + return err + } + netOut, err := strconv.ParseFloat(fields[2], 64) + if err != nil { + return err + } + uptimeMinutes, err := strconv.ParseFloat(fields[3], 64) + if err != nil { + return err + } + maps, err := strconv.ParseFloat(fields[4], 64) + if err != nil { + return err + } + fps, err := strconv.ParseFloat(fields[5], 64) + if err != nil { + return err + } + players, err := strconv.ParseFloat(fields[6], 64) + if err != nil { + return err + } + svms, err := strconv.ParseFloat(fields[7], 64) + if err != nil { + return err + } + msVar, err := strconv.ParseFloat(fields[8], 64) + if err != nil { + return err + } + tick, err := strconv.ParseFloat(fields[9], 64) + if err != nil { + return err + } + + now := time.Now() + stats := statsData{ + CPU: cpu, + NetIn: netIn, + NetOut: netOut, + UptimeMinutes: uptimeMinutes, + Maps: maps, + FPS: fps, + Players: players, + Sim: svms, + Variance: msVar, + Tick: tick, + } + + tags := map[string]string{ + "host": url, + } + + var statsMap map[string]interface{} + marshalled, err := json.Marshal(stats) + if err != nil { + return err + } + err = json.Unmarshal(marshalled, &statsMap) + if err != nil { + return err + } + + acc.AddGauge("csgo", statsMap, tags, now) + return nil +} + +func requestServer(url string, rconPw string) (string, error) { + remoteConsole, err := rcon.Dial(url, rconPw) + if err != nil { + return "", err + } + defer remoteConsole.Close() + + reqId, err := remoteConsole.Write("stats") + if err != nil { + return "", err + } + + resp, respReqId, err := remoteConsole.Read() + if err != nil { + return "", err + } else if reqId != respReqId { + return "", errors.New("response/request mismatch") + } else { + return resp, nil + } +} diff --git a/plugins/inputs/csgo/csgo_test.go b/plugins/inputs/csgo/csgo_test.go new file mode 100644 index 000000000..311e4b2b6 --- /dev/null +++ b/plugins/inputs/csgo/csgo_test.go @@ -0,0 +1,54 @@ +package csgo + +import ( + "github.com/influxdata/telegraf/testutil" + "testing" + + "github.com/stretchr/testify/assert" +) + +const testInput = `CPU NetIn NetOut Uptime Maps FPS Players Svms +-ms ~tick +10.0 1.2 3.4 100 1 120.20 15 5.23 0.01 0.02` + +var ( + expectedOutput = statsData{ + 10.0, 1.2, 3.4, 100.0, 1, 120.20, 15, 5.23, 0.01, 0.02, + } +) + +func TestCPUStats(t *testing.T) { + c := NewCSGOStats() + var acc testutil.Accumulator + err := c.gatherServer(c.Servers[0], requestMock, &acc) + if err != nil { + t.Error(err) + } + + if !acc.HasMeasurement("csgo") { + t.Errorf("acc.HasMeasurement: expected csgo") + } + + assert.Equal(t, "1.2.3.4:1234", acc.Metrics[0].Tags["host"]) + assert.Equal(t, expectedOutput.CPU, acc.Metrics[0].Fields["cpu"]) + assert.Equal(t, expectedOutput.NetIn, acc.Metrics[0].Fields["net_in"]) + assert.Equal(t, expectedOutput.NetOut, acc.Metrics[0].Fields["net_out"]) + assert.Equal(t, expectedOutput.UptimeMinutes, acc.Metrics[0].Fields["uptime_minutes"]) + assert.Equal(t, expectedOutput.Maps, acc.Metrics[0].Fields["maps"]) + assert.Equal(t, expectedOutput.FPS, acc.Metrics[0].Fields["fps"]) + assert.Equal(t, expectedOutput.Players, acc.Metrics[0].Fields["players"]) + assert.Equal(t, expectedOutput.Sim, acc.Metrics[0].Fields["sv_ms"]) + assert.Equal(t, expectedOutput.Variance, acc.Metrics[0].Fields["variance_ms"]) + assert.Equal(t, expectedOutput.Tick, acc.Metrics[0].Fields["tick_ms"]) +} + +func requestMock(_ string, _ string) (string, error) { + return testInput, nil +} + +func NewCSGOStats() *CSGO { + return &CSGO{ + Servers: [][]string{ + {"1.2.3.4:1234", "password"}, + }, + } +}