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