feat: expand metrics collection
This commit is contained in:
@@ -3,8 +3,12 @@ package com.prometheus.spigot;
|
|||||||
import com.prometheus.spigot.http.MetricsHttpHandler;
|
import com.prometheus.spigot.http.MetricsHttpHandler;
|
||||||
import com.prometheus.spigot.listeners.BlockBreakListener;
|
import com.prometheus.spigot.listeners.BlockBreakListener;
|
||||||
import com.prometheus.spigot.listeners.ChatListener;
|
import com.prometheus.spigot.listeners.ChatListener;
|
||||||
|
import com.prometheus.spigot.listeners.ChunkListener;
|
||||||
|
import com.prometheus.spigot.listeners.CommandListener;
|
||||||
|
import com.prometheus.spigot.listeners.ConnectionListener;
|
||||||
import com.prometheus.spigot.listeners.DeathListener;
|
import com.prometheus.spigot.listeners.DeathListener;
|
||||||
import com.prometheus.spigot.listeners.MoveListener;
|
import com.prometheus.spigot.listeners.MoveListener;
|
||||||
|
import com.prometheus.spigot.listeners.PluginListener;
|
||||||
import com.prometheus.spigot.metrics.MetricsRegistry;
|
import com.prometheus.spigot.metrics.MetricsRegistry;
|
||||||
import com.prometheus.spigot.util.TokenGenerator;
|
import com.prometheus.spigot.util.TokenGenerator;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
@@ -61,6 +65,10 @@ public final class PrometheusSpigotPlugin extends JavaPlugin {
|
|||||||
pluginManager.registerEvents(new DeathListener(metricsRegistry), this);
|
pluginManager.registerEvents(new DeathListener(metricsRegistry), this);
|
||||||
pluginManager.registerEvents(new ChatListener(metricsRegistry), this);
|
pluginManager.registerEvents(new ChatListener(metricsRegistry), this);
|
||||||
pluginManager.registerEvents(new MoveListener(metricsRegistry, countBlockMovementOnly), this);
|
pluginManager.registerEvents(new MoveListener(metricsRegistry, countBlockMovementOnly), this);
|
||||||
|
pluginManager.registerEvents(new ConnectionListener(metricsRegistry), this);
|
||||||
|
pluginManager.registerEvents(new CommandListener(metricsRegistry), this);
|
||||||
|
pluginManager.registerEvents(new ChunkListener(metricsRegistry), this);
|
||||||
|
pluginManager.registerEvents(new PluginListener(metricsRegistry), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startHttpServer() {
|
private void startHttpServer() {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.prometheus.spigot.listeners;
|
||||||
|
|
||||||
|
import com.prometheus.spigot.metrics.MetricsRegistry;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.world.ChunkLoadEvent;
|
||||||
|
import org.bukkit.event.world.ChunkUnloadEvent;
|
||||||
|
|
||||||
|
public final class ChunkListener implements Listener {
|
||||||
|
private final MetricsRegistry registry;
|
||||||
|
|
||||||
|
public ChunkListener(MetricsRegistry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onChunkLoad(ChunkLoadEvent event) {
|
||||||
|
registry.incrementChunkLoads();
|
||||||
|
if (event.isNewChunk()) {
|
||||||
|
registry.incrementChunkGenerations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onChunkUnload(ChunkUnloadEvent event) {
|
||||||
|
registry.incrementChunkUnloads();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.prometheus.spigot.listeners;
|
||||||
|
|
||||||
|
import com.prometheus.spigot.metrics.MetricsRegistry;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||||
|
import org.bukkit.event.server.ServerCommandEvent;
|
||||||
|
|
||||||
|
public final class CommandListener implements Listener {
|
||||||
|
private final MetricsRegistry registry;
|
||||||
|
|
||||||
|
public CommandListener(MetricsRegistry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
|
||||||
|
String command = parseCommand(event.getMessage());
|
||||||
|
registry.incrementCommandExecuted(command, "player");
|
||||||
|
if (event.isCancelled()) {
|
||||||
|
registry.incrementCommandBlocked(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onServerCommand(ServerCommandEvent event) {
|
||||||
|
String command = parseCommand(event.getCommand());
|
||||||
|
registry.incrementCommandExecuted(command, "console");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseCommand(String message) {
|
||||||
|
String sanitized = message.trim();
|
||||||
|
if (sanitized.startsWith("/")) {
|
||||||
|
sanitized = sanitized.substring(1);
|
||||||
|
}
|
||||||
|
int spaceIndex = sanitized.indexOf(' ');
|
||||||
|
if (spaceIndex > -1) {
|
||||||
|
return sanitized.substring(0, spaceIndex);
|
||||||
|
}
|
||||||
|
return sanitized.isEmpty() ? "unknown" : sanitized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.prometheus.spigot.listeners;
|
||||||
|
|
||||||
|
import com.prometheus.spigot.metrics.MetricsRegistry;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerLoginEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
|
||||||
|
public final class ConnectionListener implements Listener {
|
||||||
|
private final MetricsRegistry registry;
|
||||||
|
|
||||||
|
public ConnectionListener(MetricsRegistry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onJoin(PlayerJoinEvent event) {
|
||||||
|
registry.incrementPlayerJoins();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onQuit(PlayerQuitEvent event) {
|
||||||
|
registry.incrementPlayerQuits();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onLogin(PlayerLoginEvent event) {
|
||||||
|
if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) {
|
||||||
|
registry.incrementPlayerLoginFailed(event.getResult().name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.prometheus.spigot.listeners;
|
||||||
|
|
||||||
|
import com.prometheus.spigot.metrics.MetricsRegistry;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.server.PluginDisableEvent;
|
||||||
|
import org.bukkit.event.server.PluginEnableEvent;
|
||||||
|
|
||||||
|
public final class PluginListener implements Listener {
|
||||||
|
private final MetricsRegistry registry;
|
||||||
|
|
||||||
|
public PluginListener(MetricsRegistry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onEnable(PluginEnableEvent event) {
|
||||||
|
registry.incrementPluginEnables();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onDisable(PluginDisableEvent event) {
|
||||||
|
registry.incrementPluginDisables();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,25 @@
|
|||||||
package com.prometheus.spigot.metrics;
|
package com.prometheus.spigot.metrics;
|
||||||
|
|
||||||
|
import java.lang.management.GarbageCollectorMXBean;
|
||||||
import java.lang.management.ManagementFactory;
|
import java.lang.management.ManagementFactory;
|
||||||
import java.lang.management.MemoryUsage;
|
import java.lang.management.MemoryUsage;
|
||||||
import java.lang.management.ThreadMXBean;
|
import java.lang.management.ThreadMXBean;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.LongAdder;
|
import java.util.concurrent.LongAdder;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.Server;
|
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;
|
import org.bukkit.event.entity.EntityDamageEvent;
|
||||||
|
|
||||||
public final class MetricsRegistry {
|
public final class MetricsRegistry {
|
||||||
@@ -17,17 +27,35 @@ public final class MetricsRegistry {
|
|||||||
private final boolean enableJvm;
|
private final boolean enableJvm;
|
||||||
private final boolean enableProcess;
|
private final boolean enableProcess;
|
||||||
private final long startTimeMillis;
|
private final long startTimeMillis;
|
||||||
|
private final Method serverTpsMethod;
|
||||||
|
private final Method serverMsptMethod;
|
||||||
|
private final Method playerPingMethod;
|
||||||
|
private final Method chunkTileEntitiesMethod;
|
||||||
|
|
||||||
private final LongAdder chatMessages = new LongAdder();
|
private final LongAdder chatMessages = new LongAdder();
|
||||||
private final LongAdder playerMovements = 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> blocksBrokenByMaterial = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, LongAdder> blocksBrokenByMaterial = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<String, LongAdder> deathsByCause = 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<>();
|
||||||
|
|
||||||
public MetricsRegistry(Server server, boolean enableJvm, boolean enableProcess) {
|
public MetricsRegistry(Server server, boolean enableJvm, boolean enableProcess) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.enableJvm = enableJvm;
|
this.enableJvm = enableJvm;
|
||||||
this.enableProcess = enableProcess;
|
this.enableProcess = enableProcess;
|
||||||
this.startTimeMillis = System.currentTimeMillis();
|
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() {
|
public void incrementChatMessages() {
|
||||||
@@ -38,6 +66,18 @@ public final class MetricsRegistry {
|
|||||||
playerMovements.increment();
|
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) {
|
public void incrementBlocksBroken(Material material) {
|
||||||
incrementLabeledCounter(blocksBrokenByMaterial, material.name());
|
incrementLabeledCounter(blocksBrokenByMaterial, material.name());
|
||||||
}
|
}
|
||||||
@@ -50,18 +90,67 @@ public final class MetricsRegistry {
|
|||||||
incrementLabeledCounter(deathsByCause, 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 String render() {
|
public String render() {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
appendCounter(builder, "spigot_chat_messages_total", "Total chat messages sent", chatMessages.sum());
|
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_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());
|
||||||
|
|
||||||
appendLabeledCounter(builder, "spigot_blocks_broken_total", "Blocks broken by material", "material", blocksBrokenByMaterial);
|
appendLabeledCounter(builder, "spigot_blocks_broken_total", "Blocks broken by material", "material", blocksBrokenByMaterial);
|
||||||
appendLabeledCounter(builder, "spigot_player_deaths_total", "Player deaths by cause", "cause", deathsByCause);
|
appendLabeledCounter(builder, "spigot_player_deaths_total", "Player deaths by cause", "cause", deathsByCause);
|
||||||
|
appendLabeledCounter(builder, "spigot_player_logins_failed_total", "Failed player logins by reason", "reason", loginFailuresByReason);
|
||||||
|
appendLabeledCounter(builder, "spigot_commands_blocked_total", "Blocked commands by name", "command", commandsBlocked);
|
||||||
|
appendLabeledCounter(builder, "spigot_commands_executed_total", "Commands executed by source", "command", "source", commandsExecuted);
|
||||||
|
|
||||||
appendGauge(builder, "spigot_players_online", "Online player count", server.getOnlinePlayers().size());
|
appendGauge(builder, "spigot_players_online", "Online player count", server.getOnlinePlayers().size());
|
||||||
appendGauge(builder, "spigot_players_max", "Max player slots", server.getMaxPlayers());
|
appendGauge(builder, "spigot_players_max", "Max player slots", server.getMaxPlayers());
|
||||||
appendGauge(builder, "spigot_worlds_loaded", "Loaded world count", server.getWorlds().size());
|
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) {
|
if (enableProcess) {
|
||||||
appendProcessMetrics(builder);
|
appendProcessMetrics(builder);
|
||||||
@@ -87,6 +176,147 @@ public final class MetricsRegistry {
|
|||||||
|
|
||||||
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
|
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
|
||||||
appendGauge(builder, "jvm_threads", "Current JVM thread count", threadBean.getThreadCount());
|
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()) {
|
||||||
|
appendLabeledCounter(builder, "jvm_gc_pause_seconds_count", "GC pause count", "gc", gcCounts);
|
||||||
|
}
|
||||||
|
if (!gcTimes.isEmpty()) {
|
||||||
|
appendLabeledCounter(builder, "jvm_gc_pause_seconds_total", "Total GC pause time", "gc", gcTimes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendEntityMetrics(StringBuilder builder) {
|
||||||
|
Map<String, Long> entityCounts = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendLabeledGauge(builder, "spigot_entities_total", "Entities by type", "type", entityCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendTileEntityMetrics(StringBuilder builder) {
|
||||||
|
if (chunkTileEntitiesMethod == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Long> tileCounts = new HashMap<>();
|
||||||
|
for (var world : server.getWorlds()) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendLabeledGauge(builder, "spigot_tile_entities_total", "Tile entities by type", "type", tileCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
private void appendMemoryMetrics(StringBuilder builder, String area, MemoryUsage usage) {
|
||||||
@@ -101,6 +331,11 @@ public final class MetricsRegistry {
|
|||||||
builder.append(name).append(' ').append(value).append('\n');
|
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) {
|
private void appendGauge(StringBuilder builder, String name, String help, long value) {
|
||||||
appendHelpAndType(builder, name, help, "gauge");
|
appendHelpAndType(builder, name, help, "gauge");
|
||||||
builder.append(name).append(' ').append(value).append('\n');
|
builder.append(name).append(' ').append(value).append('\n');
|
||||||
@@ -111,6 +346,18 @@ public final class MetricsRegistry {
|
|||||||
builder.append(name).append(' ').append(value).append('\n');
|
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 appendLabeledCounter(StringBuilder builder, String name, String help, String label, Map<String, LongAdder> values) {
|
private void appendLabeledCounter(StringBuilder builder, String name, String help, String label, Map<String, LongAdder> values) {
|
||||||
appendHelpAndType(builder, name, help, "counter");
|
appendHelpAndType(builder, name, help, "counter");
|
||||||
values.entrySet().stream()
|
values.entrySet().stream()
|
||||||
@@ -123,6 +370,48 @@ public final class MetricsRegistry {
|
|||||||
.append('\n'));
|
.append('\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void appendLabeledCounter(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 appendLabeledCounter(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 appendLabeledCounter(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) {
|
private void appendLabeledGauge(StringBuilder builder, String name, String help, String label, String labelValue, long value) {
|
||||||
appendHelpAndType(builder, name, help, "gauge");
|
appendHelpAndType(builder, name, help, "gauge");
|
||||||
builder.append(name)
|
builder.append(name)
|
||||||
@@ -133,16 +422,97 @@ public final class MetricsRegistry {
|
|||||||
.append('\n');
|
.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) {
|
private void appendHelpAndType(StringBuilder builder, String name, String help, String type) {
|
||||||
builder.append("# HELP ").append(name).append(' ').append(help).append('\n');
|
builder.append("# HELP ").append(name).append(' ').append(help).append('\n');
|
||||||
builder.append("# TYPE ").append(name).append(' ').append(type).append('\n');
|
builder.append("# TYPE ").append(name).append(' ').append(type).append('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
private void incrementLabeledCounter(ConcurrentHashMap<String, LongAdder> map, String labelValue) {
|
private void incrementLabeledCounter(ConcurrentHashMap<String, LongAdder> map, String labelValue) {
|
||||||
String normalized = labelValue.toLowerCase(Locale.ROOT);
|
String normalized = normalizeLabel(labelValue);
|
||||||
map.computeIfAbsent(normalized, key -> new LongAdder()).increment();
|
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) {
|
private String escapeLabelValue(String value) {
|
||||||
return value.replace("\\", "\\\\")
|
return value.replace("\\", "\\\\")
|
||||||
.replace("\n", "\\n")
|
.replace("\n", "\\n")
|
||||||
@@ -152,4 +522,7 @@ public final class MetricsRegistry {
|
|||||||
private String formatDouble(double value) {
|
private String formatDouble(double value) {
|
||||||
return String.format(Locale.ROOT, "%.3f", value);
|
return String.format(Locale.ROOT, "%.3f", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record LabelPair(String first, String second) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user