Browse Source

feat(go.d.plugin): add spigotmc collector (#18890)

Ilya Mashchenko 4 months ago
parent
commit
48bc6d315d

+ 1 - 0
src/go/go.mod

@@ -25,6 +25,7 @@ require (
 	github.com/gofrs/flock v0.12.1
 	github.com/golang/mock v1.6.0
 	github.com/google/uuid v1.6.0
+	github.com/gorcon/rcon v1.3.5
 	github.com/gosnmp/gosnmp v1.38.0
 	github.com/ilyam8/hashstructure v1.1.0
 	github.com/jackc/pgx/v4 v4.18.3

+ 2 - 0
src/go/go.sum

@@ -143,6 +143,8 @@ github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorcon/rcon v1.3.5 h1:YE/Vrw6R99uEP08wp0EjdPAP3Jwz/ys3J8qxI1nYoeU=
+github.com/gorcon/rcon v1.3.5/go.mod h1:zR1qfKZttF8vAgH1NsP6CdpachOvLDq8jE64NboTpIM=
 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
 github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
 github.com/gosnmp/gosnmp v1.38.0 h1:I5ZOMR8kb0DXAFg/88ACurnuwGwYkXWq3eLpJPHMEYc=

+ 1 - 0
src/go/plugin/go.d/README.md

@@ -134,6 +134,7 @@ see the appropriate collector readme.
 | [squid](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/squid)                           |             Squid             |
 | [squidlog](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/squidlog)                     |             Squid             |
 | [smartctl](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/smartctl)                     |   S.M.A.R.T Storage Devices   |
+| [spigotmc](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/spigotmc)                     |           SpigotMC            |
 | [storcli](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/storcli)                       |    Broadcom Hardware RAID     |
 | [supervisord](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/supervisord)               |          Supervisor           |
 | [systemdunits](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/systemdunits)             |      Systemd unit state       |

+ 1 - 0
src/go/plugin/go.d/config/go.d.conf

@@ -107,6 +107,7 @@ modules:
 #  squid: yes
 #  squidlog: yes
 #  smartctl: yes
+#  spigotmc: yes
 #  storcli: yes
 #  supervisord: yes
 #  systemdunits: yes

+ 7 - 0
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf

@@ -132,6 +132,8 @@ classify:
         expr: '{{ and (eq .Port "11334") (eq .Comm "rspamd") }}'
       - tags: "squid"
         expr: '{{ and (eq .Port "3128") (eq .Comm "squid") }}'
+      - tags: "spigotmc"
+        expr: '{{ and (eq .Port "25575") (glob .Cmdline "*spigot*") }}'
       - tags: "supervisord"
         expr: '{{ and (eq .Port "9001") (eq .Comm "supervisord") }}'
       - tags: "tomcat"
@@ -519,6 +521,11 @@ compose:
           module: squid
           name: local
           url: http://{{.Address}}
+      - selector: "spigotmc"
+        template: |
+          module: spigotmc
+          name: local
+          address: {{.Address}}
       - selector: "supervisord"
         template: |
           module: supervisord

+ 6 - 0
src/go/plugin/go.d/config/go.d/spigotmc.conf

@@ -0,0 +1,6 @@
+## All available configuration options, their descriptions and default values:
+## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/spigotmc#readme
+
+#jobs:
+#  - name: local
+#    address: 127.0.0.1:25575

+ 1 - 0
src/go/plugin/go.d/modules/init.go

@@ -98,6 +98,7 @@ import (
 	_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/sensors"
 	_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/smartctl"
 	_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/snmp"
+	_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/spigotmc"
 	_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/squid"
 	_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/squidlog"
 	_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/storcli"

+ 59 - 0
src/go/plugin/go.d/modules/spigotmc/charts.go

@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package spigotmc
+
+import (
+	"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
+)
+
+const (
+	prioPlayers = module.Priority + iota
+	prioTps
+	prioMemory
+)
+
+var charts = module.Charts{
+	playersChart.Copy(),
+	tpsChart.Copy(),
+	memoryChart.Copy(),
+}
+
+var playersChart = module.Chart{
+	ID:       "players",
+	Title:    "Active Players",
+	Units:    "players",
+	Fam:      "players",
+	Ctx:      "spigotmc.players",
+	Priority: prioPlayers,
+	Dims: module.Dims{
+		{ID: "players", Name: "players"},
+	},
+}
+
+var tpsChart = module.Chart{
+	ID:       "avg_tps",
+	Title:    "Average Ticks Per Second",
+	Units:    "ticks",
+	Fam:      "ticks",
+	Ctx:      "spigotmc.avg_tps",
+	Priority: prioTps,
+	Dims: module.Dims{
+		{ID: "tps_1min", Name: "1min", Div: precision},
+		{ID: "tps_5min", Name: "5min", Div: precision},
+		{ID: "tps_15min", Name: "15min", Div: precision},
+	},
+}
+
+var memoryChart = module.Chart{
+	ID:       "memory",
+	Title:    "Memory Usage",
+	Units:    "bytes",
+	Fam:      "mem",
+	Ctx:      "spigotmc.memory",
+	Priority: prioMemory,
+	Type:     module.Area,
+	Dims: module.Dims{
+		{ID: "mem_used", Name: "used"},
+		{ID: "mem_alloc", Name: "alloc"},
+	},
+}

+ 75 - 0
src/go/plugin/go.d/modules/spigotmc/client.go

@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package spigotmc
+
+import (
+	"time"
+
+	"github.com/gorcon/rcon"
+)
+
+type rconConn interface {
+	connect() error
+	disconnect() error
+	queryTps() (string, error)
+	queryList() (string, error)
+}
+
+const (
+	cmdTPS  = "tps"
+	cmdList = "list"
+)
+
+func newRconConn(cfg Config) rconConn {
+	return &rconClient{
+		addr:     cfg.Address,
+		password: cfg.Password,
+		timeout:  cfg.Timeout.Duration(),
+	}
+}
+
+type rconClient struct {
+	conn     *rcon.Conn
+	addr     string
+	password string
+	timeout  time.Duration
+}
+
+func (c *rconClient) queryTps() (string, error) {
+	return c.query(cmdTPS)
+}
+
+func (c *rconClient) queryList() (string, error) {
+	return c.query(cmdList)
+}
+
+func (c *rconClient) query(cmd string) (string, error) {
+	resp, err := c.conn.Execute(cmd)
+	if err != nil {
+		return "", err
+	}
+	return resp, nil
+}
+
+func (c *rconClient) connect() error {
+	_ = c.disconnect()
+
+	conn, err := rcon.Dial(c.addr, c.password, rcon.SetDialTimeout(c.timeout), rcon.SetDeadline(c.timeout))
+	if err != nil {
+		return err
+	}
+
+	c.conn = conn
+
+	return nil
+}
+
+func (c *rconClient) disconnect() error {
+	if c.conn != nil {
+		err := c.conn.Close()
+		c.conn = nil
+		return err
+	}
+
+	return nil
+}

+ 126 - 0
src/go/plugin/go.d/modules/spigotmc/collect.go

@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package spigotmc
+
+import (
+	"errors"
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+const precision = 100
+
+var (
+	reTPS       = regexp.MustCompile(`(?ms)(?P<tps_1min>\d+.\d+),.*?(?P<tps_5min>\d+.\d+),.*?(?P<tps_15min>\d+.\d+).*?$.*?(?P<mem_used>\d+)/(?P<mem_alloc>\d+)[^:]+:\s*(?P<mem_max>\d+)`)
+	reList      = regexp.MustCompile(`(?P<players>\d+)/?(?P<hidden_players>\d+)?.*?(?P<total_players>\d+)`)
+	reCleanResp = regexp.MustCompile(`§.`)
+)
+
+func (s *SpigotMC) collect() (map[string]int64, error) {
+	if s.conn == nil {
+		conn, err := s.establishConn()
+		if err != nil {
+			return nil, err
+		}
+		s.conn = conn
+	}
+
+	mx := make(map[string]int64)
+
+	if err := s.collectTPS(mx); err != nil {
+		s.Cleanup()
+		return nil, fmt.Errorf("failed to collect '%s': %v", cmdTPS, err)
+	}
+	if err := s.collectList(mx); err != nil {
+		s.Cleanup()
+		return nil, fmt.Errorf("failed to collect '%s': %v", cmdList, err)
+	}
+
+	return mx, nil
+}
+
+func (s *SpigotMC) collectTPS(mx map[string]int64) error {
+	resp, err := s.conn.queryTps()
+	if err != nil {
+		return err
+	}
+
+	s.Debugf("cmd '%s' response: %s", cmdTPS, resp)
+
+	if err := parseResponse(resp, reTPS, func(s string, f float64) {
+		switch {
+		case strings.HasPrefix(s, "tps"):
+			f *= precision
+		case strings.HasPrefix(s, "mem"):
+			f *= 1024 * 1024 // mb to bytes
+		}
+		mx[s] = int64(f)
+	}); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *SpigotMC) collectList(mx map[string]int64) error {
+	resp, err := s.conn.queryList()
+	if err != nil {
+		return err
+	}
+	s.Debugf("cmd '%s' response: %s", cmdList, resp)
+
+	var players int64
+	if err := parseResponse(resp, reList, func(s string, f float64) {
+		switch s {
+		case "players", "hidden_players":
+			players += int64(f)
+		}
+	}); err != nil {
+		return err
+	}
+
+	mx["players"] = players
+
+	return nil
+}
+
+func parseResponse(resp string, re *regexp.Regexp, fn func(string, float64)) error {
+	if resp == "" {
+		return errors.New("empty response")
+	}
+
+	resp = reCleanResp.ReplaceAllString(resp, "")
+
+	matches := re.FindStringSubmatch(resp)
+	if len(matches) == 0 {
+		return errors.New("regexp does not match")
+	}
+
+	for i, name := range re.SubexpNames() {
+		if name == "" || len(matches) <= i || matches[i] == "" {
+			continue
+		}
+		val := matches[i]
+
+		v, err := strconv.ParseFloat(val, 64)
+		if err != nil {
+			return fmt.Errorf("failed to parse key '%s' value '%s': %v", name, val, err)
+		}
+
+		fn(name, v)
+	}
+
+	return nil
+}
+
+func (s *SpigotMC) establishConn() (rconConn, error) {
+	conn := s.newConn(s.Config)
+
+	if err := conn.connect(); err != nil {
+		return nil, err
+	}
+
+	return conn, nil
+}

Some files were not shown because too many files changed in this diff