telegraf/plugins/inputs/minecraft/minecraft.go

154 lines
3.4 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(producer RCONClientProducer) ([]string, error)
}
// Minecraft represents a connection to a minecraft server
type Minecraft struct {
Server string
Port string
Password string
client Client
clientSet bool
}
// 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 protocol to collect player and
// scoreboard stats from a minecraft server.
//var hasClient bool = false
func (s *Minecraft) Gather(acc telegraf.Accumulator) error {
// can't simply compare s.client to nil, because comparing an interface
// to nil often does not produce the desired result
if !s.clientSet {
var err error
s.client, err = NewRCON(s.Server, s.Port, s.Password)
if err != nil {
return err
}
s.clientSet = true
}
// (*RCON).Gather() takes an RCONClientProducer for testing purposes
d := defaultClientProducer{
Server: s.Server,
Port: s.Port,
}
scores, err := s.client.Gather(d)
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",
}
})
}