2017-06-23 23:54:12 +00:00
|
|
|
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 {
|
2017-06-27 20:14:07 +00:00
|
|
|
Gather(producer RCONClientProducer) ([]string, error)
|
2017-06-23 23:54:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Minecraft represents a connection to a minecraft server
|
|
|
|
type Minecraft struct {
|
2017-06-27 20:14:07 +00:00
|
|
|
Server string
|
|
|
|
Port string
|
|
|
|
Password string
|
|
|
|
client Client
|
|
|
|
clientSet bool
|
2017-06-23 23:54:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2017-06-27 20:14:07 +00:00
|
|
|
//var hasClient bool = false
|
2017-06-23 23:54:12 +00:00
|
|
|
func (s *Minecraft) Gather(acc telegraf.Accumulator) error {
|
2017-06-27 20:14:07 +00:00
|
|
|
// can't simply compare s.client to nil, because comparing an interface
|
|
|
|
// to nil often does not produce the desired result
|
|
|
|
if !s.clientSet {
|
2017-06-23 23:54:12 +00:00
|
|
|
var err error
|
|
|
|
s.client, err = NewRCON(s.Server, s.Port, s.Password)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-06-27 20:14:07 +00:00
|
|
|
s.clientSet = true
|
2017-06-23 23:54:12 +00:00
|
|
|
}
|
|
|
|
|
2017-06-27 20:14:07 +00:00
|
|
|
// (*RCON).Gather() takes an RCONClientProducer for testing purposes
|
|
|
|
d := defaultClientProducer{
|
|
|
|
Server: s.Server,
|
|
|
|
Port: s.Port,
|
|
|
|
}
|
|
|
|
|
|
|
|
scores, err := s.client.Gather(d)
|
2017-06-23 23:54:12 +00:00
|
|
|
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",
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|