143 lines
3.1 KiB
Go
143 lines
3.1 KiB
Go
|
package minecraft
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"regexp"
|
||
|
"strconv"
|
||
|
|
||
|
"github.com/influxdata/telegraf"
|
||
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||
|
)
|
||
|
|
||
|
const sampleConfig = `
|
||
|
## server address for minecraft
|
||
|
# server = "localhost"
|
||
|
## port for RCON
|
||
|
# port = "25575"
|
||
|
## password RCON for mincraft server
|
||
|
# password = ""
|
||
|
`
|
||
|
|
||
|
var (
|
||
|
playerNameRegex = regexp.MustCompile(`for\s([^:]+):-`)
|
||
|
scoreboardRegex = regexp.MustCompile(`(?U):\s(\d+)\s\((.*)\)`)
|
||
|
)
|
||
|
|
||
|
// Client is an interface for a client which gathers data from a minecraft server
|
||
|
type Client interface {
|
||
|
Gather() ([]string, error)
|
||
|
}
|
||
|
|
||
|
// Minecraft represents a connection to a minecraft server
|
||
|
type Minecraft struct {
|
||
|
Server string
|
||
|
Port string
|
||
|
Password string
|
||
|
client Client
|
||
|
}
|
||
|
|
||
|
// Description gives a brief description.
|
||
|
func (s *Minecraft) Description() string {
|
||
|
return "Collects scores from a minecraft server's scoreboard using the RCON protocol"
|
||
|
}
|
||
|
|
||
|
// SampleConfig returns our sampleConfig.
|
||
|
func (s *Minecraft) SampleConfig() string {
|
||
|
return sampleConfig
|
||
|
}
|
||
|
|
||
|
// Gather uses the RCON protocal to collect player and
|
||
|
// scoreboard stats from a minecraft server.
|
||
|
func (s *Minecraft) Gather(acc telegraf.Accumulator) error {
|
||
|
if s.client == nil {
|
||
|
var err error
|
||
|
s.client, err = NewRCON(s.Server, s.Port, s.Password)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
scores, err := s.client.Gather()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, score := range scores {
|
||
|
player, err := ParsePlayerName(score)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
tags := map[string]string{
|
||
|
"player": player,
|
||
|
"server": s.Server + ":" + s.Port,
|
||
|
}
|
||
|
|
||
|
stats, err := ParseScoreboard(score)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
var fields = make(map[string]interface{}, len(stats))
|
||
|
for _, stat := range stats {
|
||
|
fields[stat.Name] = stat.Value
|
||
|
}
|
||
|
|
||
|
acc.AddFields("minecraft", fields, tags)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// ParsePlayerName takes an input string from rcon, to parse
|
||
|
// the player.
|
||
|
func ParsePlayerName(input string) (string, error) {
|
||
|
playerMatches := playerNameRegex.FindAllStringSubmatch(input, -1)
|
||
|
if playerMatches == nil {
|
||
|
return "", fmt.Errorf("no player was matched")
|
||
|
}
|
||
|
return playerMatches[0][1], nil
|
||
|
}
|
||
|
|
||
|
// Score is an individual tracked scoreboard stat.
|
||
|
type Score struct {
|
||
|
Name string
|
||
|
Value int
|
||
|
}
|
||
|
|
||
|
// ParseScoreboard takes an input string from rcon, to parse
|
||
|
// scoreboard stats.
|
||
|
func ParseScoreboard(input string) ([]Score, error) {
|
||
|
scoreMatches := scoreboardRegex.FindAllStringSubmatch(input, -1)
|
||
|
if scoreMatches == nil {
|
||
|
return nil, fmt.Errorf("No scores found")
|
||
|
}
|
||
|
|
||
|
var scores []Score
|
||
|
|
||
|
for _, match := range scoreMatches {
|
||
|
number := match[1]
|
||
|
name := match[2]
|
||
|
n, err := strconv.Atoi(number)
|
||
|
// Not necessary in current state, because regex can only match integers,
|
||
|
// maybe become necessary if regex is modified to match more types of
|
||
|
// numbers
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Failed to parse score")
|
||
|
}
|
||
|
s := Score{
|
||
|
Name: name,
|
||
|
Value: n,
|
||
|
}
|
||
|
scores = append(scores, s)
|
||
|
}
|
||
|
return scores, nil
|
||
|
}
|
||
|
|
||
|
func init() {
|
||
|
inputs.Add("minecraft", func() telegraf.Input {
|
||
|
return &Minecraft{
|
||
|
Server: "localhost",
|
||
|
Port: "25575",
|
||
|
}
|
||
|
})
|
||
|
}
|