Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-06-15 07:03:50 +02:00
parent 0e1d193653
commit 684f300775
42 changed files with 7336 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
package de.serverpulse;
import de.serverpulse.alerts.AlertManager;
import de.serverpulse.api.RestApiServer;
import de.serverpulse.commands.ServerPulseCommand;
import de.serverpulse.database.DatabaseManager;
import de.serverpulse.discord.DiscordWebhook;
import de.serverpulse.listeners.PlayerListener;
import de.serverpulse.monitoring.EntityMonitor;
import de.serverpulse.monitoring.PerformanceMonitor;
import de.serverpulse.monitoring.TrendAnalyzer;
import de.serverpulse.utils.ConfigManager;
import de.serverpulse.utils.MessageUtil;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.logging.Level;
/**
* ServerPulse Monitoring & Analytics Suite
* Hauptklasse des Plugins
*/
public final class ServerPulse extends JavaPlugin {
private static ServerPulse instance;
// Manager & Module
private ConfigManager configManager;
private DatabaseManager databaseManager;
private PerformanceMonitor performanceMonitor;
private EntityMonitor entityMonitor;
private TrendAnalyzer trendAnalyzer;
private AlertManager alertManager;
private DiscordWebhook discordWebhook;
private RestApiServer restApiServer;
@Override
public void onEnable() {
instance = this;
printBanner();
// 1. Konfiguration laden
getLogger().info("Lade Konfiguration...");
this.configManager = new ConfigManager(this);
configManager.loadAll();
// 2. Datenbank initialisieren
getLogger().info("Verbinde mit Datenbank...");
this.databaseManager = new DatabaseManager(this);
if (!databaseManager.connect()) {
getLogger().severe("Datenbankverbindung fehlgeschlagen! Plugin wird deaktiviert.");
getServer().getPluginManager().disablePlugin(this);
return;
}
databaseManager.createTables();
// 3. Discord Webhook initialisieren
this.discordWebhook = new DiscordWebhook(this);
// 4. Alert Manager initialisieren
this.alertManager = new AlertManager(this);
// 5. Performance Monitor starten
getLogger().info("Starte Performance Monitor...");
this.performanceMonitor = new PerformanceMonitor(this);
performanceMonitor.start();
// 6. Entity Monitor starten
getLogger().info("Starte Entity Monitor...");
this.entityMonitor = new EntityMonitor(this);
entityMonitor.start();
// 7. Trend Analyzer starten
getLogger().info("Starte Trend Analyzer...");
this.trendAnalyzer = new TrendAnalyzer(this);
trendAnalyzer.start();
// 8. Commands registrieren
getLogger().info("Registriere Befehle...");
ServerPulseCommand command = new ServerPulseCommand(this);
getCommand("serverpulse").setExecutor(command);
getCommand("serverpulse").setTabCompleter(command);
// 9. Listener registrieren
getServer().getPluginManager().registerEvents(new PlayerListener(this), this);
// 10. REST API starten (optional)
if (configManager.isRestApiEnabled()) {
getLogger().info("Starte REST API...");
this.restApiServer = new RestApiServer(this);
restApiServer.start();
}
// 11. Täglichen Report planen
if (configManager.isDailyReportEnabled()) {
discordWebhook.scheduleDailyReport();
}
getLogger().info("ServerPulse v" + getDescription().getVersion() + " erfolgreich gestartet!");
getLogger().info("Überwache " + getServer().getWorlds().size() + " Welt(en).");
}
@Override
public void onDisable() {
getLogger().info("ServerPulse wird deaktiviert...");
// Alle Monitoring-Tasks stoppen
if (performanceMonitor != null) performanceMonitor.stop();
if (entityMonitor != null) entityMonitor.stop();
if (trendAnalyzer != null) trendAnalyzer.stop();
// REST API stoppen
if (restApiServer != null) restApiServer.stop();
// Datenbankverbindung schließen
if (databaseManager != null) databaseManager.disconnect();
getLogger().info("ServerPulse wurde erfolgreich deaktiviert. Auf Wiedersehen!");
}
/**
* Konfiguration neu laden
*/
public void reload() {
configManager.loadAll();
if (performanceMonitor != null) performanceMonitor.restart();
if (entityMonitor != null) entityMonitor.restart();
if (trendAnalyzer != null) trendAnalyzer.restart();
getLogger().info("ServerPulse wurde neu geladen.");
}
// ──────────────────────────────────────────
// GETTER
// ──────────────────────────────────────────
public static ServerPulse getInstance() {
return instance;
}
public ConfigManager getConfigManager() {
return configManager;
}
public DatabaseManager getDatabaseManager() {
return databaseManager;
}
public PerformanceMonitor getPerformanceMonitor() {
return performanceMonitor;
}
public EntityMonitor getEntityMonitor() {
return entityMonitor;
}
public TrendAnalyzer getTrendAnalyzer() {
return trendAnalyzer;
}
public AlertManager getAlertManager() {
return alertManager;
}
public DiscordWebhook getDiscordWebhook() {
return discordWebhook;
}
public RestApiServer getRestApiServer() {
return restApiServer;
}
// ──────────────────────────────────────────
// HILFSMETHODEN
// ──────────────────────────────────────────
private void printBanner() {
getLogger().info("╔══════════════════════════════════════╗");
getLogger().info("║ ServerPulse v" + getDescription().getVersion() + "");
getLogger().info("║ Monitoring & Analytics Suite ║");
getLogger().info("╚══════════════════════════════════════╝");
}
public void log(Level level, String message) {
getLogger().log(level, message);
}
public void debug(String message) {
if (configManager != null && configManager.isDebugMode()) {
getLogger().info("[DEBUG] " + message);
}
}
}

View File

@@ -0,0 +1,214 @@
package de.serverpulse.alerts;
import de.serverpulse.ServerPulse;
import de.serverpulse.models.AlertSeverity;
import de.serverpulse.utils.MessageUtil;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.Monster;
import java.util.HashMap;
import java.util.Map;
/**
* Verwaltet alle Warnungen, Cooldowns und Notfallmaßnahmen.
* Verhindert Alert-Spam durch einen Cooldown-Mechanismus.
*/
public class AlertManager {
private final ServerPulse plugin;
// Cooldown: alertType -> letzter Trigger-Zeitpunkt (ms)
private final Map<String, Long> alertCooldowns = new HashMap<>();
// Standard-Cooldown: 5 Minuten
private static final long DEFAULT_COOLDOWN_MS = 5 * 60 * 1000;
// Zähler für kritische Ereignisse (für Auto-Diagnose)
private int criticalEventCount = 0;
// Item-Clear läuft gerade?
private boolean itemClearRunning = false;
public AlertManager(ServerPulse plugin) {
this.plugin = plugin;
}
// ──────────────────────────────────────────
// ALERT AUSLÖSEN
// ──────────────────────────────────────────
/**
* Löst eine Warnung aus, wenn kein Cooldown aktiv ist.
*/
public void triggerAlert(String alertType, AlertSeverity severity, String worldName,
String message, Double currentValue, Double threshold) {
// Cooldown prüfen
String cooldownKey = alertType + (worldName != null ? "_" + worldName : "");
long now = System.currentTimeMillis();
if (alertCooldowns.containsKey(cooldownKey)) {
long lastAlert = alertCooldowns.get(cooldownKey);
if (now - lastAlert < DEFAULT_COOLDOWN_MS) {
plugin.debug("Alert-Cooldown aktiv für: " + cooldownKey);
return;
}
}
alertCooldowns.put(cooldownKey, now);
// Alert loggen
plugin.getLogger().warning("[" + severity.getName() + "] " + message);
// In DB speichern
if (plugin.getDatabaseManager().isConnected()) {
plugin.getDatabaseManager().saveAlert(
alertType, severity.getName(), worldName, message, currentValue, threshold
);
}
// Discord-Benachrichtigung senden
if (plugin.getConfigManager().isDiscordEnabled()) {
plugin.getDiscordWebhook().sendAlert(alertType, severity, worldName, message, currentValue, threshold);
}
// Kritische Events zählen für Auto-Diagnose
if (severity == AlertSeverity.CRITICAL) {
criticalEventCount++;
if (plugin.getConfigManager().isAutoDiagnosisEnabled()) {
if (criticalEventCount >= plugin.getConfigManager().getAutoDiagnosisTriggerCount()) {
triggerDiagnosisReport();
criticalEventCount = 0;
}
}
}
}
// ──────────────────────────────────────────
// NOTFALLMASSNAHMEN
// ──────────────────────────────────────────
/**
* Löst einen Item-Clear mit Countdown aus
*/
public void triggerItemClear(String worldName) {
if (itemClearRunning) return;
itemClearRunning = true;
int countdown = plugin.getConfigManager().getItemClearCountdown();
String broadcastMsg = plugin.getConfigManager().getItemClearBroadcastMessage();
// Countdown-Broadcasts
if (plugin.getConfigManager().isItemClearBroadcast()) {
for (int i = countdown; i > 0; i -= (i > 10 ? 10 : (i > 5 ? 5 : 1))) {
final int secondsLeft = i;
Bukkit.getScheduler().runTaskLater(plugin, () -> {
String msg = broadcastMsg.replace("{seconds}", String.valueOf(secondsLeft));
Bukkit.broadcastMessage(MessageUtil.colorize(msg));
}, (countdown - i) * 20L);
}
}
// Clear ausführen nach Countdown
Bukkit.getScheduler().runTaskLater(plugin, () -> {
World world = Bukkit.getWorld(worldName);
if (world != null) {
int cleared = 0;
for (Entity entity : world.getEntities()) {
if (entity instanceof Item) {
entity.remove();
cleared++;
}
}
plugin.getLogger().info("Item-Clear ausgeführt in " + worldName + ": " + cleared + " Items entfernt.");
if (plugin.getConfigManager().isItemClearBroadcast()) {
Bukkit.broadcastMessage(MessageUtil.colorize(
"&a[ServerPulse] Item-Clear abgeschlossen! " + cleared + " Items wurden entfernt."));
}
triggerAlert("ITEM_CLEAR_EXECUTED", AlertSeverity.INFO, worldName,
"Automatischer Item-Clear: " + cleared + " Items in " + worldName + " entfernt.",
(double) cleared, null);
}
itemClearRunning = false;
}, countdown * 20L);
}
/**
* Führt einen Mob-Clear in einer Welt durch
*/
public void triggerMobClear(String worldName) {
boolean hostileOnly = plugin.getConfigManager().isMobClearHostileOnly();
World world = Bukkit.getWorld(worldName);
if (world == null) return;
Bukkit.getScheduler().runTask(plugin, () -> {
int cleared = 0;
for (Entity entity : world.getEntities()) {
if (entity instanceof Monster) {
entity.remove();
cleared++;
}
}
plugin.getLogger().info("Mob-Clear ausgeführt in " + worldName + ": " + cleared + " Mobs entfernt.");
triggerAlert("MOB_CLEAR_EXECUTED", AlertSeverity.INFO, worldName,
"Automatischer Mob-Clear: " + cleared + " Mobs in " + worldName + " entfernt.",
(double) cleared, null);
});
}
/**
* Erstellt automatisch einen Diagnose-Report
*/
private void triggerDiagnosisReport() {
plugin.getLogger().warning("[ServerPulse] Erstelle automatischen Diagnose-Report...");
StringBuilder report = new StringBuilder();
report.append("=== AUTOMATISCHER DIAGNOSE-REPORT ===\n");
report.append("Zeitpunkt: ").append(MessageUtil.getTimestamp()).append("\n\n");
// Performance
if (plugin.getPerformanceMonitor().getLastSnapshot() != null) {
var snap = plugin.getPerformanceMonitor().getLastSnapshot();
report.append("Performance:\n");
report.append(" TPS: ").append(String.format("%.2f", snap.getTps())).append("\n");
report.append(" MSPT: ").append(String.format("%.2f", snap.getMspt())).append("ms\n");
report.append(" RAM: ").append(snap.getRamUsedMb()).append("MB / ").append(snap.getRamMaxMb()).append("MB\n");
report.append(" Spieler: ").append(snap.getOnlinePlayers()).append("\n");
report.append(" Chunks: ").append(snap.getLoadedChunks()).append("\n\n");
}
// Entities pro Welt
report.append("Entities pro Welt:\n");
for (var entry : plugin.getEntityMonitor().getAllLastSnapshots().entrySet()) {
var snap = entry.getValue();
report.append(" ").append(entry.getKey()).append(": ")
.append("Gesamt=").append(snap.getTotal())
.append(", Monster=").append(snap.getMonsters())
.append(", Items=").append(snap.getItems())
.append("\n");
}
String reportContent = report.toString();
// In DB speichern
if (plugin.getDatabaseManager().isConnected()) {
plugin.getDatabaseManager().saveReport("AUTO_DIAGNOSIS", "ServerPulse", reportContent);
}
plugin.getLogger().warning(reportContent);
}
/**
* Setzt alle Alert-Cooldowns zurück (z.B. nach reload)
*/
public void clearCooldowns() {
alertCooldowns.clear();
criticalEventCount = 0;
}
public int getCriticalEventCount() {
return criticalEventCount;
}
}

View File

@@ -0,0 +1,188 @@
package de.serverpulse.api;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import de.serverpulse.ServerPulse;
import de.serverpulse.models.EntitySnapshot;
import de.serverpulse.models.PerformanceSnapshot;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.logging.Level;
/**
* Optionaler leichtgewichtiger HTTP-Server für externe Dashboards.
* Stellt REST-Endpunkte bereit (z.B. für Grafana oder eigene Tools).
*
* Endpunkte:
* GET /api/status Aktueller Server-Status
* GET /api/performance Performance-Metriken
* GET /api/entities Entity-Daten pro Welt
* GET /api/health Einfacher Health-Check
*/
public class RestApiServer {
private final ServerPulse plugin;
private HttpServer server;
private final Gson gson = new Gson();
public RestApiServer(ServerPulse plugin) {
this.plugin = plugin;
}
public void start() {
try {
int port = plugin.getConfigManager().getRestApiPort();
server = HttpServer.create(new InetSocketAddress(
plugin.getConfigManager().getRestApiHost(), port), 0);
// Endpunkte registrieren
server.createContext("/api/health", this::handleHealth);
server.createContext("/api/status", this::handleStatus);
server.createContext("/api/performance", this::handlePerformance);
server.createContext("/api/entities", this::handleEntities);
server.setExecutor(Executors.newFixedThreadPool(4));
server.start();
plugin.getLogger().info("REST API gestartet auf Port " + port);
plugin.getLogger().info("Endpunkte: /api/health, /api/status, /api/performance, /api/entities");
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "REST API konnte nicht gestartet werden: " + e.getMessage(), e);
}
}
public void stop() {
if (server != null) {
server.stop(0);
plugin.getLogger().info("REST API gestoppt.");
}
}
// ──────────────────────────────────────────
// ENDPUNKTE
// ──────────────────────────────────────────
private void handleHealth(HttpExchange exchange) throws IOException {
if (!checkAuth(exchange)) return;
JsonObject response = new JsonObject();
response.addProperty("status", "ok");
response.addProperty("plugin", "ServerPulse");
response.addProperty("version", plugin.getDescription().getVersion());
sendJson(exchange, 200, response);
}
private void handleStatus(HttpExchange exchange) throws IOException {
if (!checkAuth(exchange)) return;
JsonObject response = new JsonObject();
response.addProperty("server", plugin.getServer().getName());
response.addProperty("version", plugin.getServer().getVersion());
response.addProperty("online_players", plugin.getServer().getOnlinePlayers().size());
response.addProperty("max_players", plugin.getServer().getMaxPlayers());
response.addProperty("loaded_worlds", plugin.getServer().getWorlds().size());
PerformanceSnapshot snap = plugin.getPerformanceMonitor().getLastSnapshot();
if (snap != null) {
response.addProperty("tps", Math.round(snap.getTps() * 100.0) / 100.0);
response.addProperty("mspt", Math.round(snap.getMspt() * 100.0) / 100.0);
response.addProperty("ram_used_mb", snap.getRamUsedMb());
response.addProperty("ram_max_mb", snap.getRamMaxMb());
response.addProperty("ram_percent", Math.round(snap.getRamPercent() * 10.0) / 10.0);
}
sendJson(exchange, 200, response);
}
private void handlePerformance(HttpExchange exchange) throws IOException {
if (!checkAuth(exchange)) return;
PerformanceSnapshot snap = plugin.getPerformanceMonitor().getLastSnapshot();
if (snap == null) {
JsonObject err = new JsonObject();
err.addProperty("error", "Noch keine Performance-Daten verfügbar");
sendJson(exchange, 503, err);
return;
}
JsonObject response = new JsonObject();
response.addProperty("tps", snap.getTps());
response.addProperty("mspt", snap.getMspt());
response.addProperty("ram_used_mb", snap.getRamUsedMb());
response.addProperty("ram_max_mb", snap.getRamMaxMb());
response.addProperty("ram_percent", snap.getRamPercent());
response.addProperty("online_players", snap.getOnlinePlayers());
response.addProperty("loaded_worlds", snap.getLoadedWorlds());
response.addProperty("loaded_chunks", snap.getLoadedChunks());
response.addProperty("recorded_at", snap.getRecordedAt().toString());
sendJson(exchange, 200, response);
}
private void handleEntities(HttpExchange exchange) throws IOException {
if (!checkAuth(exchange)) return;
JsonObject response = new JsonObject();
Map<String, EntitySnapshot> snapshots = plugin.getEntityMonitor().getAllLastSnapshots();
for (Map.Entry<String, EntitySnapshot> entry : snapshots.entrySet()) {
EntitySnapshot snap = entry.getValue();
JsonObject worldData = new JsonObject();
worldData.addProperty("monsters", snap.getMonsters());
worldData.addProperty("animals", snap.getAnimals());
worldData.addProperty("water_mobs", snap.getWaterMobs());
worldData.addProperty("villagers", snap.getVillagers());
worldData.addProperty("armor_stands", snap.getArmorStands());
worldData.addProperty("hopper_minecarts", snap.getHopperMinecarts());
worldData.addProperty("items", snap.getItems());
worldData.addProperty("players", snap.getPlayers());
worldData.addProperty("other", snap.getOther());
worldData.addProperty("total", snap.getTotal());
worldData.addProperty("recorded_at", snap.getRecordedAt().toString());
response.add(entry.getKey(), worldData);
}
sendJson(exchange, 200, response);
}
// ──────────────────────────────────────────
// HILFSMETHODEN
// ──────────────────────────────────────────
/**
* Prüft den API-Key im Authorization-Header
*/
private boolean checkAuth(HttpExchange exchange) throws IOException {
String configKey = plugin.getConfigManager().getRestApiKey();
if (configKey == null || configKey.isEmpty() || configKey.equals("YOUR_SECURE_API_KEY")) {
return true; // Kein Key konfiguriert = kein Auth
}
String authHeader = exchange.getRequestHeaders().getFirst("Authorization");
if (authHeader == null || !authHeader.equals("Bearer " + configKey)) {
JsonObject error = new JsonObject();
error.addProperty("error", "Unauthorized");
sendJson(exchange, 401, error);
return false;
}
return true;
}
private void sendJson(HttpExchange exchange, int statusCode, JsonObject body) throws IOException {
byte[] responseBytes = gson.toJson(body).getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8");
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*");
exchange.sendResponseHeaders(statusCode, responseBytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(responseBytes);
}
}
}

View File

@@ -0,0 +1,105 @@
package de.serverpulse.bungee;
import de.serverpulse.bungee.api.GrafanaApiServer;
import de.serverpulse.bungee.api.InfluxPushService;
import de.serverpulse.bungee.commands.BungeeCommand;
import de.serverpulse.bungee.discord.BungeeDiscordWebhook;
import de.serverpulse.bungee.listeners.BungeePlayerListener;
import de.serverpulse.bungee.network.BungeeAlertManager;
import de.serverpulse.bungee.network.NetworkDataStore;
import de.serverpulse.bungee.network.PluginMessageHandler;
import de.serverpulse.bungee.utils.BungeeConfig;
import de.serverpulse.network.NetworkMessage;
import net.md_5.bungee.api.plugin.Plugin;
/**
* BungeeCord-Einstiegspunkt der unified JAR.
* Wird von bungee.yml geladen wenn das Plugin auf einem BungeeCord-Proxy läuft.
*
* Beide Klassen (SpigotPlugin + BungeePlugin) sind in derselben JAR
* Minecraft/BungeeCord lädt automatisch nur die jeweils passende Klasse
* anhand der plugin.yml (Spigot) bzw. bungee.yml (BungeeCord).
*/
public final class BungeePlugin extends Plugin {
private static BungeePlugin instance;
private BungeeConfig config;
private NetworkDataStore dataStore;
private BungeeAlertManager alertManager;
private BungeeDiscordWebhook discordWebhook;
private GrafanaApiServer grafanaApiServer;
private InfluxPushService influxPushService;
@Override
public void onEnable() {
instance = this;
printBanner();
// 1. Config
this.config = new BungeeConfig(this);
config.load();
// 2. Datenspeicher
this.dataStore = new NetworkDataStore();
// 3. Alert Manager
this.alertManager = new BungeeAlertManager(this);
// 4. Discord
this.discordWebhook = new BungeeDiscordWebhook(this);
if (config.isDailyReportEnabled()) {
discordWebhook.scheduleDailyReport();
}
// 5. Plugin Messaging Channel (Daten von Sub-Servern empfangen)
getProxy().registerChannel(NetworkMessage.CHANNEL);
getProxy().getPluginManager().registerListener(this, new PluginMessageHandler(this));
// 6. Spieler-Listener
getProxy().getPluginManager().registerListener(this, new BungeePlayerListener(this));
// 7. Befehl
getProxy().getPluginManager().registerCommand(this, new BungeeCommand(this));
// 8. REST API / Grafana
if (config.isRestApiEnabled()) {
this.grafanaApiServer = new GrafanaApiServer(this);
grafanaApiServer.start();
if (config.isInfluxPushEnabled()) {
this.influxPushService = new InfluxPushService(this);
influxPushService.start();
}
}
getLogger().info("ServerPulse v" + getDescription().getVersion()
+ " (BungeeCord) erfolgreich gestartet!");
getLogger().info("Channel: " + NetworkMessage.CHANNEL);
}
@Override
public void onDisable() {
if (grafanaApiServer != null) grafanaApiServer.stop();
if (influxPushService != null) influxPushService.stop();
getProxy().unregisterChannel(NetworkMessage.CHANNEL);
getLogger().info("ServerPulse (BungeeCord) deaktiviert.");
}
private void printBanner() {
getLogger().info("╔══════════════════════════════════════╗");
getLogger().info("║ ServerPulse v1.2.0 [BUNGEECORD] ║");
getLogger().info("╚══════════════════════════════════════╝");
}
public void debug(String msg) {
if (config != null && config.isDebugMode()) getLogger().info("[DEBUG] " + msg);
}
public static BungeePlugin getInstance() { return instance; }
public BungeeConfig getBungeeConfig() { return config; }
public NetworkDataStore getDataStore() { return dataStore; }
public BungeeAlertManager getAlertManager() { return alertManager; }
public BungeeDiscordWebhook getDiscordWebhook(){ return discordWebhook; }
public GrafanaApiServer getGrafanaApiServer() { return grafanaApiServer; }
}

View File

