@cryptotaxi247 / netdata-1 / commits / 48bc6d315

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

Ilya Mashchenko committed Oct 29, 2024 at 15:03 UTC 48bc6d315dfc26a484c01bf741d8d2559b2ad133
16 files changed +933
src/go/go.mod
+1
@@ -25,6 +25,7 @@ require (
25 github.com/gofrs/flock v0.12.1
26 github.com/golang/mock v1.6.0
27 github.com/google/uuid v1.6.0
28 + github.com/gorcon/rcon v1.3.5
29 github.com/gosnmp/gosnmp v1.38.0
30 github.com/ilyam8/hashstructure v1.1.0
31 github.com/jackc/pgx/v4 v4.18.3
src/go/go.sum
+2
@@ -143,6 +143,8 @@ github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73
143 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
144 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
145 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
146 +github.com/gorcon/rcon v1.3.5 h1:YE/Vrw6R99uEP08wp0EjdPAP3Jwz/ys3J8qxI1nYoeU=
147 +github.com/gorcon/rcon v1.3.5/go.mod h1:zR1qfKZttF8vAgH1NsP6CdpachOvLDq8jE64NboTpIM=
148 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
149 github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
150 github.com/gosnmp/gosnmp v1.38.0 h1:I5ZOMR8kb0DXAFg/88ACurnuwGwYkXWq3eLpJPHMEYc=
src/go/plugin/go.d/README.md
+1
@@ -134,6 +134,7 @@ see the appropriate collector readme.
134 | [squid](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/squid) | Squid |
135 | [squidlog](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/squidlog) | Squid |
136 | [smartctl](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/smartctl) | S.M.A.R.T Storage Devices |
137 +| [spigotmc](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/spigotmc) | SpigotMC |
138 | [storcli](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/storcli) | Broadcom Hardware RAID |
139 | [supervisord](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/supervisord) | Supervisor |
140 | [systemdunits](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/systemdunits) | Systemd unit state |
src/go/plugin/go.d/config/go.d.conf
+1
@@ -107,6 +107,7 @@ modules:
107 # squid: yes
108 # squidlog: yes
109 # smartctl: yes
110 +# spigotmc: yes
111 # storcli: yes
112 # supervisord: yes
113 # systemdunits: yes
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf
+7
@@ -132,6 +132,8 @@ classify:
132 expr: '{{ and (eq .Port "11334") (eq .Comm "rspamd") }}'
133 - tags: "squid"
134 expr: '{{ and (eq .Port "3128") (eq .Comm "squid") }}'
135 + - tags: "spigotmc"
136 + expr: '{{ and (eq .Port "25575") (glob .Cmdline "*spigot*") }}'
137 - tags: "supervisord"
138 expr: '{{ and (eq .Port "9001") (eq .Comm "supervisord") }}'
139 - tags: "tomcat"
@@ -519,6 +521,11 @@ compose:
521 module: squid
522 name: local
523 url: http://{{.Address}}
524 + - selector: "spigotmc"
525 + template: |
526 + module: spigotmc
527 + name: local
528 + address: {{.Address}}
529 - selector: "supervisord"
530 template: |
531 module: supervisord
src/go/plugin/go.d/config/go.d/spigotmc.conf new
+6
@@ -0,0 +1,6 @@
1 +## All available configuration options, their descriptions and default values:
2 +## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/spigotmc#readme
3 +
4 +#jobs:
5 +# - name: local
6 +# address: 127.0.0.1:25575
src/go/plugin/go.d/modules/init.go
+1
@@ -98,6 +98,7 @@ import (
98 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/sensors"
99 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/smartctl"
100 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/snmp"
101 + _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/spigotmc"
102 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/squid"
103 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/squidlog"
104 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/storcli"
src/go/plugin/go.d/modules/spigotmc/charts.go new
+59
@@ -0,0 +1,59 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package spigotmc
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
7 +)
8 +
9 +const (
10 + prioPlayers = module.Priority + iota
11 + prioTps
12 + prioMemory
13 +)
14 +
15 +var charts = module.Charts{
16 + playersChart.Copy(),
17 + tpsChart.Copy(),
18 + memoryChart.Copy(),
19 +}
20 +
21 +var playersChart = module.Chart{
22 + ID: "players",
23 + Title: "Active Players",
24 + Units: "players",
25 + Fam: "players",
26 + Ctx: "spigotmc.players",
27 + Priority: prioPlayers,
28 + Dims: module.Dims{
29 + {ID: "players", Name: "players"},
30 + },
31 +}
32 +
33 +var tpsChart = module.Chart{
34 + ID: "avg_tps",
35 + Title: "Average Ticks Per Second",
36 + Units: "ticks",
37 + Fam: "ticks",
38 + Ctx: "spigotmc.avg_tps",
39 + Priority: prioTps,
40 + Dims: module.Dims{
41 + {ID: "tps_1min", Name: "1min", Div: precision},
42 + {ID: "tps_5min", Name: "5min", Div: precision},
43 + {ID: "tps_15min", Name: "15min", Div: precision},
44 + },
45 +}
46 +
47 +var memoryChart = module.Chart{
48 + ID: "memory",
49 + Title: "Memory Usage",
50 + Units: "bytes",
51 + Fam: "mem",
52 + Ctx: "spigotmc.memory",
53 + Priority: prioMemory,
54 + Type: module.Area,
55 + Dims: module.Dims{
56 + {ID: "mem_used", Name: "used"},
57 + {ID: "mem_alloc", Name: "alloc"},
58 + },
59 +}
src/go/plugin/go.d/modules/spigotmc/client.go new
+75
@@ -0,0 +1,75 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package spigotmc
4 +
5 +import (
6 + "time"
7 +
8 + "github.com/gorcon/rcon"
9 +)
10 +
11 +type rconConn interface {
12 + connect() error
13 + disconnect() error
14 + queryTps() (string, error)
15 + queryList() (string, error)
16 +}
17 +
18 +const (
19 + cmdTPS = "tps"
20 + cmdList = "list"
21 +)
22 +
23 +func newRconConn(cfg Config) rconConn {
24 + return &rconClient{
25 + addr: cfg.Address,
26 + password: cfg.Password,
27 + timeout: cfg.Timeout.Duration(),
28 + }
29 +}
30 +
31 +type rconClient struct {
32 + conn *rcon.Conn
33 + addr string
34 + password string
35 + timeout time.Duration
36 +}
37 +
38 +func (c *rconClient) queryTps() (string, error) {
39 + return c.query(cmdTPS)
40 +}
41 +
42 +func (c *rconClient) queryList() (string, error) {
43 + return c.query(cmdList)
44 +}
45 +
46 +func (c *rconClient) query(cmd string) (string, error) {
47 + resp, err := c.conn.Execute(cmd)
48 + if err != nil {
49 + return "", err
50 + }
51 + return resp, nil
52 +}
53 +
54 +func (c *rconClient) connect() error {
55 + _ = c.disconnect()
56 +
57 + conn, err := rcon.Dial(c.addr, c.password, rcon.SetDialTimeout(c.timeout), rcon.SetDeadline(c.timeout))
58 + if err != nil {
59 + return err
60 + }
61 +
62 + c.conn = conn
63 +
64 + return nil
65 +}
66 +
67 +func (c *rconClient) disconnect() error {
68 + if c.conn != nil {
69 + err := c.conn.Close()
70 + c.conn = nil
71 + return err
72 + }
73 +
74 + return nil
75 +}
src/go/plugin/go.d/modules/spigotmc/collect.go new
+126
@@ -0,0 +1,126 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package spigotmc
4 +
5 +import (
6 + "errors"
7 + "fmt"
8 + "regexp"
9 + "strconv"
10 + "strings"
11 +)
12 +
13 +const precision = 100
14 +
15 +var (
16 + 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+)`)
17 + reList = regexp.MustCompile(`(?P<players>\d+)/?(?P<hidden_players>\d+)?.*?(?P<total_players>\d+)`)
18 + reCleanResp = regexp.MustCompile(`§.`)
19 +)
20 +
21 +func (s *SpigotMC) collect() (map[string]int64, error) {
22 + if s.conn == nil {
23 + conn, err := s.establishConn()
24 + if err != nil {
25 + return nil, err
26 + }
27 + s.conn = conn
28 + }
29 +
30 + mx := make(map[string]int64)
31 +
32 + if err := s.collectTPS(mx); err != nil {
33 + s.Cleanup()
34 + return nil, fmt.Errorf("failed to collect '%s': %v", cmdTPS, err)
35 + }
36 + if err := s.collectList(mx); err != nil {
37 + s.Cleanup()
38 + return nil, fmt.Errorf("failed to collect '%s': %v", cmdList, err)
39 + }
40 +
41 + return mx, nil
42 +}
43 +
44 +func (s *SpigotMC) collectTPS(mx map[string]int64) error {
45 + resp, err := s.conn.queryTps()
46 + if err != nil {
47 + return err
48 + }
49 +
50 + s.Debugf("cmd '%s' response: %s", cmdTPS, resp)
51 +
52 + if err := parseResponse(resp, reTPS, func(s string, f float64) {
53 + switch {
54 + case strings.HasPrefix(s, "tps"):
55 + f *= precision
56 + case strings.HasPrefix(s, "mem"):
57 + f *= 1024 * 1024 // mb to bytes
58 + }
59 + mx[s] = int64(f)
60 + }); err != nil {
61 + return err
62 + }
63 +
64 + return nil
65 +}
66 +
67 +func (s *SpigotMC) collectList(mx map[string]int64) error {
68 + resp, err := s.conn.queryList()
69 + if err != nil {
70 + return err
71 + }
72 + s.Debugf("cmd '%s' response: %s", cmdList, resp)
73 +
74 + var players int64
75 + if err := parseResponse(resp, reList, func(s string, f float64) {
76 + switch s {
77 + case "players", "hidden_players":
78 + players += int64(f)
79 + }
80 + }); err != nil {
81 + return err
82 + }
83 +
84 + mx["players"] = players
85 +
86 + return nil
87 +}
88 +
89 +func parseResponse(resp string, re *regexp.Regexp, fn func(string, float64)) error {
90 + if resp == "" {
91 + return errors.New("empty response")
92 + }
93 +
94 + resp = reCleanResp.ReplaceAllString(resp, "")
95 +
96 + matches := re.FindStringSubmatch(resp)
97 + if len(matches) == 0 {
98 + return errors.New("regexp does not match")
99 + }
100 +
101 + for i, name := range re.SubexpNames() {
102 + if name == "" || len(matches) <= i || matches[i] == "" {
103 + continue
104 + }
105 + val := matches[i]
106 +
107 + v, err := strconv.ParseFloat(val, 64)
108 + if err != nil {
109 + return fmt.Errorf("failed to parse key '%s' value '%s': %v", name, val, err)
110 + }
111 +
112 + fn(name, v)
113 + }
114 +
115 + return nil
116 +}
117 +
118 +func (s *SpigotMC) establishConn() (rconConn, error) {
119 + conn := s.newConn(s.Config)
120 +
121 + if err := conn.connect(); err != nil {
122 + return nil, err
123 + }
124 +
125 + return conn, nil
126 +}
src/go/plugin/go.d/modules/spigotmc/config_schema.json new
+53
@@ -0,0 +1,53 @@
1 +{
2 + "jsonSchema": {
3 + "$schema": "http://json-schema.org/draft-07/schema#",
4 + "title": "SpigotMC collector configuration.",
5 + "type": "object",
6 + "properties": {
7 + "update_every": {
8 + "title": "Update every",
9 + "description": "Data collection interval, measured in seconds.",
10 + "type": "integer",
11 + "minimum": 1,
12 + "default": 1
13 + },
14 + "address": {
15 + "title": "Address",
16 + "description": "The IP address and port where the SpigotMC server listens for RCON connections.",
17 + "type": "string",
18 + "default": "127.0.0.1:25575"
19 + },
20 + "password": {
21 + "title": "Password",
22 + "description": "The password for authentication.",
23 + "type": "string",
24 + "sensitive": true
25 + },
26 + "timeout": {
27 + "title": "Timeout",
28 + "description": "Timeout for establishing a connection and communication (reading and writing) in seconds.",
29 + "type": "number",
30 + "minimum": 0.5,
31 + "default": 1
32 + }
33 + },
34 + "required": [
35 + "address"
36 + ],
37 + "additionalProperties": false,
38 + "patternProperties": {
39 + "^name$": {}
40 + }
41 + },
42 + "uiSchema": {
43 + "uiOptions": {
44 + "fullPage": true
45 + },
46 + "password": {
47 + "ui:widget": "password"
48 + },
49 + "timeout": {
50 + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
51 + }
52 + }
53 +}
src/go/plugin/go.d/modules/spigotmc/metadata.yaml new
+136
@@ -0,0 +1,136 @@
1 +plugin_name: go.d.plugin
2 +modules:
3 + - meta:
4 + id: collector-go.d.plugin-spigotmc
5 + plugin_name: go.d.plugin
6 + module_name: spigotmc
7 + monitored_instance:
8 + name: SpigotMC
9 + link: https://www.spigotmc.org/
10 + categories:
11 + - data-collection.gaming
12 + icon_filename: "spigot.jfif"
13 + related_resources:
14 + integrations:
15 + list: []
16 + info_provided_to_referring_integrations:
17 + description: ""
18 + keywords:
19 + - minecraft
20 + - spigotmc
21 + - spigot
22 + most_popular: false
23 + overview:
24 + data_collection:
25 + metrics_description: |
26 + This collector monitors SpigotMC server server performance, in the form of ticks per second average, memory utilization, and active users.
27 + method_description: |
28 + It sends the `tps` and `list` commands to the Server, and gathers the metrics from the responses.
29 + supported_platforms:
30 + include: []
31 + exclude: []
32 + multi_instance: true
33 + additional_permissions:
34 + description: ""
35 + default_behavior:
36 + auto_detection:
37 + description: |
38 + By default, it detects SpigotMC instances running on localhost that are listening on port 25575.
39 +
40 + > **Note that the SpigotMC RCON API requires a password**.
41 + > While Netdata can automatically detect SpigotMC instances and create data collection jobs, these jobs will fail unless you provide the necessary credentials.
42 + limits:
43 + description: ""
44 + performance_impact:
45 + description: ""
46 + setup:
47 + prerequisites:
48 + list: []
49 + configuration:
50 + file:
51 + name: go.d/spigotmc.conf
52 + options:
53 + description: |
54 + The following options can be defined globally: update_every, autodetection_retry.
55 + folding:
56 + title: Config options
57 + enabled: true
58 + list:
59 + - name: update_every
60 + description: Data collection frequency.
61 + default_value: 1
62 + required: false
63 + - name: autodetection_retry
64 + description: Recheck interval in seconds. Zero means no recheck will be scheduled.
65 + default_value: 0
66 + required: false
67 + - name: address
68 + description: The IP address and port where the SpigotMC server listens for RCON connections.
69 + default_value: 127.0.0.1:25575
70 + required: true
71 + - name: timeout
72 + description: Connection, read, and write timeout duration in seconds. The timeout includes name resolution.
73 + default_value: 1
74 + required: false
75 + examples:
76 + folding:
77 + enabled: true
78 + title: Config
79 + list:
80 + - name: Basic
81 + description: A basic example configuration.
82 + config: |
83 + jobs:
84 + - name: local
85 + address: 127.0.0.1:25575
86 + password: somePassword
87 + - name: Multi-instance
88 + description: |
89 + > **Note**: When you define multiple jobs, their names must be unique.
90 +
91 + Collecting metrics from local and remote instances.
92 + config: |
93 + jobs:
94 + - name: local
95 + address: 127.0.0.1:25575
96 + password: somePassword
97 +
98 + - name: remote
99 + address: 203.0.113.0:25575
100 + password: somePassword
101 + troubleshooting:
102 + problems:
103 + list: []
104 + alerts: []
105 + metrics:
106 + folding:
107 + title: Metrics
108 + enabled: false
109 + description: ""
110 + availability: []
111 + scopes:
112 + - name: global
113 + description: "These metrics refer to the entire monitored application."
114 + labels: []
115 + metrics:
116 + - name: spigotmc.players
117 + description: Active Players
118 + unit: "players"
119 + chart_type: line
120 + dimensions:
121 + - name: players
122 + - name: spigotmc.avg_tps
123 + description: Average Ticks Per Second
124 + unit: "ticks"
125 + chart_type: line
126 + dimensions:
127 + - name: 1min
128 + - name: 5min
129 + - name: 15min
130 + - name: spigotmc.memory
131 + description: Memory Usage
132 + unit: "bytes"
133 + chart_type: area
134 + dimensions:
135 + - name: used
136 + - name: alloc
src/go/plugin/go.d/modules/spigotmc/spigotmc.go new
+104
@@ -0,0 +1,104 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package spigotmc
4 +
5 +import (
6 + _ "embed"
7 + "errors"
8 + "time"
9 +
10 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/confopt"
12 +)
13 +
14 +//go:embed "config_schema.json"
15 +var configSchema string
16 +
17 +func init() {
18 + module.Register("spigotmc", module.Creator{
19 + JobConfigSchema: configSchema,
20 + Create: func() module.Module { return New() },
21 + Config: func() any { return &Config{} },
22 + })
23 +}
24 +
25 +func New() *SpigotMC {
26 + return &SpigotMC{
27 + Config: Config{
28 + Address: "127.0.0.1:25575",
29 + Timeout: confopt.Duration(time.Second * 1),
30 + },
31 + charts: charts.Copy(),
32 + newConn: newRconConn,
33 + }
34 +}
35 +
36 +type Config struct {
37 + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
38 + Address string `yaml:"address" json:"address"`
39 + Password string `yaml:"password" json:"password"`
40 + Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout"`
41 +}
42 +
43 +type SpigotMC struct {
44 + module.Base
45 + Config `yaml:",inline" json:""`
46 +
47 + charts *module.Charts
48 +
49 + newConn func(Config) rconConn
50 + conn rconConn
51 +}
52 +
53 +func (s *SpigotMC) Configuration() any {
54 + return s.Config
55 +}
56 +
57 +func (s *SpigotMC) Init() error {
58 + if s.Address == "" {
59 + return errors.New("config: 'address' required but not set")
60 + }
61 + if s.Password == "" {
62 + return errors.New("config: 'password' required but not set")
63 + }
64 + return nil
65 +}
66 +
67 +func (s *SpigotMC) Check() error {
68 + mx, err := s.collect()
69 + if err != nil {
70 + return err
71 + }
72 +
73 + if len(mx) == 0 {
74 + return errors.New("no metrics collected")
75 + }
76 +
77 + return nil
78 +}
79 +
80 +func (s *SpigotMC) Charts() *module.Charts {
81 + return s.charts
82 +}
83 +
84 +func (s *SpigotMC) Collect() map[string]int64 {
85 + mx, err := s.collect()
86 + if err != nil {
87 + s.Error(err)
88 + }
89 +
90 + if len(mx) == 0 {
91 + return nil
92 + }
93 +
94 + return mx
95 +}
96 +
97 +func (s *SpigotMC) Cleanup() {
98 + if s.conn != nil {
99 + if err := s.conn.disconnect(); err != nil {
100 + s.Warningf("error on disconnect: %s", err)
101 + }
102 + s.conn = nil
103 + }
104 +}
src/go/plugin/go.d/modules/spigotmc/spigotmc_test.go new
+351
@@ -0,0 +1,351 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package spigotmc
4 +
5 +import (
6 + "errors"
7 + "os"
8 + "testing"
9 +
10 + "github.com/stretchr/testify/assert"
11 + "github.com/stretchr/testify/require"
12 +
13 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
14 +)
15 +
16 +var (
17 + dataConfigJSON, _ = os.ReadFile("testdata/config.json")
18 + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
19 +
20 + dataRespTsp = `
21 +§6TPS from last 1m, 5m, 15m: §a*20.0, §a*20.0, §a*20.0
22 +§6Current Memory Usage: §a332/392 mb (Max: 6008 mb)
23 +`
24 + dataRespListClean = `There are 4 of a max 50 players online: player1, player2, player3, player4`
25 + dataRespListDoubleS = `There are 4 of a max 50 players online: player1, player2, player3, player4`
26 + dataRespListDoubleSWithHidden = `§6There are §c3§6/§c1§6 out of maximum §c50§6 players online.`
27 + dataRespListDoubleSNonEng = `§6当前有 §c4§6 个玩家在线,最大在线人数为 §c50§6 个玩家.`
28 +)
29 +
30 +func Test_testDataIsValid(t *testing.T) {
31 + for name, data := range map[string][]byte{
32 + "dataConfigJSON": dataConfigJSON,
33 + "dataConfigYAML": dataConfigYAML,
34 + } {
35 + require.NotNil(t, data, name)
36 + }
37 +}
38 +
39 +func TestSpigotMC_ConfigurationSerialize(t *testing.T) {
40 + module.TestConfigurationSerialize(t, &SpigotMC{}, dataConfigJSON, dataConfigYAML)
41 +}
42 +
43 +func TestSpigotMC_Init(t *testing.T) {
44 + tests := map[string]struct {
45 + config Config
46 + wantFail bool
47 + }{
48 + "fails with default config": {
49 + wantFail: true,
50 + config: New().Config,
51 + },
52 + "fails if address not set": {
53 + wantFail: true,
54 + config: func() Config {
55 + conf := New().Config
56 + conf.Address = ""
57 + conf.Password = "pass"
58 + return conf
59 + }(),
60 + },
61 + }
62 +
63 + for name, test := range tests {
64 + t.Run(name, func(t *testing.T) {
65 + smc := New()
66 + smc.Config = test.config
67 +
68 + if test.wantFail {
69 + assert.Error(t, smc.Init())
70 + } else {
71 + assert.NoError(t, smc.Init())
72 + }
73 + })
74 + }
75 +}
76 +
77 +func TestSpigotMC_Cleanup(t *testing.T) {
78 + tests := map[string]struct {
79 + prepare func() *SpigotMC
80 + }{
81 + "not initialized": {
82 + prepare: func() *SpigotMC {
83 + return New()
84 + },
85 + },
86 + "after check": {
87 + prepare: func() *SpigotMC {
88 + smc := New()
89 + smc.newConn = func(config Config) rconConn { return prepareMockOk() }
90 + _ = smc.Check()
91 + return smc
92 + },
93 + },
94 + "after collect": {
95 + prepare: func() *SpigotMC {
96 + smc := New()
97 + smc.newConn = func(config Config) rconConn { return prepareMockOk() }
98 + _ = smc.Collect()
99 + return smc
100 + },
101 + },
102 + }
103 +
104 + for name, test := range tests {
105 + t.Run(name, func(t *testing.T) {
106 + smc := test.prepare()
107 +
108 + assert.NotPanics(t, smc.Cleanup)
109 + })
110 + }
111 +}
112 +
113 +func TestSpigotMC_Charts(t *testing.T) {
114 + assert.NotNil(t, New().Charts())
115 +}
116 +
117 +func TestSpigotMC_Check(t *testing.T) {
118 + tests := map[string]struct {
119 + prepareMock func() *mockRcon
120 + wantFail bool
121 + }{
122 + "success case": {
123 + wantFail: false,
124 + prepareMock: prepareMockOk,
125 + },
126 + "err on connect": {
127 + wantFail: true,
128 + prepareMock: prepareMockErrOnConnect,
129 + },
130 + "unexpected response": {
131 + wantFail: true,
132 + prepareMock: prepareMockUnexpectedResponse,
133 + },
134 + "empty response": {
135 + wantFail: true,
136 + prepareMock: prepareMockEmptyResponse,
137 + },
138 + }
139 +
140 + for name, test := range tests {
141 + t.Run(name, func(t *testing.T) {
142 + smc := New()
143 + mock := test.prepareMock()
144 + smc.newConn = func(config Config) rconConn { return mock }
145 +
146 + if test.wantFail {
147 + assert.Error(t, smc.Check())
148 + } else {
149 + assert.NoError(t, smc.Check())
150 + }
151 + })
152 + }
153 +}
154 +
155 +func TestSpigotMC_Collect(t *testing.T) {
156 + tests := map[string]struct {
157 + prepareMock func() *mockRcon
158 + wantMetrics map[string]int64
159 + disconnectBeforeCleanup bool
160 + disconnectAfterCleanup bool
161 + }{
162 + "success case: clean": {
163 + prepareMock: prepareMockOkClean,
164 + disconnectBeforeCleanup: false,
165 + disconnectAfterCleanup: true,
166 + wantMetrics: map[string]int64{
167 + "mem_alloc": 411041792,
168 + "mem_max": 6299844608,
169 + "mem_used": 348127232,
170 + "players": 4,
171 + "tps_15min": 2000,
172 + "tps_1min": 2000,
173 + "tps_5min": 2000,
174 + },
175 + },
176 + "success case: double s": {
177 + prepareMock: prepareMockOk,
178 + disconnectBeforeCleanup: false,
179 + disconnectAfterCleanup: true,
180 + wantMetrics: map[string]int64{
181 + "mem_alloc": 411041792,
182 + "mem_max": 6299844608,
183 + "mem_used": 348127232,
184 + "players": 4,
185 + "tps_15min": 2000,
186 + "tps_1min": 2000,
187 + "tps_5min": 2000,
188 + },
189 + },
190 + "success case: double s with hidden": {
191 + prepareMock: prepareMockOkSWithHidden,
192 + disconnectBeforeCleanup: false,
193 + disconnectAfterCleanup: true,
194 + wantMetrics: map[string]int64{
195 + "mem_alloc": 411041792,
196 + "mem_max": 6299844608,
197 + "mem_used": 348127232,
198 + "players": 4,
199 + "tps_15min": 2000,
200 + "tps_1min": 2000,
201 + "tps_5min": 2000,
202 + },
203 + },
204 + "success case: non english": {
205 + prepareMock: prepareMockOkSNonEng,
206 + disconnectBeforeCleanup: false,
207 + disconnectAfterCleanup: true,
208 + wantMetrics: map[string]int64{
209 + "mem_alloc": 411041792,
210 + "mem_max": 6299844608,
211 + "mem_used": 348127232,
212 + "players": 4,
213 + "tps_15min": 2000,
214 + "tps_1min": 2000,
215 + "tps_5min": 2000,
216 + },
217 + },
218 + "unexpected response": {
219 + prepareMock: prepareMockUnexpectedResponse,
220 + disconnectBeforeCleanup: true,
221 + disconnectAfterCleanup: true,
222 + },
223 + "empty response": {
224 + prepareMock: prepareMockEmptyResponse,
225 + disconnectBeforeCleanup: true,
226 + disconnectAfterCleanup: true,
227 + wantMetrics: nil,
228 + },
229 + "err on connect": {
230 + prepareMock: prepareMockErrOnConnect,
231 + disconnectBeforeCleanup: false,
232 + disconnectAfterCleanup: false,
233 + },
234 + "err on query status": {
235 + prepareMock: prepareMockErrOnQuery,
236 + disconnectBeforeCleanup: true,
237 + disconnectAfterCleanup: true,
238 + },
239 + }
240 +
241 + for name, test := range tests {
242 + t.Run(name, func(t *testing.T) {
243 + smc := New()
244 + mock := test.prepareMock()
245 + smc.newConn = func(config Config) rconConn { return mock }
246 +
247 + mx := smc.Collect()
248 +
249 + require.Equal(t, test.wantMetrics, mx, "want metrics")
250 +
251 + if len(test.wantMetrics) > 0 {
252 + module.TestMetricsHasAllChartsDims(t, smc.Charts(), mx)
253 + assert.Equal(t, len(charts), len(*smc.Charts()), "want charts")
254 + }
255 +
256 + assert.Equal(t, test.disconnectBeforeCleanup, mock.disconnectCalled, "disconnect before cleanup")
257 + smc.Cleanup()
258 + assert.Equal(t, test.disconnectAfterCleanup, mock.disconnectCalled, "disconnect after cleanup")
259 + })
260 + }
261 +}
262 +
263 +func prepareMockOkClean() *mockRcon {
264 + return &mockRcon{
265 + responseTps: dataRespTsp,
266 + responseList: dataRespListClean,
267 + }
268 +}
269 +
270 +func prepareMockOk() *mockRcon {
271 + return &mockRcon{
272 + responseTps: dataRespTsp,
273 + responseList: dataRespListDoubleS,
274 + }
275 +}
276 +
277 +func prepareMockOkSWithHidden() *mockRcon {
278 + return &mockRcon{
279 + responseTps: dataRespTsp,
280 + responseList: dataRespListDoubleSWithHidden,
281 + }
282 +}
283 +
284 +func prepareMockOkSNonEng() *mockRcon {
285 + return &mockRcon{
286 + responseTps: dataRespTsp,
287 + responseList: dataRespListDoubleSNonEng,
288 + }
289 +}
290 +
291 +func prepareMockErrOnConnect() *mockRcon {
292 + return &mockRcon{
293 + errOnConnect: true,
294 + }
295 +}
296 +
297 +func prepareMockErrOnQuery() *mockRcon {
298 + return &mockRcon{
299 + errOnQuery: true,
300 + }
301 +}
302 +
303 +func prepareMockUnexpectedResponse() *mockRcon {
304 + resp := "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
305 +
306 + return &mockRcon{
307 + responseTps: resp,
308 + responseList: resp,
309 + }
310 +}
311 +
312 +func prepareMockEmptyResponse() *mockRcon {
313 + return &mockRcon{
314 + responseTps: "",
315 + responseList: "",
316 + }
317 +}
318 +
319 +type mockRcon struct {
320 + errOnConnect bool
321 + responseTps string
322 + responseList string
323 + errOnQuery bool
324 + disconnectCalled bool
325 +}
326 +
327 +func (m *mockRcon) connect() error {
328 + if m.errOnConnect {
329 + return errors.New("mock.connect() error")
330 + }
331 + return nil
332 +}
333 +
334 +func (m *mockRcon) disconnect() error {
335 + m.disconnectCalled = true
336 + return nil
337 +}
338 +
339 +func (m *mockRcon) queryTps() (string, error) {
340 + if m.errOnQuery {
341 + return "", errors.New("mock.queryTps() error")
342 + }
343 + return m.responseTps, nil
344 +}
345 +
346 +func (m *mockRcon) queryList() (string, error) {
347 + if m.errOnQuery {
348 + return "", errors.New("mock.queryList() error")
349 + }
350 + return m.responseList, nil
351 +}
src/go/plugin/go.d/modules/spigotmc/testdata/config.json new
+6
@@ -0,0 +1,6 @@
1 +{
2 + "update_every": 123,
3 + "address": "ok",
4 + "password": "ok",
5 + "timeout": 123.123
6 +}
src/go/plugin/go.d/modules/spigotmc/testdata/config.yaml new
+4
@@ -0,0 +1,4 @@
1 +update_every: 123
2 +address: "ok"
3 +password: "ok"
4 +timeout: 123.123