Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
100
plugins/inputs/minecraft/README.md
Normal file
100
plugins/inputs/minecraft/README.md
Normal file
|
@ -0,0 +1,100 @@
|
|||
# Minecraft Input Plugin
|
||||
|
||||
This plugin collects score metrics from a [Minecraft][minecraft] server using
|
||||
the RCON protocol.
|
||||
|
||||
> [!NOTE]
|
||||
> This plugin supports Minecraft Java Edition versions 1.11 - 1.14. When using
|
||||
> a version earlier than 1.13, be aware that the values for some criteria has
|
||||
> changed and need to be modified.
|
||||
|
||||
⭐ Telegraf v1.4.0
|
||||
🏷️ server
|
||||
💻 all
|
||||
|
||||
[minecraft]: https://www.minecraft.net/
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
In addition to the plugin-specific configuration settings, plugins support
|
||||
additional global and plugin configuration settings. These settings are used to
|
||||
modify metrics, tags, and field or create aliases and configure ordering, etc.
|
||||
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||
|
||||
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml @sample.conf
|
||||
# Collects scores from a Minecraft server's scoreboard using the RCON protocol
|
||||
[[inputs.minecraft]]
|
||||
## Address of the Minecraft server.
|
||||
# server = "localhost"
|
||||
|
||||
## Server RCON Port.
|
||||
# port = "25575"
|
||||
|
||||
## Server RCON Password.
|
||||
password = ""
|
||||
|
||||
## Uncomment to remove deprecated metric components.
|
||||
# tagdrop = ["server"]
|
||||
```
|
||||
|
||||
### Server Setup
|
||||
|
||||
Enable [RCON][rcon] on the Minecraft server and add the following to your
|
||||
[`server.properties`][propfile] file:
|
||||
|
||||
```conf
|
||||
enable-rcon=true
|
||||
rcon.password=<your password>
|
||||
rcon.port=<1-65535>
|
||||
```
|
||||
|
||||
Scoreboard [objectives][objectives] must be added using the server console for
|
||||
the plugin to collect. These can be added in game by players with op status,
|
||||
from the server console, or over an RCON connection.
|
||||
|
||||
When getting started pick an easy to test objective. This command will add an
|
||||
objective that counts the number of times a player has jumped:
|
||||
|
||||
```sh
|
||||
/scoreboard objectives add jumps minecraft.custom:minecraft.jump
|
||||
```
|
||||
|
||||
Once a player has triggered the event they will be added to the scoreboard,
|
||||
you can then list all players with recorded scores:
|
||||
|
||||
```sh
|
||||
/scoreboard players list
|
||||
```
|
||||
|
||||
View the current scores with a command, substituting your player name:
|
||||
|
||||
```sh
|
||||
/scoreboard players list Etho
|
||||
```
|
||||
|
||||
[rcon]: http://wiki.vg/RCON
|
||||
[propfile]: https://minecraft.gamepedia.com/Server.properties
|
||||
[objectives]: https://minecraft.gamepedia.com/Scoreboard#Objectives
|
||||
|
||||
## Metrics
|
||||
|
||||
- minecraft
|
||||
- tags:
|
||||
- player
|
||||
- port (port of the server)
|
||||
- server (hostname:port, deprecated in 1.11; use `source` and `port` tags)
|
||||
- source (hostname of the server)
|
||||
- fields:
|
||||
- `<objective_name>` (integer, count)
|
||||
|
||||
## Example Output
|
||||
|
||||
```text
|
||||
minecraft,player=notch,source=127.0.0.1,port=25575 jumps=178i 1498261397000000000
|
||||
minecraft,player=dinnerbone,source=127.0.0.1,port=25575 deaths=1i,jumps=1999i,cow_kills=1i 1498261397000000000
|
||||
minecraft,player=jeb,source=127.0.0.1,port=25575 d_pickaxe=1i,damage_dealt=80i,d_sword=2i,hunger=20i,health=20i,kills=1i,level=33i,jumps=264i,armor=15i 1498261397000000000
|
||||
```
|
171
plugins/inputs/minecraft/client.go
Normal file
171
plugins/inputs/minecraft/client.go
Normal file
|
@ -0,0 +1,171 @@
|
|||
package minecraft
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorcon/rcon"
|
||||
)
|
||||
|
||||
var (
|
||||
scoreboardRegexLegacy = regexp.MustCompile(`(?U):\s(?P<value>\d+)\s\((?P<name>.*)\)`)
|
||||
scoreboardRegex = regexp.MustCompile(`\[(?P<name>[^\]]+)\]: (?P<value>\d+)`)
|
||||
)
|
||||
|
||||
// connection is an established connection to the Minecraft server.
|
||||
type connection interface {
|
||||
// Execute runs a command.
|
||||
Execute(command string) (string, error)
|
||||
}
|
||||
|
||||
// conn is used to create connections to the Minecraft server.
|
||||
type conn interface {
|
||||
// connect establishes a connection to the server.
|
||||
connect() (connection, error)
|
||||
}
|
||||
|
||||
func newConnector(hostname, port, password string) *connector {
|
||||
return &connector{
|
||||
hostname: hostname,
|
||||
port: port,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
type connector struct {
|
||||
hostname string
|
||||
port string
|
||||
password string
|
||||
}
|
||||
|
||||
func (c *connector) connect() (connection, error) {
|
||||
client, err := rcon.Dial(c.hostname+":"+c.port, c.password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func newClient(connector conn) *client {
|
||||
return &client{connector: connector}
|
||||
}
|
||||
|
||||
type client struct {
|
||||
connector conn
|
||||
conn connection
|
||||
}
|
||||
|
||||
func (c *client) connect() error {
|
||||
conn, err := c.connector.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) players() ([]string, error) {
|
||||
if c.conn == nil {
|
||||
err := c.connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := c.conn.Execute("scoreboard players list")
|
||||
if err != nil {
|
||||
c.conn = nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parsePlayers(resp), nil
|
||||
}
|
||||
|
||||
func (c *client) scores(player string) ([]score, error) {
|
||||
if c.conn == nil {
|
||||
err := c.connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := c.conn.Execute("scoreboard players list " + player)
|
||||
if err != nil {
|
||||
c.conn = nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parseScores(resp), nil
|
||||
}
|
||||
|
||||
func parsePlayers(input string) []string {
|
||||
parts := strings.SplitAfterN(input, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := strings.Split(parts[1], ",")
|
||||
|
||||
// Detect Minecraft <= 1.12
|
||||
if strings.Contains(parts[0], "players on the scoreboard") && len(names) > 0 {
|
||||
// Split the last two player names: ex: "notch and dinnerbone"
|
||||
head := names[:len(names)-1]
|
||||
tail := names[len(names)-1]
|
||||
names = append(head, strings.SplitN(tail, " and ", 2)...)
|
||||
}
|
||||
|
||||
players := make([]string, 0, len(names))
|
||||
for _, name := range names {
|
||||
name := strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
players = append(players, name)
|
||||
}
|
||||
return players
|
||||
}
|
||||
|
||||
// score is an individual tracked scoreboard stat.
|
||||
type score struct {
|
||||
name string
|
||||
value int64
|
||||
}
|
||||
|
||||
func parseScores(input string) []score {
|
||||
if strings.Contains(input, "has no scores") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detect Minecraft <= 1.12
|
||||
var re *regexp.Regexp
|
||||
if strings.Contains(input, "tracked objective") {
|
||||
re = scoreboardRegexLegacy
|
||||
} else {
|
||||
re = scoreboardRegex
|
||||
}
|
||||
|
||||
matches := re.FindAllStringSubmatch(input, -1)
|
||||
scores := make([]score, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
score := score{}
|
||||
for i, subexp := range re.SubexpNames() {
|
||||
switch subexp {
|
||||
case "name":
|
||||
score.name = match[i]
|
||||
case "value":
|
||||
value, err := strconv.ParseInt(match[i], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
score.value = value
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
scores = append(scores, score)
|
||||
}
|
||||
|
||||
return scores
|
||||
}
|
187
plugins/inputs/minecraft/client_test.go
Normal file
187
plugins/inputs/minecraft/client_test.go
Normal file
|
@ -0,0 +1,187 @@
|
|||
package minecraft
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockConnection struct {
|
||||
commands map[string]string
|
||||
}
|
||||
|
||||
func (c *mockConnection) Execute(command string) (string, error) {
|
||||
return c.commands[command], nil
|
||||
}
|
||||
|
||||
type mockConnector struct {
|
||||
conn *mockConnection
|
||||
}
|
||||
|
||||
func (c *mockConnector) connect() (connection, error) {
|
||||
return c.conn, nil
|
||||
}
|
||||
|
||||
func TestClient_Player(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commands map[string]string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "minecraft 1.12 no players",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list": "There are no tracked players on the scoreboard",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.12 single player",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list": "Showing 1 tracked players on the scoreboard:Etho",
|
||||
},
|
||||
expected: []string{"Etho"},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.12 two players",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list": "Showing 2 tracked players on the scoreboard:Etho and torham",
|
||||
},
|
||||
expected: []string{"Etho", "torham"},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.12 three players",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list": "Showing 3 tracked players on the scoreboard:Etho, notch and torham",
|
||||
},
|
||||
expected: []string{"Etho", "notch", "torham"},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.12 players space in username",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list": "Showing 4 tracked players on the scoreboard:with space, Etho, notch and torham",
|
||||
},
|
||||
expected: []string{"with space", "Etho", "notch", "torham"},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.12 players and in username",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list": "Showing 5 tracked players on the scoreboard:left and right, with space,Etho, notch and torham",
|
||||
},
|
||||
expected: []string{"left and right", "with space", "Etho", "notch", "torham"},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.13 no players",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list": "There are no tracked entities",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.13 single player",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list": "There are 1 tracked entities: torham",
|
||||
},
|
||||
expected: []string{"torham"},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.13 multiple player",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list": "There are 3 tracked entities: Etho, notch, torham",
|
||||
},
|
||||
expected: []string{"Etho", "notch", "torham"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
connector := &mockConnector{
|
||||
conn: &mockConnection{commands: tt.commands},
|
||||
}
|
||||
|
||||
client := newClient(connector)
|
||||
actual, err := client.players()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Scores(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
player string
|
||||
commands map[string]string
|
||||
expected []score
|
||||
}{
|
||||
{
|
||||
name: "minecraft 1.12 player with no scores",
|
||||
player: "Etho",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list Etho": "Player Etho has no scores recorded",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.12 player with one score",
|
||||
player: "Etho",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list Etho": "Showing 1 tracked objective(s) for Etho:- jump: 2 (jump)",
|
||||
},
|
||||
expected: []score{
|
||||
{name: "jump", value: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.12 player with many scores",
|
||||
player: "Etho",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list Etho": "Showing 3 tracked objective(s) for Etho:- hopper: 2 (hopper)- dropper: 2 (dropper)- redstone: 1 (redstone)",
|
||||
},
|
||||
expected: []score{
|
||||
{name: "hopper", value: 2},
|
||||
{name: "dropper", value: 2},
|
||||
{name: "redstone", value: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.13 player with no scores",
|
||||
player: "Etho",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list Etho": "Etho has no scores to show",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.13 player with one score",
|
||||
player: "Etho",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list Etho": "Etho has 1 scores:[jumps]: 1",
|
||||
},
|
||||
expected: []score{
|
||||
{name: "jumps", value: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minecraft 1.13 player with many scores",
|
||||
player: "Etho",
|
||||
commands: map[string]string{
|
||||
"scoreboard players list Etho": "Etho has 3 scores:[hopper]: 2[dropper]: 2[redstone]: 1",
|
||||
},
|
||||
expected: []score{
|
||||
{name: "hopper", value: 2},
|
||||
{name: "dropper", value: 2},
|
||||
{name: "redstone", value: 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
connector := &mockConnector{
|
||||
conn: &mockConnection{commands: tt.commands},
|
||||
}
|
||||
|
||||
client := newClient(connector)
|
||||
actual, err := client.scores(tt.player)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
80
plugins/inputs/minecraft/minecraft.go
Normal file
80
plugins/inputs/minecraft/minecraft.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
package minecraft
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
type Minecraft struct {
|
||||
Server string `toml:"server"`
|
||||
Port string `toml:"port"`
|
||||
Password string `toml:"password"`
|
||||
|
||||
client cli
|
||||
}
|
||||
|
||||
// cli is a client for the Minecraft server.
|
||||
type cli interface {
|
||||
// connect establishes a connection to the server.
|
||||
connect() error
|
||||
|
||||
// players returns the players on the scoreboard.
|
||||
players() ([]string, error)
|
||||
|
||||
// scores returns the objective scores for a player.
|
||||
scores(player string) ([]score, error)
|
||||
}
|
||||
|
||||
func (*Minecraft) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (s *Minecraft) Gather(acc telegraf.Accumulator) error {
|
||||
if s.client == nil {
|
||||
connector := newConnector(s.Server, s.Port, s.Password)
|
||||
s.client = newClient(connector)
|
||||
}
|
||||
|
||||
players, err := s.client.players()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, player := range players {
|
||||
scores, err := s.client.scores(player)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"player": player,
|
||||
"server": s.Server + ":" + s.Port,
|
||||
"source": s.Server,
|
||||
"port": s.Port,
|
||||
}
|
||||
|
||||
var fields = make(map[string]interface{}, len(scores))
|
||||
for _, score := range scores {
|
||||
fields[score.name] = score.value
|
||||
}
|
||||
|
||||
acc.AddFields("minecraft", fields, tags)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("minecraft", func() telegraf.Input {
|
||||
return &Minecraft{
|
||||
Server: "localhost",
|
||||
Port: "25575",
|
||||
}
|
||||
})
|
||||
}
|
123
plugins/inputs/minecraft/minecraft_test.go
Normal file
123
plugins/inputs/minecraft/minecraft_test.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package minecraft
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
connectF func() error
|
||||
playersF func() ([]string, error)
|
||||
scoresF func(player string) ([]score, error)
|
||||
}
|
||||
|
||||
func (c *mockClient) connect() error {
|
||||
return c.connectF()
|
||||
}
|
||||
|
||||
func (c *mockClient) players() ([]string, error) {
|
||||
return c.playersF()
|
||||
}
|
||||
|
||||
func (c *mockClient) scores(player string) ([]score, error) {
|
||||
return c.scoresF(player)
|
||||
}
|
||||
|
||||
func TestGather(t *testing.T) {
|
||||
now := time.Unix(0, 0)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
client *mockClient
|
||||
metrics []telegraf.Metric
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "no players",
|
||||
client: &mockClient{
|
||||
connectF: func() error {
|
||||
return nil
|
||||
},
|
||||
playersF: func() ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one player without scores",
|
||||
client: &mockClient{
|
||||
connectF: func() error {
|
||||
return nil
|
||||
},
|
||||
playersF: func() ([]string, error) {
|
||||
return []string{"Etho"}, nil
|
||||
},
|
||||
scoresF: func(player string) ([]score, error) {
|
||||
switch player {
|
||||
case "Etho":
|
||||
return nil, nil
|
||||
default:
|
||||
panic("unknown player")
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one player with scores",
|
||||
client: &mockClient{
|
||||
connectF: func() error {
|
||||
return nil
|
||||
},
|
||||
playersF: func() ([]string, error) {
|
||||
return []string{"Etho"}, nil
|
||||
},
|
||||
scoresF: func(player string) ([]score, error) {
|
||||
switch player {
|
||||
case "Etho":
|
||||
return []score{{name: "jumps", value: 42}}, nil
|
||||
default:
|
||||
panic("unknown player")
|
||||
}
|
||||
},
|
||||
},
|
||||
metrics: []telegraf.Metric{
|
||||
testutil.MustMetric(
|
||||
"minecraft",
|
||||
map[string]string{
|
||||
"player": "Etho",
|
||||
"server": "example.org:25575",
|
||||
"source": "example.org",
|
||||
"port": "25575",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"jumps": 42,
|
||||
},
|
||||
now,
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
plugin := &Minecraft{
|
||||
Server: "example.org",
|
||||
Port: "25575",
|
||||
Password: "xyzzy",
|
||||
client: tt.client,
|
||||
}
|
||||
|
||||
var acc testutil.Accumulator
|
||||
acc.TimeFunc = func() time.Time { return now }
|
||||
|
||||
err := plugin.Gather(&acc)
|
||||
|
||||
require.Equal(t, tt.err, err)
|
||||
testutil.RequireMetricsEqual(t, tt.metrics, acc.GetTelegrafMetrics())
|
||||
})
|
||||
}
|
||||
}
|
13
plugins/inputs/minecraft/sample.conf
Normal file
13
plugins/inputs/minecraft/sample.conf
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Collects scores from a Minecraft server's scoreboard using the RCON protocol
|
||||
[[inputs.minecraft]]
|
||||
## Address of the Minecraft server.
|
||||
# server = "localhost"
|
||||
|
||||
## Server RCON Port.
|
||||
# port = "25575"
|
||||
|
||||
## Server RCON Password.
|
||||
password = ""
|
||||
|
||||
## Uncomment to remove deprecated metric components.
|
||||
# tagdrop = ["server"]
|
Loading…
Add table
Add a link
Reference in a new issue