@@ -0,0 +1,454 @@
package de.serverpulse.bungee.api;
import com.google.gson.*;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import de.serverpulse.bungee.BungeePlugin;
import de.serverpulse.bungee.models.ServerData;
import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.logging.Level;
/**
* Multi-Format REST API für Grafana-Integration.
*
* Unterstützt (je nach Config):
* → /metrics Prometheus Text Format
* → /grafana Grafana SimpleJSON Plugin
* → /influx/metrics InfluxDB Line Protocol
* → /api/... Standard JSON (network, servers, performance)
*
* Alle Endpunkte können unabhängig in der config.yml aktiviert werden.
*/
public class GrafanaApiServer {
private final BungeePlugin plugin;
private HttpServer server;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
public GrafanaApiServer(BungeePlugin plugin) {
this.plugin = plugin;
}
public void start() {
try {
int port = plugin.getBungeeConfig().getRestApiPort();
server = HttpServer.create(
new InetSocketAddress(plugin.getBungeeConfig().getRestApiHost(), port), 0);
registerEndpoints();
server.setExecutor(Executors.newFixedThreadPool(4));
server.start();
plugin.getLogger().info("REST API gestartet auf Port " + port);
logActiveEndpoints();
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "REST API konnte nicht gestartet werden: " + e.getMessage(), e);
}
}
public void stop() {
if (server != null) {
server.stop(0);
plugin.getLogger().info("REST API gestoppt.");
}
}
// ──────────────────────────────────────────
// ENDPUNKTE REGISTRIEREN
// ──────────────────────────────────────────
private void registerEndpoints() {
// Immer: Health-Check
server.createContext("/health", this::handleHealth);
// Prometheus
if (plugin.getBungeeConfig().isPrometheusEnabled()) {
String ep = plugin.getBungeeConfig().getPrometheusEndpoint();
server.createContext(ep, this::handlePrometheus);
plugin.getLogger().info(" → Prometheus: " + ep);
}
// Grafana SimpleJSON
if (plugin.getBungeeConfig().isSimpleJsonEnabled()) {
String ep = plugin.getBungeeConfig().getSimpleJsonEndpoint();
server.createContext(ep, this::handleSimpleJson);
server.createContext(ep + "/search", this::handleSimpleJsonSearch);
server.createContext(ep + "/query", this::handleSimpleJsonQuery);
plugin.getLogger().info(" → SimpleJSON: " + ep);
}
// InfluxDB Line Protocol
if (plugin.getBungeeConfig().isInfluxEnabled()) {
String ep = plugin.getBungeeConfig().getInfluxEndpoint();
server.createContext(ep + "/metrics", this::handleInflux);
plugin.getLogger().info(" → InfluxDB: " + ep + "/metrics");
}
// Standard JSON API
if (plugin.getBungeeConfig().isJsonApiEnabled()) {
String ep = plugin.getBungeeConfig().getJsonApiEndpoint();
server.createContext(ep + "/network", this::handleJsonNetwork);
server.createContext(ep + "/servers", this::handleJsonServers);
server.createContext(ep + "/performance", this::handleJsonPerformance);
server.createContext(ep + "/entities", this::handleJsonEntities);
plugin.getLogger().info(" → JSON API: " + ep + "/{network,servers,performance,entities}");
}
}
private void logActiveEndpoints() {
int port = plugin.getBungeeConfig().getRestApiPort();
plugin.getLogger().info("Grafana-Datasource URLs:");
if (plugin.getBungeeConfig().isPrometheusEnabled())
plugin.getLogger().info(" Prometheus: http://HOST:" + port
+ plugin.getBungeeConfig().getPrometheusEndpoint());
if (plugin.getBungeeConfig().isSimpleJsonEnabled())
plugin.getLogger().info(" SimpleJSON: http://HOST:" + port
+ plugin.getBungeeConfig().getSimpleJsonEndpoint());
if (plugin.getBungeeConfig().isInfluxEnabled())
plugin.getLogger().info(" InfluxDB: http://HOST:" + port
+ plugin.getBungeeConfig().getInfluxEndpoint() + "/metrics");
}
// ──────────────────────────────────────────
// HEALTH CHECK
// ──────────────────────────────────────────
private void handleHealth(HttpExchange ex) throws IOException {
JsonObject r = new JsonObject();
r.addProperty("status", "ok");
r.addProperty("plugin", "BungeePlugin");
r.addProperty("version", plugin.getDescription().getVersion());
r.addProperty("online_servers", plugin.getDataStore().getOnlineServerCount());
r.addProperty("total_players", plugin.getDataStore().getTotalOnlinePlayers());
sendResponse(ex, 200, "application/json", gson.toJson(r));
}
// ──────────────────────────────────────────
// PROMETHEUS FORMAT
// ──────────────────────────────────────────
/**
* Prometheus Text Exposition Format 0.0.4
* Jeder Metric-Eintrag: metric_name{label="value"} value timestamp
*/
private void handlePrometheus(HttpExchange ex) throws IOException {
if (!checkAuth(ex)) return;
StringBuilder sb = new StringBuilder();
// ── HELP & TYPE Definitionen ──────────
appendPrometheusHelp(sb, "serverpulse_tps", "gauge", "Current TPS of the sub-server");
appendPrometheusHelp(sb, "serverpulse_mspt", "gauge", "Current MSPT of the sub-server in ms");
appendPrometheusHelp(sb, "serverpulse_ram_used_mb", "gauge", "RAM used in megabytes");
appendPrometheusHelp(sb, "serverpulse_ram_max_mb", "gauge", "Max RAM in megabytes");
appendPrometheusHelp(sb, "serverpulse_ram_percent", "gauge", "RAM usage in percent");
appendPrometheusHelp(sb, "serverpulse_online_players", "gauge", "Online players on this sub-server");
appendPrometheusHelp(sb, "serverpulse_loaded_chunks", "gauge", "Loaded chunks");
appendPrometheusHelp(sb, "serverpulse_entities_total", "gauge", "Total entity count across all worlds");
appendPrometheusHelp(sb, "serverpulse_network_players_total", "gauge", "Total players across all servers");
appendPrometheusHelp(sb, "serverpulse_network_tps_avg", "gauge", "Average TPS across all servers");
appendPrometheusHelp(sb, "serverpulse_network_tps_min", "gauge", "Lowest TPS across all servers");
long timestamp = Instant.now().toEpochMilli();
// ── Netzwerk-Metriken ─────────────────
sb.append(String.format("serverpulse_network_players_total %d %d%n",
plugin.getDataStore().getTotalOnlinePlayers(), timestamp));
sb.append(String.format("serverpulse_network_tps_avg %.4f %d%n",
plugin.getDataStore().getNetworkAverageTps(), timestamp));
sb.append(String.format("serverpulse_network_tps_min %.4f %d%n",
plugin.getDataStore().getLowestTps(), timestamp));
// ── Pro-Server-Metriken ───────────────
for (ServerData data : plugin.getDataStore().getAll()) {
if (!data.isOnline()) continue;
String label = "server=\"" + data.getServerName() + "\"";
sb.append(String.format("serverpulse_tps{%s} %.4f %d%n", label, data.getTps(), timestamp));
sb.append(String.format("serverpulse_mspt{%s} %.4f %d%n", label, data.getMspt(), timestamp));
sb.append(String.format("serverpulse_ram_used_mb{%s} %d %d%n", label, data.getRamUsedMb(), timestamp));
sb.append(String.format("serverpulse_ram_max_mb{%s} %d %d%n", label, data.getRamMaxMb(), timestamp));
sb.append(String.format("serverpulse_ram_percent{%s} %.2f %d%n", label, data.getRamPercent(), timestamp));
sb.append(String.format("serverpulse_online_players{%s} %d %d%n", label, data.getOnlinePlayers(), timestamp));
sb.append(String.format("serverpulse_loaded_chunks{%s} %d %d%n", label, data.getLoadedChunks(), timestamp));
sb.append(String.format("serverpulse_entities_total{%s} %d %d%n", label, data.getTotalEntities(), timestamp));
// Entity-Typen pro Welt
for (Map.Entry<String, Map<String, Integer>> worldEntry : data.getEntityData().entrySet()) {
String worldLabel = "server=\"" + data.getServerName() + "\",world=\"" + worldEntry.getKey() + "\"";
for (Map.Entry<String, Integer> entityEntry : worldEntry.getValue().entrySet()) {
sb.append(String.format("serverpulse_entities_%s{%s} %d %d%n",
entityEntry.getKey(), worldLabel, entityEntry.getValue(), timestamp));
}
}
}
sendResponse(ex, 200, "text/plain; version=0.0.4; charset=utf-8", sb.toString());
}
private void appendPrometheusHelp(StringBuilder sb, String name, String type, String help) {
sb.append("# HELP ").append(name).append(" ").append(help).append("\n");
sb.append("# TYPE ").append(name).append(" ").append(type).append("\n");
}
// ──────────────────────────────────────────
// GRAFANA SIMPLE JSON FORMAT
// ──────────────────────────────────────────
/** Root: Grafana testet ob der Endpoint erreichbar ist */
private void handleSimpleJson(HttpExchange ex) throws IOException {
if (!checkAuth(ex)) return;
sendResponse(ex, 200, "application/json", "\"ServerPulse SimpleJSON API ready\"");
}
/** /search → Liste aller verfügbaren Metriken */
private void handleSimpleJsonSearch(HttpExchange ex) throws IOException {
if (!checkAuth(ex)) return;
JsonArray targets = new JsonArray();
// Netzwerk
targets.add("network.players_total");
targets.add("network.tps_avg");
targets.add("network.tps_min");
targets.add("network.ram_used_mb");
// Pro Server (dynamisch)
for (ServerData data : plugin.getDataStore().getAll()) {
String s = data.getServerName();
targets.add(s + ".tps");
targets.add(s + ".mspt");
targets.add(s + ".ram_percent");
targets.add(s + ".online_players");
targets.add(s + ".entities_total");
}
sendResponse(ex, 200, "application/json", gson.toJson(targets));
}
/** /query → Zeitreihendaten für angefragte Targets */
private void handleSimpleJsonQuery(HttpExchange ex) throws IOException {
if (!checkAuth(ex)) return;
if (!ex.getRequestMethod().equalsIgnoreCase("POST")) {
sendResponse(ex, 405, "application/json", "{\"error\":\"POST required\"}");
return;
}
String body = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
JsonObject req = gson.fromJson(body, JsonObject.class);
JsonArray reqTargets = req.getAsJsonArray("targets");
JsonArray response = new JsonArray();
long now = Instant.now().toEpochMilli();
for (JsonElement targetEl : reqTargets) {
String target = targetEl.getAsJsonObject().get("target").getAsString();
double value = resolveTarget(target);
JsonObject series = new JsonObject();
series.addProperty("target", target);
JsonArray datapoints = new JsonArray();
JsonArray point = new JsonArray();
point.add(value);
point.add(now);
datapoints.add(point);
series.add("datapoints", datapoints);
response.add(series);
}
sendResponse(ex, 200, "application/json", gson.toJson(response));
}
private double resolveTarget(String target) {
// Netzwerk-Targets
return switch (target) {
case "network.players_total" -> plugin.getDataStore().getTotalOnlinePlayers();
case "network.tps_avg" -> plugin.getDataStore().getNetworkAverageTps();
case "network.tps_min" -> plugin.getDataStore().getLowestTps();
case "network.ram_used_mb" -> plugin.getDataStore().getTotalRamUsedMb();
default -> {
// Format: "serverName.metric"
String[] parts = target.split("\\.", 2);
if (parts.length == 2) {
ServerData data = plugin.getDataStore().get(parts[0]);
if (data != null) {
yield switch (parts[1]) {
case "tps" -> data.getTps();
case "mspt" -> data.getMspt();
case "ram_percent" -> data.getRamPercent();
case "ram_used_mb" -> data.getRamUsedMb();
case "online_players" -> data.getOnlinePlayers();
case "entities_total" -> data.getTotalEntities();
case "loaded_chunks" -> data.getLoadedChunks();
default -> 0.0;
};
}
}
yield 0.0;
}
};
}
// ──────────────────────────────────────────
// INFLUXDB LINE PROTOCOL
// ──────────────────────────────────────────
/**
* InfluxDB Line Protocol Format:
* measurement,tag_key=tag_value field_key=field_value timestamp_ns
*/
private void handleInflux(HttpExchange ex) throws IOException {
if (!checkAuth(ex)) return;
StringBuilder sb = new StringBuilder();
String measurement = plugin.getBungeeConfig().getInfluxMeasurement();
long timestampNs = Instant.now().toEpochMilli() * 1_000_000L;
// Netzwerk-Metriken
sb.append(String.format("%s,source=network ",measurement));
sb.append(String.format("players_total=%di,", plugin.getDataStore().getTotalOnlinePlayers()));
sb.append(String.format("tps_avg=%.4f,", plugin.getDataStore().getNetworkAverageTps()));
sb.append(String.format("tps_min=%.4f,", plugin.getDataStore().getLowestTps()));
sb.append(String.format("ram_used_mb=%di,", plugin.getDataStore().getTotalRamUsedMb()));
sb.append(String.format("online_servers=%di", plugin.getDataStore().getOnlineServerCount()));
sb.append(" ").append(timestampNs).append("\n");
// Pro-Server-Metriken
for (ServerData data : plugin.getDataStore().getAll()) {
if (!data.isOnline()) continue;
// Leerzeichen in Server-Namen escapen (InfluxDB-Regel)
String serverTag = data.getServerName().replace(" ", "\\ ");
sb.append(String.format("%s,source=server,server=%s ", measurement, serverTag));
sb.append(String.format("tps=%.4f,", data.getTps()));
sb.append(String.format("mspt=%.4f,", data.getMspt()));
sb.append(String.format("ram_used_mb=%di,", data.getRamUsedMb()));
sb.append(String.format("ram_max_mb=%di,", data.getRamMaxMb()));
sb.append(String.format("ram_percent=%.2f,", data.getRamPercent()));
sb.append(String.format("online_players=%di,", data.getOnlinePlayers()));
sb.append(String.format("loaded_chunks=%di,", data.getLoadedChunks()));
sb.append(String.format("entities_total=%di", data.getTotalEntities()));
sb.append(" ").append(timestampNs).append("\n");
// Entity-Daten pro Welt
for (Map.Entry<String, Map<String, Integer>> worldEntry : data.getEntityData().entrySet()) {
String worldTag = worldEntry.getKey().replace(" ", "\\ ");
sb.append(String.format("%s,source=entities,server=%s,world=%s ",
measurement, serverTag, worldTag));
Map<String, Integer> entities = worldEntry.getValue();
List<String> fields = new ArrayList<>();
entities.forEach((k, v) -> fields.add(k + "=" + v + "i"));
sb.append(String.join(",", fields));
sb.append(" ").append(timestampNs).append("\n");
}
}
sendResponse(ex, 200, "text/plain; charset=utf-8", sb.toString());
}
// ──────────────────────────────────────────
// STANDARD JSON API
// ──────────────────────────────────────────
private void handleJsonNetwork(HttpExchange ex) throws IOException {
if (!checkAuth(ex)) return;
JsonObject r = new JsonObject();
r.addProperty("total_players", plugin.getDataStore().getTotalOnlinePlayers());
r.addProperty("online_servers", plugin.getDataStore().getOnlineServerCount());
r.addProperty("total_servers", plugin.getDataStore().getServerCount());
r.addProperty("network_tps_avg", plugin.getDataStore().getNetworkAverageTps());
r.addProperty("network_tps_min", plugin.getDataStore().getLowestTps());
r.addProperty("ram_used_mb", plugin.getDataStore().getTotalRamUsedMb());
r.addProperty("ram_max_mb", plugin.getDataStore().getTotalRamMaxMb());
sendResponse(ex, 200, "application/json", gson.toJson(r));
}
private void handleJsonServers(HttpExchange ex) throws IOException {
if (!checkAuth(ex)) return;
JsonArray arr = new JsonArray();
for (ServerData data : plugin.getDataStore().getAll()) {
JsonObject s = new JsonObject();
s.addProperty("name", data.getServerName());
s.addProperty("online", data.isOnline());
s.addProperty("tps", data.getTps());
s.addProperty("mspt", data.getMspt());
s.addProperty("ram_used_mb", data.getRamUsedMb());
s.addProperty("ram_max_mb", data.getRamMaxMb());
s.addProperty("ram_percent", data.getRamPercent());
s.addProperty("online_players", data.getOnlinePlayers());
s.addProperty("loaded_chunks", data.getLoadedChunks());
s.addProperty("entities_total", data.getTotalEntities());
if (data.getLastUpdate() != null)
s.addProperty("last_update", data.getLastUpdate().toString());
arr.add(s);
}
sendResponse(ex, 200, "application/json", gson.toJson(arr));
}
private void handleJsonPerformance(HttpExchange ex) throws IOException {
if (!checkAuth(ex)) return;
JsonObject r = new JsonObject();
for (ServerData data : plugin.getDataStore().getAll()) {
if (!data.isOnline()) continue;
JsonObject perf = new JsonObject();
perf.addProperty("tps", data.getTps());
perf.addProperty("mspt", data.getMspt());
perf.addProperty("ram_used_mb", data.getRamUsedMb());
perf.addProperty("ram_max_mb", data.getRamMaxMb());
perf.addProperty("ram_percent", data.getRamPercent());
perf.addProperty("online_players", data.getOnlinePlayers());
r.add(data.getServerName(), perf);
}
sendResponse(ex, 200, "application/json", gson.toJson(r));
}
private void handleJsonEntities(HttpExchange ex) throws IOException {
if (!checkAuth(ex)) return;
JsonObject r = new JsonObject();
for (ServerData data : plugin.getDataStore().getAll()) {
if (!data.isOnline()) continue;
JsonObject serverObj = new JsonObject();
for (Map.Entry<String, Map<String, Integer>> world : data.getEntityData().entrySet()) {
JsonObject worldObj = new JsonObject();
world.getValue().forEach(worldObj::addProperty);
serverObj.add(world.getKey(), worldObj);
}
r.add(data.getServerName(), serverObj);
}
sendResponse(ex, 200, "application/json", gson.toJson(r));
}
// ──────────────────────────────────────────
// HILFSMETHODEN
// ──────────────────────────────────────────
private boolean checkAuth(HttpExchange ex) throws IOException {
String key = plugin.getBungeeConfig().getRestApiKey();
if (key == null || key.isEmpty() || key.equals("YOUR_SECURE_API_KEY")) return true;
String auth = ex.getRequestHeaders().getFirst("Authorization");
if (auth == null || !auth.equals("Bearer " + key)) {
sendResponse(ex, 401, "application/json", "{\"error\":\"Unauthorized\"}");
return false;
}
return true;
}
private void sendResponse(HttpExchange ex, int code, String contentType, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().set("Content-Type", contentType);
ex.getResponseHeaders().set("Access-Control-Allow-Origin", "*");
ex.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
ex.getResponseHeaders().set("Access-Control-Allow-Headers", "Authorization, Content-Type");
if (ex.getRequestMethod().equalsIgnoreCase("OPTIONS")) {
ex.sendResponseHeaders(204, -1);
return;
}
ex.sendResponseHeaders(code, bytes.length);
try (OutputStream os = ex.getResponseBody()) {
os.write(bytes);
}
}
}

View File

@@ -0,0 +1,106 @@
package de.serverpulse.bungee.api;
import de.serverpulse.bungee.BungeePlugin;
import de.serverpulse.bungee.models.ServerData;
import okhttp3.*;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
/**
* Optionaler Service der Metriken aktiv an eine InfluxDB v2 Instanz pusht.
* Aktivierbar über rest-api.formats.influxdb.push.enabled=true in der config.
*/
public class InfluxPushService {
private final BungeePlugin plugin;
private final OkHttpClient httpClient;
private net.md_5.bungee.api.scheduler.ScheduledTask task;
public InfluxPushService(BungeePlugin plugin) {
this.plugin = plugin;
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build();
}
public void start() {
int interval = plugin.getBungeeConfig().getPollInterval();
task = plugin.getProxy().getScheduler().schedule(plugin, this::push,
interval, interval, TimeUnit.SECONDS);
plugin.getLogger().info("InfluxDB Push-Service gestartet (Intervall: " + interval + "s)");
}
public void stop() {
if (task != null) task.cancel();
}
private void push() {
String body = buildLineProtocol();
if (body.isEmpty()) return;
String url = plugin.getBungeeConfig().getInfluxPushUrl()
+ "/api/v2/write?org=" + plugin.getBungeeConfig().getInfluxPushOrg()
+ "&bucket=" + plugin.getBungeeConfig().getInfluxPushBucket()
+ "&precision=ns";
String token = plugin.getBungeeConfig().getInfluxPushToken();
RequestBody requestBody = RequestBody.create(body, MediaType.get("text/plain; charset=utf-8"));
Request request = new Request.Builder()
.url(url)
.addHeader("Authorization", "Token " + token)
.post(requestBody)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
plugin.getLogger().warning("InfluxDB Push fehlgeschlagen: HTTP " + response.code());
} else {
plugin.debug("InfluxDB Push erfolgreich (" + body.lines().count() + " Zeilen)");
}
} catch (IOException e) {
plugin.getLogger().log(Level.WARNING, "InfluxDB Push Verbindungsfehler: " + e.getMessage());
}
}
private String buildLineProtocol() {
StringBuilder sb = new StringBuilder();
String measurement = plugin.getBungeeConfig().getInfluxMeasurement();
long ts = Instant.now().toEpochMilli() * 1_000_000L;
for (ServerData data : plugin.getDataStore().getAll()) {
if (!data.isOnline()) continue;
String serverTag = data.getServerName().replace(" ", "\\ ");
sb.append(measurement)
.append(",source=server,server=").append(serverTag).append(" ")
.append(String.format("tps=%.4f,mspt=%.4f,ram_used_mb=%di,ram_max_mb=%di," +
"ram_percent=%.2f,online_players=%di,loaded_chunks=%di,entities_total=%di",
data.getTps(), data.getMspt(), data.getRamUsedMb(), data.getRamMaxMb(),
data.getRamPercent(), data.getOnlinePlayers(),
data.getLoadedChunks(), data.getTotalEntities()))
.append(" ").append(ts).append("\n");
}
// Netzwerk-Gesamt
sb.append(measurement)
.append(",source=network ")
.append(String.format("players_total=%di,tps_avg=%.4f,tps_min=%.4f," +
"ram_used_mb=%di,online_servers=%di",
plugin.getDataStore().getTotalOnlinePlayers(),
plugin.getDataStore().getNetworkAverageTps(),
plugin.getDataStore().getLowestTps(),
plugin.getDataStore().getTotalRamUsedMb(),
plugin.getDataStore().getOnlineServerCount()))
.append(" ").append(ts).append("\n");
return sb.toString();
}
}

View File

@@ -0,0 +1,179 @@
package de.serverpulse.bungee.commands;
import de.serverpulse.bungee.BungeePlugin;
import de.serverpulse.bungee.models.ServerData;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.plugin.Command;
/**
* /bpulse Befehlsverarbeitung für BungeeCord.
* Unterbefehle: status | network | servers | reload
*/
public class BungeeCommand extends Command {
private final BungeePlugin plugin;
public BungeeCommand(BungeePlugin plugin) {
super("bpulse", "serverpulse.bungee.use", "bsp", "networkpulse");
this.plugin = plugin;
}
@Override
public void execute(CommandSender sender, String[] args) {
if (args.length == 0) {
sendHelp(sender);
return;
}
switch (args[0].toLowerCase()) {
case "status" -> handleStatus(sender);
case "network" -> handleNetwork(sender);
case "servers" -> handleServers(sender);
case "reload" -> handleReload(sender);
case "report" -> handleReport(sender);
default -> sendHelp(sender);
}
}
// ──────────────────────────────────────────
// STATUS
// ──────────────────────────────────────────
private void handleStatus(CommandSender sender) {
send(sender, "");
send(sender, "§8§m──────────────────────────────────────────");
send(sender, " §b§lServerPulse §8 §7Netzwerk-Status");
send(sender, "§8§m──────────────────────────────────────────");
send(sender, " §7Spieler gesamt: §f" + plugin.getDataStore().getTotalOnlinePlayers());
send(sender, " §7Server online: §f"
+ plugin.getDataStore().getOnlineServerCount()
+ " §8/ §f" + plugin.getDataStore().getServerCount());
send(sender, " §7Ø TPS (Netz): " + formatTps(plugin.getDataStore().getNetworkAverageTps()));
send(sender, " §7Min TPS: " + formatTps(plugin.getDataStore().getLowestTps()));
send(sender, " §7RAM gesamt: §f"
+ plugin.getDataStore().getTotalRamUsedMb() + " MB"
+ " §8/ §f" + plugin.getDataStore().getTotalRamMaxMb() + " MB");
send(sender, "§8§m──────────────────────────────────────────");
}
// ──────────────────────────────────────────
// NETWORK
// ──────────────────────────────────────────
private void handleNetwork(CommandSender sender) {
send(sender, "");
send(sender, "§b§lServerPulse §8 §7Netzwerk-Übersicht");
send(sender, "§8§m──────────────────────────────────────────");
for (DataEntry entry : java.util.List.of(
new DataEntry("Online Spieler",
String.valueOf(plugin.getDataStore().getTotalOnlinePlayers())),
new DataEntry("Aktive Server",
plugin.getDataStore().getOnlineServerCount() + " / " + plugin.getDataStore().getServerCount()),
new DataEntry("Ø Netz-TPS",
String.format("%.2f", plugin.getDataStore().getNetworkAverageTps())),
new DataEntry("Niedrigste TPS",
String.format("%.2f", plugin.getDataStore().getLowestTps())),
new DataEntry("RAM gesamt",
plugin.getDataStore().getTotalRamUsedMb() + " MB / "
+ plugin.getDataStore().getTotalRamMaxMb() + " MB"))) {
send(sender, " §7" + entry.label + ": §f" + entry.value);
}
send(sender, "§8§m──────────────────────────────────────────");
}
// ──────────────────────────────────────────
// SERVERS
// ──────────────────────────────────────────
private void handleServers(CommandSender sender) {
send(sender, "");
send(sender, "§b§lServerPulse §8 §7Server-Details");
send(sender, "§8§m──────────────────────────────────────────");
for (ServerData data : plugin.getDataStore().getAll()) {
String status = data.isOnline() ? "§a●" : "§c●";
if (data.isOnline()) {
send(sender, " " + status + " §f§l" + data.getServerName());
send(sender, " §7TPS: " + formatTps(data.getTps())
+ " §7MSPT: §f" + String.format("%.1f", data.getMspt()) + "ms");
send(sender, " §7RAM: §f" + data.getRamUsedMb() + "MB §8/ §f" + data.getRamMaxMb() + "MB"
+ " §7Spieler: §f" + data.getOnlinePlayers());
send(sender, " §7Entities: §f" + data.getTotalEntities()
+ " §7Chunks: §f" + data.getLoadedChunks());
if (!data.getLastAlert().isEmpty()) {
String color = data.getLastAlertSeverity().equals("CRITICAL") ? "§c" : "§e";
send(sender, " §7Letzter Alert: " + color + data.getLastAlert());
}
} else {
send(sender, " " + status + " §7" + data.getServerName() + " §8(Offline)");
}
}
if (plugin.getDataStore().getAll().isEmpty()) {
send(sender, " §7Noch keine Server-Daten empfangen.");
}
send(sender, "§8§m──────────────────────────────────────────");
}
// ──────────────────────────────────────────
// REPORT
// ──────────────────────────────────────────
private void handleReport(CommandSender sender) {
if (!plugin.getBungeeConfig().isDiscordEnabled()) {
send(sender, "§cDiscord ist nicht aktiviert. Report kann nicht gesendet werden.");
return;
}
send(sender, "§aSende täglichen Netzwerk-Report an Discord...");
plugin.getProxy().getScheduler().runAsync(plugin,
() -> plugin.getDiscordWebhook().sendDailyReport());
send(sender, "§aReport wurde gesendet!");
}
// ──────────────────────────────────────────
// RELOAD
// ──────────────────────────────────────────
private void handleReload(CommandSender sender) {
if (!sender.hasPermission("serverpulse.bungee.admin")) {
send(sender, "§cKeine Berechtigung.");
return;
}
plugin.getBungeeConfig().load();
send(sender, "§aKonfiguration wurde neu geladen.");
}
// ──────────────────────────────────────────
// HILFE
// ──────────────────────────────────────────
private void sendHelp(CommandSender sender) {
send(sender, "§8§m──────────────────────────────────────────");
send(sender, " §b§lServerPulse BungeeCord §8 §7Befehle");
send(sender, "§8§m──────────────────────────────────────────");
send(sender, " §b/bpulse status §8 §7Netzwerk-Schnellübersicht");
send(sender, " §b/bpulse network §8 §7Netzwerk-Statistiken");
send(sender, " §b/bpulse servers §8 §7Details aller Sub-Server");
send(sender, " §b/bpulse report §8 §7Discord-Report senden");
send(sender, " §b/bpulse reload §8 §7Konfiguration neu laden");
send(sender, "§8§m──────────────────────────────────────────");
}
// ──────────────────────────────────────────
// HILFSMETHODEN
// ──────────────────────────────────────────
private void send(CommandSender sender, String message) {
sender.sendMessage(new TextComponent(ChatColor.translateAlternateColorCodes('§', message)));
}
private String formatTps(double tps) {
if (tps >= 18.0) return "§a" + String.format("%.2f", tps);
if (tps >= 15.0) return "§e" + String.format("%.2f", tps);
return "§c" + String.format("%.2f", tps);
}
private record DataEntry(String label, String value) {}
}

View File

@@ -0,0 +1,205 @@
package de.serverpulse.bungee.discord;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import de.serverpulse.bungee.BungeePlugin;
import de.serverpulse.bungee.models.ServerData;
import okhttp3.*;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
/**
* Discord-Webhook-Integration für das BungeeCord-Plugin.
* Unterstützt Server-Alerts, Chat-Warnungen und den täglichen Netzwerk-Report.
*/
public class BungeeDiscordWebhook {
private final BungeePlugin plugin;
private final OkHttpClient httpClient;
private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");
public BungeeDiscordWebhook(BungeePlugin plugin) {
this.plugin = plugin;
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build();
}
// ──────────────────────────────────────────
// SERVER ALERT
// ──────────────────────────────────────────
public void sendServerAlert(String serverName, String alertType, String severity, String message) {
int color = switch (severity) {
case "CRITICAL" -> plugin.getBungeeConfig().getColorCritical();
case "WARNING" -> plugin.getBungeeConfig().getColorWarning();
default -> plugin.getBungeeConfig().getColorInfo();
};
String emoji = severity.equals("CRITICAL") ? "🔴" : (severity.equals("WARNING") ? "🟡" : "🔵");
JsonObject embed = buildEmbed(
emoji + " " + severity + " " + alertType.replace("_", " "),
message, color);
JsonArray fields = new JsonArray();
fields.add(field("🖥 Server", serverName, true));
fields.add(field("🕐 Zeitpunkt", LocalDateTime.now().format(DT), true));
// Performance-Snapshot für den betroffenen Server
ServerData data = plugin.getDataStore().get(serverName);
if (data != null) {
fields.add(field("⚡ TPS", String.format("%.2f", data.getTps()), true));
fields.add(field("⏱ MSPT", String.format("%.2f ms", data.getMspt()), true));
fields.add(field("💾 RAM",
data.getRamUsedMb() + " MB / " + data.getRamMaxMb() + " MB", true));
fields.add(field("👥 Online", data.getOnlinePlayers() + " Spieler", true));
}
// Netzwerk-Überblick
fields.add(field("🌐 Netzwerk gesamt",
plugin.getDataStore().getTotalOnlinePlayers() + " Spieler | "
+ plugin.getDataStore().getOnlineServerCount() + " Server online", false));
embed.add("fields", fields);
send(embed);
}
// ──────────────────────────────────────────
// CHAT ALERT
// ──────────────────────────────────────────
public void sendChatAlert(String serverName, String playerName, String message, String reason) {
String emoji = reason.equals("SPAM") ? "📢" : "⚠️";
String title = emoji + " Chat-Monitor " + reason;
String desc = "Verdächtige Chat-Aktivität erkannt.";
JsonObject embed = buildEmbed(title, desc, plugin.getBungeeConfig().getColorWarning());
JsonArray fields = new JsonArray();
fields.add(field("🖥 Server", serverName, true));
fields.add(field("👤 Spieler", playerName, true));
fields.add(field("💬 Nachricht", "`" + message + "`", false));
fields.add(field("🔍 Grund", reason, true));
fields.add(field("🕐 Zeit", LocalDateTime.now().format(DT), true));
embed.add("fields", fields);
send(embed);
}
// ──────────────────────────────────────────
// TÄGLICHER NETZWERK-REPORT
// ──────────────────────────────────────────
public void scheduleDailyReport() {
String timeStr = plugin.getBungeeConfig().getDailyReportTime();
try {
LocalTime reportTime = LocalTime.parse(timeStr, DateTimeFormatter.ofPattern("HH:mm"));
LocalTime now = LocalTime.now();
long secondsUntil = now.isBefore(reportTime)
? now.until(reportTime, java.time.temporal.ChronoUnit.SECONDS)
: now.until(reportTime.plusHours(24), java.time.temporal.ChronoUnit.SECONDS);
plugin.getProxy().getScheduler().schedule(plugin, this::sendDailyReport,
secondsUntil, 86400L, TimeUnit.SECONDS);
plugin.getLogger().info("Täglicher Discord-Report geplant für " + timeStr);
} catch (Exception e) {
plugin.getLogger().warning("Ungültige Report-Zeit: " + timeStr);
}
}
public void sendDailyReport() {
JsonObject embed = buildEmbed(
"📊 Täglicher Netzwerk-Statusbericht",
"Automatischer Tagesbericht für das gesamte Minecraft-Netzwerk.",
plugin.getBungeeConfig().getColorInfo());
JsonArray fields = new JsonArray();
// Netzwerk-Gesamtstatistik
fields.add(field("🌐 Netzwerk-Überblick",
"Online: **" + plugin.getDataStore().getTotalOnlinePlayers() + " Spieler**\n"
+ "Server: " + plugin.getDataStore().getOnlineServerCount()
+ "/" + plugin.getDataStore().getServerCount() + " online\n"
+ "Ø TPS: " + String.format("%.2f", plugin.getDataStore().getNetworkAverageTps()) + "\n"
+ "Min TPS: " + String.format("%.2f", plugin.getDataStore().getLowestTps()) + "\n"
+ "RAM gesamt: " + plugin.getDataStore().getTotalRamUsedMb()
+ " MB / " + plugin.getDataStore().getTotalRamMaxMb() + " MB",
false));
// Pro-Server-Daten
for (ServerData data : plugin.getDataStore().getAll()) {
String status = data.isOnline() ? "🟢" : "🔴";
String content;
if (data.isOnline()) {
content = "TPS: **" + String.format("%.2f", data.getTps()) + "**"
+ " | MSPT: " + String.format("%.2f", data.getMspt()) + "ms"
+ "\nRAM: " + data.getRamUsedMb() + "/" + data.getRamMaxMb() + " MB"
+ " | Spieler: " + data.getOnlinePlayers()
+ "\nEntities: " + data.getTotalEntities()
+ " | Chunks: " + data.getLoadedChunks();
} else {
content = "❌ Offline";
}
fields.add(field(status + " " + data.getServerName(), content, false));
}
fields.add(field("🕐 Erstellt", LocalDateTime.now().format(DT), false));
JsonObject footer = new JsonObject();
footer.addProperty("text", "ServerPulse Network Monitoring");
embed.add("footer", footer);
embed.add("fields", fields);
send(embed);
}
// ──────────────────────────────────────────
// HILFSMETHODEN
// ──────────────────────────────────────────
private JsonObject buildEmbed(String title, String description, int color) {
JsonObject embed = new JsonObject();
embed.addProperty("title", title);
embed.addProperty("description", description);
embed.addProperty("color", color);
return embed;
}
private JsonObject field(String name, String value, boolean inline) {
JsonObject f = new JsonObject();
f.addProperty("name", name);
f.addProperty("value", value.isEmpty() ? "N/A" : value);
f.addProperty("inline", inline);
return f;
}
private void send(JsonObject embed) {
String url = plugin.getBungeeConfig().getDiscordWebhookUrl();
if (url == null || url.contains("YOUR_WEBHOOK")) return;
JsonObject payload = new JsonObject();
payload.addProperty("username", plugin.getBungeeConfig().getDiscordBotName());
String avatar = plugin.getBungeeConfig().getDiscordBotAvatar();
if (!avatar.isEmpty()) payload.addProperty("avatar_url", avatar);
JsonArray embeds = new JsonArray();
embeds.add(embed);
payload.add("embeds", embeds);
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
try (Response response = httpClient.newCall(new Request.Builder()
.url(url)
.post(RequestBody.create(payload.toString(), MediaType.get("application/json")))
.build()).execute()) {
if (!response.isSuccessful())
plugin.getLogger().warning("Discord Webhook Fehler: " + response.code());
} catch (IOException e) {
plugin.getLogger().log(Level.WARNING, "Discord Verbindungsfehler: " + e.getMessage());
}
});
}
}

View File

