Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minecraft Input Plugin using RCON #2960

Merged
merged 29 commits into from
Jun 23, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
48d7258
Add boilerplate for minecraft plugin
Jun 20, 2017
5cbb550
Add RCON minecraft client lib
Jun 20, 2017
9d737b7
Change rcon.go to package main
Jun 21, 2017
8fd5848
Isolate username with regex
Jun 21, 2017
a4b87da
Add username parsing test for minecraft plugin
Jun 21, 2017
96994e3
Update ParseUsername test for minecraft plugin
Jun 21, 2017
54b1778
Add method to parse scoreboard data from rcon for minecraft input plugin
Jun 22, 2017
b1c970c
Remove main.go from minecraft plugin
Jun 22, 2017
e775ba0
Change field names in RCON struct for minecraft input plugin
Jun 22, 2017
93c36f6
Add gathering of data from RCON client for minecraft input plugin
Jun 22, 2017
c3c6a8e
Fix empty username, fix uninitialized map for minecraft input plugin
Jun 22, 2017
80ed9f3
Change all instances of username to playername
Jun 22, 2017
c4fab38
Add gather test for minecraft plugin
Jun 23, 2017
db22951
Add rcon_test.go to verify that RCON server output is processed corre…
Jun 23, 2017
6a8b6b5
Remove unused variable in minecraft plugin
Jun 23, 2017
e72948e
Add README.md to minecraft plugin
Jun 23, 2017
846b487
Update minecraft input plugin README.md to conform to telegraf README…
Jun 23, 2017
6fd5718
Update README.md in minecraft input plugin to include server properti…
Jun 23, 2017
c48cf8d
Update minecraft plugin README.md to futher conform to telegraf READM…
Jun 23, 2017
f0f5f8b
Update LICENSE_OF_DEPENDENCIES.md to include license for chuckpreslar…
Jun 23, 2017
bfbb286
Update playerName tag to player in minecraft plugin
Jun 23, 2017
a0ed0cf
Update server tag format to include port in minecraft plugin
Jun 23, 2017
f850a78
Refactor regexps into package variables in minecraft plugin
Jun 23, 2017
3e4559d
Update comment spacing in minecraft plugin
Jun 23, 2017
7d33813
Add configuration defaults in minecraft package init
Jun 23, 2017
2de9da1
Add RCON network reconnect in minecraft plugin
Jun 23, 2017
66adba4
Remove extraneous comment and update formatting in minecraft plugin
Jun 23, 2017
5818f5c
Update default config file formatting in minecraft plugin
Jun 23, 2017
8ed6801
Update test data in README.md for minecraft plugin
Jun 23, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add method to parse scoreboard data from rcon for minecraft input plugin
Signed-off-by: Ayrdrie Palmer <ayrdriepalmer@gmail.com>
  • Loading branch information
