1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View 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
```

View 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
}

View 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)
})
}
}

View 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",
}
})
}

View 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())
})
}
}

View 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"]