@@ -0,0 +1,156 @@
package de.serverpulse.bungee.listeners;
import de.serverpulse.bungee.BungeePlugin;
import net.md_5.bungee.api.event.*;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.List;
import java.util.ArrayList;
/**
* Lauscht auf BungeeCord-Events:
* - Login/Logout (Proxy-Ebene)
* - Server-Wechsel
* - Chat-Events (Spam + Suspicious Patterns)
*/
public class BungeePlayerListener implements Listener {
private final BungeePlugin plugin;
// Spam-Erkennung: UUID → Liste der letzten Nachrichten-Zeitpunkte (ms)
private final Map<String, List<Long>> chatTimestamps = new ConcurrentHashMap<>();
// Kompilierte Pattern für Suspicious-Chat-Erkennung
private final List<Pattern> suspiciousPatterns = new ArrayList<>();
public BungeePlayerListener(BungeePlugin plugin) {
this.plugin = plugin;
compileSuspiciousPatterns();
}
// ── Login ─────────────────────────────────
@EventHandler(priority = EventPriority.LOWEST)
public void onLogin(PostLoginEvent event) {
plugin.debug("Spieler verbunden (Proxy): " + event.getPlayer().getName()
+ " | Netzwerk-Spieler: " + plugin.getProxy().getOnlineCount());
}
// ── Logout ────────────────────────────────
@EventHandler(priority = EventPriority.LOWEST)
public void onDisconnect(PlayerDisconnectEvent event) {
chatTimestamps.remove(event.getPlayer().getUniqueId().toString());
plugin.debug("Spieler getrennt (Proxy): " + event.getPlayer().getName());
}
// ── Server-Wechsel ────────────────────────
@EventHandler(priority = EventPriority.HIGHEST)
public void onServerSwitch(ServerSwitchEvent event) {
String from = event.getFrom() != null ? event.getFrom().getName() : "Proxy";
String to = event.getPlayer().getServer() != null
? event.getPlayer().getServer().getInfo().getName() : "?";
plugin.debug("Server-Wechsel: " + event.getPlayer().getName()
+ " | " + from + "" + to);
}
// ── Chat-Monitoring ───────────────────────
@EventHandler(priority = EventPriority.HIGHEST)
public void onChat(ChatEvent event) {
if (event.isCommand()) return;
if (!plugin.getBungeeConfig().isChatMonitoringEnabled()) return;
String message = event.getMessage();
String playerUuid = event.getSender() instanceof net.md_5.bungee.api.connection.ProxiedPlayer p
? p.getUniqueId().toString() : "unknown";
String playerName = event.getSender() instanceof net.md_5.bungee.api.connection.ProxiedPlayer p
? p.getName() : "unknown";
String serverName = event.getSender() instanceof net.md_5.bungee.api.connection.ProxiedPlayer p
&& p.getServer() != null
? p.getServer().getInfo().getName() : "unknown";
// Spam-Erkennung
if (plugin.getBungeeConfig().isSpamDetectionEnabled()) {
if (isSpam(playerUuid)) {
plugin.debug("Spam erkannt von: " + playerName);
// Über PluginMessageHandler wird das bereits vom Spigot-Server gemeldet
// Proxy-seitig loggen wir es auch
plugin.getLogger().warning("[CHAT-SPAM] " + serverName
+ " | " + playerName + ": " + message);
if (plugin.getBungeeConfig().isDiscordEnabled()) {
plugin.getDiscordWebhook().sendChatAlert(serverName, playerName, message, "SPAM");
}
return;
}
}
// Suspicious-Pattern-Erkennung
for (Pattern pattern : suspiciousPatterns) {
if (pattern.matcher(message).find()) {
plugin.getLogger().warning("[CHAT-SUSPICIOUS] " + serverName
+ " | " + playerName + ": " + message);
if (plugin.getBungeeConfig().isDiscordEnabled()) {
plugin.getDiscordWebhook().sendChatAlert(serverName, playerName, message, "SUSPICIOUS");
}
break;
}
}
}
// ── Server-Connect/Disconnect ─────────────
@EventHandler
public void onServerConnect(ServerConnectedEvent event) {
String serverName = event.getServer().getInfo().getName();
plugin.getDataStore().getOrCreate(serverName);
}
@EventHandler
public void onServerDisconnect(ServerDisconnectEvent event) {
String serverName = event.getTarget().getName();
// Nur markieren wenn wirklich kein Spieler mehr drauf ist
long playersOnServer = plugin.getProxy().getPlayers().stream()
.filter(p -> p.getServer() != null
&& p.getServer().getInfo().getName().equals(serverName))
.count();
if (playersOnServer == 0) {
plugin.debug("Server ohne Spieler: " + serverName);
}
}
// ──────────────────────────────────────────
// HILFSMETHODEN
// ──────────────────────────────────────────
private boolean isSpam(String playerUuid) {
long now = System.currentTimeMillis();
List<Long> timestamps = chatTimestamps.computeIfAbsent(playerUuid, k -> new ArrayList<>());
// Alte Einträge entfernen (> 1 Sekunde)
timestamps.removeIf(t -> now - t > 1000);
timestamps.add(now);
int limit = plugin.getBungeeConfig().getSpamMessagesPerSecond();
return timestamps.size() > limit;
}
private void compileSuspiciousPatterns() {
List<String> rawPatterns = plugin.getBungeeConfig().getSuspiciousPatterns();
for (String raw : rawPatterns) {
try {
suspiciousPatterns.add(Pattern.compile(raw));
} catch (Exception e) {
plugin.getLogger().warning("Ungültiges Regex-Pattern: " + raw);
}
}
plugin.getLogger().info(suspiciousPatterns.size() + " Chat-Pattern(s) geladen.");
}
}

View File

@@ -0,0 +1,109 @@
package de.serverpulse.bungee.models;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* Hält alle empfangenen Monitoring-Daten eines einzelnen Sub-Servers.
* Wird über den Plugin Messaging Channel vom Spigot-Plugin befüllt.
*/
public class ServerData {
private final String serverName;
// Performance
private double tps = 20.0;
private double mspt = 0.0;
private long ramUsedMb = 0;
private long ramMaxMb = 0;
private double ramPercent = 0.0;
// Spieler
private int onlinePlayers = 0;
private int loadedChunks = 0;
// Entities (Welt → Kategorie → Anzahl)
private Map<String, Map<String, Integer>> entityData = new HashMap<>();
// Alerts (letzte X Alerts des Servers)
private String lastAlert = "";
private String lastAlertSeverity = "";
// Timestamps
private LocalDateTime lastUpdate = null;
private LocalDateTime connectedSince = LocalDateTime.now();
// Status
private boolean online = false;
public ServerData(String serverName) {
this.serverName = serverName;
}
// ── Performance ──────────────────────────
public void updatePerformance(double tps, double mspt,
long ramUsedMb, long ramMaxMb,
double ramPercent, int onlinePlayers, int loadedChunks) {
this.tps = tps;
this.mspt = mspt;
this.ramUsedMb = ramUsedMb;
this.ramMaxMb = ramMaxMb;
this.ramPercent = ramPercent;
this.onlinePlayers = onlinePlayers;
this.loadedChunks = loadedChunks;
this.lastUpdate = LocalDateTime.now();
this.online = true;
}
// ── Entity-Daten ─────────────────────────
public void updateEntityData(String worldName, int monsters, int animals,
int waterMobs, int villagers, int armorStands,
int hopperMinecarts, int items, int players,
int other, int total) {
Map<String, Integer> worldEntities = new HashMap<>();
worldEntities.put("monsters", monsters);
worldEntities.put("animals", animals);
worldEntities.put("water_mobs", waterMobs);
worldEntities.put("villagers", villagers);
worldEntities.put("armor_stands", armorStands);
worldEntities.put("hopper_minecarts", hopperMinecarts);
worldEntities.put("items", items);
worldEntities.put("players", players);
worldEntities.put("other", other);
worldEntities.put("total", total);
this.entityData.put(worldName, worldEntities);
}
// ── Alert ────────────────────────────────
public void updateAlert(String message, String severity) {
this.lastAlert = message;
this.lastAlertSeverity = severity;
}
public void markOffline() {
this.online = false;
}
// ── Getter ───────────────────────────────
public String getServerName() { return serverName; }
public double getTps() { return tps; }
public double getMspt() { return mspt; }
public long getRamUsedMb() { return ramUsedMb; }
public long getRamMaxMb() { return ramMaxMb; }
public double getRamPercent() { return ramPercent; }
public int getOnlinePlayers() { return onlinePlayers; }
public int getLoadedChunks() { return loadedChunks; }
public Map<String, Map<String, Integer>> getEntityData() { return entityData; }
public String getLastAlert() { return lastAlert; }
public String getLastAlertSeverity() { return lastAlertSeverity; }
public LocalDateTime getLastUpdate() { return lastUpdate; }
public LocalDateTime getConnectedSince() { return connectedSince; }
public boolean isOnline() { return online; }
public int getTotalEntities() {
return entityData.values().stream()
.mapToInt(m -> m.getOrDefault("total", 0))
.sum();
}
}

View File

@@ -0,0 +1,85 @@
package de.serverpulse.bungee.network;
import de.serverpulse.network.NetworkMessage;
import de.serverpulse.bungee.BungeePlugin;
import de.serverpulse.bungee.models.ServerData;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Proxy-seitiger Alert Manager.
* Prüft Grenzwerte für alle Sub-Server und verhindert Alert-Spam via Cooldown.
*/
public class BungeeAlertManager {
private final BungeePlugin plugin;
private final Map<String, Long> cooldowns = new ConcurrentHashMap<>();
private static final long COOLDOWN_MS = 5 * 60 * 1000L;
public BungeeAlertManager(BungeePlugin plugin) {
this.plugin = plugin;
}
/**
* Prüft Performance-Grenzwerte eines Sub-Servers und löst Alerts aus.
*/
public void checkServerThresholds(String serverName, ServerData data) {
double tps = data.getTps();
double mspt = data.getMspt();
double ramPercent = data.getRamPercent();
if (tps < plugin.getBungeeConfig().getTpsCritical()) {
trigger(serverName + "_TPS_CRIT", "CRITICAL",
serverName, "TPS_CRITICAL",
String.format("Kritische TPS auf %s: %.2f", serverName, tps));
} else if (tps < plugin.getBungeeConfig().getTpsWarning()) {
trigger(serverName + "_TPS_WARN", "WARNING",
serverName, "TPS_WARNING",
String.format("Niedrige TPS auf %s: %.2f", serverName, tps));
}
if (mspt > plugin.getBungeeConfig().getMsptCritical()) {
trigger(serverName + "_MSPT_CRIT", "CRITICAL",
serverName, "MSPT_CRITICAL",
String.format("Kritische MSPT auf %s: %.2f ms", serverName, mspt));
}
if (ramPercent > plugin.getBungeeConfig().getRamCritical()) {
trigger(serverName + "_RAM_CRIT", "CRITICAL",
serverName, "RAM_CRITICAL",
String.format("Kritische RAM-Auslastung auf %s: %.1f%%", serverName, ramPercent));
} else if (ramPercent > plugin.getBungeeConfig().getRamWarning()) {
trigger(serverName + "_RAM_WARN", "WARNING",
serverName, "RAM_WARNING",
String.format("Hohe RAM-Auslastung auf %s: %.1f%%", serverName, ramPercent));
}
// Netzwerk-Spieler prüfen
int totalPlayers = plugin.getDataStore().getTotalOnlinePlayers();
int maxPlayers = plugin.getProxy().getConfig().getPlayerLimit();
if (maxPlayers > 0) {
double fillPercent = (double) totalPlayers / maxPlayers * 100;
if (fillPercent >= plugin.getBungeeConfig().getNetworkPlayersCritical()) {
trigger("NETWORK_PLAYERS_CRIT", "CRITICAL",
"network", "NETWORK_PLAYERS_CRITICAL",
String.format("Netzwerk fast voll: %d/%d Spieler (%.1f%%)",
totalPlayers, maxPlayers, fillPercent));
}
}
}
private void trigger(String cooldownKey, String severity,
String serverName, String alertType, String message) {
long now = System.currentTimeMillis();
if (cooldowns.containsKey(cooldownKey)
&& now - cooldowns.get(cooldownKey) < COOLDOWN_MS) return;
cooldowns.put(cooldownKey, now);
plugin.getLogger().warning("[ALERT][" + severity + "] " + message);
if (plugin.getBungeeConfig().isDiscordEnabled()) {
plugin.getDiscordWebhook().sendServerAlert(serverName, alertType, severity, message);
}
}
}

View File

@@ -0,0 +1,85 @@
package de.serverpulse.bungee.network;
import de.serverpulse.bungee.models.ServerData;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Zentraler In-Memory-Datenspeicher für alle Sub-Server-Daten.
* Thread-safe durch ConcurrentHashMap.
*/
public class NetworkDataStore {
// serverName → ServerData
private final Map<String, ServerData> servers = new ConcurrentHashMap<>();
/**
* Gibt ServerData für einen Server zurück, erstellt ihn bei Bedarf.
*/
public ServerData getOrCreate(String serverName) {
return servers.computeIfAbsent(serverName, ServerData::new);
}
public ServerData get(String serverName) {
return servers.get(serverName);
}
public Collection<ServerData> getAll() {
return Collections.unmodifiableCollection(servers.values());
}
public int getServerCount() {
return servers.size();
}
public int getTotalOnlinePlayers() {
return servers.values().stream()
.filter(ServerData::isOnline)
.mapToInt(ServerData::getOnlinePlayers)
.sum();
}
public double getNetworkAverageTps() {
return servers.values().stream()
.filter(ServerData::isOnline)
.mapToDouble(ServerData::getTps)
.average()
.orElse(20.0);
}
public double getLowestTps() {
return servers.values().stream()
.filter(ServerData::isOnline)
.mapToDouble(ServerData::getTps)
.min()
.orElse(20.0);
}
public long getTotalRamUsedMb() {
return servers.values().stream()
.filter(ServerData::isOnline)
.mapToLong(ServerData::getRamUsedMb)
.sum();
}
public long getTotalRamMaxMb() {
return servers.values().stream()
.filter(ServerData::isOnline)
.mapToLong(ServerData::getRamMaxMb)
.sum();
}
public int getOnlineServerCount() {
return (int) servers.values().stream()
.filter(ServerData::isOnline)
.count();
}
public void markOffline(String serverName) {
ServerData data = servers.get(serverName);
if (data != null) data.markOffline();
}
}

View File

