Merge branch 'master' into master

This commit is contained in:
Joshi 2022-02-23 22:44:20 +01:00 committed by GitHub
commit 7aa6e1a170
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 221 additions and 163 deletions

View file

@ -1,8 +1,8 @@
FROM python:3.8.12-alpine3.14 FROM python:3.10-alpine
COPY requirements.txt minecraft_exporter.py / COPY requirements.txt minecraft_exporter.py /
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 8000 EXPOSE 8000
ENTRYPOINT ["python","minecraft_exporter.py"] ENTRYPOINT ["python","-u","minecraft_exporter.py"]

View file

@ -1,118 +1,159 @@
from prometheus_client import start_http_server, REGISTRY, Metric
import time
import requests
import json import json
import nbt
import re
import os import os
import schedule import re
from mcrcon import MCRcon import time
from os import listdir from os import listdir
from os.path import isfile, join from os.path import isfile, join
import nbt
import requests
import schedule
from mcrcon import MCRcon, MCRconException
from prometheus_client import Metric, REGISTRY, start_http_server
class MinecraftCollector(object): class MinecraftCollector(object):
def __init__(self): def __init__(self):
self.statsdirectory = "/world/stats" self.stats_directory = "/world/stats"
self.playerdirectory = "/world/playerdata" self.player_directory = "/world/playerdata"
self.advancementsdirectory = "/world/advancements" self.advancements_directory = "/world/advancements"
self.betterquesting = "/world/betterquesting" self.better_questing = "/world/betterquesting"
self.map = dict() self.player_map = dict()
self.questsEnabled = False self.quests_enabled = False
self.rcon = None self.rcon = None
if os.path.isdir(self.betterquesting): self.rcon_connected = False
self.questsEnabled = True if all(x in os.environ for x in ['RCON_HOST', 'RCON_PASSWORD']):
self.rcon = MCRcon(os.environ['RCON_HOST'], os.environ['RCON_PASSWORD'], port=int(os.environ['RCON_PORT']))
print("RCON is enabled for " + os.environ['RCON_HOST'])
if os.path.isdir(self.better_questing):
self.quests_enabled = True
schedule.every().day.at("01:00").do(self.flush_playernamecache) schedule.every().day.at("01:00").do(self.flush_playernamecache)
def get_players(self): def get_players(self):
return [f[:-5] for f in listdir(self.statsdirectory) if isfile(join(self.statsdirectory, f))] return [f[:-5] for f in listdir(self.stats_directory) if isfile(join(self.stats_directory, f))]
def flush_playernamecache(self): def flush_playernamecache(self):
print("flushing playername cache") print("flushing playername cache")
self.map = dict() self.player_map = dict()
return
def uuid_to_player(self,uuid): def uuid_to_player(self, uuid):
uuid = uuid.replace('-','') uuid = uuid.replace('-', '')
if uuid in self.map: if uuid in self.player_map:
return self.map[uuid] return self.player_map[uuid]
else: else:
try: try:
result = requests.get('https://api.mojang.com/user/profiles/' + uuid + '/names') result = requests.get('https://api.mojang.com/user/profiles/' + uuid + '/names')
self.map[uuid] = result.json()[-1]['name'] self.player_map[uuid] = result.json()[-1]['name']
return(result.json()[-1]['name']) return (result.json()[-1]['name'])
except: except:
return return
def rcon_command(self,command): def rcon_connect(self):
if self.rcon == None: try:
self.rcon = MCRcon(os.environ['RCON_HOST'],os.environ['RCON_PASSWORD'],port=int(os.environ['RCON_PORT']))
self.rcon.connect() self.rcon.connect()
self.rcon_connected = True
print("Successfully connected to RCON")
return True
except Exception as e:
print("Failed to connect to RCON")
print(e)
return False
def rcon_disconnect(self):
self.rcon.disconnect()
self.rcon_connected = False
def rcon_command(self, command):
try: try:
response = self.rcon.command(command) response = self.rcon.command(command)
except BrokenPipeError: except MCRconException as e:
print("Lost RCON Connection, trying to reconnect") response = None
self.rcon.connect() if e == "Connection timeout error":
response = self.rcon.command(command) print("Lost RCON Connection")
self.rcon_disconnect()
else:
print("RCON command failed")
except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError):
print("Lost RCON Connection")
self.rcon_disconnect()
response = None
return response return response
def get_server_stats(self): def get_server_stats(self):
metrics = [] if self.rcon is None or (not self.rcon_connected and not self.rcon_connect()):
if not all(x in os.environ for x in ['RCON_HOST','RCON_PASSWORD']):
return [] return []
dim_tps = Metric('dim_tps','TPS of a dimension',"counter")
dim_ticktime = Metric('dim_ticktime',"Time a Tick took in a Dimension","counter")
overall_tps = Metric('overall_tps','overall TPS',"counter")
overall_ticktime = Metric('overall_ticktime',"overall Ticktime","counter")
player_online = Metric('player_online',"is 1 if player is online","counter")
entities = Metric('entities',"type and count of active entites", "counter")
tps_1m = Metric('paper_tps_1m','1 Minute TPS',"counter")
tps_5m = Metric('paper_tps_5m','5 Minute TPS',"counter")
tps_15m = Metric('paper_tps_15m','15 Minute TPS',"counter")
metrics.extend([dim_tps,dim_ticktime,overall_tps,overall_ticktime,player_online,entities,tps_1m,tps_5m,tps_15m]) metrics = []
dim_tps = Metric('dim_tps', 'TPS of a dimension', "counter")
dim_ticktime = Metric('dim_ticktime', "Time a Tick took in a Dimension", "counter")
overall_tps = Metric('overall_tps', 'overall TPS', "counter")
overall_ticktime = Metric('overall_ticktime', "overall Ticktime", "counter")
player_online = Metric('player_online', "is 1 if player is online", "counter")
entities = Metric('entities', "type and count of active entites", "counter")
tps_1m = Metric('paper_tps_1m', '1 Minute TPS', "counter")
tps_5m = Metric('paper_tps_5m', '5 Minute TPS', "counter")
tps_15m = Metric('paper_tps_15m', '15 Minute TPS', "counter")
metrics.extend(
[dim_tps, dim_ticktime, overall_tps, overall_ticktime, player_online, entities, tps_1m, tps_5m, tps_15m])
if 'PAPER_SERVER' in os.environ and os.environ['PAPER_SERVER'] == "True": if 'PAPER_SERVER' in os.environ and os.environ['PAPER_SERVER'] == "True":
resp = str(self.rcon_command("tps")).strip().replace("§a","") resp = str(self.rcon_command("tps")).strip().replace("§a", "")
tpsregex = re.compile("TPS from last 1m, 5m, 15m: (\d*\.\d*), (\d*\.\d*), (\d*\.\d*)") tpsregex = re.compile("TPS from last 1m, 5m, 15m: (\d*\.\d*), (\d*\.\d*), (\d*\.\d*)")
for m1,m5,m15 in tpsregex.findall(resp): for m1, m5, m15 in tpsregex.findall(resp):
tps_1m.add_sample('paper_tps_1m',value=m1,labels={'tps':'1m'}) tps_1m.add_sample('paper_tps_1m', value=m1, labels={'tps': '1m'})
tps_5m.add_sample('paper_tps_5m',value=m5,labels={'tps':'5m'}) tps_5m.add_sample('paper_tps_5m', value=m5, labels={'tps': '5m'})
tps_15m.add_sample('paper_tps_15m',value=m15,labels={'tps':'15m'}) tps_15m.add_sample('paper_tps_15m', value=m15, labels={'tps': '15m'})
if 'FORGE_SERVER' in os.environ and os.environ['FORGE_SERVER'] == "True": if 'FORGE_SERVER' in os.environ and os.environ['FORGE_SERVER'] == "True":
# dimensions # dimensions
resp = self.rcon_command("forge tps") resp = self.rcon_command("forge tps")
dimtpsregex = re.compile("Dim\s*(-*\d*)\s\((.*?)\)\s:\sMean tick time:\s(.*?) ms\. Mean TPS: (\d*\.\d*)") dimtpsregex = re.compile("Dim\s*(-*\d*)\s\((.*?)\)\s:\sMean tick time:\s(.*?) ms\. Mean TPS: (\d*\.\d*)")
for dimid, dimname, meanticktime, meantps in dimtpsregex.findall(resp): for dimid, dimname, meanticktime, meantps in dimtpsregex.findall(resp):
dim_tps.add_sample('dim_tps',value=meantps,labels={'dimension_id':dimid,'dimension_name':dimname}) dim_tps.add_sample('dim_tps', value=meantps, labels={'dimension_id': dimid, 'dimension_name': dimname})
dim_ticktime.add_sample('dim_ticktime',value=meanticktime,labels={'dimension_id':dimid,'dimension_name':dimname}) dim_ticktime.add_sample('dim_ticktime', value=meanticktime,
labels={'dimension_id': dimid, 'dimension_name': dimname})
overallregex = re.compile("Overall\s?: Mean tick time: (.*) ms. Mean TPS: (.*)") overallregex = re.compile("Overall\s?: Mean tick time: (.*) ms. Mean TPS: (.*)")
overall_tps.add_sample('overall_tps',value=overallregex.findall(resp)[0][1],labels={}) overall_tps.add_sample('overall_tps', value=overallregex.findall(resp)[0][1], labels={})
overall_ticktime.add_sample('overall_ticktime',value=overallregex.findall(resp)[0][0],labels={}) overall_ticktime.add_sample('overall_ticktime', value=overallregex.findall(resp)[0][0], labels={})
# entites # entites
resp = self.rcon_command("forge entity list") resp = self.rcon_command("forge entity list")
entityregex = re.compile("(\d+): (.*?:.*?)\s") entityregex = re.compile("(\d+): (.*?:.*?)\s")
for entitycount, entityname in entityregex.findall(resp): for entitycount, entityname in entityregex.findall(resp):
entities.add_sample('entities',value=entitycount,labels={'entity':entityname}) entities.add_sample('entities', value=entitycount, labels={'entity': entityname})
# dynmap # dynmap
if 'DYNMAP_ENABLED' in os.environ and os.environ['DYNMAP_ENABLED'] == "True": if 'DYNMAP_ENABLED' in os.environ and os.environ['DYNMAP_ENABLED'] == "True":
dynmap_tile_render_statistics = Metric('dynmap_tile_render_statistics','Tile Render Statistics reported by Dynmap',"counter") dynmap_tile_render_statistics = Metric('dynmap_tile_render_statistics',
dynmap_chunk_loading_statistics_count = Metric('dynmap_chunk_loading_statistics_count','Chunk Loading Statistics reported by Dynmap',"counter") 'Tile Render Statistics reported by Dynmap', "counter")
dynmap_chunk_loading_statistics_duration = Metric('dynmap_chunk_loading_statistics_duration','Chunk Loading Statistics reported by Dynmap',"counter") dynmap_chunk_loading_statistics_count = Metric('dynmap_chunk_loading_statistics_count',
metrics.extend([dynmap_tile_render_statistics,dynmap_chunk_loading_statistics_count,dynmap_chunk_loading_statistics_duration]) 'Chunk Loading Statistics reported by Dynmap', "counter")
dynmap_chunk_loading_statistics_duration = Metric('dynmap_chunk_loading_statistics_duration',
'Chunk Loading Statistics reported by Dynmap', "counter")
metrics.extend([dynmap_tile_render_statistics, dynmap_chunk_loading_statistics_count,
dynmap_chunk_loading_statistics_duration])
resp = self.rcon_command("dynmap stats") resp = self.rcon_command("dynmap stats")
dynmaptilerenderregex = re.compile(" (.*?): processed=(\d*), rendered=(\d*), updated=(\d*)") dynmaptilerenderregex = re.compile(" (.*?): processed=(\d*), rendered=(\d*), updated=(\d*)")
for dim, processed, rendered, updated in dynmaptilerenderregex.findall(resp): for dim, processed, rendered, updated in dynmaptilerenderregex.findall(resp):
dynmap_tile_render_statistics.add_sample('dynmap_tile_render_statistics',value=processed,labels={'type':'processed','file':dim}) dynmap_tile_render_statistics.add_sample('dynmap_tile_render_statistics', value=processed,
dynmap_tile_render_statistics.add_sample('dynmap_tile_render_statistics',value=rendered,labels={'type':'rendered','file':dim}) labels={'type': 'processed', 'file': dim})
dynmap_tile_render_statistics.add_sample('dynmap_tile_render_statistics',value=updated,labels={'type':'updated','file':dim}) dynmap_tile_render_statistics.add_sample('dynmap_tile_render_statistics', value=rendered,
labels={'type': 'rendered', 'file': dim})
dynmap_tile_render_statistics.add_sample('dynmap_tile_render_statistics', value=updated,
labels={'type': 'updated', 'file': dim})
dynmapchunkloadingregex = re.compile("Chunks processed: (.*?): count=(\d*), (\d*.\d*)") dynmapchunkloadingregex = re.compile("Chunks processed: (.*?): count=(\d*), (\d*.\d*)")
for state, count, duration_per_chunk in dynmapchunkloadingregex.findall(resp): for state, count, duration_per_chunk in dynmapchunkloadingregex.findall(resp):
dynmap_chunk_loading_statistics_count.add_sample('dynmap_chunk_loading_statistics',value=count,labels={'type': state}) dynmap_chunk_loading_statistics_count.add_sample('dynmap_chunk_loading_statistics', value=count,
dynmap_chunk_loading_statistics_duration.add_sample('dynmap_chunk_loading_duration',value=duration_per_chunk,labels={'type': state}) labels={'type': state})
dynmap_chunk_loading_statistics_duration.add_sample('dynmap_chunk_loading_duration',
value=duration_per_chunk, labels={'type': state})
# player # player
resp = self.rcon_command("list") resp = self.rcon_command("list")
@ -120,32 +161,32 @@ class MinecraftCollector(object):
if playerregex.findall(resp): if playerregex.findall(resp):
for player in playerregex.findall(resp)[0].split(","): for player in playerregex.findall(resp)[0].split(","):
if not player.isspace(): if not player.isspace():
player_online.add_sample('player_online',value=1,labels={'player':player.lstrip()}) player_online.add_sample('player_online', value=1, labels={'player': player.lstrip()})
return metrics return metrics
def get_player_quests_finished(self,uuid): def get_player_quests_finished(self, uuid):
with open(self.betterquesting+"/QuestProgress.json") as json_file: with open(self.better_questing + "/QuestProgress.json") as json_file:
data = json.load(json_file) data = json.load(json_file)
json_file.close() json_file.close()
counter = 0 counter = 0
for _, value in data['questProgress:9'].items(): for _, value in data['questProgress:9'].items():
for _, u in value['tasks:9']['0:10']['completeUsers:9'].items(): for _, u in value['tasks:9']['0:10']['completeUsers:9'].items():
if u == uuid: if u == uuid:
counter +=1 counter += 1
return counter return counter
def get_player_stats(self,uuid): def get_player_stats(self, uuid):
with open(self.statsdirectory+"/"+uuid+".json") as json_file: with open(self.stats_directory + "/" + uuid + ".json") as json_file:
data = json.load(json_file) data = json.load(json_file)
json_file.close() json_file.close()
nbtfile = nbt.nbt.NBTFile(self.playerdirectory+"/"+uuid+".dat",'rb') nbtfile = nbt.nbt.NBTFile(self.player_directory + "/" + uuid + ".dat", 'rb')
data["stat.XpTotal"] = nbtfile.get("XpTotal").value data["stat.XpTotal"] = nbtfile.get("XpTotal").value
data["stat.XpLevel"] = nbtfile.get("XpLevel").value data["stat.XpLevel"] = nbtfile.get("XpLevel").value
data["stat.Score"] = nbtfile.get("Score").value data["stat.Score"] = nbtfile.get("Score").value
data["stat.Health"] = nbtfile.get("Health").value data["stat.Health"] = nbtfile.get("Health").value
data["stat.foodLevel"]= nbtfile.get("foodLevel").value data["stat.foodLevel"] = nbtfile.get("foodLevel").value
with open(self.advancementsdirectory+"/"+uuid+".json") as json_file: with open(self.advancements_directory + "/" + uuid + ".json") as json_file:
count = 0 count = 0
advancements = json.load(json_file) advancements = json.load(json_file)
for key, value in advancements.items(): for key, value in advancements.items():
@ -154,121 +195,132 @@ class MinecraftCollector(object):
if value["done"] == True: if value["done"] == True:
count += 1 count += 1
data["stat.advancements"] = count data["stat.advancements"] = count
if self.questsEnabled: if self.quests_enabled:
data["stat.questsFinished"] = self.get_player_quests_finished(uuid) data["stat.questsFinished"] = self.get_player_quests_finished(uuid)
return data return data
def update_metrics_for_player(self,uuid): def update_metrics_for_player(self, uuid):
name = self.uuid_to_player(uuid) name = self.uuid_to_player(uuid)
if not name: return if not name:
return
data = self.get_player_stats(uuid) data = self.get_player_stats(uuid)
blocks_mined = Metric('blocks_mined','Blocks a Player mined',"counter") blocks_mined = Metric('blocks_mined', 'Blocks a Player mined', "counter")
blocks_picked_up = Metric('blocks_picked_up','Blocks a Player picked up',"counter") blocks_picked_up = Metric('blocks_picked_up', 'Blocks a Player picked up', "counter")
player_deaths = Metric('player_deaths','How often a Player died',"counter") player_deaths = Metric('player_deaths', 'How often a Player died', "counter")
player_jumps = Metric('player_jumps','How often a Player has jumped',"counter") player_jumps = Metric('player_jumps', 'How often a Player has jumped', "counter")
cm_traveled = Metric('cm_traveled','How many cm a Player traveled, whatever that means',"counter") cm_traveled = Metric('cm_traveled', 'How many cm a Player traveled, whatever that means', "counter")
player_xp_total = Metric('player_xp_total',"How much total XP a player has","counter") player_xp_total = Metric('player_xp_total', "How much total XP a player has", "counter")
player_current_level= Metric('player_current_level',"How much current XP a player has","counter") player_current_level = Metric('player_current_level', "How much current XP a player has", "counter")
player_food_level = Metric('player_food_level',"How much food the player currently has","counter") player_food_level = Metric('player_food_level', "How much food the player currently has", "counter")
player_health = Metric('player_health',"How much Health the player currently has","counter") player_health = Metric('player_health', "How much Health the player currently has", "counter")
player_score = Metric('player_score',"The Score of the player","counter") player_score = Metric('player_score', "The Score of the player", "counter")
entities_killed = Metric('entities_killed',"Entities killed by player","counter") entities_killed = Metric('entities_killed', "Entities killed by player", "counter")
damage_taken = Metric('damage_taken',"Damage Taken by Player","counter") damage_taken = Metric('damage_taken', "Damage Taken by Player", "counter")
damage_dealt = Metric('damage_dealt',"Damage dealt by Player","counter") damage_dealt = Metric('damage_dealt', "Damage dealt by Player", "counter")
blocks_crafted = Metric('blocks_crafted',"Items a Player crafted","counter") blocks_crafted = Metric('blocks_crafted', "Items a Player crafted", "counter")
player_playtime = Metric('player_playtime',"Time in Minutes a Player was online","counter") player_playtime = Metric('player_playtime', "Time in Minutes a Player was online", "counter")
player_advancements = Metric('player_advancements', "Number of completed advances of a player","counter") player_advancements = Metric('player_advancements', "Number of completed advances of a player", "counter")
player_slept = Metric('player_slept',"Times a Player slept in a bed","counter") player_slept = Metric('player_slept', "Times a Player slept in a bed", "counter")
player_quests_finished = Metric('player_quests_finished', 'Number of quests a Player has finished', 'counter') player_quests_finished = Metric('player_quests_finished', 'Number of quests a Player has finished', 'counter')
player_used_crafting_table = Metric('player_used_crafting_table',"Times a Player used a Crafting Table","counter") player_used_crafting_table = Metric('player_used_crafting_table', "Times a Player used a Crafting Table",
mc_custom = Metric('mc_custom',"Custom Minectaft stat","counter") "counter")
mc_custom = Metric('mc_custom', "Custom Minecraft stat", "counter")
for key, value in data.items(): # pre 1.15 for key, value in data.items(): # pre 1.15
if key in ("stats", "DataVersion"): if key in ("stats", "DataVersion"):
continue continue
stat = key.split(".")[1] # entityKilledBy stat = key.split(".")[1] # entityKilledBy
if stat == "mineBlock": if stat == "mineBlock":
blocks_mined.add_sample("blocks_mined",value=value,labels={'player':name,'block':'.'.join((key.split(".")[2],key.split(".")[3]))}) blocks_mined.add_sample("blocks_mined", value=value, labels={'player': name, 'block': '.'.join(
(key.split(".")[2], key.split(".")[3]))})
elif stat == "pickup": elif stat == "pickup":
blocks_picked_up.add_sample("blocks_picked_up",value=value,labels={'player':name,'block':'.'.join((key.split(".")[2],key.split(".")[3]))}) blocks_picked_up.add_sample("blocks_picked_up", value=value, labels={'player': name, 'block': '.'.join(
(key.split(".")[2], key.split(".")[3]))})
elif stat == "entityKilledBy": elif stat == "entityKilledBy":
if len(key.split(".")) == 4: if len(key.split(".")) == 4:
player_deaths.add_sample('player_deaths',value=value,labels={'player':name,'cause':'.'.join((key.split(".")[2],key.split(".")[3]))}) player_deaths.add_sample('player_deaths', value=value, labels={'player': name, 'cause': '.'.join(
(key.split(".")[2], key.split(".")[3]))})
else: else:
player_deaths.add_sample('player_deaths',value=value,labels={'player':name,'cause':key.split(".")[2]}) player_deaths.add_sample('player_deaths', value=value,
labels={'player': name, 'cause': key.split(".")[2]})
elif stat == "jump": elif stat == "jump":
player_jumps.add_sample("player_jumps",value=value,labels={'player':name}) player_jumps.add_sample("player_jumps", value=value, labels={'player': name})
elif stat == "walkOneCm": elif stat == "walkOneCm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"walking"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "walking"})
elif stat == "swimOneCm": elif stat == "swimOneCm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"swimming"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "swimming"})
elif stat == "sprintOneCm": elif stat == "sprintOneCm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"sprinting"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "sprinting"})
elif stat == "diveOneCm": elif stat == "diveOneCm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"diving"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "diving"})
elif stat == "fallOneCm": elif stat == "fallOneCm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"falling"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "falling"})
elif stat == "flyOneCm": elif stat == "flyOneCm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"flying"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "flying"})
elif stat == "boatOneCm": elif stat == "boatOneCm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"boat"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "boat"})
elif stat == "horseOneCm": elif stat == "horseOneCm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"horse"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "horse"})
elif stat == "climbOneCm": elif stat == "climbOneCm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"climbing"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "climbing"})
elif stat == "XpTotal": elif stat == "XpTotal":
player_xp_total.add_sample('player_xp_total',value=value,labels={'player':name}) player_xp_total.add_sample('player_xp_total', value=value, labels={'player': name})
elif stat == "XpLevel": elif stat == "XpLevel":
player_current_level.add_sample('player_current_level',value=value,labels={'player':name}) player_current_level.add_sample('player_current_level', value=value, labels={'player': name})
elif stat == "foodLevel": elif stat == "foodLevel":
player_food_level.add_sample('player_food_level',value=value,labels={'player':name}) player_food_level.add_sample('player_food_level', value=value, labels={'player': name})
elif stat == "Health": elif stat == "Health":
player_health.add_sample('player_health',value=value,labels={'player':name}) player_health.add_sample('player_health', value=value, labels={'player': name})
elif stat == "Score": elif stat == "Score":
player_score.add_sample('player_score',value=value,labels={'player':name}) player_score.add_sample('player_score', value=value, labels={'player': name})
elif stat == "killEntity": elif stat == "killEntity":
entities_killed.add_sample('entities_killed',value=value,labels={'player':name,"entity":key.split(".")[2]}) entities_killed.add_sample('entities_killed', value=value,
labels={'player': name, "entity": key.split(".")[2]})
elif stat == "damageDealt": elif stat == "damageDealt":
damage_dealt.add_sample('damage_dealt',value=value,labels={'player':name}) damage_dealt.add_sample('damage_dealt', value=value, labels={'player': name})
elif stat == "damageTaken": elif stat == "damageTaken":
damage_dealt.add_sample('damage_taken',value=value,labels={'player':name}) damage_dealt.add_sample('damage_taken', value=value, labels={'player': name})
elif stat == "craftItem": elif stat == "craftItem":
blocks_crafted.add_sample('blocks_crafted',value=value,labels={'player':name,'block':'.'.join((key.split(".")[2],key.split(".")[3]))}) blocks_crafted.add_sample('blocks_crafted', value=value, labels={'player': name, 'block': '.'.join(
(key.split(".")[2], key.split(".")[3]))})
elif stat == "playOneMinute": elif stat == "playOneMinute":
player_playtime.add_sample('player_playtime',value=value,labels={'player':name}) player_playtime.add_sample('player_playtime', value=value, labels={'player': name})
elif stat == "advancements": elif stat == "advancements":
player_advancements.add_sample('player_advancements',value=value,labels={'player':name}) player_advancements.add_sample('player_advancements', value=value, labels={'player': name})
elif stat == "sleepInBed": elif stat == "sleepInBed":
player_slept.add_sample('player_slept',value=value,labels={'player':name}) player_slept.add_sample('player_slept', value=value, labels={'player': name})
elif stat == "craftingTableInteraction": elif stat == "craftingTableInteraction":
player_used_crafting_table.add_sample('player_used_crafting_table',value=value,labels={'player':name}) player_used_crafting_table.add_sample('player_used_crafting_table', value=value,
labels={'player': name})
elif stat == "questsFinished": elif stat == "questsFinished":
player_quests_finished.add_sample('player_quests_finished',value=value,labels={'player':name}) player_quests_finished.add_sample('player_quests_finished', value=value, labels={'player': name})
if "stats" in data: # Minecraft > 1.15 if "stats" in data: # Minecraft > 1.15
if "minecraft:crafted" in data["stats"]: if "minecraft:crafted" in data["stats"]:
for block, value in data["stats"]["minecraft:crafted"].items(): for block, value in data["stats"]["minecraft:crafted"].items():
blocks_crafted.add_sample('blocks_crafted',value=value,labels={'player':name,'block':block}) blocks_crafted.add_sample('blocks_crafted', value=value, labels={'player': name, 'block': block})
if "minecraft:mined" in data["stats"]: if "minecraft:mined" in data["stats"]:
for block, value in data["stats"]["minecraft:mined"].items(): for block, value in data["stats"]["minecraft:mined"].items():
blocks_mined.add_sample("blocks_mined",value=value,labels={'player':name,'block':block}) blocks_mined.add_sample("blocks_mined", value=value, labels={'player': name, 'block': block})
if "minecraft:picked_up" in data["stats"]: if "minecraft:picked_up" in data["stats"]:
for block, value in data["stats"]["minecraft:picked_up"].items(): for block, value in data["stats"]["minecraft:picked_up"].items():
blocks_picked_up.add_sample("blocks_picked_up",value=value,labels={'player':name,'block':block}) blocks_picked_up.add_sample("blocks_picked_up", value=value,
labels={'player': name, 'block': block})
if "minecraft:killed" in data["stats"]: if "minecraft:killed" in data["stats"]:
for entity, value in data["stats"]["minecraft:killed"].items(): for entity, value in data["stats"]["minecraft:killed"].items():
entities_killed.add_sample('entities_killed',value=value,labels={'player':name,"entity":entity}) entities_killed.add_sample('entities_killed', value=value,
labels={'player': name, "entity": entity})
if "minecraft:killed_by" in data["stats"]: if "minecraft:killed_by" in data["stats"]:
for entity, value in data["stats"]["minecraft:killed_by"].items(): for entity, value in data["stats"]["minecraft:killed_by"].items():
player_deaths.add_sample('player_deaths',value=value,labels={'player':name,'cause': entity}) player_deaths.add_sample('player_deaths', value=value, labels={'player': name, 'cause': entity})
for stat, value in data["stats"]["minecraft:custom"].items(): for stat, value in data["stats"]["minecraft:custom"].items():
if stat == "minecraft:jump": if stat == "minecraft:jump":
player_jumps.add_sample("player_jumps",value=value,labels={'player':name}) player_jumps.add_sample("player_jumps", value=value, labels={'player': name})
elif stat == "minecraft:deaths": elif stat == "minecraft:deaths":
player_deaths.add_sample('player_deaths',value=value,labels={'player':name}) player_deaths.add_sample('player_deaths', value=value, labels={'player': name})
elif stat == "minecraft:damage_taken": elif stat == "minecraft:damage_taken":
damage_taken.add_sample('damage_taken',value=value,labels={'player':name}) damage_taken.add_sample('damage_taken', value=value, labels={'player': name})
elif stat == "minecraft:damage_dealt": elif stat == "minecraft:damage_dealt":
damage_dealt.add_sample('damage_dealt',value=value,labels={'player':name}) damage_dealt.add_sample('damage_dealt',value=value,labels={'player':name})
elif stat == "minecraft:play_time": elif stat == "minecraft:play_time":
@ -276,35 +328,40 @@ class MinecraftCollector(object):
elif stat == "minecraft:play_one_minute": # pre 1.17 elif stat == "minecraft:play_one_minute": # pre 1.17
player_playtime.add_sample('player_playtime',value=value,labels={'player':name}) player_playtime.add_sample('player_playtime',value=value,labels={'player':name})
elif stat == "minecraft:walk_one_cm": elif stat == "minecraft:walk_one_cm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"walking"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "walking"})
elif stat == "minecraft:walk_on_water_one_cm": elif stat == "minecraft:walk_on_water_one_cm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"swimming"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "swimming"})
elif stat == "minecraft:sprint_one_cm": elif stat == "minecraft:sprint_one_cm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"sprinting"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "sprinting"})
elif stat == "minecraft:walk_under_water_one_cm": elif stat == "minecraft:walk_under_water_one_cm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"diving"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "diving"})
elif stat == "minecraft:fall_one_cm": elif stat == "minecraft:fall_one_cm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"falling"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "falling"})
elif stat == "minecraft:fly_one_cm": elif stat == "minecraft:fly_one_cm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"flying"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "flying"})
elif stat == "minecraft:boat_one_cm": elif stat == "minecraft:boat_one_cm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"boat"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "boat"})
elif stat == "minecraft:horse_one_cm": elif stat == "minecraft:horse_one_cm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"horse"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "horse"})
elif stat == "minecraft:climb_one_cm": elif stat == "minecraft:climb_one_cm":
cm_traveled.add_sample("cm_traveled",value=value,labels={'player':name,'method':"climbing"}) cm_traveled.add_sample("cm_traveled", value=value, labels={'player': name, 'method': "climbing"})
elif stat == "minecraft:sleep_in_bed": elif stat == "minecraft:sleep_in_bed":
player_slept.add_sample('player_slept',value=value,labels={'player':name}) player_slept.add_sample('player_slept', value=value, labels={'player': name})
elif stat == "minecraft:interact_with_crafting_table": elif stat == "minecraft:interact_with_crafting_table":
player_used_crafting_table.add_sample('player_used_crafting_table',value=value,labels={'player':name}) player_used_crafting_table.add_sample('player_used_crafting_table', value=value,
labels={'player': name})
else: else:
mc_custom.add_sample('mc_custom',value=value,labels={'stat':stat}) mc_custom.add_sample('mc_custom', value=value, labels={'stat': stat})
return [blocks_mined,blocks_picked_up,player_deaths,player_jumps,cm_traveled,player_xp_total,player_current_level,player_food_level,player_health,player_score,entities_killed,damage_taken,damage_dealt,blocks_crafted,player_playtime,player_advancements,player_slept,player_used_crafting_table,player_quests_finished,mc_custom] return [blocks_mined, blocks_picked_up, player_deaths, player_jumps, cm_traveled, player_xp_total,
player_current_level, player_food_level, player_health, player_score, entities_killed, damage_taken,
damage_dealt, blocks_crafted, player_playtime, player_advancements, player_slept,
player_used_crafting_table, player_quests_finished, mc_custom]
def collect(self): def collect(self):
for player in self.get_players(): for player in self.get_players():
metrics = self.update_metrics_for_player(player) metrics = self.update_metrics_for_player(player)
if not metrics: continue if not metrics:
continue
for metric in metrics: for metric in metrics:
yield metric yield metric
@ -312,9 +369,6 @@ class MinecraftCollector(object):
for metric in self.get_server_stats(): for metric in self.get_server_stats():
yield metric yield metric
if __name__ == '__main__':
if all(x in os.environ for x in ['RCON_HOST','RCON_PASSWORD']):
print("RCON is enabled for "+ os.environ['RCON_HOST'])
HTTP_PORT = int(os.environ.get('HTTP_PORT')) HTTP_PORT = int(os.environ.get('HTTP_PORT'))
if HTTP_PORT == None: if HTTP_PORT == None:
@ -326,5 +380,9 @@ if __name__ == '__main__':
print(f'Exporter started on Port {HTTP_PORT}') print(f'Exporter started on Port {HTTP_PORT}')
while True: while True:
try:
time.sleep(1) time.sleep(1)
schedule.run_pending() schedule.run_pending()
except MCRconException:
# RCON timeout
collector.rcon_disconnect()

View file

@ -1,5 +1,5 @@
mcrcon==0.5.2 mcrcon==0.7.0
NBT==1.5.0 NBT==1.5.1
prometheus-client==0.7.1 prometheus-client==0.12.0
requests==2.20.0 requests==2.27.1
schedule==0.6.0 schedule==1.1.0