Adam Perlin authored and Ayrdrie Palmer committed Jun 23, 2017
commit 54b177873174a4c52df55e1503590658878d8f7f
47 changes: 47 additions & 0 deletions plugins/inputs/minecraft/minecraft.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package minecraft
import (
"fmt"
"regexp"
"strconv"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs"
Expand All @@ -19,20 +20,25 @@ const sampleConfig = `
password = "replace_me"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done inconsistently in our current plugins, but comments should have two hashes, items with default values should have a single hash, and items that must be filled should be with no hashes.

`

// Minecraft represents a connection to a minecraft server
type Minecraft struct {
Server string
Port string
Password string
}

// Description gives a brief description.
func (s *Minecraft) Description() string {
return "it collects stats from Minecraft servers"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make more better: "Collect scores from a Minecraft server's scoreboard"

}

// SampleConfig returns our sampleConfig.
func (s *Minecraft) SampleConfig() string {
return sampleConfig
}

// Gather uses the RCON protocal to collect username and
// scoreboard stats from a minecraft server.
func (s *Minecraft) Gather(acc telegraf.Accumulator) error {
if s.Port == " " {
acc.AddFields("state", map[string]interface{}{"value": "pretty good"}, nil)
Expand All @@ -43,6 +49,8 @@ func (s *Minecraft) Gather(acc telegraf.Accumulator) error {
return nil
}

// ParseUsername takes an input string from rcon, to parse
// the username.
func ParseUsername(input string) (string, error) {
var re = regexp.MustCompile(`for\s(.*):-`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these regex be package level so they are only compiled once?

I suggest something along the lines of for\s([^:]+):-


Expand All @@ -53,6 +61,45 @@ func ParseUsername(input string) (string, error) {
return usernameMatches[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) {
var re = regexp.MustCompile(`(?U):\s(\d+)\s\((.*)\)`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these regex be package level so they are only compiled once?

scoreMatches := re.FindAllStringSubmatch(input, -1)
if scoreMatches == nil {
return nil, fmt.Errorf("No scores found")
}

var scores []Score

for _, match := range scoreMatches {
//fmt.Println(match)
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 statistic")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use consistent terminology: "failed to parse score", also fix up the spacing on comment above :)

}
s := Score{
Name: name,
Value: n,
}
// fmt.Println(s)
scores = append(scores, s)
}
//fmt.Println(scores)
return scores, nil
}

func init() {
inputs.Add("minecraft", func() telegraf.Input { return &Minecraft{} })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to set config file defaults here.

}
143 changes: 142 additions & 1 deletion plugins/inputs/minecraft/minecraft_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package minecraft

import "testing"
import (
"fmt"
"reflect"
"testing"
)

// TestParseUsername tests different Minecraft RCON inputs for usernames
func TestParseUsername(t *testing.T) {
Expand All @@ -25,4 +29,141 @@ func TestParseUsername(t *testing.T) {
if got != want {
t.Errorf("got %s\n want %s\n", got, want)
}

// Test an invalid input string to ensure error is returned
input = "1 tracked objective(s) for 😂:- jumps: 178 (jumps)"
got, err = ParseUsername(input)
want = "😂"
if err != nil {
t.Fatalf("username returned error. Error: %s\n", err)
}
if got != want {
t.Errorf("got %s\n want %s\n", got, want)
}
}

// TestParseScoreboard tests different Minecraft RCON inputs for scoreboard stats.
func TestParseScoreboard(t *testing.T) {
// test a valid input string to ensure stats are parsed correctly.
input := `1 tracked objective(s) for divislight:- jumps: 178 (jumps)- sword: 5 (sword)`
got, err := ParseScoreboard(input)

fmt.Println(got)

if err != nil {
t.Fatal("Unexpected error")
}

want := []Score{
{
Name: "jumps",
Value: 178,
},
{
Name: "sword",
Value: 5,
},
}

if !reflect.DeepEqual(got, want) {
t.Errorf("Got: \n%#v\nWant: %#v", got, want)
}

// Tests a partial input string.
input = `1 tracked objective(s) for divislight:- jumps: (jumps)- sword: 5 (sword)`
got, err = ParseScoreboard(input)

fmt.Println(got)

if err != nil {
t.Fatal("Unexpected error")
}

want = []Score{
{
Name: "sword",
Value: 5,
},
}

if !reflect.DeepEqual(got, want) {
t.Errorf("Got: \n%#v\nWant:\n%#v", got, want)
}

// Tests an empty string.
input = ``
got, err = ParseScoreboard(input)

fmt.Println(got)

if err == nil {
t.Fatal("Expected input error, but error was nil")
}

// Tests when a number isn't an integer.
input = `1 tracked objective(s) for divislight:- jumps: 178.5 (jumps)- sword: 5 (sword)`
got, err = ParseScoreboard(input)

fmt.Println(got)

if err != nil {
t.Fatal("Unexpected error")
}

want = []Score{
{
Name: "sword",
Value: 5,
},
}

if !reflect.DeepEqual(got, want) {
t.Errorf("Got: \n%#v\nWant: %#v", got, want)
}

//Testing a real life data scenario with unicode characters
input = `7 tracked objective(s) for mauxlaim:- total_kills: 39 (total_kills)- "howdy doody": 37 (dalevel)- howdy: 37 (lvl)- jumps: 1290 (jumps)- iron_pickaxe: 284 (iron_pickaxe)- cow_kills: 1 (cow_kills)- "asdf": 37 (😂)`
got, err = ParseScoreboard(input)

fmt.Println(got)

if err != nil {
t.Fatal("Unexpected error")
}

want = []Score{
{
Name: "total_kills",
Value: 39,
},
{
Name: "dalevel",
Value: 37,
},
{
Name: "lvl",
Value: 37,
},
{
Name: "jumps",
Value: 1290,
},
{
Name: "iron_pickaxe",
Value: 284,
},
{
Name: "cow_kills",
Value: 1,
},
{
Name: "😂",
Value: 37,
},
}

if !reflect.DeepEqual(got, want) {
t.Errorf("Got: \n%#v\nWant: %#v", got, want)
}

}