@@ -0,0 +1,143 @@
package de.serverpulse.bungee.network;
import de.serverpulse.network.NetworkMessage;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import de.serverpulse.bungee.BungeePlugin;
import de.serverpulse.bungee.models.ServerData;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* Empfängt Plugin Messaging Channel Nachrichten von den Spigot Sub-Servern.
*
* Kanal-Schema: de.serverpulse.network.NetworkMessage.CHANNEL
*
* Nachrichten-Typen (JSON):
* - PERFORMANCE → TPS, MSPT, RAM, Spieler, Chunks
* - ENTITY → Entity-Daten einer Welt
* - ALERT → Warnung von einem Sub-Server
* - CHAT_EVENT → Verdächtige Chat-Nachricht
* - HEARTBEAT → Server ist noch online
*/
public class PluginMessageHandler implements Listener {
private final BungeePlugin plugin;
private final Gson gson = new Gson();
public PluginMessageHandler(BungeePlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPluginMessage(PluginMessageEvent event) {
if (!event.getTag().equals(NetworkMessage.CHANNEL)) return;
if (!(event.getSender() instanceof net.md_5.bungee.api.connection.Server server)) return;
String serverName = server.getInfo().getName();
try (DataInputStream in = new DataInputStream(
new ByteArrayInputStream(event.getData()))) {
// Erste Bytes = JSON-String
byte[] jsonBytes = event.getData();
String json = new String(jsonBytes, StandardCharsets.UTF_8);
JsonObject msg = gson.fromJson(json, JsonObject.class);
String type = msg.get("type").getAsString();
plugin.debug("Nachricht von " + serverName + ": " + type);
switch (type) {
case "PERFORMANCE" -> handlePerformance(serverName, msg);
case "ENTITY" -> handleEntity(serverName, msg);
case "ALERT" -> handleAlert(serverName, msg);
case "CHAT_EVENT" -> handleChatEvent(serverName, msg);
case "HEARTBEAT" -> handleHeartbeat(serverName);
default -> plugin.debug("Unbekannter Nachrichtentyp: " + type);
}
} catch (Exception e) {
plugin.getLogger().warning("Fehler beim Verarbeiten der Nachricht von "
+ serverName + ": " + e.getMessage());
}
}
// ──────────────────────────────────────────
// HANDLER
// ──────────────────────────────────────────
private void handlePerformance(String serverName, JsonObject msg) {
ServerData data = plugin.getDataStore().getOrCreate(serverName);
data.updatePerformance(
msg.get("tps").getAsDouble(),
msg.get("mspt").getAsDouble(),
msg.get("ram_used_mb").getAsLong(),
msg.get("ram_max_mb").getAsLong(),
msg.get("ram_percent").getAsDouble(),
msg.get("online_players").getAsInt(),
msg.get("loaded_chunks").getAsInt()
);
// Proxy-seitige Grenzwert-Prüfung
plugin.getAlertManager().checkServerThresholds(serverName, data);
}
private void handleEntity(String serverName, JsonObject msg) {
ServerData data = plugin.getDataStore().getOrCreate(serverName);
data.updateEntityData(
msg.get("world").getAsString(),
msg.get("monsters").getAsInt(),
msg.get("animals").getAsInt(),
msg.get("water_mobs").getAsInt(),
msg.get("villagers").getAsInt(),
msg.get("armor_stands").getAsInt(),
msg.get("hopper_minecarts").getAsInt(),
msg.get("items").getAsInt(),
msg.get("players").getAsInt(),
msg.get("other").getAsInt(),
msg.get("total").getAsInt()
);
}
private void handleAlert(String serverName, JsonObject msg) {
String alertMsg = msg.get("message").getAsString();
String severity = msg.get("severity").getAsString();
String alertType = msg.get("alert_type").getAsString();
ServerData data = plugin.getDataStore().getOrCreate(serverName);
data.updateAlert(alertMsg, severity);
plugin.getLogger().warning("[" + serverName + "] [" + severity + "] " + alertMsg);
// An Discord weiterleiten
if (plugin.getBungeeConfig().isDiscordEnabled()) {
plugin.getDiscordWebhook().sendServerAlert(serverName, alertType, severity, alertMsg);
}
}
private void handleChatEvent(String serverName, JsonObject msg) {
String playerName = msg.get("player").getAsString();
String message = msg.get("message").getAsString();
String reason = msg.get("reason").getAsString(); // SUSPICIOUS / SPAM
plugin.getLogger().warning("[CHAT-MONITOR] " + serverName
+ " | " + playerName + ": " + message + " (" + reason + ")");
if (plugin.getBungeeConfig().isDiscordEnabled()) {
plugin.getDiscordWebhook().sendChatAlert(serverName, playerName, message, reason);
}
}
private void handleHeartbeat(String serverName) {
plugin.getDataStore().getOrCreate(serverName);
plugin.debug("Heartbeat von: " + serverName);
}
}

View File

@@ -0,0 +1,103 @@
package de.serverpulse.bungee.utils;
import de.serverpulse.bungee.BungeePlugin;
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.config.YamlConfiguration;
import java.io.*;
import java.nio.file.Files;
import java.util.List;
/**
* Konfigurationsverwaltung für das BungeeCord-Plugin.
*/
public class BungeeConfig {
private final BungeePlugin plugin;
private Configuration config;
public BungeeConfig(BungeePlugin plugin) {
this.plugin = plugin;
}
public void load() {
try {
if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
File configFile = new File(plugin.getDataFolder(), "config.yml");
if (!configFile.exists()) {
try (InputStream in = plugin.getResourceAsStream("config.yml")) {
Files.copy(in, configFile.toPath());
}
}
config = ConfigurationProvider.getProvider(YamlConfiguration.class)
.load(configFile);
} catch (IOException e) {
plugin.getLogger().severe("Fehler beim Laden der Konfiguration: " + e.getMessage());
}
}
// ── General ──────────────────────────────
public boolean isDebugMode() { return config.getBoolean("general.debug", false); }
public int getPollInterval() { return config.getInt("general.poll-interval", 30); }
// ── Discord ──────────────────────────────
public boolean isDiscordEnabled() { return config.getBoolean("discord.enabled", false); }
public String getDiscordWebhookUrl() { return config.getString("discord.webhook-url", ""); }
public String getDiscordBotName() { return config.getString("discord.bot-name", "ServerPulse Network"); }
public String getDiscordBotAvatar() { return config.getString("discord.bot-avatar", ""); }
public boolean isDailyReportEnabled() { return config.getBoolean("discord.daily-report.enabled", true); }
public String getDailyReportTime() { return config.getString("discord.daily-report.time", "08:00"); }
public int getColorInfo() { return config.getInt("discord.colors.info", 3447003); }
public int getColorWarning() { return config.getInt("discord.colors.warning", 16776960); }
public int getColorCritical() { return config.getInt("discord.colors.critical", 16711680); }
public int getColorSuccess() { return config.getInt("discord.colors.success", 65280); }
// ── Thresholds ───────────────────────────
public double getTpsWarning() { return config.getDouble("thresholds.tps-warning", 18.0); }
public double getTpsCritical() { return config.getDouble("thresholds.tps-critical", 15.0); }
public double getMsptWarning() { return config.getDouble("thresholds.mspt-warning", 40.0); }
public double getMsptCritical() { return config.getDouble("thresholds.mspt-critical", 50.0); }
public int getRamWarning() { return config.getInt("thresholds.ram-warning", 75); }
public int getRamCritical() { return config.getInt("thresholds.ram-critical", 90); }
public int getNetworkPlayersWarning() { return config.getInt("thresholds.network-players-warning", 80); }
public int getNetworkPlayersCritical() { return config.getInt("thresholds.network-players-critical", 95); }
// ── REST API ─────────────────────────────
public boolean isRestApiEnabled() { return config.getBoolean("rest-api.enabled", false); }
public String getRestApiHost() { return config.getString("rest-api.host", "0.0.0.0"); }
public int getRestApiPort() { return config.getInt("rest-api.port", 8081); }
public String getRestApiKey() { return config.getString("rest-api.api-key", ""); }
// ── Formate ──────────────────────────────
public boolean isPrometheusEnabled() { return config.getBoolean("rest-api.formats.prometheus.enabled", true); }
public String getPrometheusEndpoint() { return config.getString("rest-api.formats.prometheus.endpoint", "/metrics"); }
public boolean isSimpleJsonEnabled() { return config.getBoolean("rest-api.formats.simple-json.enabled", true); }
public String getSimpleJsonEndpoint() { return config.getString("rest-api.formats.simple-json.endpoint", "/grafana"); }
public boolean isInfluxEnabled() { return config.getBoolean("rest-api.formats.influxdb.enabled", true); }
public String getInfluxEndpoint() { return config.getString("rest-api.formats.influxdb.endpoint", "/influx"); }
public String getInfluxMeasurement() { return config.getString("rest-api.formats.influxdb.measurement", "serverpulse"); }
public boolean isInfluxPushEnabled() { return config.getBoolean("rest-api.formats.influxdb.push.enabled", false); }
public String getInfluxPushUrl() { return config.getString("rest-api.formats.influxdb.push.url", ""); }
public String getInfluxPushToken() { return config.getString("rest-api.formats.influxdb.push.token", ""); }
public String getInfluxPushOrg() { return config.getString("rest-api.formats.influxdb.push.org", ""); }
public String getInfluxPushBucket() { return config.getString("rest-api.formats.influxdb.push.bucket", "serverpulse"); }
public boolean isJsonApiEnabled() { return config.getBoolean("rest-api.formats.json.enabled", true); }
public String getJsonApiEndpoint() { return config.getString("rest-api.formats.json.endpoint", "/api"); }
// ── Chat-Monitoring ──────────────────────
public boolean isChatMonitoringEnabled() { return config.getBoolean("chat-monitoring.enabled", true); }
public boolean isSpamDetectionEnabled() { return config.getBoolean("chat-monitoring.spam.enabled", true); }
public int getSpamMessagesPerSecond() { return config.getInt("chat-monitoring.spam.messages-per-second", 5); }
@SuppressWarnings("unchecked")
public List<String> getSuspiciousPatterns() {
return (List<String>) config.getList("chat-monitoring.suspicious-patterns",
List.of("(?i)(hack|cheat|xray)"));
}
}

View File

@@ -0,0 +1,429 @@
package de.serverpulse.commands;
import de.serverpulse.ServerPulse;
import de.serverpulse.models.EntitySnapshot;
import de.serverpulse.models.PerformanceSnapshot;
import de.serverpulse.utils.MessageUtil;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Verarbeitet alle /serverpulse (Alias: /sp, /pulse) Unterbefehle.
*/
public class ServerPulseCommand implements CommandExecutor, TabCompleter {
private final ServerPulse plugin;
public ServerPulseCommand(ServerPulse plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
String prefix = plugin.getConfigManager().getPrefix();
if (args.length == 0) {
sendHelp(sender, prefix, label);
return true;
}
switch (args[0].toLowerCase()) {
case "status" -> handleStatus(sender, prefix);
case "report" -> handleReport(sender, prefix);
case "world" -> handleWorld(sender, prefix, args);
case "entities" -> handleEntities(sender, prefix, args);
case "reload" -> handleReload(sender, prefix);
case "debug" -> handleDebug(sender, prefix);
case "clear" -> handleClear(sender, prefix, args);
default -> sendHelp(sender, prefix, label);
}
return true;
}
// ──────────────────────────────────────────
// /sp status
// ──────────────────────────────────────────
private void handleStatus(CommandSender sender, String prefix) {
if (!sender.hasPermission("serverpulse.status")) {
sender.sendMessage(prefix + plugin.getConfigManager().getMessage("no-permission"));
return;
}
sender.sendMessage(MessageUtil.colorize(MessageUtil.header("ServerPulse Status")));
PerformanceSnapshot snap = plugin.getPerformanceMonitor().getLastSnapshot();
if (snap == null) {
sender.sendMessage(prefix + "&eNoch keine Daten verfügbar. Bitte warte einen Moment...");
return;
}
double tps = snap.getTps();
double mspt = snap.getMspt();
long ramUsed = snap.getRamUsedMb();
long ramMax = snap.getRamMaxMb();
sender.sendMessage(MessageUtil.colorize(
prefix + "&7TPS: " + MessageUtil.formatTps(tps) + " &8| &7MSPT: " + MessageUtil.formatMspt(mspt)
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7RAM: " + MessageUtil.formatRam(ramUsed, ramMax)
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7RAM-Auslastung: " + MessageUtil.progressBar(ramUsed, ramMax, 20)
+ " &7" + ramUsed + "/" + ramMax + "MB"
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Spieler: &f" + snap.getOnlinePlayers()
+ " &8| &7Welten: &f" + snap.getLoadedWorlds()
+ " &8| &7Chunks: &f" + snap.getLoadedChunks()
));
if (plugin.getDatabaseManager().isConnected()) {
double avgTps15m = plugin.getDatabaseManager().getAverageTps(15);
double avgTps60m = plugin.getDatabaseManager().getAverageTps(60);
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Ø TPS: &715min=&f" + String.format("%.2f", avgTps15m)
+ " &860min=&f" + String.format("%.2f", avgTps60m)
));
}
sender.sendMessage(MessageUtil.colorize(MessageUtil.separator()));
}
// ──────────────────────────────────────────
// /sp report
// ──────────────────────────────────────────
private void handleReport(CommandSender sender, String prefix) {
if (!sender.hasPermission("serverpulse.report")) {
sender.sendMessage(prefix + plugin.getConfigManager().getMessage("no-permission"));
return;
}
sender.sendMessage(prefix + "&aErstelle Report...");
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
sender.sendMessage(MessageUtil.colorize(MessageUtil.header("ServerPulse Report")));
// Performance
PerformanceSnapshot perf = plugin.getPerformanceMonitor().getLastSnapshot();
if (perf != null) {
sender.sendMessage(MessageUtil.colorize(prefix + "&b=== Performance ==="));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7TPS: " + MessageUtil.formatTps(perf.getTps())
+ " MSPT: " + MessageUtil.formatMspt(perf.getMspt())
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7RAM: " + MessageUtil.formatRam(perf.getRamUsedMb(), perf.getRamMaxMb())
));
}
// Datenbank-Statistiken
if (plugin.getDatabaseManager().isConnected()) {
sender.sendMessage(MessageUtil.colorize(prefix + "&b=== Warnungen (24h) ==="));
int critAlerts = plugin.getDatabaseManager().getAlertCount(24, "CRITICAL");
int warnAlerts = plugin.getDatabaseManager().getAlertCount(24, "WARNING");
sender.sendMessage(MessageUtil.colorize(
prefix + "&c" + critAlerts + " kritisch &e" + warnAlerts + " Warnungen"
));
}
// Entities
sender.sendMessage(MessageUtil.colorize(prefix + "&b=== Entities ==="));
for (Map.Entry<String, EntitySnapshot> entry : plugin.getEntityMonitor().getAllLastSnapshots().entrySet()) {
EntitySnapshot snap = entry.getValue();
sender.sendMessage(MessageUtil.colorize(
prefix + "&7" + entry.getKey() + ": &fGesamt=" + snap.getTotal()
+ " Monster=" + MessageUtil.formatEntityCount(
snap.getMonsters(),
plugin.getConfigManager().getWorldMonstersWarning(entry.getKey()),
plugin.getConfigManager().getWorldMonstersCritical(entry.getKey()))
));
}
// In DB speichern
if (plugin.getDatabaseManager().isConnected()) {
plugin.getDatabaseManager().saveReport("MANUAL", sender.getName(), "Manueller Report erstellt.");
}
sender.sendMessage(MessageUtil.colorize(MessageUtil.separator()));
});
}
// ──────────────────────────────────────────
// /sp world <weltname>
// ──────────────────────────────────────────
private void handleWorld(CommandSender sender, String prefix, String[] args) {
if (!sender.hasPermission("serverpulse.world")) {
sender.sendMessage(prefix + plugin.getConfigManager().getMessage("no-permission"));
return;
}
if (args.length < 2) {
sender.sendMessage(prefix + "&cBenutzung: /sp world <weltname>");
sender.sendMessage(prefix + "&7Verfügbare Welten: &f"
+ Bukkit.getWorlds().stream()
.map(World::getName)
.collect(Collectors.joining(", ")));
return;
}
String worldName = args[1];
World world = Bukkit.getWorld(worldName);
if (world == null) {
sender.sendMessage(prefix + "&cWelt nicht gefunden: &f" + worldName);
return;
}
String displayName = plugin.getConfigManager().getWorldDisplayName(worldName);
sender.sendMessage(MessageUtil.colorize(MessageUtil.header("Welt: " + displayName)));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Typ: &f" + world.getEnvironment().name()
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Geladene Chunks: &f" + world.getLoadedChunks().length
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Spieler: &f" + world.getPlayers().size()
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Entities gesamt: &f" + world.getEntities().size()
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Schwierigkeitsgrad: &f" + world.getDifficulty().name()
));
EntitySnapshot snap = plugin.getEntityMonitor().getLastSnapshot(worldName);
if (snap != null) {
sender.sendMessage(MessageUtil.colorize(prefix + "&b--- Letzter Entity-Snapshot ---"));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Monster: " + MessageUtil.formatEntityCount(
snap.getMonsters(),
plugin.getConfigManager().getWorldMonstersWarning(worldName),
plugin.getConfigManager().getWorldMonstersCritical(worldName))
+ " Tiere: &a" + snap.getAnimals()
+ " Villager: " + MessageUtil.formatEntityCount(
snap.getVillagers(),
plugin.getConfigManager().getVillagersWarning(),
plugin.getConfigManager().getVillagersCritical())
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Items: " + MessageUtil.formatEntityCount(
snap.getItems(),
plugin.getConfigManager().getItemsWarning(),
plugin.getConfigManager().getItemsCritical())
+ " ArmorStands: &7" + snap.getArmorStands()
+ " HopperCarts: &7" + snap.getHopperMinecarts()
));
}
sender.sendMessage(MessageUtil.colorize(MessageUtil.separator()));
}
// ──────────────────────────────────────────
// /sp entities [welt]
// ──────────────────────────────────────────
private void handleEntities(CommandSender sender, String prefix, String[] args) {
if (!sender.hasPermission("serverpulse.entities")) {
sender.sendMessage(prefix + plugin.getConfigManager().getMessage("no-permission"));
return;
}
sender.sendMessage(MessageUtil.colorize(MessageUtil.header("Entity-Übersicht")));
Map<String, EntitySnapshot> snapshots;
if (args.length >= 2) {
String worldName = args[1];
EntitySnapshot snap = plugin.getEntityMonitor().getLastSnapshot(worldName);
if (snap == null) {
sender.sendMessage(prefix + "&cKeine Daten für Welt: &f" + worldName);
return;
}
snapshots = Map.of(worldName, snap);
} else {
snapshots = plugin.getEntityMonitor().getAllLastSnapshots();
}
if (snapshots.isEmpty()) {
sender.sendMessage(prefix + "&eNoch keine Entity-Daten verfügbar.");
return;
}
for (Map.Entry<String, EntitySnapshot> entry : snapshots.entrySet()) {
EntitySnapshot snap = entry.getValue();
String wName = entry.getKey();
String displayName = plugin.getConfigManager().getWorldDisplayName(wName);
sender.sendMessage(MessageUtil.colorize(
prefix + "&b" + displayName + " &8(" + wName + ")"
));
sender.sendMessage(MessageUtil.colorize(
" &7Monster: " + MessageUtil.formatEntityCount(
snap.getMonsters(),
plugin.getConfigManager().getWorldMonstersWarning(wName),
plugin.getConfigManager().getWorldMonstersCritical(wName))
+ " &7Tiere: &a" + snap.getAnimals()
+ " &7Wasser: &a" + snap.getWaterMobs()
));
sender.sendMessage(MessageUtil.colorize(
" &7Villager: " + MessageUtil.formatEntityCount(
snap.getVillagers(),
plugin.getConfigManager().getVillagersWarning(),
plugin.getConfigManager().getVillagersCritical())
+ " &7Items: " + MessageUtil.formatEntityCount(
snap.getItems(),
plugin.getConfigManager().getItemsWarning(),
plugin.getConfigManager().getItemsCritical())
+ " &7Gesamt: &f" + snap.getTotal()
));
}
sender.sendMessage(MessageUtil.colorize(MessageUtil.separator()));
}
// ──────────────────────────────────────────
// /sp reload
// ──────────────────────────────────────────
private void handleReload(CommandSender sender, String prefix) {
if (!sender.hasPermission("serverpulse.reload")) {
sender.sendMessage(prefix + plugin.getConfigManager().getMessage("no-permission"));
return;
}
plugin.reload();
sender.sendMessage(prefix + plugin.getConfigManager().getMessage("reload-success"));
}
// ──────────────────────────────────────────
// /sp debug
// ──────────────────────────────────────────
private void handleDebug(CommandSender sender, String prefix) {
if (!sender.hasPermission("serverpulse.debug")) {
sender.sendMessage(prefix + plugin.getConfigManager().getMessage("no-permission"));
return;
}
sender.sendMessage(MessageUtil.colorize(MessageUtil.header("Debug-Informationen")));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Plugin-Version: &f" + plugin.getDescription().getVersion()
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Debug-Modus: " + (plugin.getConfigManager().isDebugMode() ? "&aAktiv" : "&cInaktiv")
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Datenbank: " + (plugin.getDatabaseManager().isConnected() ? "&aVerbunden" : "&cGetrennt")
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Discord: " + (plugin.getConfigManager().isDiscordEnabled() ? "&aAktiv" : "&cInaktiv")
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7REST API: " + (plugin.getConfigManager().isRestApiEnabled() ? "&aAktiv" : "&cInaktiv")
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Trend-Analyse: " + (plugin.getConfigManager().isTrendAnalysisEnabled() ? "&aAktiv" : "&cInaktiv")
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Überwachte Welten: &f" + Bukkit.getWorlds().size()
));
sender.sendMessage(MessageUtil.colorize(
prefix + "&7Kritische Events (seit Start): &f" + plugin.getAlertManager().getCriticalEventCount()
));
sender.sendMessage(MessageUtil.colorize(MessageUtil.separator()));
}
// ──────────────────────────────────────────
// /sp clear <items|mobs> <welt>
// ──────────────────────────────────────────
private void handleClear(CommandSender sender, String prefix, String[] args) {
if (!sender.hasPermission("serverpulse.admin")) {
sender.sendMessage(prefix + plugin.getConfigManager().getMessage("no-permission"));
return;
}
if (args.length < 3) {
sender.sendMessage(prefix + "&cBenutzung: /sp clear <items|mobs> <weltname>");
return;
}
String type = args[1].toLowerCase();
String worldName = args[2];
if (Bukkit.getWorld(worldName) == null) {
sender.sendMessage(prefix + "&cWelt nicht gefunden: &f" + worldName);
return;
}
switch (type) {
case "items" -> {
sender.sendMessage(prefix + "&eStarte Item-Clear in &f" + worldName + "&e...");
plugin.getAlertManager().triggerItemClear(worldName);
}
case "mobs" -> {
sender.sendMessage(prefix + "&eStarte Mob-Clear in &f" + worldName + "&e...");
plugin.getAlertManager().triggerMobClear(worldName);
}
default -> sender.sendMessage(prefix + "&cUnbekannter Typ. Nutze &fitems &coder &fmobs&c.");
}
}
// ──────────────────────────────────────────
// HILFE
// ──────────────────────────────────────────
private void sendHelp(CommandSender sender, String prefix, String label) {
sender.sendMessage(MessageUtil.colorize(MessageUtil.header("ServerPulse Hilfe")));
sender.sendMessage(MessageUtil.colorize(prefix + "&b/" + label + " status &8 &7Server-Performance-Übersicht"));
sender.sendMessage(MessageUtil.colorize(prefix + "&b/" + label + " report &8 &7Vollständiger Statusbericht"));
sender.sendMessage(MessageUtil.colorize(prefix + "&b/" + label + " world <name> &8 &7Welt-Statistiken"));
sender.sendMessage(MessageUtil.colorize(prefix + "&b/" + label + " entities [welt] &8 &7Entity-Übersicht"));
sender.sendMessage(MessageUtil.colorize(prefix + "&b/" + label + " clear <items|mobs> <welt> &8 &7Notfall-Clear"));
sender.sendMessage(MessageUtil.colorize(prefix + "&b/" + label + " reload &8 &7Konfiguration neu laden"));
sender.sendMessage(MessageUtil.colorize(prefix + "&b/" + label + " debug &8 &7Debug-Informationen"));
sender.sendMessage(MessageUtil.colorize(MessageUtil.separator()));
}
// ──────────────────────────────────────────
// TAB-COMPLETION
// ──────────────────────────────────────────
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
completions.addAll(Arrays.asList("status", "report", "world", "entities", "reload", "debug", "clear"));
} else if (args.length == 2) {
switch (args[0].toLowerCase()) {
case "world", "entities" ->
Bukkit.getWorlds().forEach(w -> completions.add(w.getName()));
case "clear" ->
completions.addAll(Arrays.asList("items", "mobs"));
}
} else if (args.length == 3 && args[0].equalsIgnoreCase("clear")) {
Bukkit.getWorlds().forEach(w -> completions.add(w.getName()));
}
return completions.stream()
.filter(s -> s.toLowerCase().startsWith(args[args.length - 1].toLowerCase()))
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,446 @@
package de.serverpulse.database;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import de.serverpulse.ServerPulse;
import de.serverpulse.models.EntitySnapshot;
import de.serverpulse.models.PerformanceSnapshot;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
/**
* Verwaltet die MySQL-Datenbankverbindung via HikariCP
* und stellt Methoden für alle CRUD-Operationen bereit.
*/
public class DatabaseManager {
private final ServerPulse plugin;
private HikariDataSource dataSource;
public DatabaseManager(ServerPulse plugin) {
this.plugin = plugin;
}
// ──────────────────────────────────────────
// VERBINDUNG
// ──────────────────────────────────────────
/**
* Verbindet mit der MySQL-Datenbank via HikariCP
*/
public boolean connect() {
try {
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl("jdbc:mysql://"
+ plugin.getConfigManager().getDbHost() + ":"
+ plugin.getConfigManager().getDbPort() + "/"
+ plugin.getConfigManager().getDbName()
+ "?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8");
hikariConfig.setUsername(plugin.getConfigManager().getDbUsername());
hikariConfig.setPassword(plugin.getConfigManager().getDbPassword());
// Pool-Einstellungen
hikariConfig.setMaximumPoolSize(plugin.getConfigManager().getPoolMaxSize());
hikariConfig.setMinimumIdle(plugin.getConfigManager().getPoolMinIdle());
hikariConfig.setConnectionTimeout(plugin.getConfigManager().getPoolConnectionTimeout());
hikariConfig.setIdleTimeout(plugin.getConfigManager().getPoolIdleTimeout());
hikariConfig.setMaxLifetime(plugin.getConfigManager().getPoolMaxLifetime());
hikariConfig.setPoolName("ServerPulse-Pool");
// Performance-Eigenschaften
hikariConfig.addDataSourceProperty("cachePrepStmts", "true");
hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250");
hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
hikariConfig.addDataSourceProperty("useServerPrepStmts", "true");
this.dataSource = new HikariDataSource(hikariConfig);
plugin.getLogger().info("Datenbankverbindung erfolgreich hergestellt!");
return true;
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Datenbankverbindung fehlgeschlagen: " + e.getMessage(), e);
return false;
}
}
/**
* Schließt den Connection Pool
*/
public void disconnect() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
plugin.getLogger().info("Datenbankverbindung getrennt.");
}
}
/**
* Gibt eine Verbindung aus dem Pool zurück
*/
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public boolean isConnected() {
return dataSource != null && !dataSource.isClosed();
}
// ──────────────────────────────────────────
// TABELLEN ERSTELLEN
// ──────────────────────────────────────────
/**
* Erstellt alle benötigten Tabellen, falls nicht vorhanden
*/
public void createTables() {
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
// server_metrics: TPS, MSPT, RAM, CPU, Spieler
stmt.execute("""
CREATE TABLE IF NOT EXISTS server_metrics (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
recorded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
tps DOUBLE NOT NULL,
mspt DOUBLE NOT NULL,
ram_used_mb BIGINT NOT NULL,
ram_max_mb BIGINT NOT NULL,
ram_percent DOUBLE NOT NULL,
online_players INT NOT NULL,
loaded_worlds INT NOT NULL,
loaded_chunks INT NOT NULL,
INDEX idx_recorded_at (recorded_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""");
// world_metrics: Welt-spezifische Daten
stmt.execute("""
CREATE TABLE IF NOT EXISTS world_metrics (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
recorded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
world_name VARCHAR(100) NOT NULL,
loaded_chunks INT NOT NULL,
total_entities INT NOT NULL,
online_players INT NOT NULL,
INDEX idx_recorded_at (recorded_at),
INDEX idx_world_name (world_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""");
// entity_metrics: Entity-Zahlen pro Welt und Kategorie
stmt.execute("""
CREATE TABLE IF NOT EXISTS entity_metrics (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
recorded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
world_name VARCHAR(100) NOT NULL,
monsters INT NOT NULL DEFAULT 0,
animals INT NOT NULL DEFAULT 0,
water_mobs INT NOT NULL DEFAULT 0,
villagers INT NOT NULL DEFAULT 0,
armor_stands INT NOT NULL DEFAULT 0,
hopper_minecarts INT NOT NULL DEFAULT 0,
items INT NOT NULL DEFAULT 0,
players INT NOT NULL DEFAULT 0,
other INT NOT NULL DEFAULT 0,
total INT NOT NULL DEFAULT 0,
INDEX idx_recorded_at (recorded_at),
INDEX idx_world_name (world_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""");
// alert_history: Protokoll aller Warnungen
stmt.execute("""
CREATE TABLE IF NOT EXISTS alert_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
alert_type VARCHAR(50) NOT NULL,
severity VARCHAR(20) NOT NULL,
world_name VARCHAR(100),
message TEXT NOT NULL,
value_current DOUBLE,
value_threshold DOUBLE,
discord_sent BOOLEAN DEFAULT FALSE,
INDEX idx_created_at (created_at),
INDEX idx_severity (severity),
INDEX idx_alert_type (alert_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""");
// report_history: Protokoll generierter Reports
stmt.execute("""
CREATE TABLE IF NOT EXISTS report_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
report_type VARCHAR(50) NOT NULL,
generated_by VARCHAR(100) NOT NULL,
content TEXT,
discord_sent BOOLEAN DEFAULT FALSE,
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""");
plugin.getLogger().info("Datenbanktabellen erfolgreich erstellt/verifiziert.");
} catch (SQLException e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), e);
}
}
// ──────────────────────────────────────────
// SERVER METRICS
// ──────────────────────────────────────────
/**
* Speichert einen Performance-Snapshot
*/
public void saveServerMetrics(PerformanceSnapshot snapshot) {
String sql = """
INSERT INTO server_metrics
(recorded_at, tps, mspt, ram_used_mb, ram_max_mb, ram_percent, online_players, loaded_worlds, loaded_chunks)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setTimestamp(1, Timestamp.valueOf(snapshot.getRecordedAt()));
ps.setDouble(2, snapshot.getTps());
ps.setDouble(3, snapshot.getMspt());
ps.setLong(4, snapshot.getRamUsedMb());
ps.setLong(5, snapshot.getRamMaxMb());
ps.setDouble(6, snapshot.getRamPercent());
ps.setInt(7, snapshot.getOnlinePlayers());
ps.setInt(8, snapshot.getLoadedWorlds());
ps.setInt(9, snapshot.getLoadedChunks());
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Speichern der Server-Metriken: " + e.getMessage());
}
}
/**
* Gibt die letzten N Performance-Snapshots zurück
*/
public List<PerformanceSnapshot> getRecentServerMetrics(int limit) {
List<PerformanceSnapshot> results = new ArrayList<>();
String sql = "SELECT * FROM server_metrics ORDER BY recorded_at DESC LIMIT ?";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, limit);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
results.add(mapServerMetrics(rs));
}
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Laden der Server-Metriken: " + e.getMessage());
}
return results;
}
/**
* Durchschnittliche TPS der letzten X Minuten
*/
public double getAverageTps(int minutes) {
String sql = "SELECT AVG(tps) FROM server_metrics WHERE recorded_at >= NOW() - INTERVAL ? MINUTE";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, minutes);
ResultSet rs = ps.executeQuery();
if (rs.next()) return rs.getDouble(1);
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Berechnen der Durchschnitts-TPS: " + e.getMessage());
}
return 20.0;
}
// ──────────────────────────────────────────
// ENTITY METRICS
// ──────────────────────────────────────────
/**
* Speichert einen Entity-Snapshot
*/
public void saveEntityMetrics(EntitySnapshot snapshot) {
String sql = """
INSERT INTO entity_metrics
(recorded_at, world_name, monsters, animals, water_mobs, villagers,
armor_stands, hopper_minecarts, items, players, other, total)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setTimestamp(1, Timestamp.valueOf(snapshot.getRecordedAt()));
ps.setString(2, snapshot.getWorldName());
ps.setInt(3, snapshot.getMonsters());
ps.setInt(4, snapshot.getAnimals());
ps.setInt(5, snapshot.getWaterMobs());
ps.setInt(6, snapshot.getVillagers());
ps.setInt(7, snapshot.getArmorStands());
ps.setInt(8, snapshot.getHopperMinecarts());
ps.setInt(9, snapshot.getItems());
ps.setInt(10, snapshot.getPlayers());
ps.setInt(11, snapshot.getOther());
ps.setInt(12, snapshot.getTotal());
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Speichern der Entity-Metriken: " + e.getMessage());
}
}
/**
* Letzte Entity-Metriken für eine Welt
*/
public EntitySnapshot getLatestEntityMetrics(String worldName) {
String sql = "SELECT * FROM entity_metrics WHERE world_name = ? ORDER BY recorded_at DESC LIMIT 1";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, worldName);
ResultSet rs = ps.executeQuery();
if (rs.next()) return mapEntityMetrics(rs);
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Laden der Entity-Metriken: " + e.getMessage());
}
return null;
}
/**
* Entity-Verlauf für Trendanalyse
*/
public List<EntitySnapshot> getEntityHistory(String worldName, int dataPoints) {
List<EntitySnapshot> results = new ArrayList<>();
String sql = "SELECT * FROM entity_metrics WHERE world_name = ? ORDER BY recorded_at DESC LIMIT ?";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, worldName);
ps.setInt(2, dataPoints);
ResultSet rs = ps.executeQuery();
while (rs.next()) results.add(mapEntityMetrics(rs));
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Laden der Entity-Historie: " + e.getMessage());
}
return results;
}
// ──────────────────────────────────────────
// ALERT HISTORY
// ──────────────────────────────────────────
/**
* Speichert eine Warnung in der Datenbank
*/
public void saveAlert(String alertType, String severity, String worldName,
String message, Double currentValue, Double threshold) {
String sql = """
INSERT INTO alert_history (alert_type, severity, world_name, message, value_current, value_threshold)
VALUES (?, ?, ?, ?, ?, ?)
""";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, alertType);
ps.setString(2, severity);
ps.setString(3, worldName);
ps.setString(4, message);
if (currentValue != null) ps.setDouble(5, currentValue);
else ps.setNull(5, Types.DOUBLE);
if (threshold != null) ps.setDouble(6, threshold);
else ps.setNull(6, Types.DOUBLE);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Speichern der Warnung: " + e.getMessage());
}
}
/**
* Anzahl der Warnungen der letzten X Stunden
*/
public int getAlertCount(int hours, String severity) {
String sql = "SELECT COUNT(*) FROM alert_history WHERE created_at >= NOW() - INTERVAL ? HOUR AND severity = ?";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, hours);
ps.setString(2, severity);
ResultSet rs = ps.executeQuery();
if (rs.next()) return rs.getInt(1);
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Zählen der Warnungen: " + e.getMessage());
}
return 0;
}
// ──────────────────────────────────────────
// REPORT HISTORY
// ──────────────────────────────────────────
/**
* Speichert einen generierten Report
*/
public void saveReport(String reportType, String generatedBy, String content) {
String sql = "INSERT INTO report_history (report_type, generated_by, content) VALUES (?, ?, ?)";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, reportType);
ps.setString(2, generatedBy);
ps.setString(3, content);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Speichern des Reports: " + e.getMessage());
}
}
// ──────────────────────────────────────────
// DATEN-BEREINIGUNG
// ──────────────────────────────────────────
/**
* Löscht alte Daten basierend auf der konfigurierten Retention-Zeit
*/
public void cleanupOldData() {
int days = plugin.getConfigManager().getDataRetentionDays();
if (days <= 0) return;
String[] tables = {"server_metrics", "world_metrics", "entity_metrics", "alert_history", "report_history"};
try (Connection conn = getConnection()) {
for (String table : tables) {
try (PreparedStatement ps = conn.prepareStatement(
"DELETE FROM " + table + " WHERE recorded_at < NOW() - INTERVAL ? DAY")) {
ps.setInt(1, days);
int deleted = ps.executeUpdate();
if (deleted > 0) {
plugin.debug("Bereinigt: " + deleted + " Zeilen aus " + table);
}
}
}
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler bei der Datenbereinigung: " + e.getMessage());
}
}
// ──────────────────────────────────────────
// MAPPER
// ──────────────────────────────────────────
private PerformanceSnapshot mapServerMetrics(ResultSet rs) throws SQLException {
return new PerformanceSnapshot(
rs.getTimestamp("recorded_at").toLocalDateTime(),
rs.getDouble("tps"),
rs.getDouble("mspt"),
rs.getLong("ram_used_mb"),
rs.getLong("ram_max_mb"),
rs.getDouble("ram_percent"),
rs.getInt("online_players"),
rs.getInt("loaded_worlds"),
rs.getInt("loaded_chunks")
);
}
private EntitySnapshot mapEntityMetrics(ResultSet rs) throws SQLException {
return new EntitySnapshot(
rs.getTimestamp("recorded_at").toLocalDateTime(),
rs.getString("world_name"),
rs.getInt("monsters"),
rs.getInt("animals"),
rs.getInt("water_mobs"),
rs.getInt("villagers"),
rs.getInt("armor_stands"),
rs.getInt("hopper_minecarts"),
rs.getInt("items"),
rs.getInt("players"),
rs.getInt("other"),
rs.getInt("total")
);
}
}

View File

@@ -0,0 +1,270 @@
package de.serverpulse.discord;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import de.serverpulse.ServerPulse;
import de.serverpulse.models.AlertSeverity;
import de.serverpulse.models.EntitySnapshot;
import de.serverpulse.models.PerformanceSnapshot;
import de.serverpulse.utils.MessageUtil;
import okhttp3.*;
import java.io.IOException;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
/**
* Verwaltet alle Discord-Webhook-Benachrichtigungen.
* Unterstützt Rich Embeds mit Farben, Feldern und Avataren.
*/
public class DiscordWebhook {
private final ServerPulse plugin;
private final OkHttpClient httpClient;
public DiscordWebhook(ServerPulse plugin) {
this.plugin = plugin;
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build();
}
// ──────────────────────────────────────────
// ALERT SENDEN
// ──────────────────────────────────────────
/**
* Sendet eine Warnung als Discord-Embed
*/
public void sendAlert(String alertType, AlertSeverity severity, String worldName,
String message, Double currentValue, Double threshold) {
if (!plugin.getConfigManager().isDiscordEnabled()) return;
int color = switch (severity) {
case CRITICAL -> plugin.getConfigManager().getDiscordColorCritical();
case WARNING -> plugin.getConfigManager().getDiscordColorWarning();
default -> plugin.getConfigManager().getDiscordColorInfo();
};
String emoji = switch (severity) {
case CRITICAL -> "🔴";
case WARNING -> "🟡";
default -> "🔵";
};
JsonObject embed = createEmbed(
emoji + " " + severity.getName() + " " + formatAlertType(alertType),
message,
color
);
// Felder hinzufügen
JsonArray fields = new JsonArray();
if (worldName != null) {
fields.add(createField("🌍 Welt", worldName, true));
}
if (currentValue != null) {
fields.add(createField("📊 Aktueller Wert", String.format("%.2f", currentValue), true));
}
if (threshold != null) {
fields.add(createField("⚠️ Grenzwert", String.format("%.2f", threshold), true));
}
fields.add(createField("🕐 Zeitpunkt", MessageUtil.getTimestamp(), false));
embed.add("fields", fields);
sendEmbed(plugin.getConfigManager().getDiscordWebhookUrl(), embed);
}
// ──────────────────────────────────────────
// TÄGLICHER REPORT
// ──────────────────────────────────────────
/**
* Plant den täglichen Status-Report
*/
public void scheduleDailyReport() {
String timeStr = plugin.getConfigManager().getDailyReportTime();
try {
LocalTime reportTime = LocalTime.parse(timeStr, DateTimeFormatter.ofPattern("HH:mm"));
LocalTime now = LocalTime.now();
long secondsUntilReport;
if (now.isBefore(reportTime)) {
secondsUntilReport = now.until(reportTime, java.time.temporal.ChronoUnit.SECONDS);
} else {
secondsUntilReport = now.until(reportTime.plusHours(24), java.time.temporal.ChronoUnit.SECONDS);
}
long ticksUntilFirst = secondsUntilReport * 20;
long ticksPerDay = 24 * 60 * 60 * 20;
org.bukkit.Bukkit.getScheduler().runTaskTimerAsynchronously(plugin,
this::sendDailyReport, ticksUntilFirst, ticksPerDay);
plugin.getLogger().info("Täglicher Discord-Report geplant für " + timeStr + " Uhr.");
} catch (Exception e) {
plugin.getLogger().warning("Ungültige Report-Zeit in Config: " + timeStr);
}
}
/**
* Erstellt und sendet den täglichen Status-Report
*/
public void sendDailyReport() {
if (!plugin.getConfigManager().isDiscordEnabled()) return;
JsonObject embed = createEmbed(
"📊 Täglicher Server-Statusbericht",
"Hier ist der automatische Tagesbericht für deinen Minecraft-Server.",
plugin.getConfigManager().getDiscordColorInfo()
);
JsonArray fields = new JsonArray();
// Performance-Daten
PerformanceSnapshot perf = plugin.getPerformanceMonitor().getLastSnapshot();
if (perf != null) {
fields.add(createField("⚡ TPS (aktuell)",
String.format("%.2f / 20.0", perf.getTps()), true));
fields.add(createField("⏱ MSPT",
String.format("%.2f ms", perf.getMspt()), true));
fields.add(createField("💾 RAM",
perf.getRamUsedMb() + " MB / " + perf.getRamMaxMb() + " MB", true));
fields.add(createField("👥 Online",
perf.getOnlinePlayers() + " Spieler", true));
fields.add(createField("🗺 Welten",
perf.getLoadedWorlds() + " geladen", true));
fields.add(createField("📦 Chunks",
perf.getLoadedChunks() + " geladen", true));
}
// Durchschnittliche TPS der letzten 24h
if (plugin.getDatabaseManager().isConnected()) {
double avgTps24h = plugin.getDatabaseManager().getAverageTps(60 * 24);
fields.add(createField("📈 Ø TPS (24h)",
String.format("%.2f", avgTps24h), true));
int criticalAlerts = plugin.getDatabaseManager().getAlertCount(24, "CRITICAL");
int warningAlerts = plugin.getDatabaseManager().getAlertCount(24, "WARNING");
fields.add(createField("🚨 Kritische Warnungen (24h)",
String.valueOf(criticalAlerts), true));
fields.add(createField("⚠️ Warnungen (24h)",
String.valueOf(warningAlerts), true));
}
// Entity-Daten pro Welt
for (var entry : plugin.getEntityMonitor().getAllLastSnapshots().entrySet()) {
EntitySnapshot snap = entry.getValue();
String displayName = plugin.getConfigManager().getWorldDisplayName(entry.getKey());
fields.add(createField("🌍 " + displayName,
"Gesamt: **" + snap.getTotal() + "** | Monster: " + snap.getMonsters()
+ " | Tiere: " + snap.getAnimals()
+ " | Villager: " + snap.getVillagers()
+ " | Items: " + snap.getItems(),
false));
}
fields.add(createField("🕐 Erstellt", MessageUtil.getTimestamp(), false));
embed.add("fields", fields);
// Footer
JsonObject footer = new JsonObject();
footer.addProperty("text", "ServerPulse Monitoring System");
embed.add("footer", footer);
sendEmbed(plugin.getConfigManager().getDiscordReportWebhookUrl(), embed);
}
/**
* Sendet eine einfache Info-Nachricht
*/
public void sendInfo(String title, String message) {
if (!plugin.getConfigManager().isDiscordEnabled()) return;
JsonObject embed = createEmbed(title, message, plugin.getConfigManager().getDiscordColorInfo());
sendEmbed(plugin.getConfigManager().getDiscordWebhookUrl(), embed);
}
// ──────────────────────────────────────────
// HTTP-VERSAND
// ──────────────────────────────────────────
/**
* Sendet ein Discord-Embed an einen Webhook
*/
private void sendEmbed(String webhookUrl, JsonObject embed) {
if (webhookUrl == null || webhookUrl.isEmpty() || webhookUrl.contains("YOUR_WEBHOOK")) {
plugin.debug("Discord-Webhook nicht konfiguriert, überspringe.");
return;
}
JsonObject payload = new JsonObject();
payload.addProperty("username", plugin.getConfigManager().getDiscordBotName());
String avatarUrl = plugin.getConfigManager().getDiscordBotAvatar();
if (!avatarUrl.isEmpty()) {
payload.addProperty("avatar_url", avatarUrl);
}
JsonArray embeds = new JsonArray();
embeds.add(embed);
payload.add("embeds", embeds);
org.bukkit.Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
RequestBody body = RequestBody.create(
payload.toString(),
MediaType.get("application/json")
);
Request request = new Request.Builder()
.url(webhookUrl)
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
plugin.getLogger().warning("Discord-Webhook Fehler: " + response.code());
} else {
plugin.debug("Discord-Nachricht erfolgreich gesendet.");
}
}
} catch (IOException e) {
plugin.getLogger().log(Level.WARNING, "Discord-Webhook Verbindungsfehler: " + e.getMessage());
}
});
}
// ──────────────────────────────────────────
// HILFSMETHODEN
// ──────────────────────────────────────────
private JsonObject createEmbed(String title, String description, int color) {
JsonObject embed = new JsonObject();
embed.addProperty("title", title);
embed.addProperty("description", description);
embed.addProperty("color", color);
return embed;
}
private JsonObject createField(String name, String value, boolean inline) {
JsonObject field = new JsonObject();
field.addProperty("name", name);
field.addProperty("value", value.isEmpty() ? "N/A" : value);
field.addProperty("inline", inline);
return field;
}
private String formatAlertType(String alertType) {
return alertType.replace("_", " ");
}
}

View File

@@ -0,0 +1,32 @@
package de.serverpulse.listeners;
import de.serverpulse.ServerPulse;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
/**
* Lauscht auf Spieler-Events für Monitoring-Zwecke.
*/
public class PlayerListener implements Listener {
private final ServerPulse plugin;
public PlayerListener(ServerPulse plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
plugin.debug("Spieler verbunden: " + event.getPlayer().getName()
+ " | Gesamt online: " + (plugin.getServer().getOnlinePlayers().size()));
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
plugin.debug("Spieler getrennt: " + event.getPlayer().getName()
+ " | Verbleibend: " + (plugin.getServer().getOnlinePlayers().size() - 1));
}
}

View File

@@ -0,0 +1,13 @@
package de.serverpulse.models;
public enum AlertSeverity {
INFO("INFO", "&b"),
WARNING("WARNING", "&e"),
CRITICAL("CRITICAL", "&c");
private final String name, color;
AlertSeverity(String name, String color) { this.name = name; this.color = color; }
public String getName() { return name; }
public String getColor() { return color; }
@Override public String toString() { return name; }
}

View File

@@ -0,0 +1,34 @@
package de.serverpulse.models;
import java.time.LocalDateTime;
public class EntitySnapshot {
private final LocalDateTime recordedAt;
private final String worldName;
private final int monsters, animals, waterMobs, villagers;
private final int armorStands, hopperMinecarts, items, players, other, total;
public EntitySnapshot(LocalDateTime recordedAt, String worldName,
int monsters, int animals, int waterMobs, int villagers,
int armorStands, int hopperMinecarts, int items,
int players, int other, int total) {
this.recordedAt = recordedAt; this.worldName = worldName;
this.monsters = monsters; this.animals = animals; this.waterMobs = waterMobs;
this.villagers = villagers; this.armorStands = armorStands;
this.hopperMinecarts = hopperMinecarts; this.items = items;
this.players = players; this.other = other; this.total = total;
}
public LocalDateTime getRecordedAt() { return recordedAt; }
public String getWorldName() { return worldName; }
public int getMonsters() { return monsters; }
public int getAnimals() { return animals; }
public int getWaterMobs() { return waterMobs; }
public int getVillagers() { return villagers; }
public int getArmorStands() { return armorStands; }
public int getHopperMinecarts() { return hopperMinecarts; }
public int getItems() { return items; }
public int getPlayers() { return players; }
public int getOther() { return other; }
public int getTotal() { return total; }
}

View File

