Files
prometheus-spigot/src/main/java/com/prometheus/spigot/metrics/MetricsRegistry.java
T
dmg aca7a81ad3
Build / build (push) Successful in 1m45s
feat: track entity kills by player
2026-01-27 22:41:32 -05:00

560 lines
24 KiB
Java

package com.prometheus.spigot.metrics;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
import java.lang.management.ThreadMXBean;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageEvent;
public final class MetricsRegistry {
private final Server server;
private final boolean enableJvm;
private final boolean enableProcess;
private final long startTimeMillis;
private final Method serverTpsMethod;
private final Method serverMsptMethod;
private final Method playerPingMethod;
private final Method chunkTileEntitiesMethod;
private volatile Map<String, Long> entityCountsSnapshot = Collections.emptyMap();
private volatile Map<String, Long> tileEntityCountsSnapshot = Collections.emptyMap();
private final LongAdder chatMessages = new LongAdder();
private final LongAdder playerMovements = new LongAdder();
private final LongAdder playerJoins = new LongAdder();
private final LongAdder playerQuits = new LongAdder();
private final LongAdder chunkLoads = new LongAdder();
private final LongAdder chunkUnloads = new LongAdder();
private final LongAdder chunkGenerations = new LongAdder();
private final LongAdder pluginEnables = new LongAdder();
private final LongAdder pluginDisables = new LongAdder();
private final ConcurrentHashMap<String, LongAdder> blocksPlacedByMaterial = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LongAdder> blocksBrokenByMaterial = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LongAdder> deathsByCause = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LongAdder> loginFailuresByReason = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LongAdder> commandsBlocked = new ConcurrentHashMap<>();
private final ConcurrentHashMap<LabelPair, LongAdder> commandsExecuted = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LongAdder> itemsCraftedByMaterial = new ConcurrentHashMap<>();
private final ConcurrentHashMap<LabelPair, LongAdder> entityKills = new ConcurrentHashMap<>();
public MetricsRegistry(Server server, boolean enableJvm, boolean enableProcess) {
this.server = server;
this.enableJvm = enableJvm;
this.enableProcess = enableProcess;
this.startTimeMillis = System.currentTimeMillis();
this.serverTpsMethod = findMethod(server.getClass(), "getTPS");
this.serverMsptMethod = findMethod(server.getClass(), "getAverageTickTime");
this.playerPingMethod = findMethod(Player.class, "getPing");
this.chunkTileEntitiesMethod = findMethod(Chunk.class, "getTileEntities");
}
public void incrementChatMessages() {
chatMessages.increment();
}
public void incrementPlayerMovements() {
playerMovements.increment();
}
public void incrementPlayerJoins() {
playerJoins.increment();
}
public void incrementPlayerQuits() {
playerQuits.increment();
}
public void incrementPlayerLoginFailed(String reason) {
incrementLabeledCounter(loginFailuresByReason, reason);
}
public void incrementBlocksBroken(Material material) {
incrementLabeledCounter(blocksBrokenByMaterial, material.name());
}
public void incrementBlocksPlaced(Material material) {
incrementLabeledCounter(blocksPlacedByMaterial, material.name());
}
public void incrementPlayerDeath(EntityDamageEvent.DamageCause cause) {
incrementLabeledCounter(deathsByCause, cause.name());
}
public void incrementPlayerDeath(String cause) {
incrementLabeledCounter(deathsByCause, cause);
}
public void incrementChunkLoads() {
chunkLoads.increment();
}
public void incrementChunkUnloads() {
chunkUnloads.increment();
}
public void incrementChunkGenerations() {
chunkGenerations.increment();
}
public void incrementCommandExecuted(String command, String source) {
String normalizedCommand = normalizeLabel(command);
String normalizedSource = normalizeLabel(source);
commandsExecuted.computeIfAbsent(new LabelPair(normalizedCommand, normalizedSource), key -> new LongAdder()).increment();
}
public void incrementCommandBlocked(String command) {
incrementLabeledCounter(commandsBlocked, command);
}
public void incrementPluginEnables() {
pluginEnables.increment();
}
public void incrementPluginDisables() {
pluginDisables.increment();
}
public void incrementItemsCrafted(Material material) {
incrementLabeledCounter(itemsCraftedByMaterial, material.name());
}
public void incrementEntityKill(EntityType entityType, String killer) {
String normalizedEntity = normalizeLabel(entityType.name());
String normalizedKiller = normalizeLabel(killer);
entityKills.computeIfAbsent(new LabelPair(normalizedEntity, normalizedKiller), key -> new LongAdder()).increment();
}
public void refreshEntitySnapshots() {
Map<String, Long> entityCounts = new HashMap<>();
Map<String, Long> tileCounts = new HashMap<>();
for (var world : server.getWorlds()) {
for (Entity entity : world.getEntities()) {
EntityType type = entity.getType();
String label = normalizeLabel(type.name());
entityCounts.merge(label, 1L, Long::sum);
}
if (chunkTileEntitiesMethod != null) {
for (Chunk chunk : world.getLoadedChunks()) {
Object result = invoke(chunkTileEntitiesMethod, chunk);
if (!(result instanceof Object[] states)) {
continue;
}
for (Object state : states) {
if (state instanceof BlockState blockState) {
String label = normalizeLabel(blockState.getType().name());
tileCounts.merge(label, 1L, Long::sum);
}
}
}
}
}
entityCountsSnapshot = Collections.unmodifiableMap(entityCounts);
tileEntityCountsSnapshot = Collections.unmodifiableMap(tileCounts);
}
public String render() {
StringBuilder builder = new StringBuilder();
appendCounter(builder, "spigot_chat_messages_total", "Total chat messages sent", chatMessages.sum());
appendCounter(builder, "spigot_player_movements_total", "Total player movement events", playerMovements.sum());
appendCounter(builder, "spigot_players_joined_total", "Total player joins", playerJoins.sum());
appendCounter(builder, "spigot_players_quit_total", "Total player quits", playerQuits.sum());
appendCounter(builder, "spigot_chunk_loads_total", "Total chunk load events", chunkLoads.sum());
appendCounter(builder, "spigot_chunk_unloads_total", "Total chunk unload events", chunkUnloads.sum());
appendCounter(builder, "spigot_chunks_generated_total", "Total generated chunks", chunkGenerations.sum());
appendCounter(builder, "spigot_plugin_enable_total", "Total plugin enable events", pluginEnables.sum());
appendCounter(builder, "spigot_plugin_disable_total", "Total plugin disable events", pluginDisables.sum());
appendLabeledCounterAdder(builder, "spigot_blocks_broken_total", "Blocks broken by material", "material", blocksBrokenByMaterial);
appendLabeledCounterAdder(builder, "spigot_blocks_placed_total", "Blocks placed by material", "material", blocksPlacedByMaterial);
appendLabeledCounterAdder(builder, "spigot_player_deaths_total", "Player deaths by cause", "cause", deathsByCause);
appendLabeledCounterAdder(builder, "spigot_player_logins_failed_total", "Failed player logins by reason", "reason", loginFailuresByReason);
appendLabeledCounterAdder(builder, "spigot_commands_blocked_total", "Blocked commands by name", "command", commandsBlocked);
appendLabeledCounterAdder(builder, "spigot_commands_executed_total", "Commands executed by source", "command", "source", commandsExecuted);
appendLabeledCounterAdder(builder, "spigot_items_crafted_total", "Items crafted by material", "material", itemsCraftedByMaterial);
appendLabeledCounterAdder(builder, "spigot_entities_killed_total", "Entities killed by player", "entity", "killer", entityKills);
appendGauge(builder, "spigot_players_online", "Online player count", server.getOnlinePlayers().size());
appendGauge(builder, "spigot_players_max", "Max player slots", server.getMaxPlayers());
appendGauge(builder, "spigot_worlds_loaded", "Loaded world count", server.getWorlds().size());
appendGauge(builder, "spigot_chunks_loaded", "Loaded chunk count", countLoadedChunks());
appendGauge(builder, "spigot_plugins_loaded", "Loaded plugin count", server.getPluginManager().getPlugins().length);
appendGauge(builder, "spigot_tasks_pending", "Pending scheduled tasks", server.getScheduler().getPendingTasks().size());
appendGauge(builder, "spigot_tasks_running", "Active scheduled tasks", server.getScheduler().getActiveWorkers().size());
appendEntityMetrics(builder);
appendTileEntityMetrics(builder);
appendPlayerPingMetrics(builder);
appendPerformanceMetrics(builder);
if (enableProcess) {
appendProcessMetrics(builder);
}
if (enableJvm) {
appendJvmMetrics(builder);
}
return builder.toString();
}
private void appendProcessMetrics(StringBuilder builder) {
double uptimeSeconds = (System.currentTimeMillis() - startTimeMillis) / 1000.0;
appendGauge(builder, "process_uptime_seconds", "Process uptime in seconds", formatDouble(uptimeSeconds));
}
private void appendJvmMetrics(StringBuilder builder) {
MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
MemoryUsage nonHeap = ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage();
appendMemoryMetrics(builder, "heap", heap);
appendMemoryMetrics(builder, "nonheap", nonHeap);
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
appendGauge(builder, "jvm_threads", "Current JVM thread count", threadBean.getThreadCount());
appendThreadCpuMetrics(builder, threadBean);
appendGcMetrics(builder);
}
private void appendThreadCpuMetrics(StringBuilder builder, ThreadMXBean threadBean) {
if (!threadBean.isThreadCpuTimeSupported()) {
return;
}
if (!threadBean.isThreadCpuTimeEnabled()) {
try {
threadBean.setThreadCpuTimeEnabled(true);
} catch (SecurityException ex) {
return;
}
}
long totalNanos = 0L;
for (long threadId : threadBean.getAllThreadIds()) {
long time = threadBean.getThreadCpuTime(threadId);
if (time > 0) {
totalNanos += time;
}
}
double totalSeconds = totalNanos / 1_000_000_000.0;
appendCounter(builder, "jvm_thread_cpu_seconds_total", "Total CPU seconds used by JVM threads", formatDouble(totalSeconds));
}
private void appendGcMetrics(StringBuilder builder) {
Map<String, Long> gcCounts = new HashMap<>();
Map<String, String> gcTimes = new HashMap<>();
for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) {
String name = normalizeLabel(gcBean.getName());
long count = gcBean.getCollectionCount();
long timeMillis = gcBean.getCollectionTime();
if (count >= 0) {
gcCounts.put(name, count);
}
if (timeMillis >= 0) {
double seconds = timeMillis / 1000.0;
gcTimes.put(name, formatDouble(seconds));
}
}
if (!gcCounts.isEmpty()) {
appendLabeledCounterLong(builder, "jvm_gc_pause_seconds_count", "GC pause count", "gc", gcCounts);
}
if (!gcTimes.isEmpty()) {
appendLabeledCounterString(builder, "jvm_gc_pause_seconds_total", "Total GC pause time", "gc", gcTimes);
}
}
private void appendEntityMetrics(StringBuilder builder) {
appendLabeledGauge(builder, "spigot_entities_total", "Entities by type", "type", entityCountsSnapshot);
}
private void appendTileEntityMetrics(StringBuilder builder) {
if (chunkTileEntitiesMethod == null) {
return;
}
appendLabeledGauge(builder, "spigot_tile_entities_total", "Tile entities by type", "type", tileEntityCountsSnapshot);
}
private void appendPlayerPingMetrics(StringBuilder builder) {
if (playerPingMethod == null) {
return;
}
List<Integer> pings = new ArrayList<>();
for (Player player : server.getOnlinePlayers()) {
Integer ping = readPing(player);
if (ping != null) {
pings.add(ping);
appendLabeledGauge(builder, "spigot_player_ping_ms_player", "Player ping in milliseconds", "player", player.getName(), ping);
}
}
appendPingHistogram(builder, pings);
}
private void appendPingHistogram(StringBuilder builder, List<Integer> pings) {
double[] buckets = {25, 50, 100, 150, 200, 300, 500, 1000};
long[] bucketCounts = new long[buckets.length + 1];
long total = 0;
long sum = 0;
for (int ping : pings) {
total++;
sum += ping;
boolean placed = false;
for (int i = 0; i < buckets.length; i++) {
if (ping <= buckets[i]) {
bucketCounts[i]++;
placed = true;
break;
}
}
if (!placed) {
bucketCounts[bucketCounts.length - 1]++;
}
}
appendHelpAndType(builder, "spigot_player_ping_ms", "Player ping histogram", "histogram");
long cumulative = 0;
for (int i = 0; i < buckets.length; i++) {
cumulative += bucketCounts[i];
appendHistogramBucket(builder, "spigot_player_ping_ms_bucket", buckets[i], cumulative);
}
cumulative += bucketCounts[bucketCounts.length - 1];
appendHistogramBucket(builder, "spigot_player_ping_ms_bucket", Double.POSITIVE_INFINITY, cumulative);
builder.append("spigot_player_ping_ms_count ").append(total).append('\n');
builder.append("spigot_player_ping_ms_sum ").append(sum).append('\n');
}
private void appendPerformanceMetrics(StringBuilder builder) {
double[] tps = readTps();
if (tps != null && tps.length >= 3) {
appendLabeledGauge(builder, "spigot_tps", "Server TPS by interval", "interval", "1m", formatDouble(tps[0]));
appendLabeledGauge(builder, "spigot_tps", "Server TPS by interval", "interval", "5m", formatDouble(tps[1]));
appendLabeledGauge(builder, "spigot_tps", "Server TPS by interval", "interval", "15m", formatDouble(tps[2]));
}
Double mspt = readMspt();
if (mspt != null) {
appendGauge(builder, "spigot_mspt", "Average milliseconds per tick", formatDouble(mspt));
}
}
private void appendMemoryMetrics(StringBuilder builder, String area, MemoryUsage usage) {
appendLabeledGauge(builder, "jvm_memory_used_bytes", "JVM memory used in bytes", "area", area, usage.getUsed());
if (usage.getMax() >= 0) {
appendLabeledGauge(builder, "jvm_memory_max_bytes", "JVM memory max in bytes", "area", area, usage.getMax());
}
}
private void appendCounter(StringBuilder builder, String name, String help, long value) {
appendHelpAndType(builder, name, help, "counter");
builder.append(name).append(' ').append(value).append('\n');
}
private void appendCounter(StringBuilder builder, String name, String help, String value) {
appendHelpAndType(builder, name, help, "counter");
builder.append(name).append(' ').append(value).append('\n');
}
private void appendGauge(StringBuilder builder, String name, String help, long value) {
appendHelpAndType(builder, name, help, "gauge");
builder.append(name).append(' ').append(value).append('\n');
}
private void appendGauge(StringBuilder builder, String name, String help, String value) {
appendHelpAndType(builder, name, help, "gauge");
builder.append(name).append(' ').append(value).append('\n');
}
private void appendLabeledGauge(StringBuilder builder, String name, String help, String label, Map<String, Long> values) {
appendHelpAndType(builder, name, help, "gauge");
values.entrySet().stream()
.sorted(Map.Entry.comparingByKey(Comparator.naturalOrder()))
.forEach(entry -> builder.append(name)
.append('{').append(label).append("=\"")
.append(escapeLabelValue(entry.getKey()))
.append("\"} ")
.append(entry.getValue())
.append('\n'));
}
private void appendLabeledCounterAdder(StringBuilder builder, String name, String help, String label, Map<String, LongAdder> values) {
appendHelpAndType(builder, name, help, "counter");
values.entrySet().stream()
.sorted(Map.Entry.comparingByKey(Comparator.naturalOrder()))
.forEach(entry -> builder.append(name)
.append('{').append(label).append("=\"")
.append(escapeLabelValue(entry.getKey()))
.append("\"} ")
.append(entry.getValue().sum())
.append('\n'));
}
private void appendLabeledCounterLong(StringBuilder builder, String name, String help, String label, Map<String, Long> values) {
appendHelpAndType(builder, name, help, "counter");
values.entrySet().stream()
.sorted(Map.Entry.comparingByKey(Comparator.naturalOrder()))
.forEach(entry -> builder.append(name)
.append('{').append(label).append("=\"")
.append(escapeLabelValue(entry.getKey()))
.append("\"} ")
.append(entry.getValue())
.append('\n'));
}
private void appendLabeledCounterString(StringBuilder builder, String name, String help, String label, Map<String, String> values) {
appendHelpAndType(builder, name, help, "counter");
values.entrySet().stream()
.sorted(Map.Entry.comparingByKey(Comparator.naturalOrder()))
.forEach(entry -> builder.append(name)
.append('{').append(label).append("=\"")
.append(escapeLabelValue(entry.getKey()))
.append("\"} ")
.append(entry.getValue())
.append('\n'));
}
private void appendLabeledCounterAdder(StringBuilder builder, String name, String help, String labelOne, String labelTwo,
Map<LabelPair, LongAdder> values) {
appendHelpAndType(builder, name, help, "counter");
values.entrySet().stream()
.sorted(Map.Entry.comparingByKey(Comparator.comparing(LabelPair::first).thenComparing(LabelPair::second)))
.forEach(entry -> builder.append(name)
.append('{')
.append(labelOne).append("=\"")
.append(escapeLabelValue(entry.getKey().first()))
.append("\",")
.append(labelTwo).append("=\"")
.append(escapeLabelValue(entry.getKey().second()))
.append("\"} ")
.append(entry.getValue().sum())
.append('\n'));
}
private void appendLabeledGauge(StringBuilder builder, String name, String help, String label, String labelValue, long value) {
appendHelpAndType(builder, name, help, "gauge");
builder.append(name)
.append('{').append(label).append("=\"")
.append(escapeLabelValue(labelValue))
.append("\"} ")
.append(value)
.append('\n');
}
private void appendLabeledGauge(StringBuilder builder, String name, String help, String label, String labelValue, String value) {
appendHelpAndType(builder, name, help, "gauge");
builder.append(name)
.append('{').append(label).append("=\"")
.append(escapeLabelValue(labelValue))
.append("\"} ")
.append(value)
.append('\n');
}
private void appendHelpAndType(StringBuilder builder, String name, String help, String type) {
builder.append("# HELP ").append(name).append(' ').append(help).append('\n');
builder.append("# TYPE ").append(name).append(' ').append(type).append('\n');
}
private void incrementLabeledCounter(ConcurrentHashMap<String, LongAdder> map, String labelValue) {
String normalized = normalizeLabel(labelValue);
map.computeIfAbsent(normalized, key -> new LongAdder()).increment();
}
private long countLoadedChunks() {
long total = 0;
for (var world : server.getWorlds()) {
total += world.getLoadedChunks().length;
}
return total;
}
private String normalizeLabel(String labelValue) {
String trimmed = labelValue.trim();
return trimmed.isEmpty() ? "unknown" : trimmed.toLowerCase(Locale.ROOT);
}
private Integer readPing(Player player) {
if (playerPingMethod == null) {
return null;
}
Object result = invoke(playerPingMethod, player);
if (result instanceof Integer ping) {
return ping;
}
return null;
}
private double[] readTps() {
if (serverTpsMethod == null) {
return null;
}
Object result = invoke(serverTpsMethod, server);
if (result instanceof double[] tps) {
return tps;
}
return null;
}
private Double readMspt() {
if (serverMsptMethod == null) {
return null;
}
Object result = invoke(serverMsptMethod, server);
if (result instanceof Double value) {
return value;
}
return null;
}
private Object invoke(Method method, Object target) {
try {
return method.invoke(target);
} catch (ReflectiveOperationException ex) {
return null;
}
}
private Method findMethod(Class<?> type, String name) {
try {
return type.getMethod(name);
} catch (NoSuchMethodException ex) {
return null;
}
}
private void appendHistogramBucket(StringBuilder builder, String name, double upperBound, long value) {
builder.append(name)
.append("{le=\"")
.append(Double.isInfinite(upperBound) ? "+Inf" : formatDouble(upperBound))
.append("\"} ")
.append(value)
.append('\n');
}
private String escapeLabelValue(String value) {
return value.replace("\\", "\\\\")
.replace("\n", "\\n")
.replace("\"", "\\\"");
}
private String formatDouble(double value) {
return String.format(Locale.ROOT, "%.3f", value);
}
private record LabelPair(String first, String second) {
}
}