@@ -0,0 +1,28 @@
package de.serverpulse.models;
import java.time.LocalDateTime;
public class PerformanceSnapshot {
private final LocalDateTime recordedAt;
private final double tps, mspt, ramPercent;
private final long ramUsedMb, ramMaxMb;
private final int onlinePlayers, loadedWorlds, loadedChunks;
public PerformanceSnapshot(LocalDateTime recordedAt, double tps, double mspt,
long ramUsedMb, long ramMaxMb, double ramPercent,
int onlinePlayers, int loadedWorlds, int loadedChunks) {
this.recordedAt = recordedAt; this.tps = tps; this.mspt = mspt;
this.ramUsedMb = ramUsedMb; this.ramMaxMb = ramMaxMb; this.ramPercent = ramPercent;
this.onlinePlayers = onlinePlayers; this.loadedWorlds = loadedWorlds; this.loadedChunks = loadedChunks;
}
public LocalDateTime getRecordedAt() { return recordedAt; }
public double getTps() { return tps; }
public double getMspt() { return mspt; }
public long getRamUsedMb() { return ramUsedMb; }
public long getRamMaxMb() { return ramMaxMb; }
public double getRamPercent() { return ramPercent; }
public int getOnlinePlayers() { return onlinePlayers; }
public int getLoadedWorlds() { return loadedWorlds; }
public int getLoadedChunks() { return loadedChunks; }
}

View File

@@ -0,0 +1,212 @@
package de.serverpulse.monitoring;
import de.serverpulse.ServerPulse;
import de.serverpulse.models.AlertSeverity;
import de.serverpulse.models.EntitySnapshot;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.*;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.entity.minecart.HopperMinecart;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Erfasst und kategorisiert alle Entitäten pro Welt.
* Kategorien: Monster, Tiere, Wasser-Mobs, Villager,
* ArmorStands, Hopper-Minecarts, Items, Spieler, Sonstige.
*/
public class EntityMonitor {
private final ServerPulse plugin;
private BukkitTask task;
// Cache: letzter Snapshot pro Welt
private final Map<String, EntitySnapshot> lastSnapshots = new ConcurrentHashMap<>();
public EntityMonitor(ServerPulse plugin) {
this.plugin = plugin;
}
public void start() {
int intervalSeconds = plugin.getConfigManager().getEntityInterval();
int intervalTicks = intervalSeconds * 20;
// Entity-Zählung muss auf dem Haupt-Thread laufen (Bukkit API)
task = Bukkit.getScheduler().runTaskTimer(plugin, this::collect, 200L, intervalTicks);
plugin.getLogger().info("Entity Monitor gestartet (Intervall: " + intervalSeconds + "s)");
}
public void stop() {
if (task != null && !task.isCancelled()) {
task.cancel();
plugin.getLogger().info("Entity Monitor gestoppt.");
}
}
public void restart() {
stop();
start();
}
// ──────────────────────────────────────────
// DATENERFASSUNG
// ──────────────────────────────────────────
private void collect() {
for (World world : Bukkit.getWorlds()) {
if (!plugin.getConfigManager().isWorldMonitored(world.getName())) continue;
EntitySnapshot snapshot = countEntities(world);
lastSnapshots.put(world.getName(), snapshot);
// Async in DB speichern
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
if (plugin.getDatabaseManager().isConnected()) {
plugin.getDatabaseManager().saveEntityMetrics(snapshot);
}
checkEntityThresholds(snapshot);
});
plugin.debug("Entities in " + world.getName() + ": " + snapshot.getTotal() + " gesamt");
}
}
/**
* Zählt und kategorisiert alle Entitäten einer Welt
*/
public EntitySnapshot countEntities(World world) {
int monsters = 0, animals = 0, waterMobs = 0, villagers = 0;
int armorStands = 0, hopperMinecarts = 0, items = 0, players = 0, other = 0;
for (Entity entity : world.getEntities()) {
if (entity instanceof Monster) {
monsters++;
} else if (entity instanceof WaterMob) {
waterMobs++;
} else if (entity instanceof Villager || entity instanceof WanderingTrader) {
villagers++;
} else if (entity instanceof Animals || entity instanceof Golem) {
animals++;
} else if (entity instanceof ArmorStand) {
armorStands++;
} else if (entity instanceof HopperMinecart) {
hopperMinecarts++;
} else if (entity instanceof Item) {
items++;
} else if (entity instanceof Player) {
players++;
} else {
other++;
}
}
int total = monsters + animals + waterMobs + villagers
+ armorStands + hopperMinecarts + items + players + other;
return new EntitySnapshot(
LocalDateTime.now(), world.getName(),
monsters, animals, waterMobs, villagers,
armorStands, hopperMinecarts, items, players, other, total
);
}
// ──────────────────────────────────────────
// GRENZWERT-PRÜFUNG
// ──────────────────────────────────────────
private void checkEntityThresholds(EntitySnapshot snapshot) {
String world = snapshot.getWorldName();
// Gesamt-Entities
int totalWarning = plugin.getConfigManager().getWorldTotalWarning(world);
int totalCritical = plugin.getConfigManager().getWorldTotalCritical(world);
if (snapshot.getTotal() >= totalCritical) {
plugin.getAlertManager().triggerAlert(
"ENTITY_TOTAL_CRITICAL", AlertSeverity.CRITICAL, world,
"Kritisch hohe Entity-Anzahl in " + world + ": " + snapshot.getTotal(),
(double) snapshot.getTotal(), (double) totalCritical
);
} else if (snapshot.getTotal() >= totalWarning) {
plugin.getAlertManager().triggerAlert(
"ENTITY_TOTAL_WARNING", AlertSeverity.WARNING, world,
"Hohe Entity-Anzahl in " + world + ": " + snapshot.getTotal(),
(double) snapshot.getTotal(), (double) totalWarning
);
}
// Monster
int monstersWarning = plugin.getConfigManager().getWorldMonstersWarning(world);
int monstersCritical = plugin.getConfigManager().getWorldMonstersCritical(world);
if (snapshot.getMonsters() >= monstersCritical) {
plugin.getAlertManager().triggerAlert(
"MONSTERS_CRITICAL", AlertSeverity.CRITICAL, world,
"Kritisch viele Monster in " + world + ": " + snapshot.getMonsters(),
(double) snapshot.getMonsters(), (double) monstersCritical
);
} else if (snapshot.getMonsters() >= monstersWarning) {
plugin.getAlertManager().triggerAlert(
"MONSTERS_WARNING", AlertSeverity.WARNING, world,
"Viele Monster in " + world + ": " + snapshot.getMonsters(),
(double) snapshot.getMonsters(), (double) monstersWarning
);
}
// Items
int itemsWarning = plugin.getConfigManager().getItemsWarning();
int itemsCritical = plugin.getConfigManager().getItemsCritical();
if (snapshot.getItems() >= itemsCritical) {
plugin.getAlertManager().triggerAlert(
"ITEMS_CRITICAL", AlertSeverity.CRITICAL, world,
"Kritisch viele Items in " + world + ": " + snapshot.getItems(),
(double) snapshot.getItems(), (double) itemsCritical
);
// Notfall-Item-Clear auslösen
if (plugin.getConfigManager().isItemClearEnabled()) {
plugin.getAlertManager().triggerItemClear(world);
}
} else if (snapshot.getItems() >= itemsWarning) {
plugin.getAlertManager().triggerAlert(
"ITEMS_WARNING", AlertSeverity.WARNING, world,
"Viele Items in " + world + ": " + snapshot.getItems(),
(double) snapshot.getItems(), (double) itemsWarning
);
}
// Villager
int villagersWarning = plugin.getConfigManager().getVillagersWarning();
int villagersCritical = plugin.getConfigManager().getVillagersCritical();
if (snapshot.getVillagers() >= villagersCritical) {
plugin.getAlertManager().triggerAlert(
"VILLAGERS_CRITICAL", AlertSeverity.CRITICAL, world,
"Kritisch viele Villager in " + world + ": " + snapshot.getVillagers(),
(double) snapshot.getVillagers(), (double) villagersCritical
);
} else if (snapshot.getVillagers() >= villagersWarning) {
plugin.getAlertManager().triggerAlert(
"VILLAGERS_WARNING", AlertSeverity.WARNING, world,
"Viele Villager in " + world + ": " + snapshot.getVillagers(),
(double) snapshot.getVillagers(), (double) villagersWarning
);
}
}
// ──────────────────────────────────────────
// GETTER
// ──────────────────────────────────────────
public Map<String, EntitySnapshot> getAllLastSnapshots() {
return new HashMap<>(lastSnapshots);
}
public EntitySnapshot getLastSnapshot(String worldName) {
return lastSnapshots.get(worldName);
}
}

View File

@@ -0,0 +1,213 @@
package de.serverpulse.monitoring;
import de.serverpulse.ServerPulse;
import de.serverpulse.models.AlertSeverity;
import de.serverpulse.models.PerformanceSnapshot;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.scheduler.BukkitTask;
import java.time.LocalDateTime;
/**
* Erfasst Server-Performance-Daten: TPS, MSPT, RAM, Spieler, Chunks.
* Läuft als wiederkehrender Bukkit-Task.
*/
public class PerformanceMonitor {
private final ServerPulse plugin;
private BukkitTask task;
// Letzter bekannter Snapshot (für Status-Befehle)
private PerformanceSnapshot lastSnapshot;
public PerformanceMonitor(ServerPulse plugin) {
this.plugin = plugin;
}
public void start() {
int intervalSeconds = plugin.getConfigManager().getMetricsInterval();
int intervalTicks = intervalSeconds * 20;
task = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::collect, 100L, intervalTicks);
plugin.getLogger().info("Performance Monitor gestartet (Intervall: " + intervalSeconds + "s)");
}
public void stop() {
if (task != null && !task.isCancelled()) {
task.cancel();
plugin.getLogger().info("Performance Monitor gestoppt.");
}
}
public void restart() {
stop();
start();
}
// ──────────────────────────────────────────
// DATENERFASSUNG
// ──────────────────────────────────────────
private void collect() {
try {
double tps = getTps();
double mspt = getMspt();
long ramUsed = getRamUsedMb();
long ramMax = getRamMaxMb();
double ramPercent = (double) ramUsed / ramMax * 100;
int onlinePlayers = Bukkit.getOnlinePlayers().size();
int loadedWorlds = Bukkit.getWorlds().size();
int loadedChunks = getTotalLoadedChunks();
PerformanceSnapshot snapshot = new PerformanceSnapshot(
LocalDateTime.now(), tps, mspt,
ramUsed, ramMax, ramPercent,
onlinePlayers, loadedWorlds, loadedChunks
);
this.lastSnapshot = snapshot;
// In DB speichern
if (plugin.getDatabaseManager().isConnected()) {
plugin.getDatabaseManager().saveServerMetrics(snapshot);
}
// Grenzwerte prüfen
checkThresholds(snapshot);
plugin.debug("Performance erfasst: TPS=" + String.format("%.2f", tps)
+ " MSPT=" + String.format("%.2f", mspt)
+ " RAM=" + ramUsed + "MB/" + ramMax + "MB");
} catch (Exception e) {
plugin.getLogger().warning("Fehler beim Erfassen der Performance: " + e.getMessage());
}
}
// ──────────────────────────────────────────
// GRENZWERT-PRÜFUNG
// ──────────────────────────────────────────
private void checkThresholds(PerformanceSnapshot snapshot) {
double tps = snapshot.getTps();
double mspt = snapshot.getMspt();
double ramPercent = snapshot.getRamPercent();
// TPS prüfen
if (tps < plugin.getConfigManager().getTpsCritical()) {
plugin.getAlertManager().triggerAlert(
"TPS_CRITICAL", AlertSeverity.CRITICAL, null,
"Kritisch niedrige TPS: " + String.format("%.2f", tps),
tps, plugin.getConfigManager().getTpsCritical()
);
} else if (tps < plugin.getConfigManager().getTpsWarning()) {
plugin.getAlertManager().triggerAlert(
"TPS_WARNING", AlertSeverity.WARNING, null,
"Niedrige TPS: " + String.format("%.2f", tps),
tps, plugin.getConfigManager().getTpsWarning()
);
}
// MSPT prüfen
if (mspt > plugin.getConfigManager().getMsptCritical()) {
plugin.getAlertManager().triggerAlert(
"MSPT_CRITICAL", AlertSeverity.CRITICAL, null,
"Kritisch hohe MSPT: " + String.format("%.2f", mspt) + "ms",
mspt, plugin.getConfigManager().getMsptCritical()
);
} else if (mspt > plugin.getConfigManager().getMsptWarning()) {
plugin.getAlertManager().triggerAlert(
"MSPT_WARNING", AlertSeverity.WARNING, null,
"Hohe MSPT: " + String.format("%.2f", mspt) + "ms",
mspt, plugin.getConfigManager().getMsptWarning()
);
}
// RAM prüfen
if (ramPercent > plugin.getConfigManager().getRamCritical()) {
plugin.getAlertManager().triggerAlert(
"RAM_CRITICAL", AlertSeverity.CRITICAL, null,
"Kritische RAM-Auslastung: " + String.format("%.1f", ramPercent) + "%",
ramPercent, (double) plugin.getConfigManager().getRamCritical()
);
} else if (ramPercent > plugin.getConfigManager().getRamWarning()) {
plugin.getAlertManager().triggerAlert(
"RAM_WARNING", AlertSeverity.WARNING, null,
"Hohe RAM-Auslastung: " + String.format("%.1f", ramPercent) + "%",
ramPercent, (double) plugin.getConfigManager().getRamWarning()
);
}
}
// ──────────────────────────────────────────
// TPS & MSPT ABRUFEN
// ──────────────────────────────────────────
/**
* Liest den aktuellen TPS-Wert vom Server.
* Paper API bietet getTPS(), Spigot hat recentTps[].
*/
public double getTps() {
try {
// Paper API: getServer().getTPS()
double[] tps = Bukkit.getServer().getTPS();
return Math.min(20.0, tps[0]); // 1-Minuten-Durchschnitt
} catch (Exception e) {
// Fallback für Spigot
return 20.0;
}
}
/**
* Liest den aktuellen MSPT-Wert.
* Paper: getAverageTickTime()
*/
public double getMspt() {
try {
return Bukkit.getServer().getAverageTickTime();
} catch (Exception e) {
// Berechnung aus TPS als Fallback
double tps = getTps();
return tps > 0 ? 1000.0 / tps : 50.0;
}
}
// ──────────────────────────────────────────
// RAM
// ──────────────────────────────────────────
public long getRamUsedMb() {
Runtime runtime = Runtime.getRuntime();
return (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
}
public long getRamMaxMb() {
return Runtime.getRuntime().maxMemory() / (1024 * 1024);
}
public long getRamFreeMb() {
Runtime runtime = Runtime.getRuntime();
return (runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory()) / (1024 * 1024);
}
// ──────────────────────────────────────────
// CHUNKS
// ──────────────────────────────────────────
private int getTotalLoadedChunks() {
int total = 0;
for (World world : Bukkit.getWorlds()) {
total += world.getLoadedChunks().length;
}
return total;
}
// ──────────────────────────────────────────
// GETTER
// ──────────────────────────────────────────
public PerformanceSnapshot getLastSnapshot() {
return lastSnapshot;
}
}

View File

@@ -0,0 +1,200 @@
package de.serverpulse.monitoring;
import de.serverpulse.ServerPulse;
import de.serverpulse.models.AlertSeverity;
import de.serverpulse.models.EntitySnapshot;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import java.util.List;
/**
* Analysiert Trends in den gesammelten Daten.
* Erkennt kontinuierlich steigende Werte wie Monster-Anzahl,
* MSPT-Werte oder RAM-Auslastung.
*/
public class TrendAnalyzer {
private final ServerPulse plugin;
private BukkitTask task;
public TrendAnalyzer(ServerPulse plugin) {
this.plugin = plugin;
}
public void start() {
if (!plugin.getConfigManager().isTrendAnalysisEnabled()) {
plugin.getLogger().info("Trend-Analyse ist deaktiviert.");
return;
}
int intervalMinutes = plugin.getConfigManager().getTrendCheckInterval();
long intervalTicks = intervalMinutes * 60L * 20L;
task = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::analyze, 600L, intervalTicks);
plugin.getLogger().info("Trend Analyzer gestartet (Intervall: " + intervalMinutes + " Minuten)");
}
public void stop() {
if (task != null && !task.isCancelled()) {
task.cancel();
plugin.getLogger().info("Trend Analyzer gestoppt.");
}
}
public void restart() {
stop();
start();
}
// ──────────────────────────────────────────
// ANALYSE
// ──────────────────────────────────────────
private void analyze() {
if (!plugin.getDatabaseManager().isConnected()) return;
// TPS-Trend analysieren
analyzeTpsTrend();
// Entity-Trends analysieren (pro Welt)
for (org.bukkit.World world : Bukkit.getWorlds()) {
if (!plugin.getConfigManager().isWorldMonitored(world.getName())) continue;
analyzeEntityTrend(world.getName());
}
plugin.debug("Trend-Analyse abgeschlossen.");
}
/**
* Analysiert den TPS-Trend der letzten Datenpunkte
*/
private void analyzeTpsTrend() {
int dataPoints = plugin.getConfigManager().getTrendDataPoints();
List<de.serverpulse.models.PerformanceSnapshot> snapshots =
plugin.getDatabaseManager().getRecentServerMetrics(dataPoints);
if (snapshots.size() < 3) return;
// Prüfen ob TPS kontinuierlich fällt
boolean continuousDecline = true;
for (int i = 0; i < snapshots.size() - 1; i++) {
if (snapshots.get(i).getTps() >= snapshots.get(i + 1).getTps()) {
continuousDecline = false;
break;
}
}
if (continuousDecline) {
double startTps = snapshots.get(snapshots.size() - 1).getTps();
double endTps = snapshots.get(0).getTps();
double decline = ((startTps - endTps) / startTps) * 100;
if (decline >= plugin.getConfigManager().getTrendIncreaseThreshold()) {
plugin.getAlertManager().triggerAlert(
"TPS_TREND_DECLINE", AlertSeverity.WARNING, null,
String.format("TPS-Abnahme-Trend erkannt: %.1f%% Rückgang über %d Messungen",
decline, dataPoints),
endTps, startTps
);
}
}
// Prüfen ob MSPT kontinuierlich steigt
boolean continuousMsptIncrease = true;
for (int i = 1; i < snapshots.size(); i++) {
if (snapshots.get(i).getMspt() <= snapshots.get(i - 1).getMspt()) {
continuousMsptIncrease = false;
break;
}
}
if (continuousMsptIncrease && snapshots.size() >= 3) {
double startMspt = snapshots.get(snapshots.size() - 1).getMspt();
double endMspt = snapshots.get(0).getMspt();
double increase = ((endMspt - startMspt) / startMspt) * 100;
if (increase >= plugin.getConfigManager().getTrendIncreaseThreshold()) {
plugin.getAlertManager().triggerAlert(
"MSPT_TREND_INCREASE", AlertSeverity.WARNING, null,
String.format("MSPT-Anstiegs-Trend erkannt: %.1f%% Anstieg über %d Messungen",
increase, dataPoints),
endMspt, startMspt
);
}
}
}
/**
* Analysiert Entity-Trends für eine spezifische Welt
*/
private void analyzeEntityTrend(String worldName) {
int dataPoints = plugin.getConfigManager().getTrendDataPoints();
List<EntitySnapshot> snapshots =
plugin.getDatabaseManager().getEntityHistory(worldName, dataPoints);
if (snapshots.size() < 3) return;
// Monster-Trend analysieren
boolean monsterIncrease = checkIncreasingTrend(
snapshots.stream().mapToDouble(EntitySnapshot::getMonsters).toArray()
);
if (monsterIncrease) {
double startMonsters = snapshots.get(snapshots.size() - 1).getMonsters();
double endMonsters = snapshots.get(0).getMonsters();
if (startMonsters > 0) {
double increase = ((endMonsters - startMonsters) / startMonsters) * 100;
if (increase >= plugin.getConfigManager().getTrendIncreaseThreshold()) {
plugin.getAlertManager().triggerAlert(
"MONSTER_TREND_INCREASE", AlertSeverity.WARNING, worldName,
String.format("Monster-Anstiegs-Trend in %s: %.1f%% Anstieg (%d → %d)",
worldName, increase, (int) startMonsters, (int) endMonsters),
endMonsters, startMonsters
);
}
}
}
// Item-Trend analysieren
boolean itemIncrease = checkIncreasingTrend(
snapshots.stream().mapToDouble(EntitySnapshot::getItems).toArray()
);
if (itemIncrease) {
double startItems = snapshots.get(snapshots.size() - 1).getItems();
double endItems = snapshots.get(0).getItems();
if (startItems > 0) {
double increase = ((endItems - startItems) / startItems) * 100;
if (increase >= plugin.getConfigManager().getTrendIncreaseThreshold() * 1.5) {
plugin.getAlertManager().triggerAlert(
"ITEMS_TREND_INCREASE", AlertSeverity.WARNING, worldName,
String.format("Item-Anstiegs-Trend in %s: %.1f%% Anstieg (%d → %d)",
worldName, increase, (int) startItems, (int) endItems),
endItems, startItems
);
}
}
}
}
/**
* Prüft ob ein Array von Werten kontinuierlich steigt
* (erlaubt kleine Rückgänge)
*/
private boolean checkIncreasingTrend(double[] values) {
if (values.length < 3) return false;
int increasingCount = 0;
for (int i = 0; i < values.length - 1; i++) {
if (values[i] > values[i + 1]) increasingCount++;
}
// Mindestens 70% der Messungen müssen steigen
return (double) increasingCount / (values.length - 1) >= 0.7;
}
}

View File

@@ -0,0 +1,21 @@
package de.serverpulse.network;
/**
* Konstanten für das Plugin-Messaging-Protokoll zwischen Spigot und BungeeCord.
* Kanal: "serverpulse:data"
*
* Alle Nachrichten werden als UTF-8 JSON-String übertragen.
*/
public final class NetworkMessage {
public static final String CHANNEL = "serverpulse:data";
// Nachrichtentypen (Feld "type" im JSON)
public static final String TYPE_PERFORMANCE = "PERFORMANCE";
public static final String TYPE_ENTITY = "ENTITY";
public static final String TYPE_ALERT = "ALERT";
public static final String TYPE_CHAT_EVENT = "CHAT_EVENT";
public static final String TYPE_HEARTBEAT = "HEARTBEAT";
private NetworkMessage() {}
}

View File

@@ -0,0 +1,103 @@
package de.serverpulse.spigot;
import de.serverpulse.spigot.alerts.AlertManager;
import de.serverpulse.spigot.commands.SpigotCommand;
import de.serverpulse.spigot.database.DatabaseManager;
import de.serverpulse.spigot.discord.SpigotDiscordWebhook;
import de.serverpulse.spigot.listeners.SpigotPlayerListener;
import de.serverpulse.spigot.monitoring.EntityMonitor;
import de.serverpulse.spigot.monitoring.PerformanceMonitor;
import de.serverpulse.spigot.monitoring.TrendAnalyzer;
import de.serverpulse.spigot.network.SpigotMessenger;
import de.serverpulse.spigot.utils.SpigotConfig;
import org.bukkit.plugin.java.JavaPlugin;
public final class SpigotPlugin extends JavaPlugin {
private static SpigotPlugin instance;
private SpigotConfig config;
private DatabaseManager databaseManager;
private PerformanceMonitor performanceMonitor;
private EntityMonitor entityMonitor;
private TrendAnalyzer trendAnalyzer;
private AlertManager alertManager;
private SpigotDiscordWebhook discordWebhook;
private SpigotMessenger messenger;
@Override
public void onEnable() {
instance = this;
getLogger().info("╔══════════════════════════════════════╗");
getLogger().info("║ ServerPulse v1.2.0 [SPIGOT/PAPER] ║");
getLogger().info("╚══════════════════════════════════════╝");
this.config = new SpigotConfig(this);
config.load();
this.databaseManager = new DatabaseManager(this);
if (config.isDatabaseEnabled()) {
if (!databaseManager.connect()) {
getLogger().severe("Datenbankverbindung fehlgeschlagen! Plugin wird deaktiviert.");
getServer().getPluginManager().disablePlugin(this);
return;
}
databaseManager.createTables();
}
this.discordWebhook = new SpigotDiscordWebhook(this);
this.alertManager = new AlertManager(this);
this.performanceMonitor = new PerformanceMonitor(this);
performanceMonitor.start();
this.entityMonitor = new EntityMonitor(this);
entityMonitor.start();
this.trendAnalyzer = new TrendAnalyzer(this);
trendAnalyzer.start();
this.messenger = new SpigotMessenger(this);
messenger.start();
SpigotCommand cmd = new SpigotCommand(this);
getCommand("serverpulse").setExecutor(cmd);
getCommand("serverpulse").setTabCompleter(cmd);
getServer().getPluginManager().registerEvents(new SpigotPlayerListener(this), this);
if (config.isDailyReportEnabled()) discordWebhook.scheduleDailyReport();
getLogger().info("ServerPulse v" + getDescription().getVersion() + " (Spigot/Paper) gestartet!");
}
@Override
public void onDisable() {
if (messenger != null) messenger.stop();
if (performanceMonitor != null) performanceMonitor.stop();
if (entityMonitor != null) entityMonitor.stop();
if (trendAnalyzer != null) trendAnalyzer.stop();
if (databaseManager != null) databaseManager.disconnect();
}
public void reload() {
config.load();
if (performanceMonitor != null) performanceMonitor.restart();
if (entityMonitor != null) entityMonitor.restart();
if (trendAnalyzer != null) trendAnalyzer.restart();
alertManager.clearCooldowns();
}
public void debug(String msg) {
if (config != null && config.isDebugMode()) getLogger().info("[DEBUG] " + msg);
}
public static SpigotPlugin getInstance() { return instance; }
public SpigotConfig getSpigotConfig() { return config; }
public DatabaseManager getDatabaseManager() { return databaseManager; }
public PerformanceMonitor getPerformanceMonitor() { return performanceMonitor; }
public EntityMonitor getEntityMonitor() { return entityMonitor; }
public TrendAnalyzer getTrendAnalyzer() { return trendAnalyzer; }
public AlertManager getAlertManager() { return alertManager; }
public SpigotDiscordWebhook getDiscordWebhook() { return discordWebhook; }
public SpigotMessenger getMessenger() { return messenger; }
}

View File

@@ -0,0 +1,219 @@
package de.serverpulse.spigot.alerts;
import de.serverpulse.spigot.SpigotPlugin;
import de.serverpulse.models.AlertSeverity;
import de.serverpulse.spigot.utils.MsgUtil;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.Monster;
import java.util.HashMap;
import java.util.Map;
/**
* Verwaltet alle Warnungen, Cooldowns und Notfallmaßnahmen.
* Verhindert Alert-Spam durch einen Cooldown-Mechanismus.
*/
public class AlertManager {
private final SpigotPlugin plugin;
// Cooldown: alertType -> letzter Trigger-Zeitpunkt (ms)
private final Map<String, Long> alertCooldowns = new HashMap<>();
// Standard-Cooldown: 5 Minuten
private static final long DEFAULT_COOLDOWN_MS = 5 * 60 * 1000;
// Zähler für kritische Ereignisse (für Auto-Diagnose)
private int criticalEventCount = 0;
// Item-Clear läuft gerade?
private boolean itemClearRunning = false;
public AlertManager(SpigotPlugin plugin) {
this.plugin = plugin;
}
// ──────────────────────────────────────────
// ALERT AUSLÖSEN
// ──────────────────────────────────────────
/**
* Löst eine Warnung aus, wenn kein Cooldown aktiv ist.
*/
public void triggerAlert(String alertType, AlertSeverity severity, String worldName,
String message, Double currentValue, Double threshold) {
// Cooldown prüfen
String cooldownKey = alertType + (worldName != null ? "_" + worldName : "");
long now = System.currentTimeMillis();
if (alertCooldowns.containsKey(cooldownKey)) {
long lastAlert = alertCooldowns.get(cooldownKey);
if (now - lastAlert < DEFAULT_COOLDOWN_MS) {
plugin.debug("Alert-Cooldown aktiv für: " + cooldownKey);
return;
}
}
alertCooldowns.put(cooldownKey, now);
// Alert loggen
plugin.getLogger().warning("[" + severity.getName() + "] " + message);
// In DB speichern
if (plugin.getDatabaseManager().isConnected()) {
plugin.getDatabaseManager().saveAlert(
alertType, severity.getName(), worldName, message, currentValue, threshold
);
}
// Discord-Benachrichtigung senden
if (plugin.getSpigotConfig().isDiscordEnabled()) {
plugin.getDiscordWebhook().sendAlert(alertType, severity, worldName, message, currentValue, threshold);
}
// Alert an BungeeCord weitergeben
if (plugin.getMessenger() != null) {
plugin.getMessenger().sendAlert(alertType, severity.getName(), message);
}
// Kritische Events zählen für Auto-Diagnose
if (severity == AlertSeverity.CRITICAL) {
criticalEventCount++;
if (plugin.getSpigotConfig().isAutoDiagnosisEnabled()) {
if (criticalEventCount >= plugin.getSpigotConfig().getAutoDiagnosisTrigger()) {
triggerDiagnosisReport();
criticalEventCount = 0;
}
}
}
}
// ──────────────────────────────────────────
// NOTFALLMASSNAHMEN
// ──────────────────────────────────────────
/**
* Löst einen Item-Clear mit Countdown aus
*/
public void triggerItemClear(String worldName) {
if (itemClearRunning) return;
itemClearRunning = true;
int countdown = plugin.getSpigotConfig().getItemClearCountdown();
String broadcastMsg = plugin.getSpigotConfig().getItemClearMessage();
// Countdown-Broadcasts
if (plugin.getSpigotConfig().isItemClearBroadcast()) {
for (int i = countdown; i > 0; i -= (i > 10 ? 10 : (i > 5 ? 5 : 1))) {
final int secondsLeft = i;
Bukkit.getScheduler().runTaskLater(plugin, () -> {
String msg = broadcastMsg.replace("{seconds}", String.valueOf(secondsLeft));
Bukkit.broadcastMessage(MsgUtil.colorize(msg));
}, (countdown - i) * 20L);
}
}
// Clear ausführen nach Countdown
Bukkit.getScheduler().runTaskLater(plugin, () -> {
World world = Bukkit.getWorld(worldName);
if (world != null) {
int cleared = 0;
for (Entity entity : world.getEntities()) {
if (entity instanceof Item) {
entity.remove();
cleared++;
}
}
plugin.getLogger().info("Item-Clear ausgeführt in " + worldName + ": " + cleared + " Items entfernt.");
if (plugin.getSpigotConfig().isItemClearBroadcast()) {
Bukkit.broadcastMessage(MsgUtil.colorize(
"&a[ServerPulse] Item-Clear abgeschlossen! " + cleared + " Items wurden entfernt."));
}
triggerAlert("ITEM_CLEAR_EXECUTED", AlertSeverity.INFO, worldName,
"Automatischer Item-Clear: " + cleared + " Items in " + worldName + " entfernt.",
(double) cleared, null);
}
itemClearRunning = false;
}, countdown * 20L);
}
/**
* Führt einen Mob-Clear in einer Welt durch
*/
public void triggerMobClear(String worldName) {
boolean hostileOnly = plugin.getSpigotConfig().isMobClearHostileOnly();
World world = Bukkit.getWorld(worldName);
if (world == null) return;
Bukkit.getScheduler().runTask(plugin, () -> {
int cleared = 0;
for (Entity entity : world.getEntities()) {
if (entity instanceof Monster) {
entity.remove();
cleared++;
}
}
plugin.getLogger().info("Mob-Clear ausgeführt in " + worldName + ": " + cleared + " Mobs entfernt.");
triggerAlert("MOB_CLEAR_EXECUTED", AlertSeverity.INFO, worldName,
"Automatischer Mob-Clear: " + cleared + " Mobs in " + worldName + " entfernt.",
(double) cleared, null);
});
}
/**
* Erstellt automatisch einen Diagnose-Report
*/
private void triggerDiagnosisReport() {
plugin.getLogger().warning("[ServerPulse] Erstelle automatischen Diagnose-Report...");
StringBuilder report = new StringBuilder();
report.append("=== AUTOMATISCHER DIAGNOSE-REPORT ===\n");
report.append("Zeitpunkt: ").append(MsgUtil.timestamp()).append("\n\n");
// Performance
if (plugin.getPerformanceMonitor().getLastSnapshot() != null) {
var snap = plugin.getPerformanceMonitor().getLastSnapshot();
report.append("Performance:\n");
report.append(" TPS: ").append(String.format("%.2f", snap.getTps())).append("\n");
report.append(" MSPT: ").append(String.format("%.2f", snap.getMspt())).append("ms\n");
report.append(" RAM: ").append(snap.getRamUsedMb()).append("MB / ").append(snap.getRamMaxMb()).append("MB\n");
report.append(" Spieler: ").append(snap.getOnlinePlayers()).append("\n");
report.append(" Chunks: ").append(snap.getLoadedChunks()).append("\n\n");
}
// Entities pro Welt
report.append("Entities pro Welt:\n");
for (var entry : plugin.getEntityMonitor().getAllLastSnapshots().entrySet()) {
var snap = entry.getValue();
report.append(" ").append(entry.getKey()).append(": ")
.append("Gesamt=").append(snap.getTotal())
.append(", Monster=").append(snap.getMonsters())
.append(", Items=").append(snap.getItems())
.append("\n");
}
String reportContent = report.toString();
// In DB speichern
if (plugin.getDatabaseManager().isConnected()) {
plugin.getDatabaseManager().saveReport("AUTO_DIAGNOSIS", "ServerPulse", reportContent);
}
plugin.getLogger().warning(reportContent);
}
/**
* Setzt alle Alert-Cooldowns zurück (z.B. nach reload)
*/
public void clearCooldowns() {
alertCooldowns.clear();
criticalEventCount = 0;
}
public int getCriticalEventCount() {
return criticalEventCount;
}
}

View File

@@ -0,0 +1,429 @@
package de.serverpulse.spigot.commands;
import de.serverpulse.spigot.SpigotPlugin;
import de.serverpulse.models.EntitySnapshot;
import de.serverpulse.models.PerformanceSnapshot;
import de.serverpulse.spigot.utils.MsgUtil;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Verarbeitet alle /serverpulse (Alias: /sp, /pulse) Unterbefehle.
*/
public class SpigotCommand implements CommandExecutor, TabCompleter {
private final SpigotPlugin plugin;
public SpigotCommand(SpigotPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
String prefix = plugin.getSpigotConfig().getPrefix();
if (args.length == 0) {
sendHelp(sender, prefix, label);
return true;
}
switch (args[0].toLowerCase()) {
case "status" -> handleStatus(sender, prefix);
case "report" -> handleReport(sender, prefix);
case "world" -> handleWorld(sender, prefix, args);
case "entities" -> handleEntities(sender, prefix, args);
case "reload" -> handleReload(sender, prefix);
case "debug" -> handleDebug(sender, prefix);
case "clear" -> handleClear(sender, prefix, args);
default -> sendHelp(sender, prefix, label);
}
return true;
}
// ──────────────────────────────────────────
// /sp status
// ──────────────────────────────────────────
private void handleStatus(CommandSender sender, String prefix) {
if (!sender.hasPermission("serverpulse.status")) {
sender.sendMessage(MsgUtil.colorize(prefix + plugin.getSpigotConfig().getMessage("no-permission")));
return;
}
sender.sendMessage(MsgUtil.colorize(MsgUtil.header("ServerPulse Status")));
PerformanceSnapshot snap = plugin.getPerformanceMonitor().getLastSnapshot();
if (snap == null) {
sender.sendMessage(MsgUtil.colorize(prefix + "&eNoch keine Daten verfügbar. Bitte warte einen Moment..."));
return;
}
double tps = snap.getTps();
double mspt = snap.getMspt();
long ramUsed = snap.getRamUsedMb();
long ramMax = snap.getRamMaxMb();
sender.sendMessage(MsgUtil.colorize(
prefix + "&7TPS: " + MsgUtil.formatTps(tps) + " &8| &7MSPT: " + MsgUtil.formatMspt(mspt)
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7RAM: " + MsgUtil.formatRam(ramUsed, ramMax)
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7RAM-Auslastung: " + MsgUtil.bar(ramUsed, ramMax, 20)
+ " &7" + ramUsed + "/" + ramMax + "MB"
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Spieler: &f" + snap.getOnlinePlayers()
+ " &8| &7Welten: &f" + snap.getLoadedWorlds()
+ " &8| &7Chunks: &f" + snap.getLoadedChunks()
));
if (plugin.getDatabaseManager().isConnected()) {
double avgTps15m = plugin.getDatabaseManager().getAverageTps(15);
double avgTps60m = plugin.getDatabaseManager().getAverageTps(60);
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Ø TPS: &715min=&f" + String.format("%.2f", avgTps15m)
+ " &860min=&f" + String.format("%.2f", avgTps60m)
));
}
sender.sendMessage(MsgUtil.colorize(MsgUtil.separator()));
}
// ──────────────────────────────────────────
// /sp report
// ──────────────────────────────────────────
private void handleReport(CommandSender sender, String prefix) {
if (!sender.hasPermission("serverpulse.report")) {
sender.sendMessage(MsgUtil.colorize(prefix + plugin.getSpigotConfig().getMessage("no-permission")));
return;
}
sender.sendMessage(MsgUtil.colorize(prefix + "&aErstelle Report..."));
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
sender.sendMessage(MsgUtil.colorize(MsgUtil.header("ServerPulse Report")));
// Performance
PerformanceSnapshot perf = plugin.getPerformanceMonitor().getLastSnapshot();
if (perf != null) {
sender.sendMessage(MsgUtil.colorize(prefix + "&b=== Performance ==="));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7TPS: " + MsgUtil.formatTps(perf.getTps())
+ " MSPT: " + MsgUtil.formatMspt(perf.getMspt())
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7RAM: " + MsgUtil.formatRam(perf.getRamUsedMb(), perf.getRamMaxMb())
));
}
// Datenbank-Statistiken
if (plugin.getDatabaseManager().isConnected()) {
sender.sendMessage(MsgUtil.colorize(prefix + "&b=== Warnungen (24h) ==="));
int critAlerts = plugin.getDatabaseManager().getAlertCount(24, "CRITICAL");
int warnAlerts = plugin.getDatabaseManager().getAlertCount(24, "WARNING");
sender.sendMessage(MsgUtil.colorize(
prefix + "&c" + critAlerts + " kritisch &e" + warnAlerts + " Warnungen"
));
}
// Entities
sender.sendMessage(MsgUtil.colorize(prefix + "&b=== Entities ==="));
for (Map.Entry<String, EntitySnapshot> entry : plugin.getEntityMonitor().getAllLastSnapshots().entrySet()) {
EntitySnapshot snap = entry.getValue();
sender.sendMessage(MsgUtil.colorize(
prefix + "&7" + entry.getKey() + ": &fGesamt=" + snap.getTotal()
+ " Monster=" + MsgUtil.formatCount(
snap.getMonsters(),
plugin.getSpigotConfig().getWorldMonstersWarning(entry.getKey()),
plugin.getSpigotConfig().getWorldMonstersCritical(entry.getKey()))
));
}
// In DB speichern
if (plugin.getDatabaseManager().isConnected()) {
plugin.getDatabaseManager().saveReport("MANUAL", sender.getName(), "Manueller Report erstellt.");
}
sender.sendMessage(MsgUtil.colorize(MsgUtil.separator()));
});
}
// ──────────────────────────────────────────
// /sp world <weltname>
// ──────────────────────────────────────────
private void handleWorld(CommandSender sender, String prefix, String[] args) {
if (!sender.hasPermission("serverpulse.world")) {
sender.sendMessage(MsgUtil.colorize(prefix + plugin.getSpigotConfig().getMessage("no-permission")));
return;
}
if (args.length < 2) {
sender.sendMessage(MsgUtil.colorize(prefix + "&cBenutzung: /sp world <weltname>"));
sender.sendMessage(MsgUtil.colorize(prefix + "&7Verfügbare Welten: &f"
+ Bukkit.getWorlds().stream()
.map(World::getName)
.collect(Collectors.joining(", "))));
return;
}
String worldName = args[1];
World world = Bukkit.getWorld(worldName);
if (world == null) {
sender.sendMessage(MsgUtil.colorize(prefix + "&cWelt nicht gefunden: &f" + worldName));
return;
}
String displayName = plugin.getSpigotConfig().getWorldDisplayName(worldName);
sender.sendMessage(MsgUtil.colorize(MsgUtil.header("Welt: " + displayName)));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Typ: &f" + world.getEnvironment().name()
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Geladene Chunks: &f" + world.getLoadedChunks().length
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Spieler: &f" + world.getPlayers().size()
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Entities gesamt: &f" + world.getEntities().size()
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Schwierigkeitsgrad: &f" + world.getDifficulty().name()
));
EntitySnapshot snap = plugin.getEntityMonitor().getLastSnapshot(worldName);
if (snap != null) {
sender.sendMessage(MsgUtil.colorize(prefix + "&b--- Letzter Entity-Snapshot ---"));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Monster: " + MsgUtil.formatCount(
snap.getMonsters(),
plugin.getSpigotConfig().getWorldMonstersWarning(worldName),
plugin.getSpigotConfig().getWorldMonstersCritical(worldName))
+ " Tiere: &a" + snap.getAnimals()
+ " Villager: " + MsgUtil.formatCount(
snap.getVillagers(),
plugin.getSpigotConfig().getVillagersWarning(),
plugin.getSpigotConfig().getVillagersCritical())
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Items: " + MsgUtil.formatCount(
snap.getItems(),
plugin.getSpigotConfig().getItemsWarning(),
plugin.getSpigotConfig().getItemsCritical())
+ " ArmorStands: &7" + snap.getArmorStands()
+ " HopperCarts: &7" + snap.getHopperMinecarts()
));
}
sender.sendMessage(MsgUtil.colorize(MsgUtil.separator()));
}
// ──────────────────────────────────────────
// /sp entities [welt]
// ──────────────────────────────────────────
private void handleEntities(CommandSender sender, String prefix, String[] args) {
if (!sender.hasPermission("serverpulse.entities")) {
sender.sendMessage(MsgUtil.colorize(prefix + plugin.getSpigotConfig().getMessage("no-permission")));
return;
}
sender.sendMessage(MsgUtil.colorize(MsgUtil.header("Entity-Übersicht")));
Map<String, EntitySnapshot> snapshots;
if (args.length >= 2) {
String worldName = args[1];
EntitySnapshot snap = plugin.getEntityMonitor().getLastSnapshot(worldName);
if (snap == null) {
sender.sendMessage(MsgUtil.colorize(prefix + "&cKeine Daten für Welt: &f" + worldName));
return;
}
snapshots = Map.of(worldName, snap);
} else {
snapshots = plugin.getEntityMonitor().getAllLastSnapshots();
}
if (snapshots.isEmpty()) {
sender.sendMessage(MsgUtil.colorize(prefix + "&eNoch keine Entity-Daten verfügbar."));
return;
}
for (Map.Entry<String, EntitySnapshot> entry : snapshots.entrySet()) {
EntitySnapshot snap = entry.getValue();
String wName = entry.getKey();
String displayName = plugin.getSpigotConfig().getWorldDisplayName(wName);
sender.sendMessage(MsgUtil.colorize(
prefix + "&b" + displayName + " &8(" + wName + ")"
));
sender.sendMessage(MsgUtil.colorize(
" &7Monster: " + MsgUtil.formatCount(
snap.getMonsters(),
plugin.getSpigotConfig().getWorldMonstersWarning(wName),
plugin.getSpigotConfig().getWorldMonstersCritical(wName))
+ " &7Tiere: &a" + snap.getAnimals()
+ " &7Wasser: &a" + snap.getWaterMobs()
));
sender.sendMessage(MsgUtil.colorize(
" &7Villager: " + MsgUtil.formatCount(
snap.getVillagers(),
plugin.getSpigotConfig().getVillagersWarning(),
plugin.getSpigotConfig().getVillagersCritical())
+ " &7Items: " + MsgUtil.formatCount(
snap.getItems(),
plugin.getSpigotConfig().getItemsWarning(),
plugin.getSpigotConfig().getItemsCritical())
+ " &7Gesamt: &f" + snap.getTotal()
));
}
sender.sendMessage(MsgUtil.colorize(MsgUtil.separator()));
}
// ──────────────────────────────────────────
// /sp reload
// ──────────────────────────────────────────
private void handleReload(CommandSender sender, String prefix) {
if (!sender.hasPermission("serverpulse.reload")) {
sender.sendMessage(MsgUtil.colorize(prefix + plugin.getSpigotConfig().getMessage("no-permission")));
return;
}
plugin.reload();
sender.sendMessage(MsgUtil.colorize(prefix + plugin.getSpigotConfig().getMessage("reload-success")));
}
// ──────────────────────────────────────────
// /sp debug
// ──────────────────────────────────────────
private void handleDebug(CommandSender sender, String prefix) {
if (!sender.hasPermission("serverpulse.debug")) {
sender.sendMessage(MsgUtil.colorize(prefix + plugin.getSpigotConfig().getMessage("no-permission")));
return;
}
sender.sendMessage(MsgUtil.colorize(MsgUtil.header("Debug-Informationen")));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Plugin-Version: &f" + plugin.getDescription().getVersion()
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Debug-Modus: " + (plugin.getSpigotConfig().isDebugMode() ? "&aAktiv" : "&cInaktiv")
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Datenbank: " + (plugin.getDatabaseManager().isConnected() ? "&aVerbunden" : "&cGetrennt")
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Discord: " + (plugin.getSpigotConfig().isDiscordEnabled() ? "&aAktiv" : "&cInaktiv")
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7REST API: &8(wird auf BungeeCord konfiguriert)"
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Trend-Analyse: " + (plugin.getSpigotConfig().isTrendAnalysisEnabled() ? "&aAktiv" : "&cInaktiv")
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Überwachte Welten: &f" + Bukkit.getWorlds().size()
));
sender.sendMessage(MsgUtil.colorize(
prefix + "&7Kritische Events (seit Start): &f" + plugin.getAlertManager().getCriticalEventCount()
));
sender.sendMessage(MsgUtil.colorize(MsgUtil.separator()));
}
// ──────────────────────────────────────────
// /sp clear <items|mobs> <welt>
// ──────────────────────────────────────────
private void handleClear(CommandSender sender, String prefix, String[] args) {
if (!sender.hasPermission("serverpulse.admin")) {
sender.sendMessage(MsgUtil.colorize(prefix + plugin.getSpigotConfig().getMessage("no-permission")));
return;
}
if (args.length < 3) {
sender.sendMessage(MsgUtil.colorize(prefix + "&cBenutzung: /sp clear <items|mobs> <weltname>"));
return;
}
String type = args[1].toLowerCase();
String worldName = args[2];
if (Bukkit.getWorld(worldName) == null) {
sender.sendMessage(MsgUtil.colorize(prefix + "&cWelt nicht gefunden: &f" + worldName));
return;
}
switch (type) {
case "items" -> {
sender.sendMessage(MsgUtil.colorize(prefix + "&eStarte Item-Clear in &f" + worldName + "&e..."));
plugin.getAlertManager().triggerItemClear(worldName);
}
case "mobs" -> {
sender.sendMessage(MsgUtil.colorize(prefix + "&eStarte Mob-Clear in &f" + worldName + "&e..."));
plugin.getAlertManager().triggerMobClear(worldName);
}
default -> sender.sendMessage(MsgUtil.colorize(prefix + "&cUnbekannter Typ. Nutze &fitems &coder &fmobs&c."));
}
}
// ──────────────────────────────────────────
// HILFE
// ──────────────────────────────────────────
private void sendHelp(CommandSender sender, String prefix, String label) {
sender.sendMessage(MsgUtil.colorize(MsgUtil.header("ServerPulse Hilfe")));
sender.sendMessage(MsgUtil.colorize(prefix + "&b/" + label + " status &8 &7Server-Performance-Übersicht"));
sender.sendMessage(MsgUtil.colorize(prefix + "&b/" + label + " report &8 &7Vollständiger Statusbericht"));
sender.sendMessage(MsgUtil.colorize(prefix + "&b/" + label + " world <name> &8 &7Welt-Statistiken"));
sender.sendMessage(MsgUtil.colorize(prefix + "&b/" + label + " entities [welt] &8 &7Entity-Übersicht"));
sender.sendMessage(MsgUtil.colorize(prefix + "&b/" + label + " clear <items|mobs> <welt> &8 &7Notfall-Clear"));
sender.sendMessage(MsgUtil.colorize(prefix + "&b/" + label + " reload &8 &7Konfiguration neu laden"));
sender.sendMessage(MsgUtil.colorize(prefix + "&b/" + label + " debug &8 &7Debug-Informationen"));
sender.sendMessage(MsgUtil.colorize(MsgUtil.separator()));
}
// ──────────────────────────────────────────
// TAB-COMPLETION
// ──────────────────────────────────────────
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
completions.addAll(Arrays.asList("status", "report", "world", "entities", "reload", "debug", "clear"));
} else if (args.length == 2) {
switch (args[0].toLowerCase()) {
case "world", "entities" ->
Bukkit.getWorlds().forEach(w -> completions.add(w.getName()));
case "clear" ->
completions.addAll(Arrays.asList("items", "mobs"));
}
} else if (args.length == 3 && args[0].equalsIgnoreCase("clear")) {
Bukkit.getWorlds().forEach(w -> completions.add(w.getName()));
}
return completions.stream()
.filter(s -> s.toLowerCase().startsWith(args[args.length - 1].toLowerCase()))
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,446 @@
package de.serverpulse.spigot.database;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import de.serverpulse.spigot.SpigotPlugin;
import de.serverpulse.models.EntitySnapshot;
import de.serverpulse.models.PerformanceSnapshot;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
/**
* Verwaltet die MySQL-Datenbankverbindung via HikariCP
* und stellt Methoden für alle CRUD-Operationen bereit.
*/
public class DatabaseManager {
private final SpigotPlugin plugin;
private HikariDataSource dataSource;
public DatabaseManager(SpigotPlugin plugin) {
this.plugin = plugin;
}
// ──────────────────────────────────────────
// VERBINDUNG
// ──────────────────────────────────────────
/**
* Verbindet mit der MySQL-Datenbank via HikariCP
*/
public boolean connect() {
try {
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl("jdbc:mysql://"
+ plugin.getSpigotConfig().getDbHost() + ":"
+ plugin.getSpigotConfig().getDbPort() + "/"
+ plugin.getSpigotConfig().getDbName()
+ "?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8");
hikariConfig.setUsername(plugin.getSpigotConfig().getDbUser());
hikariConfig.setPassword(plugin.getSpigotConfig().getDbPass());
// Pool-Einstellungen
hikariConfig.setMaximumPoolSize(plugin.getSpigotConfig().getPoolMaxSize());
hikariConfig.setMinimumIdle(plugin.getSpigotConfig().getPoolMinIdle());
hikariConfig.setConnectionTimeout(plugin.getSpigotConfig().getPoolConnTimeout());
hikariConfig.setIdleTimeout(plugin.getSpigotConfig().getPoolIdleTimeout());
hikariConfig.setMaxLifetime(plugin.getSpigotConfig().getPoolMaxLifetime());
hikariConfig.setPoolName("ServerPulse-Pool");
// Performance-Eigenschaften
hikariConfig.addDataSourceProperty("cachePrepStmts", "true");
hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250");
hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
hikariConfig.addDataSourceProperty("useServerPrepStmts", "true");
this.dataSource = new HikariDataSource(hikariConfig);
plugin.getLogger().info("Datenbankverbindung erfolgreich hergestellt!");
return true;
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Datenbankverbindung fehlgeschlagen: " + e.getMessage(), e);
return false;
}
}
/**
* Schließt den Connection Pool
*/
public void disconnect() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
plugin.getLogger().info("Datenbankverbindung getrennt.");
}
}
/**
* Gibt eine Verbindung aus dem Pool zurück
*/
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public boolean isConnected() {
return dataSource != null && !dataSource.isClosed();
}
// ──────────────────────────────────────────
// TABELLEN ERSTELLEN
// ──────────────────────────────────────────
/**
* Erstellt alle benötigten Tabellen, falls nicht vorhanden
*/
public void createTables() {
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
// server_metrics: TPS, MSPT, RAM, CPU, Spieler
stmt.execute("""
CREATE TABLE IF NOT EXISTS server_metrics (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
recorded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
tps DOUBLE NOT NULL,
mspt DOUBLE NOT NULL,
ram_used_mb BIGINT NOT NULL,
ram_max_mb BIGINT NOT NULL,
ram_percent DOUBLE NOT NULL,
online_players INT NOT NULL,
loaded_worlds INT NOT NULL,
loaded_chunks INT NOT NULL,
INDEX idx_recorded_at (recorded_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""");
// world_metrics: Welt-spezifische Daten
stmt.execute("""
CREATE TABLE IF NOT EXISTS world_metrics (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
recorded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
world_name VARCHAR(100) NOT NULL,
loaded_chunks INT NOT NULL,
total_entities INT NOT NULL,
online_players INT NOT NULL,
INDEX idx_recorded_at (recorded_at),
INDEX idx_world_name (world_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""");
// entity_metrics: Entity-Zahlen pro Welt und Kategorie
stmt.execute("""
CREATE TABLE IF NOT EXISTS entity_metrics (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
recorded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
world_name VARCHAR(100) NOT NULL,
monsters INT NOT NULL DEFAULT 0,
animals INT NOT NULL DEFAULT 0,
water_mobs INT NOT NULL DEFAULT 0,
villagers INT NOT NULL DEFAULT 0,
armor_stands INT NOT NULL DEFAULT 0,
hopper_minecarts INT NOT NULL DEFAULT 0,
items INT NOT NULL DEFAULT 0,
players INT NOT NULL DEFAULT 0,
other INT NOT NULL DEFAULT 0,
total INT NOT NULL DEFAULT 0,
INDEX idx_recorded_at (recorded_at),
INDEX idx_world_name (world_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""");
// alert_history: Protokoll aller Warnungen
stmt.execute("""
CREATE TABLE IF NOT EXISTS alert_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
alert_type VARCHAR(50) NOT NULL,
severity VARCHAR(20) NOT NULL,
world_name VARCHAR(100),
message TEXT NOT NULL,
value_current DOUBLE,
value_threshold DOUBLE,
discord_sent BOOLEAN DEFAULT FALSE,
INDEX idx_created_at (created_at),
INDEX idx_severity (severity),
INDEX idx_alert_type (alert_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""");
// report_history: Protokoll generierter Reports
stmt.execute("""
CREATE TABLE IF NOT EXISTS report_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
report_type VARCHAR(50) NOT NULL,
generated_by VARCHAR(100) NOT NULL,
content TEXT,
discord_sent BOOLEAN DEFAULT FALSE,
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""");
plugin.getLogger().info("Datenbanktabellen erfolgreich erstellt/verifiziert.");
} catch (SQLException e) {
plugin.getLogger().log(Level.SEVERE, "Fehler beim Erstellen der Tabellen: " + e.getMessage(), e);
}
}
// ──────────────────────────────────────────
// SERVER METRICS
// ──────────────────────────────────────────
/**
* Speichert einen Performance-Snapshot
*/
public void saveServerMetrics(PerformanceSnapshot snapshot) {
String sql = """
INSERT INTO server_metrics
(recorded_at, tps, mspt, ram_used_mb, ram_max_mb, ram_percent, online_players, loaded_worlds, loaded_chunks)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setTimestamp(1, Timestamp.valueOf(snapshot.getRecordedAt()));
ps.setDouble(2, snapshot.getTps());
ps.setDouble(3, snapshot.getMspt());
ps.setLong(4, snapshot.getRamUsedMb());
ps.setLong(5, snapshot.getRamMaxMb());
ps.setDouble(6, snapshot.getRamPercent());
ps.setInt(7, snapshot.getOnlinePlayers());
ps.setInt(8, snapshot.getLoadedWorlds());
ps.setInt(9, snapshot.getLoadedChunks());
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Speichern der Server-Metriken: " + e.getMessage());
}
}
/**
* Gibt die letzten N Performance-Snapshots zurück
*/
public List<PerformanceSnapshot> getRecentServerMetrics(int limit) {
List<PerformanceSnapshot> results = new ArrayList<>();
String sql = "SELECT * FROM server_metrics ORDER BY recorded_at DESC LIMIT ?";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, limit);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
results.add(mapServerMetrics(rs));
}
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Laden der Server-Metriken: " + e.getMessage());
}
return results;
}
/**
* Durchschnittliche TPS der letzten X Minuten
*/
public double getAverageTps(int minutes) {
String sql = "SELECT AVG(tps) FROM server_metrics WHERE recorded_at >= NOW() - INTERVAL ? MINUTE";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, minutes);
ResultSet rs = ps.executeQuery();
if (rs.next()) return rs.getDouble(1);
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Berechnen der Durchschnitts-TPS: " + e.getMessage());
}
return 20.0;
}
// ──────────────────────────────────────────
// ENTITY METRICS
// ──────────────────────────────────────────
/**
* Speichert einen Entity-Snapshot
*/
public void saveEntityMetrics(EntitySnapshot snapshot) {
String sql = """
INSERT INTO entity_metrics
(recorded_at, world_name, monsters, animals, water_mobs, villagers,
armor_stands, hopper_minecarts, items, players, other, total)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setTimestamp(1, Timestamp.valueOf(snapshot.getRecordedAt()));
ps.setString(2, snapshot.getWorldName());
ps.setInt(3, snapshot.getMonsters());
ps.setInt(4, snapshot.getAnimals());
ps.setInt(5, snapshot.getWaterMobs());
ps.setInt(6, snapshot.getVillagers());
ps.setInt(7, snapshot.getArmorStands());
ps.setInt(8, snapshot.getHopperMinecarts());
ps.setInt(9, snapshot.getItems());
ps.setInt(10, snapshot.getPlayers());
ps.setInt(11, snapshot.getOther());
ps.setInt(12, snapshot.getTotal());
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Speichern der Entity-Metriken: " + e.getMessage());
}
}
/**
* Letzte Entity-Metriken für eine Welt
*/
public EntitySnapshot getLatestEntityMetrics(String worldName) {
String sql = "SELECT * FROM entity_metrics WHERE world_name = ? ORDER BY recorded_at DESC LIMIT 1";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, worldName);
ResultSet rs = ps.executeQuery();
if (rs.next()) return mapEntityMetrics(rs);
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Laden der Entity-Metriken: " + e.getMessage());
}
return null;
}
/**
* Entity-Verlauf für Trendanalyse
*/
public List<EntitySnapshot> getEntityHistory(String worldName, int dataPoints) {
List<EntitySnapshot> results = new ArrayList<>();
String sql = "SELECT * FROM entity_metrics WHERE world_name = ? ORDER BY recorded_at DESC LIMIT ?";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, worldName);
ps.setInt(2, dataPoints);
ResultSet rs = ps.executeQuery();
while (rs.next()) results.add(mapEntityMetrics(rs));
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Laden der Entity-Historie: " + e.getMessage());
}
return results;
}
// ──────────────────────────────────────────
// ALERT HISTORY
// ──────────────────────────────────────────
/**
* Speichert eine Warnung in der Datenbank
*/
public void saveAlert(String alertType, String severity, String worldName,
String message, Double currentValue, Double threshold) {
String sql = """
INSERT INTO alert_history (alert_type, severity, world_name, message, value_current, value_threshold)
VALUES (?, ?, ?, ?, ?, ?)
""";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, alertType);
ps.setString(2, severity);
ps.setString(3, worldName);
ps.setString(4, message);
if (currentValue != null) ps.setDouble(5, currentValue);
else ps.setNull(5, Types.DOUBLE);
if (threshold != null) ps.setDouble(6, threshold);
else ps.setNull(6, Types.DOUBLE);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Speichern der Warnung: " + e.getMessage());
}
}
/**
* Anzahl der Warnungen der letzten X Stunden
*/
public int getAlertCount(int hours, String severity) {
String sql = "SELECT COUNT(*) FROM alert_history WHERE created_at >= NOW() - INTERVAL ? HOUR AND severity = ?";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, hours);
ps.setString(2, severity);
ResultSet rs = ps.executeQuery();
if (rs.next()) return rs.getInt(1);
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Zählen der Warnungen: " + e.getMessage());
}
return 0;
}
// ──────────────────────────────────────────
// REPORT HISTORY
// ──────────────────────────────────────────
/**
* Speichert einen generierten Report
*/
public void saveReport(String reportType, String generatedBy, String content) {
String sql = "INSERT INTO report_history (report_type, generated_by, content) VALUES (?, ?, ?)";
try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, reportType);
ps.setString(2, generatedBy);
ps.setString(3, content);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler beim Speichern des Reports: " + e.getMessage());
}
}
// ──────────────────────────────────────────
// DATEN-BEREINIGUNG
// ──────────────────────────────────────────
/**
* Löscht alte Daten basierend auf der konfigurierten Retention-Zeit
*/
public void cleanupOldData() {
int days = plugin.getSpigotConfig().getDataRetentionDays();
if (days <= 0) return;
String[] tables = {"server_metrics", "world_metrics", "entity_metrics", "alert_history", "report_history"};
try (Connection conn = getConnection()) {
for (String table : tables) {
try (PreparedStatement ps = conn.prepareStatement(
"DELETE FROM " + table + " WHERE recorded_at < NOW() - INTERVAL ? DAY")) {
ps.setInt(1, days);
int deleted = ps.executeUpdate();
if (deleted > 0) {
plugin.debug("Bereinigt: " + deleted + " Zeilen aus " + table);
}
}
}
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Fehler bei der Datenbereinigung: " + e.getMessage());
}
}
// ──────────────────────────────────────────
// MAPPER
// ──────────────────────────────────────────
private PerformanceSnapshot mapServerMetrics(ResultSet rs) throws SQLException {
return new PerformanceSnapshot(
rs.getTimestamp("recorded_at").toLocalDateTime(),
rs.getDouble("tps"),
rs.getDouble("mspt"),
rs.getLong("ram_used_mb"),
rs.getLong("ram_max_mb"),
rs.getDouble("ram_percent"),
rs.getInt("online_players"),
rs.getInt("loaded_worlds"),
rs.getInt("loaded_chunks")
);
}
private EntitySnapshot mapEntityMetrics(ResultSet rs) throws SQLException {
return new EntitySnapshot(
rs.getTimestamp("recorded_at").toLocalDateTime(),
rs.getString("world_name"),
rs.getInt("monsters"),
rs.getInt("animals"),
rs.getInt("water_mobs"),
rs.getInt("villagers"),
rs.getInt("armor_stands"),
rs.getInt("hopper_minecarts"),
rs.getInt("items"),
rs.getInt("players"),
rs.getInt("other"),
rs.getInt("total")
);
}
}

View File

@@ -0,0 +1,270 @@
package de.serverpulse.spigot.discord;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import de.serverpulse.spigot.SpigotPlugin;
import de.serverpulse.models.AlertSeverity;
import de.serverpulse.models.EntitySnapshot;
import de.serverpulse.models.PerformanceSnapshot;
import de.serverpulse.spigot.utils.MsgUtil;
import okhttp3.*;
import java.io.IOException;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
/**
* Verwaltet alle Discord-Webhook-Benachrichtigungen.
* Unterstützt Rich Embeds mit Farben, Feldern und Avataren.
*/
public class SpigotDiscordWebhook {
private final SpigotPlugin plugin;
private final OkHttpClient httpClient;
public SpigotDiscordWebhook(SpigotPlugin plugin) {
this.plugin = plugin;
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build();
}
// ──────────────────────────────────────────
// ALERT SENDEN
// ──────────────────────────────────────────
/**
* Sendet eine Warnung als Discord-Embed
*/
public void sendAlert(String alertType, AlertSeverity severity, String worldName,
String message, Double currentValue, Double threshold) {
if (!plugin.getSpigotConfig().isDiscordEnabled()) return;
int color = switch (severity) {
case CRITICAL -> plugin.getSpigotConfig().getColorCritical();
case WARNING -> plugin.getSpigotConfig().getColorWarning();
default -> plugin.getSpigotConfig().getColorInfo();
};
String emoji = switch (severity) {
case CRITICAL -> "🔴";
case WARNING -> "🟡";
default -> "🔵";
};
JsonObject embed = createEmbed(
emoji + " " + severity.getName() + " " + formatAlertType(alertType),
message,
color
);
// Felder hinzufügen
JsonArray fields = new JsonArray();
if (worldName != null) {
fields.add(createField("🌍 Welt", worldName, true));
}
if (currentValue != null) {
fields.add(createField("📊 Aktueller Wert", String.format("%.2f", currentValue), true));
}
if (threshold != null) {
fields.add(createField("⚠️ Grenzwert", String.format("%.2f", threshold), true));
}
fields.add(createField("🕐 Zeitpunkt", MsgUtil.timestamp(), false));
embed.add("fields", fields);
sendEmbed(plugin.getSpigotConfig().getWebhookUrl(), embed);
}
// ──────────────────────────────────────────
// TÄGLICHER REPORT
// ──────────────────────────────────────────
/**
* Plant den täglichen Status-Report
*/
public void scheduleDailyReport() {
String timeStr = plugin.getSpigotConfig().getDailyReportTime();
try {
LocalTime reportTime = LocalTime.parse(timeStr, DateTimeFormatter.ofPattern("HH:mm"));
LocalTime now = LocalTime.now();
long secondsUntilReport;
if (now.isBefore(reportTime)) {
secondsUntilReport = now.until(reportTime, java.time.temporal.ChronoUnit.SECONDS);
} else {
secondsUntilReport = now.until(reportTime.plusHours(24), java.time.temporal.ChronoUnit.SECONDS);
}
long ticksUntilFirst = secondsUntilReport * 20;
long ticksPerDay = 24 * 60 * 60 * 20;
org.bukkit.Bukkit.getScheduler().runTaskTimerAsynchronously(plugin,
this::sendDailyReport, ticksUntilFirst, ticksPerDay);
plugin.getLogger().info("Täglicher Discord-Report geplant für " + timeStr + " Uhr.");
} catch (Exception e) {
plugin.getLogger().warning("Ungültige Report-Zeit in Config: " + timeStr);
}
}
/**
* Erstellt und sendet den täglichen Status-Report
*/
public void sendDailyReport() {
if (!plugin.getSpigotConfig().isDiscordEnabled()) return;
JsonObject embed = createEmbed(
"📊 Täglicher Server-Statusbericht",
"Hier ist der automatische Tagesbericht für deinen Minecraft-Server.",
plugin.getSpigotConfig().getColorInfo()
);
JsonArray fields = new JsonArray();
// Performance-Daten
PerformanceSnapshot perf = plugin.getPerformanceMonitor().getLastSnapshot();
if (perf != null) {
fields.add(createField("⚡ TPS (aktuell)",
String.format("%.2f / 20.0", perf.getTps()), true));
fields.add(createField("⏱ MSPT",
String.format("%.2f ms", perf.getMspt()), true));
fields.add(createField("💾 RAM",
perf.getRamUsedMb() + " MB / " + perf.getRamMaxMb() + " MB", true));
fields.add(createField("👥 Online",
perf.getOnlinePlayers() + " Spieler", true));
fields.add(createField("🗺 Welten",
perf.getLoadedWorlds() + " geladen", true));
fields.add(createField("📦 Chunks",
perf.getLoadedChunks() + " geladen", true));
}
// Durchschnittliche TPS der letzten 24h
if (plugin.getDatabaseManager().isConnected()) {
double avgTps24h = plugin.getDatabaseManager().getAverageTps(60 * 24);
fields.add(createField("📈 Ø TPS (24h)",
String.format("%.2f", avgTps24h), true));
int criticalAlerts = plugin.getDatabaseManager().getAlertCount(24, "CRITICAL");
int warningAlerts = plugin.getDatabaseManager().getAlertCount(24, "WARNING");
fields.add(createField("🚨 Kritische Warnungen (24h)",
String.valueOf(criticalAlerts), true));
fields.add(createField("⚠️ Warnungen (24h)",
String.valueOf(warningAlerts), true));
}
// Entity-Daten pro Welt
for (var entry : plugin.getEntityMonitor().getAllLastSnapshots().entrySet()) {
EntitySnapshot snap = entry.getValue();
String displayName = plugin.getSpigotConfig().getWorldDisplayName(entry.getKey());
fields.add(createField("🌍 " + displayName,
"Gesamt: **" + snap.getTotal() + "** | Monster: " + snap.getMonsters()
+ " | Tiere: " + snap.getAnimals()
+ " | Villager: " + snap.getVillagers()
+ " | Items: " + snap.getItems(),
false));
}
fields.add(createField("🕐 Erstellt", MsgUtil.timestamp(), false));
embed.add("fields", fields);
// Footer
JsonObject footer = new JsonObject();
footer.addProperty("text", "ServerPulse Monitoring System");
embed.add("footer", footer);
sendEmbed(plugin.getSpigotConfig().getReportWebhookUrl(), embed);
}
/**
* Sendet eine einfache Info-Nachricht
*/
public void sendInfo(String title, String message) {
if (!plugin.getSpigotConfig().isDiscordEnabled()) return;
JsonObject embed = createEmbed(title, message, plugin.getSpigotConfig().getColorInfo());
sendEmbed(plugin.getSpigotConfig().getWebhookUrl(), embed);
}
// ──────────────────────────────────────────
// HTTP-VERSAND
// ──────────────────────────────────────────
/**
* Sendet ein Discord-Embed an einen Webhook
*/
private void sendEmbed(String webhookUrl, JsonObject embed) {
if (webhookUrl == null || webhookUrl.isEmpty() || webhookUrl.contains("YOUR_WEBHOOK")) {
plugin.debug("Discord-Webhook nicht konfiguriert, überspringe.");
return;
}
JsonObject payload = new JsonObject();
payload.addProperty("username", plugin.getSpigotConfig().getBotName());
String avatarUrl = plugin.getSpigotConfig().getBotAvatar();
if (!avatarUrl.isEmpty()) {
payload.addProperty("avatar_url", avatarUrl);
}
JsonArray embeds = new JsonArray();
embeds.add(embed);
payload.add("embeds", embeds);
org.bukkit.Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
RequestBody body = RequestBody.create(
payload.toString(),
MediaType.get("application/json")
);
Request request = new Request.Builder()
.url(webhookUrl)
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
plugin.getLogger().warning("Discord-Webhook Fehler: " + response.code());
} else {
plugin.debug("Discord-Nachricht erfolgreich gesendet.");
}
}
} catch (IOException e) {
plugin.getLogger().log(Level.WARNING, "Discord-Webhook Verbindungsfehler: " + e.getMessage());
}
});
}
// ──────────────────────────────────────────
// HILFSMETHODEN
// ──────────────────────────────────────────
private JsonObject createEmbed(String title, String description, int color) {
JsonObject embed = new JsonObject();
embed.addProperty("title", title);
embed.addProperty("description", description);
embed.addProperty("color", color);
return embed;
}
private JsonObject createField(String name, String value, boolean inline) {
JsonObject field = new JsonObject();
field.addProperty("name", name);
field.addProperty("value", value.isEmpty() ? "N/A" : value);
field.addProperty("inline", inline);
return field;
}
private String formatAlertType(String alertType) {
return alertType.replace("_", " ");
}
}

View File

@@ -0,0 +1,27 @@
package de.serverpulse.spigot.listeners;
import de.serverpulse.spigot.SpigotPlugin;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public class SpigotPlayerListener implements Listener {
private final SpigotPlugin plugin;
public SpigotPlayerListener(SpigotPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.MONITOR)
public void onJoin(PlayerJoinEvent e) {
plugin.debug("Spieler verbunden: " + e.getPlayer().getName());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onQuit(PlayerQuitEvent e) {
plugin.debug("Spieler getrennt: " + e.getPlayer().getName());
}
}

View File

@@ -0,0 +1,212 @@
package de.serverpulse.spigot.monitoring;
import de.serverpulse.spigot.SpigotPlugin;
import de.serverpulse.models.AlertSeverity;
import de.serverpulse.models.EntitySnapshot;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.*;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.entity.minecart.HopperMinecart;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Erfasst und kategorisiert alle Entitäten pro Welt.
* Kategorien: Monster, Tiere, Wasser-Mobs, Villager,
* ArmorStands, Hopper-Minecarts, Items, Spieler, Sonstige.
*/
public class EntityMonitor {
private final SpigotPlugin plugin;
private BukkitTask task;
// Cache: letzter Snapshot pro Welt
private final Map<String, EntitySnapshot> lastSnapshots = new ConcurrentHashMap<>();
public EntityMonitor(SpigotPlugin plugin) {
this.plugin = plugin;
}
public void start() {
int intervalSeconds = plugin.getSpigotConfig().getEntityInterval();
int intervalTicks = intervalSeconds * 20;
// Entity-Zählung muss auf dem Haupt-Thread laufen (Bukkit API)
task = Bukkit.getScheduler().runTaskTimer(plugin, this::collect, 200L, intervalTicks);
plugin.getLogger().info("Entity Monitor gestartet (Intervall: " + intervalSeconds + "s)");
}
public void stop() {
if (task != null && !task.isCancelled()) {
task.cancel();
plugin.getLogger().info("Entity Monitor gestoppt.");
}
}
public void restart() {
stop();
start();
}
// ──────────────────────────────────────────
// DATENERFASSUNG
// ──────────────────────────────────────────
private void collect() {
for (World world : Bukkit.getWorlds()) {
if (!plugin.getSpigotConfig().isWorldMonitored(world.getName())) continue;
EntitySnapshot snapshot = countEntities(world);
lastSnapshots.put(world.getName(), snapshot);
// Async in DB speichern
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
if (plugin.getDatabaseManager().isConnected()) {
plugin.getDatabaseManager().saveEntityMetrics(snapshot);
}
checkEntityThresholds(snapshot);
});
plugin.debug("Entities in " + world.getName() + ": " + snapshot.getTotal() + " gesamt");
}
}
/**
* Zählt und kategorisiert alle Entitäten einer Welt
*/
public EntitySnapshot countEntities(World world) {
int monsters = 0, animals = 0, waterMobs = 0, villagers = 0;
int armorStands = 0, hopperMinecarts = 0, items = 0, players = 0, other = 0;
for (Entity entity : world.getEntities()) {
if (entity instanceof Monster) {
monsters++;
} else if (entity instanceof WaterMob) {
waterMobs++;
} else if (entity instanceof Villager || entity instanceof WanderingTrader) {
villagers++;
} else if (entity instanceof Animals || entity instanceof Golem) {
animals++;
} else if (entity instanceof ArmorStand) {
armorStands++;
} else if (entity instanceof HopperMinecart) {
hopperMinecarts++;
} else if (entity instanceof Item) {
items++;
} else if (entity instanceof Player) {
players++;
} else {
other++;
}
}
int total = monsters + animals + waterMobs + villagers
+ armorStands + hopperMinecarts + items + players + other;
return new EntitySnapshot(
LocalDateTime.now(), world.getName(),
monsters, animals, waterMobs, villagers,
armorStands, hopperMinecarts, items, players, other, total
);
}
// ──────────────────────────────────────────
// GRENZWERT-PRÜFUNG
// ──────────────────────────────────────────
private void checkEntityThresholds(EntitySnapshot snapshot) {
String world = snapshot.getWorldName();
// Gesamt-Entities
int totalWarning = plugin.getSpigotConfig().getWorldTotalWarning(world);
int totalCritical = plugin.getSpigotConfig().getWorldTotalCritical(world);
if (snapshot.getTotal() >= totalCritical) {
plugin.getAlertManager().triggerAlert(
"ENTITY_TOTAL_CRITICAL", AlertSeverity.CRITICAL, world,
"Kritisch hohe Entity-Anzahl in " + world + ": " + snapshot.getTotal(),
(double) snapshot.getTotal(), (double) totalCritical
);
} else if (snapshot.getTotal() >= totalWarning) {
plugin.getAlertManager().triggerAlert(
"ENTITY_TOTAL_WARNING", AlertSeverity.WARNING, world,
"Hohe Entity-Anzahl in " + world + ": " + snapshot.getTotal(),
(double) snapshot.getTotal(), (double) totalWarning
);
}
// Monster
int monstersWarning = plugin.getSpigotConfig().getWorldMonstersWarning(world);
int monstersCritical = plugin.getSpigotConfig().getWorldMonstersCritical(world);
if (snapshot.getMonsters() >= monstersCritical) {
plugin.getAlertManager().triggerAlert(
"MONSTERS_CRITICAL", AlertSeverity.CRITICAL, world,
"Kritisch viele Monster in " + world + ": " + snapshot.getMonsters(),
(double) snapshot.getMonsters(), (double) monstersCritical
);
} else if (snapshot.getMonsters() >= monstersWarning) {
plugin.getAlertManager().triggerAlert(
"MONSTERS_WARNING", AlertSeverity.WARNING, world,
"Viele Monster in " + world + ": " + snapshot.getMonsters(),
(double) snapshot.getMonsters(), (double) monstersWarning
);
}
// Items
int itemsWarning = plugin.getSpigotConfig().getItemsWarning();
int itemsCritical = plugin.getSpigotConfig().getItemsCritical();
if (snapshot.getItems() >= itemsCritical) {
plugin.getAlertManager().triggerAlert(
"ITEMS_CRITICAL", AlertSeverity.CRITICAL, world,
"Kritisch viele Items in " + world + ": " + snapshot.getItems(),
(double) snapshot.getItems(), (double) itemsCritical
);
// Notfall-Item-Clear auslösen
if (plugin.getSpigotConfig().isItemClearEnabled()) {
plugin.getAlertManager().triggerItemClear(world);
}
} else if (snapshot.getItems() >= itemsWarning) {
plugin.getAlertManager().triggerAlert(
"ITEMS_WARNING", AlertSeverity.WARNING, world,
"Viele Items in " + world + ": " + snapshot.getItems(),
(double) snapshot.getItems(), (double) itemsWarning
);
}
// Villager
int villagersWarning = plugin.getSpigotConfig().getVillagersWarning();
int villagersCritical = plugin.getSpigotConfig().getVillagersCritical();
if (snapshot.getVillagers() >= villagersCritical) {
plugin.getAlertManager().triggerAlert(
"VILLAGERS_CRITICAL", AlertSeverity.CRITICAL, world,
"Kritisch viele Villager in " + world + ": " + snapshot.getVillagers(),
(double) snapshot.getVillagers(), (double) villagersCritical
);
} else if (snapshot.getVillagers() >= villagersWarning) {
plugin.getAlertManager().triggerAlert(
"VILLAGERS_WARNING", AlertSeverity.WARNING, world,
"Viele Villager in " + world + ": " + snapshot.getVillagers(),
(double) snapshot.getVillagers(), (double) villagersWarning
);
}
}
// ──────────────────────────────────────────
// GETTER
// ──────────────────────────────────────────
public Map<String, EntitySnapshot> getAllLastSnapshots() {
return new HashMap<>(lastSnapshots);
}
public EntitySnapshot getLastSnapshot(String worldName) {
return lastSnapshots.get(worldName);
}
}

View File

@@ -0,0 +1,213 @@
package de.serverpulse.spigot.monitoring;
import de.serverpulse.spigot.SpigotPlugin;
import de.serverpulse.models.AlertSeverity;
import de.serverpulse.models.PerformanceSnapshot;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.scheduler.BukkitTask;
import java.time.LocalDateTime;
/**
* Erfasst Server-Performance-Daten: TPS, MSPT, RAM, Spieler, Chunks.
* Läuft als wiederkehrender Bukkit-Task.
*/
public class PerformanceMonitor {
private final SpigotPlugin plugin;
private BukkitTask task;
// Letzter bekannter Snapshot (für Status-Befehle)
private PerformanceSnapshot lastSnapshot;
public PerformanceMonitor(SpigotPlugin plugin) {
this.plugin = plugin;
}
public void start() {
int intervalSeconds = plugin.getSpigotConfig().getMetricsInterval();
int intervalTicks = intervalSeconds * 20;
task = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::collect, 100L, intervalTicks);
plugin.getLogger().info("Performance Monitor gestartet (Intervall: " + intervalSeconds + "s)");
}
public void stop() {
if (task != null && !task.isCancelled()) {
task.cancel();
plugin.getLogger().info("Performance Monitor gestoppt.");
}
}
public void restart() {
stop();
start();
}
// ──────────────────────────────────────────
// DATENERFASSUNG
// ──────────────────────────────────────────
private void collect() {
try {
double tps = getTps();
double mspt = getMspt();
long ramUsed = getRamUsedMb();
long ramMax = getRamMaxMb();
double ramPercent = (double) ramUsed / ramMax * 100;
int onlinePlayers = Bukkit.getOnlinePlayers().size();
int loadedWorlds = Bukkit.getWorlds().size();
int loadedChunks = getTotalLoadedChunks();
PerformanceSnapshot snapshot = new PerformanceSnapshot(
LocalDateTime.now(), tps, mspt,
ramUsed, ramMax, ramPercent,
onlinePlayers, loadedWorlds, loadedChunks
);
this.lastSnapshot = snapshot;
// In DB speichern
if (plugin.getDatabaseManager().isConnected()) {
plugin.getDatabaseManager().saveServerMetrics(snapshot);
}
// Grenzwerte prüfen
checkThresholds(snapshot);
plugin.debug("Performance erfasst: TPS=" + String.format("%.2f", tps)
+ " MSPT=" + String.format("%.2f", mspt)
+ " RAM=" + ramUsed + "MB/" + ramMax + "MB");
} catch (Exception e) {
plugin.getLogger().warning("Fehler beim Erfassen der Performance: " + e.getMessage());
}
}
// ──────────────────────────────────────────
// GRENZWERT-PRÜFUNG
// ──────────────────────────────────────────
private void checkThresholds(PerformanceSnapshot snapshot) {
double tps = snapshot.getTps();
double mspt = snapshot.getMspt();
double ramPercent = snapshot.getRamPercent();
// TPS prüfen
if (tps < plugin.getSpigotConfig().getTpsCritical()) {
plugin.getAlertManager().triggerAlert(
"TPS_CRITICAL", AlertSeverity.CRITICAL, null,
"Kritisch niedrige TPS: " + String.format("%.2f", tps),
tps, plugin.getSpigotConfig().getTpsCritical()
);
} else if (tps < plugin.getSpigotConfig().getTpsWarning()) {
plugin.getAlertManager().triggerAlert(
"TPS_WARNING", AlertSeverity.WARNING, null,
"Niedrige TPS: " + String.format("%.2f", tps),
tps, plugin.getSpigotConfig().getTpsWarning()
);
}
// MSPT prüfen
if (mspt > plugin.getSpigotConfig().getMsptCritical()) {
plugin.getAlertManager().triggerAlert(
"MSPT_CRITICAL", AlertSeverity.CRITICAL, null,
"Kritisch hohe MSPT: " + String.format("%.2f", mspt) + "ms",
mspt, plugin.getSpigotConfig().getMsptCritical()
);
} else if (mspt > plugin.getSpigotConfig().getMsptWarning()) {
plugin.getAlertManager().triggerAlert(
"MSPT_WARNING", AlertSeverity.WARNING, null,
"Hohe MSPT: " + String.format("%.2f", mspt) + "ms",
mspt, plugin.getSpigotConfig().getMsptWarning()
);
}
// RAM prüfen
if (ramPercent > plugin.getSpigotConfig().getRamCritical()) {
plugin.getAlertManager().triggerAlert(
"RAM_CRITICAL", AlertSeverity.CRITICAL, null,
"Kritische RAM-Auslastung: " + String.format("%.1f", ramPercent) + "%",
ramPercent, (double) plugin.getSpigotConfig().getRamCritical()
);
} else if (ramPercent > plugin.getSpigotConfig().getRamWarning()) {
plugin.getAlertManager().triggerAlert(
"RAM_WARNING", AlertSeverity.WARNING, null,
"Hohe RAM-Auslastung: " + String.format("%.1f", ramPercent) + "%",
ramPercent, (double) plugin.getSpigotConfig().getRamWarning()
);
}
}
// ──────────────────────────────────────────
// TPS & MSPT ABRUFEN
// ──────────────────────────────────────────
/**
* Liest den aktuellen TPS-Wert vom Server.
* Paper API bietet getTPS(), Spigot hat recentTps[].
*/
public double getTps() {
try {
// Paper API: getServer().getTPS()
double[] tps = Bukkit.getServer().getTPS();
return Math.min(20.0, tps[0]); // 1-Minuten-Durchschnitt
} catch (Exception e) {
// Fallback für Spigot
return 20.0;
}
}
/**
* Liest den aktuellen MSPT-Wert.
* Paper: getAverageTickTime()
*/
public double getMspt() {
try {
return Bukkit.getServer().getAverageTickTime();
} catch (Exception e) {
// Berechnung aus TPS als Fallback
double tps = getTps();
return tps > 0 ? 1000.0 / tps : 50.0;
}
}
// ──────────────────────────────────────────
// RAM
// ──────────────────────────────────────────
public long getRamUsedMb() {
Runtime runtime = Runtime.getRuntime();
return (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
}
public long getRamMaxMb() {
return Runtime.getRuntime().maxMemory() / (1024 * 1024);
}
public long getRamFreeMb() {
Runtime runtime = Runtime.getRuntime();
return (runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory()) / (1024 * 1024);
}
// ──────────────────────────────────────────
// CHUNKS
// ──────────────────────────────────────────
private int getTotalLoadedChunks() {
int total = 0;
for (World world : Bukkit.getWorlds()) {
total += world.getLoadedChunks().length;
}
return total;
}
// ──────────────────────────────────────────
// GETTER
// ──────────────────────────────────────────
public PerformanceSnapshot getLastSnapshot() {
return lastSnapshot;
}
}

View File

@@ -0,0 +1,200 @@
package de.serverpulse.spigot.monitoring;
import de.serverpulse.spigot.SpigotPlugin;
import de.serverpulse.models.AlertSeverity;
import de.serverpulse.models.EntitySnapshot;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import java.util.List;
/**
* Analysiert Trends in den gesammelten Daten.
* Erkennt kontinuierlich steigende Werte wie Monster-Anzahl,
* MSPT-Werte oder RAM-Auslastung.
*/
public class TrendAnalyzer {
private final SpigotPlugin plugin;
private BukkitTask task;
public TrendAnalyzer(SpigotPlugin plugin) {
this.plugin = plugin;
}
public void start() {
if (!plugin.getSpigotConfig().isTrendAnalysisEnabled()) {
plugin.getLogger().info("Trend-Analyse ist deaktiviert.");
return;
}
int intervalMinutes = plugin.getSpigotConfig().getTrendCheckInterval();
long intervalTicks = intervalMinutes * 60L * 20L;
task = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::analyze, 600L, intervalTicks);
plugin.getLogger().info("Trend Analyzer gestartet (Intervall: " + intervalMinutes + " Minuten)");
}
public void stop() {
if (task != null && !task.isCancelled()) {
task.cancel();
plugin.getLogger().info("Trend Analyzer gestoppt.");
}
}
public void restart() {
stop();
start();
}
// ──────────────────────────────────────────
// ANALYSE
// ──────────────────────────────────────────
private void analyze() {
if (!plugin.getDatabaseManager().isConnected()) return;
// TPS-Trend analysieren
analyzeTpsTrend();
// Entity-Trends analysieren (pro Welt)
for (org.bukkit.World world : Bukkit.getWorlds()) {
if (!plugin.getSpigotConfig().isWorldMonitored(world.getName())) continue;
analyzeEntityTrend(world.getName());
}
plugin.debug("Trend-Analyse abgeschlossen.");
}
/**
* Analysiert den TPS-Trend der letzten Datenpunkte
*/
private void analyzeTpsTrend() {
int dataPoints = plugin.getSpigotConfig().getTrendDataPoints();
List<de.serverpulse.models.PerformanceSnapshot> snapshots =
plugin.getDatabaseManager().getRecentServerMetrics(dataPoints);
if (snapshots.size() < 3) return;
// Prüfen ob TPS kontinuierlich fällt
boolean continuousDecline = true;
for (int i = 0; i < snapshots.size() - 1; i++) {
if (snapshots.get(i).getTps() >= snapshots.get(i + 1).getTps()) {
continuousDecline = false;
break;
}
}
if (continuousDecline) {
double startTps = snapshots.get(snapshots.size() - 1).getTps();
double endTps = snapshots.get(0).getTps();
double decline = ((startTps - endTps) / startTps) * 100;
if (decline >= plugin.getSpigotConfig().getTrendIncreaseThreshold()) {
plugin.getAlertManager().triggerAlert(
"TPS_TREND_DECLINE", AlertSeverity.WARNING, null,
String.format("TPS-Abnahme-Trend erkannt: %.1f%% Rückgang über %d Messungen",
decline, dataPoints),
endTps, startTps
);
}
}
// Prüfen ob MSPT kontinuierlich steigt
boolean continuousMsptIncrease = true;
for (int i = 1; i < snapshots.size(); i++) {
if (snapshots.get(i).getMspt() <= snapshots.get(i - 1).getMspt()) {
continuousMsptIncrease = false;
break;
}
}
if (continuousMsptIncrease && snapshots.size() >= 3) {
double startMspt = snapshots.get(snapshots.size() - 1).getMspt();
double endMspt = snapshots.get(0).getMspt();
double increase = ((endMspt - startMspt) / startMspt) * 100;
if (increase >= plugin.getSpigotConfig().getTrendIncreaseThreshold()) {
plugin.getAlertManager().triggerAlert(
"MSPT_TREND_INCREASE", AlertSeverity.WARNING, null,
String.format("MSPT-Anstiegs-Trend erkannt: %.1f%% Anstieg über %d Messungen",
increase, dataPoints),
endMspt, startMspt
);
}
}
}
/**
* Analysiert Entity-Trends für eine spezifische Welt
*/
private void analyzeEntityTrend(String worldName) {
int dataPoints = plugin.getSpigotConfig().getTrendDataPoints();
List<EntitySnapshot> snapshots =
plugin.getDatabaseManager().getEntityHistory(worldName, dataPoints);
if (snapshots.size() < 3) return;
// Monster-Trend analysieren
boolean monsterIncrease = checkIncreasingTrend(
snapshots.stream().mapToDouble(EntitySnapshot::getMonsters).toArray()
);
if (monsterIncrease) {
double startMonsters = snapshots.get(snapshots.size() - 1).getMonsters();
double endMonsters = snapshots.get(0).getMonsters();
if (startMonsters > 0) {
double increase = ((endMonsters - startMonsters) / startMonsters) * 100;
if (increase >= plugin.getSpigotConfig().getTrendIncreaseThreshold()) {
plugin.getAlertManager().triggerAlert(
"MONSTER_TREND_INCREASE", AlertSeverity.WARNING, worldName,
String.format("Monster-Anstiegs-Trend in %s: %.1f%% Anstieg (%d → %d)",
worldName, increase, (int) startMonsters, (int) endMonsters),
endMonsters, startMonsters
);
}
}
}
// Item-Trend analysieren
boolean itemIncrease = checkIncreasingTrend(
snapshots.stream().mapToDouble(EntitySnapshot::getItems).toArray()
);
if (itemIncrease) {
double startItems = snapshots.get(snapshots.size() - 1).getItems();
double endItems = snapshots.get(0).getItems();
if (startItems > 0) {
double increase = ((endItems - startItems) / startItems) * 100;
if (increase >= plugin.getSpigotConfig().getTrendIncreaseThreshold() * 1.5) {
plugin.getAlertManager().triggerAlert(
"ITEMS_TREND_INCREASE", AlertSeverity.WARNING, worldName,
String.format("Item-Anstiegs-Trend in %s: %.1f%% Anstieg (%d → %d)",
worldName, increase, (int) startItems, (int) endItems),
endItems, startItems
);
}
}
}
}
/**
* Prüft ob ein Array von Werten kontinuierlich steigt
* (erlaubt kleine Rückgänge)
*/
private boolean checkIncreasingTrend(double[] values) {
if (values.length < 3) return false;
int increasingCount = 0;
for (int i = 0; i < values.length - 1; i++) {
if (values[i] > values[i + 1]) increasingCount++;
}
// Mindestens 70% der Messungen müssen steigen
return (double) increasingCount / (values.length - 1) >= 0.7;
}
}

View File

@@ -0,0 +1,129 @@
package de.serverpulse.spigot.network;
import com.google.gson.JsonObject;
import de.serverpulse.network.NetworkMessage;
import de.serverpulse.spigot.SpigotPlugin;
import de.serverpulse.models.EntitySnapshot;
import de.serverpulse.models.PerformanceSnapshot;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Map;
/**
* Sendet Monitoring-Daten vom Spigot-Server an den BungeeCord-Proxy
* via Plugin Messaging Channel "serverpulse:data".
*/
public class SpigotMessenger {
private final SpigotPlugin plugin;
private BukkitTask performanceTask;
private BukkitTask heartbeatTask;
public SpigotMessenger(SpigotPlugin plugin) {
this.plugin = plugin;
}
public void start() {
Bukkit.getMessenger().registerOutgoingPluginChannel(plugin, NetworkMessage.CHANNEL);
int intervalTicks = plugin.getSpigotConfig().getMetricsInterval() * 20;
performanceTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin,
this::sendPerformance, 200L, intervalTicks);
heartbeatTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin,
this::sendHeartbeat, 300L, 1200L);
plugin.getLogger().info("BungeeCord-Messenger gestartet (Kanal: " + NetworkMessage.CHANNEL + ")");
}
public void stop() {
if (performanceTask != null) performanceTask.cancel();
if (heartbeatTask != null) heartbeatTask.cancel();
Bukkit.getMessenger().unregisterOutgoingPluginChannel(plugin, NetworkMessage.CHANNEL);
}
private void sendPerformance() {
PerformanceSnapshot snap = plugin.getPerformanceMonitor().getLastSnapshot();
if (snap == null) return;
JsonObject msg = new JsonObject();
msg.addProperty("type", NetworkMessage.TYPE_PERFORMANCE);
msg.addProperty("tps", snap.getTps());
msg.addProperty("mspt", snap.getMspt());
msg.addProperty("ram_used_mb", snap.getRamUsedMb());
msg.addProperty("ram_max_mb", snap.getRamMaxMb());
msg.addProperty("ram_percent", snap.getRamPercent());
msg.addProperty("online_players", snap.getOnlinePlayers());
msg.addProperty("loaded_chunks", snap.getLoadedChunks());
sendToProxy(msg);
for (Map.Entry<String, EntitySnapshot> entry :
plugin.getEntityMonitor().getAllLastSnapshots().entrySet()) {
sendEntityData(entry.getValue());
}
}
private void sendEntityData(EntitySnapshot snap) {
JsonObject msg = new JsonObject();
msg.addProperty("type", NetworkMessage.TYPE_ENTITY);
msg.addProperty("world", snap.getWorldName());
msg.addProperty("monsters", snap.getMonsters());
msg.addProperty("animals", snap.getAnimals());
msg.addProperty("water_mobs", snap.getWaterMobs());
msg.addProperty("villagers", snap.getVillagers());
msg.addProperty("armor_stands", snap.getArmorStands());
msg.addProperty("hopper_minecarts", snap.getHopperMinecarts());
msg.addProperty("items", snap.getItems());
msg.addProperty("players", snap.getPlayers());
msg.addProperty("other", snap.getOther());
msg.addProperty("total", snap.getTotal());
sendToProxy(msg);
}
public void sendAlert(String alertType, String severity, String message) {
JsonObject msg = new JsonObject();
msg.addProperty("type", NetworkMessage.TYPE_ALERT);
msg.addProperty("alert_type", alertType);
msg.addProperty("severity", severity);
msg.addProperty("message", message);
sendToProxy(msg);
}
public void sendChatEvent(String playerName, String message, String reason) {
JsonObject msg = new JsonObject();
msg.addProperty("type", NetworkMessage.TYPE_CHAT_EVENT);
msg.addProperty("player", playerName);
msg.addProperty("message", message);
msg.addProperty("reason", reason);
sendToProxy(msg);
}
private void sendHeartbeat() {
JsonObject msg = new JsonObject();
msg.addProperty("type", NetworkMessage.TYPE_HEARTBEAT);
sendToProxy(msg);
}
private void sendToProxy(JsonObject json) {
Bukkit.getScheduler().runTask(plugin, () -> {
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
if (players.isEmpty()) return;
try {
byte[] jsonBytes = json.toString().getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (DataOutputStream dos = new DataOutputStream(baos)) {
dos.write(jsonBytes);
}
players.iterator().next().sendPluginMessage(plugin, NetworkMessage.CHANNEL, baos.toByteArray());
} catch (Exception e) {
plugin.getLogger().warning("BungeeCord-Nachricht fehlgeschlagen: " + e.getMessage());
}
});
}
}

View File

@@ -0,0 +1,99 @@
package de.serverpulse.spigot.utils;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandSender;
import java.text.DecimalFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class MsgUtil {
private static final DecimalFormat DF2 = new DecimalFormat("##.##");
private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");
// '─' (U+2500) ist in Minecraft's Standard-Schrift ca. 6px breit.
// Der Chat ist ~320px breit → ~53 Zeichen.
// Mit Prefix "[ServerPulse] " (~14 Zeichen) bleiben ~39 Zeichen übrig.
// Wir setzen LINE_LEN daher auf 38 damit es mit Prefix in eine Zeile passt.
private static final int LINE_LEN = 38;
private static final char LINE_CHAR = '─';
private MsgUtil() {}
// ── Farben ───────────────────────────────────────────────────────────────
public static String colorize(String s) {
return s == null ? "" : ChatColor.translateAlternateColorCodes('&', s);
}
public static void send(CommandSender sender, String msg) {
sender.sendMessage(colorize(msg));
}
// ── Zahlenformatierung ───────────────────────────────────────────────────
public static String formatTps(double tps) {
return colorize((tps >= 18 ? "&a" : tps >= 15 ? "&e" : "&c")
+ DF2.format(Math.min(20.0, tps)));
}
public static String formatMspt(double mspt) {
return colorize((mspt <= 40 ? "&a" : mspt <= 50 ? "&e" : "&c")
+ DF2.format(mspt) + "ms");
}
public static String formatRam(long used, long max) {
double pct = (double) used / max * 100;
return colorize((pct <= 75 ? "&a" : pct <= 90 ? "&e" : "&c")
+ used + "MB / " + max + "MB (" + (int) pct + "%)");
}
public static String formatCount(int val, int warn, int crit) {
return colorize((val >= crit ? "&c" : val >= warn ? "&e" : "&a") + val);
}
// ── Fortschrittsbalken ───────────────────────────────────────────────────
public static String bar(double val, double max, int len) {
int filled = (int) Math.min(Math.round((val / max) * len), len);
double pct = val / max * 100;
String col = pct <= 50 ? "&a" : pct <= 75 ? "&e" : "&c";
return colorize(col + "".repeat(filled) + "&8" + "".repeat(Math.max(0, len - filled)));
}
// ── Zeitstempel ──────────────────────────────────────────────────────────
public static String timestamp() {
return LocalDateTime.now().format(DT);
}
// ── Trennlinien ──────────────────────────────────────────────────────────
/**
* Einfache Trennlinie passt zur Chat-Breite mit Prefix.
*/
public static String separator() {
return colorize("&8" + String.valueOf(LINE_CHAR).repeat(LINE_LEN));
}
/**
* Zentrierter Header.
* Beispiel: ───── ServerPulse Status ─────
*
* Die Striche werden so berechnet, dass Titel + Striche = LINE_LEN ergibt.
* Wenn der Titel zu lang ist, werden mindestens 3 Striche pro Seite gesetzt.
*/
public static String header(String title) {
int available = LINE_LEN - title.length() - 2; // -2 für Leerzeichen
int left = Math.max(3, available / 2);
int right = Math.max(3, available - left);
String l = String.valueOf(LINE_CHAR);
return colorize(
"&8" + l.repeat(left) +
" &b&l" + title +
" &8" + l.repeat(right)
);
}
}

View File

@@ -0,0 +1,76 @@
package de.serverpulse.spigot.utils;
import de.serverpulse.spigot.SpigotPlugin;
import org.bukkit.configuration.file.FileConfiguration;
public class SpigotConfig {
private final SpigotPlugin plugin;
private FileConfiguration cfg;
public SpigotConfig(SpigotPlugin plugin) { this.plugin = plugin; }
public void load() { plugin.saveDefaultConfig(); plugin.reloadConfig(); this.cfg = plugin.getConfig(); }
public boolean isDebugMode() { return cfg.getBoolean("general.debug", false); }
public int getMetricsInterval() { return Math.max(10, cfg.getInt("general.metrics-interval", 30)); }
public int getEntityInterval() { return Math.max(10, cfg.getInt("general.entity-interval", 60)); }
public int getDataRetentionDays() { return cfg.getInt("general.data-retention-days", 90); }
public boolean isDatabaseEnabled() { return cfg.getBoolean("database.enabled", true); }
public String getDbHost() { return cfg.getString("database.host", "localhost"); }
public int getDbPort() { return cfg.getInt("database.port", 3306); }
public String getDbName() { return cfg.getString("database.database", "serverpulse"); }
public String getDbUser() { return cfg.getString("database.username", "root"); }
public String getDbPass() { return cfg.getString("database.password", ""); }
public int getPoolMaxSize() { return cfg.getInt("database.pool.maximum-pool-size", 10); }
public int getPoolMinIdle() { return cfg.getInt("database.pool.minimum-idle", 2); }
public long getPoolConnTimeout() { return cfg.getLong("database.pool.connection-timeout", 30000); }
public long getPoolIdleTimeout() { return cfg.getLong("database.pool.idle-timeout", 600000); }
public long getPoolMaxLifetime() { return cfg.getLong("database.pool.max-lifetime", 1800000); }
public boolean isDiscordEnabled() { return cfg.getBoolean("discord.enabled", false); }
public String getWebhookUrl() { return cfg.getString("discord.webhook-url", ""); }
public String getReportWebhookUrl() { String u = cfg.getString("discord.report-webhook-url",""); return (u==null||u.isEmpty()) ? getWebhookUrl() : u; }
public String getBotName() { return cfg.getString("discord.bot-name", "ServerPulse"); }
public String getBotAvatar() { return cfg.getString("discord.bot-avatar", ""); }
public boolean isDailyReportEnabled() { return cfg.getBoolean("discord.daily-report.enabled", true) && isDiscordEnabled(); }
public String getDailyReportTime() { return cfg.getString("discord.daily-report.time", "08:00"); }
public int getColorInfo() { return cfg.getInt("discord.colors.info", 3447003); }
public int getColorWarning() { return cfg.getInt("discord.colors.warning", 16776960); }
public int getColorCritical() { return cfg.getInt("discord.colors.critical", 16711680); }
public double getTpsWarning() { return cfg.getDouble("thresholds.performance.tps-warning", 18.0); }
public double getTpsCritical() { return cfg.getDouble("thresholds.performance.tps-critical", 15.0); }
public double getMsptWarning() { return cfg.getDouble("thresholds.performance.mspt-warning", 40.0); }
public double getMsptCritical() { return cfg.getDouble("thresholds.performance.mspt-critical", 50.0); }
public int getRamWarning() { return cfg.getInt("thresholds.performance.ram-warning", 75); }
public int getRamCritical() { return cfg.getInt("thresholds.performance.ram-critical", 90); }
public int getEntityTotalWarning() { return cfg.getInt("thresholds.entities.total-warning", 1000); }
public int getEntityTotalCritical() { return cfg.getInt("thresholds.entities.total-critical", 2000); }
public int getMonstersWarning() { return cfg.getInt("thresholds.entities.monsters-warning", 300); }
public int getMonstersCritical() { return cfg.getInt("thresholds.entities.monsters-critical", 600); }
public int getAnimalsWarning() { return cfg.getInt("thresholds.entities.animals-warning", 200); }
public int getAnimalsCritical() { return cfg.getInt("thresholds.entities.animals-critical", 400); }
public int getVillagersWarning() { return cfg.getInt("thresholds.entities.villagers-warning", 100); }
public int getVillagersCritical() { return cfg.getInt("thresholds.entities.villagers-critical", 200); }
public int getItemsWarning() { return cfg.getInt("thresholds.entities.items-warning", 150); }
public int getItemsCritical() { return cfg.getInt("thresholds.entities.items-critical", 300); }
public boolean isWorldMonitored(String w) { return cfg.getBoolean("worlds."+w+".enabled", true); }
public String getWorldDisplayName(String w) { return cfg.getString("worlds."+w+".display-name", w); }
public int getWorldMonstersWarning(String w) { return cfg.getInt("worlds."+w+".thresholds.monsters-warning", getMonstersWarning()); }
public int getWorldMonstersCritical(String w) { return cfg.getInt("worlds."+w+".thresholds.monsters-critical", getMonstersCritical()); }
public int getWorldTotalWarning(String w) { return cfg.getInt("worlds."+w+".thresholds.total-warning", getEntityTotalWarning()); }
public int getWorldTotalCritical(String w) { return cfg.getInt("worlds."+w+".thresholds.total-critical", getEntityTotalCritical()); }
public boolean isTrendAnalysisEnabled() { return cfg.getBoolean("trend-analysis.enabled", true); }
public int getTrendDataPoints() { return cfg.getInt("trend-analysis.data-points", 10); }
public double getTrendIncreaseThreshold() { return cfg.getDouble("trend-analysis.increase-threshold", 20.0); }
public int getTrendCheckInterval() { return cfg.getInt("trend-analysis.check-interval", 5); }
public boolean isItemClearEnabled() { return cfg.getBoolean("emergency-actions.item-clear.enabled", false); }
public boolean isItemClearBroadcast() { return cfg.getBoolean("emergency-actions.item-clear.broadcast", true); }
public String getItemClearMessage() { return cfg.getString("emergency-actions.item-clear.broadcast-message", "&c[ServerPulse] Item-Clear in {seconds}s!"); }
public int getItemClearCountdown() { return cfg.getInt("emergency-actions.item-clear.countdown-seconds", 30); }
public boolean isMobClearEnabled() { return cfg.getBoolean("emergency-actions.mob-clear.enabled", false); }
public boolean isMobClearHostileOnly() { return cfg.getBoolean("emergency-actions.mob-clear.hostile-only", true); }
public boolean isAutoDiagnosisEnabled() { return cfg.getBoolean("emergency-actions.auto-diagnosis.enabled", true); }
public int getAutoDiagnosisTrigger() { return cfg.getInt("emergency-actions.auto-diagnosis.trigger-count", 3); }
public String getPrefix() { return MsgUtil.colorize(cfg.getString("messages.prefix", "&8[&bServerPulse&8] &r")); }
public String getMessage(String key) { return MsgUtil.colorize(cfg.getString("messages."+key, "")); }
public FileConfiguration getCfg() { return cfg; }
}

View File

@@ -0,0 +1,347 @@
package de.serverpulse.utils;
import de.serverpulse.ServerPulse;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
/**
* Verwaltet alle Konfigurationsdateien von ServerPulse.
*/
public class ConfigManager {
private final ServerPulse plugin;
private FileConfiguration config;
public ConfigManager(ServerPulse plugin) {
this.plugin = plugin;
}
/**
* Lädt alle Konfigurationsdateien
*/
public void loadAll() {
plugin.saveDefaultConfig();
plugin.reloadConfig();
this.config = plugin.getConfig();
}
// ──────────────────────────────────────────
// ALLGEMEINE EINSTELLUNGEN
// ──────────────────────────────────────────
public boolean isDebugMode() {
return config.getBoolean("general.debug", false);
}
public int getMetricsInterval() {
return Math.max(10, config.getInt("general.metrics-interval", 30));
}
public int getEntityInterval() {
return Math.max(10, config.getInt("general.entity-interval", 60));
}
public int getDataRetentionDays() {
return config.getInt("general.data-retention-days", 90);
}
// ──────────────────────────────────────────
// DATENBANK
// ──────────────────────────────────────────
public boolean isDatabaseEnabled() {
return config.getBoolean("database.enabled", true);
}
public String getDbHost() {
return config.getString("database.host", "localhost");
}
public int getDbPort() {
return config.getInt("database.port", 3306);
}
public String getDbName() {
return config.getString("database.database", "serverpulse");
}
public String getDbUsername() {
return config.getString("database.username", "root");
}
public String getDbPassword() {
return config.getString("database.password", "");
}
public int getPoolMaxSize() {
return config.getInt("database.pool.maximum-pool-size", 10);
}
public int getPoolMinIdle() {
return config.getInt("database.pool.minimum-idle", 2);
}
public long getPoolConnectionTimeout() {
return config.getLong("database.pool.connection-timeout", 30000);
}
public long getPoolIdleTimeout() {
return config.getLong("database.pool.idle-timeout", 600000);
}
public long getPoolMaxLifetime() {
return config.getLong("database.pool.max-lifetime", 1800000);
}
// ──────────────────────────────────────────
// DISCORD
// ──────────────────────────────────────────
public boolean isDiscordEnabled() {
return config.getBoolean("discord.enabled", false);
}
public String getDiscordWebhookUrl() {
return config.getString("discord.webhook-url", "");
}
public String getDiscordReportWebhookUrl() {
String url = config.getString("discord.report-webhook-url", "");
return (url == null || url.isEmpty()) ? getDiscordWebhookUrl() : url;
}
public String getDiscordBotName() {
return config.getString("discord.bot-name", "ServerPulse");
}
public String getDiscordBotAvatar() {
return config.getString("discord.bot-avatar", "");
}
public boolean isDailyReportEnabled() {
return config.getBoolean("discord.daily-report.enabled", true) && isDiscordEnabled();
}
public String getDailyReportTime() {
return config.getString("discord.daily-report.time", "08:00");
}
public int getDiscordColorInfo() {
return config.getInt("discord.colors.info", 3447003);
}
public int getDiscordColorWarning() {
return config.getInt("discord.colors.warning", 16776960);
}
public int getDiscordColorCritical() {
return config.getInt("discord.colors.critical", 16711680);
}
public int getDiscordColorSuccess() {
return config.getInt("discord.colors.success", 65280);
}
// ──────────────────────────────────────────
// PERFORMANCE-GRENZWERTE
// ──────────────────────────────────────────
public double getTpsWarning() {
return config.getDouble("thresholds.performance.tps-warning", 18.0);
}
public double getTpsCritical() {
return config.getDouble("thresholds.performance.tps-critical", 15.0);
}
public double getMsptWarning() {
return config.getDouble("thresholds.performance.mspt-warning", 40.0);
}
public double getMsptCritical() {
return config.getDouble("thresholds.performance.mspt-critical", 50.0);
}
public int getRamWarning() {
return config.getInt("thresholds.performance.ram-warning", 75);
}
public int getRamCritical() {
return config.getInt("thresholds.performance.ram-critical", 90);
}
// ──────────────────────────────────────────
// ENTITY-GRENZWERTE (global)
// ──────────────────────────────────────────
public int getEntityTotalWarning() {
return config.getInt("thresholds.entities.total-warning", 1000);
}
public int getEntityTotalCritical() {
return config.getInt("thresholds.entities.total-critical", 2000);
}
public int getMonstersWarning() {
return config.getInt("thresholds.entities.monsters-warning", 300);
}
public int getMonstersCritical() {
return config.getInt("thresholds.entities.monsters-critical", 600);
}
public int getAnimalsWarning() {
return config.getInt("thresholds.entities.animals-warning", 200);
}
public int getAnimalsCritical() {
return config.getInt("thresholds.entities.animals-critical", 400);
}
public int getVillagersWarning() {
return config.getInt("thresholds.entities.villagers-warning", 100);
}
public int getVillagersCritical() {
return config.getInt("thresholds.entities.villagers-critical", 200);
}
public int getItemsWarning() {
return config.getInt("thresholds.entities.items-warning", 150);
}
public int getItemsCritical() {
return config.getInt("thresholds.entities.items-critical", 300);
}
// ──────────────────────────────────────────
// WELTBEZOGENE GRENZWERTE
// ──────────────────────────────────────────
public int getWorldMonstersWarning(String worldName) {
String path = "worlds." + worldName + ".thresholds.monsters-warning";
return config.getInt(path, getMonstersWarning());
}
public int getWorldMonstersCritical(String worldName) {
String path = "worlds." + worldName + ".thresholds.monsters-critical";
return config.getInt(path, getMonstersCritical());
}
public int getWorldTotalWarning(String worldName) {
String path = "worlds." + worldName + ".thresholds.total-warning";
return config.getInt(path, getEntityTotalWarning());
}
public int getWorldTotalCritical(String worldName) {
String path = "worlds." + worldName + ".thresholds.total-critical";
return config.getInt(path, getEntityTotalCritical());
}
public String getWorldDisplayName(String worldName) {
String path = "worlds." + worldName + ".display-name";
return config.getString(path, worldName);
}
public boolean isWorldMonitored(String worldName) {
String path = "worlds." + worldName + ".enabled";
// Wenn nicht explizit konfiguriert, wird die Welt standardmäßig überwacht
return config.getBoolean(path, true);
}
// ──────────────────────────────────────────
// TREND-ANALYSE
// ──────────────────────────────────────────
public boolean isTrendAnalysisEnabled() {
return config.getBoolean("trend-analysis.enabled", true);
}
public int getTrendDataPoints() {
return config.getInt("trend-analysis.data-points", 10);
}
public double getTrendIncreaseThreshold() {
return config.getDouble("trend-analysis.increase-threshold", 20.0);
}
public int getTrendCheckInterval() {
return config.getInt("trend-analysis.check-interval", 5);
}
// ──────────────────────────────────────────
// NOTFALLMASSNAHMEN
// ──────────────────────────────────────────
public boolean isItemClearEnabled() {
return config.getBoolean("emergency-actions.item-clear.enabled", false);
}
public boolean isItemClearBroadcast() {
return config.getBoolean("emergency-actions.item-clear.broadcast", true);
}
public String getItemClearBroadcastMessage() {
return config.getString("emergency-actions.item-clear.broadcast-message",
"&c[ServerPulse] Automatischer Item-Clear in {seconds} Sekunden!");
}
public int getItemClearCountdown() {
return config.getInt("emergency-actions.item-clear.countdown-seconds", 30);
}
public boolean isMobClearEnabled() {
return config.getBoolean("emergency-actions.mob-clear.enabled", false);
}
public boolean isMobClearHostileOnly() {
return config.getBoolean("emergency-actions.mob-clear.hostile-only", true);
}
public boolean isAutoDiagnosisEnabled() {
return config.getBoolean("emergency-actions.auto-diagnosis.enabled", true);
}
public int getAutoDiagnosisTriggerCount() {
return config.getInt("emergency-actions.auto-diagnosis.trigger-count", 3);
}
// ──────────────────────────────────────────
// REST API
// ──────────────────────────────────────────
public boolean isRestApiEnabled() {
return config.getBoolean("rest-api.enabled", false);
}
public int getRestApiPort() {
return config.getInt("rest-api.port", 8080);
}
public String getRestApiHost() {
return config.getString("rest-api.host", "0.0.0.0");
}
public String getRestApiKey() {
return config.getString("rest-api.api-key", "");
}
// ──────────────────────────────────────────
// NACHRICHTEN
// ──────────────────────────────────────────
public String getPrefix() {
return MessageUtil.colorize(config.getString("messages.prefix", "&8[&bServerPulse&8] &r"));
}
public String getMessage(String key) {
return MessageUtil.colorize(config.getString("messages." + key, ""));
}
public FileConfiguration getConfig() {
return config;
}
}

View File

@@ -0,0 +1,133 @@
package de.serverpulse.utils;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandSender;
import java.text.DecimalFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* Hilfsmethoden für Nachrichten und Formatierung.
*/
public class MessageUtil {
private static final DecimalFormat TPS_FORMAT = new DecimalFormat("##.##");
private static final DecimalFormat MSPT_FORMAT = new DecimalFormat("##.##");
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");
private MessageUtil() {}
/**
* Konvertiert Farb-Codes (&x) in Minecraft-Farben
*/
public static String colorize(String message) {
if (message == null) return "";
return ChatColor.translateAlternateColorCodes('&', message);
}
/**
* Sendet eine formatierte Nachricht an einen CommandSender
*/
public static void send(CommandSender sender, String message) {
sender.sendMessage(colorize(message));
}
/**
* Sendet eine Nachricht mit Prefix
*/
public static void sendWithPrefix(CommandSender sender, String prefix, String message) {
sender.sendMessage(colorize(prefix + message));
}
/**
* Formatiert TPS-Wert mit Farbe
*/
public static String formatTps(double tps) {
String formatted = TPS_FORMAT.format(Math.min(20.0, tps));
if (tps >= 18.0) return "&a" + formatted;
if (tps >= 15.0) return "&e" + formatted;
return "&c" + formatted;
}
/**
* Formatiert MSPT-Wert mit Farbe
*/
public static String formatMspt(double mspt) {
String formatted = MSPT_FORMAT.format(mspt) + "ms";
if (mspt <= 40.0) return "&a" + formatted;
if (mspt <= 50.0) return "&e" + formatted;
return "&c" + formatted;
}
/**
* Formatiert RAM-Auslastung mit Farbe
*/
public static String formatRam(long usedMB, long maxMB) {
double percent = (double) usedMB / maxMB * 100;
String formatted = usedMB + "MB / " + maxMB + "MB (" + (int) percent + "%)";
if (percent <= 75) return "&a" + formatted;
if (percent <= 90) return "&e" + formatted;
return "&c" + formatted;
}
/**
* Formatiert einen Entity-Wert gegen einen Grenzwert
*/
public static String formatEntityCount(int count, int warning, int critical) {
if (count >= critical) return "&c" + count;
if (count >= warning) return "&e" + count;
return "&a" + count;
}
/**
* Erstellt eine visuelle Fortschrittsleiste
*/
public static String progressBar(double value, double max, int length) {
int filled = (int) Math.round((value / max) * length);
filled = Math.min(filled, length);
StringBuilder bar = new StringBuilder();
double percent = (value / max) * 100;
String color = percent <= 50 ? "&a" : (percent <= 75 ? "&e" : "&c");
bar.append(color);
for (int i = 0; i < filled; i++) bar.append("");
bar.append("&7");
for (int i = filled; i < length; i++) bar.append("");
return colorize(bar.toString());
}
/**
* Gibt aktuellen Timestamp zurück
*/
public static String getTimestamp() {
return LocalDateTime.now().format(DATE_FORMAT);
}
/**
* Formatiert Bytes in lesbare Einheiten
*/
public static String formatBytes(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024));
return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
}
/**
* Erstellt eine Trennlinie
*/
public static String separator() {
return colorize("&8&m" + "".repeat(50));
}
/**
* Erstellt einen Header
*/
public static String header(String title) {
int padding = (48 - title.length()) / 2;
return colorize("&8&m" + "".repeat(padding) + "&r &b" + title + " &8&m" + "".repeat(padding));
}
}

View File

@@ -0,0 +1,12 @@
name: ServerPulse
version: '1.0.0'
main: de.serverpulse.bungee.BungeePlugin
author: ServerPulse-Team
description: ServerPulse BungeeCord Module (unified JAR)
commands:
bpulse:
description: ServerPulse BungeeCord monitoring
usage: /bpulse <status|network|servers|report|reload>
aliases: [ bsp, networkpulse ]
permission: serverpulse.bungee.use

View File

@@ -0,0 +1,169 @@
# ╔══════════════════════════════════════════╗
# ║ ServerPulse v1.0.0 Configuration ║
# ║ Unified JAR Spigot / Paper / Bungee ║
# ╚══════════════════════════════════════════╝
#
# Diese Datei gilt für BEIDE Seiten (Spigot + BungeeCord).
# Spigot-Server und BungeeCord-Proxy lesen ihre jeweiligen Abschnitte.
# ──────────────────────────────────────────
# ALLGEMEIN
# ──────────────────────────────────────────
general:
debug: false
metrics-interval: 30 # Sekunden (Spigot: Performance-Erfassung)
entity-interval: 60 # Sekunden (Spigot: Entity-Erfassung)
poll-interval: 30 # Sekunden (BungeeCord: Daten-Weiterleitung)
data-retention-days: 90 # Tage (0 = unbegrenzt, nur Spigot)
# ──────────────────────────────────────────
# MYSQL (nur Spigot-Seite)
# ──────────────────────────────────────────
database:
enabled: true
host: "localhost"
port: 3306
database: "serverpulse"
username: "root"
password: "password"
pool:
maximum-pool-size: 10
minimum-idle: 2
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# ──────────────────────────────────────────
# DISCORD (gilt für beide Seiten)
# ──────────────────────────────────────────
discord:
enabled: false
webhook-url: "https://discord.com/api/webhooks/YOUR_WEBHOOK"
report-webhook-url: ""
bot-name: "ServerPulse"
bot-avatar: ""
daily-report:
enabled: true
time: "08:00"
colors:
info: 3447003
warning: 16776960
critical: 16711680
success: 65280
# ──────────────────────────────────────────
# GRENZWERTE
# ──────────────────────────────────────────
thresholds:
performance:
tps-warning: 18.0
tps-critical: 15.0
mspt-warning: 40.0
mspt-critical: 50.0
ram-warning: 75
ram-critical: 90
entities:
total-warning: 1000
total-critical: 2000
monsters-warning: 300
monsters-critical: 600
animals-warning: 200
animals-critical: 400
villagers-warning: 100
villagers-critical: 200
items-warning: 150
items-critical: 300
network:
players-warning: 80 # % des Slot-Limits
players-critical: 95
# ──────────────────────────────────────────
# WELTBEZOGENE KONFIGURATION (Spigot)
# ──────────────────────────────────────────
worlds:
# Beispiel:
# world:
# enabled: true
# display-name: "Überwelt"
# thresholds:
# monsters-warning: 400
# monsters-critical: 800
# total-warning: 1500
# total-critical: 3000
# ──────────────────────────────────────────
# TREND-ANALYSE (Spigot)
# ──────────────────────────────────────────
trend-analysis:
enabled: true
data-points: 10
increase-threshold: 20
check-interval: 5
# ──────────────────────────────────────────
# NOTFALLMASSNAHMEN (Spigot)
# ──────────────────────────────────────────
emergency-actions:
item-clear:
enabled: false
broadcast: true
broadcast-message: "&c[ServerPulse] Automatischer Item-Clear in {seconds} Sekunden!"
countdown-seconds: 30
mob-clear:
enabled: false
hostile-only: true
broadcast: true
broadcast-message: "&c[ServerPulse] Automatischer Mob-Clear wird ausgeführt!"
auto-diagnosis:
enabled: true
trigger-count: 3
# ──────────────────────────────────────────
# CHAT-MONITORING (BungeeCord)
# ──────────────────────────────────────────
chat-monitoring:
enabled: true
suspicious-patterns:
- "(?i)(hack|cheat|xray|fly|kill ?aura)"
- "(?i)(buy .{0,20}rank|cheap .{0,20}coins)"
spam:
enabled: true
messages-per-second: 5
# ──────────────────────────────────────────
# REST API / GRAFANA (BungeeCord)
# ──────────────────────────────────────────
rest-api:
enabled: false
host: "0.0.0.0"
port: 8081
api-key: "YOUR_SECURE_API_KEY"
formats:
prometheus:
enabled: true
endpoint: "/metrics"
simple-json:
enabled: true
endpoint: "/grafana"
influxdb:
enabled: true
endpoint: "/influx"
measurement: "serverpulse"
push:
enabled: false
url: "http://localhost:8086"
token: "YOUR_INFLUX_TOKEN"
org: "YOUR_ORG"
bucket: "serverpulse"
json:
enabled: true
endpoint: "/api"
# ──────────────────────────────────────────
# NACHRICHTEN (Spigot)
# ──────────────────────────────────────────
messages:
prefix: "&8[&bServerPulse&8] &r"
no-permission: "&cDu hast keine Berechtigung für diesen Befehl."
reload-success: "&aKonfiguration wurde erfolgreich neu geladen."
database-error: "&cDatenbankfehler! Bitte prüfe die Logs."

View File

@@ -0,0 +1,28 @@
name: ServerPulse
version: '1.0.0'
main: de.serverpulse.spigot.SpigotPlugin
api-version: '1.21'
description: Monitoring & Analytics Suite (Spigot/Paper/BungeeCord unified)
authors: [ M_Viper ]
prefix: ServerPulse
commands:
serverpulse:
description: ServerPulse main command
usage: /serverpulse <status|report|world|entities|reload|debug|clear>
aliases: [ pulse ]
permission: serverpulse.use
permissions:
serverpulse.use:
default: op
serverpulse.admin:
default: op
children:
serverpulse.use: true
serverpulse.status: { default: op }
serverpulse.report: { default: op }
serverpulse.world: { default: op }
serverpulse.entities: { default: op }
serverpulse.reload: { default: op }
serverpulse.debug: { default: op }