Upload folder via GUI - src
This commit is contained in:
194
src/main/java/de/serverpulse/ServerPulse.java
Normal file
194
src/main/java/de/serverpulse/ServerPulse.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
214
src/main/java/de/serverpulse/alerts/AlertManager.java
Normal file
214
src/main/java/de/serverpulse/alerts/AlertManager.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
188
src/main/java/de/serverpulse/api/RestApiServer.java
Normal file
188
src/main/java/de/serverpulse/api/RestApiServer.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/main/java/de/serverpulse/bungee/BungeePlugin.java
Normal file
105
src/main/java/de/serverpulse/bungee/BungeePlugin.java
Normal 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; }
|
||||||
|
}
|
||||||
454
src/main/java/de/serverpulse/bungee/api/GrafanaApiServer.java
Normal file
454
src/main/java/de/serverpulse/bungee/api/GrafanaApiServer.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/main/java/de/serverpulse/bungee/api/InfluxPushService.java
Normal file
106
src/main/java/de/serverpulse/bungee/api/InfluxPushService.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
179
src/main/java/de/serverpulse/bungee/commands/BungeeCommand.java
Normal file
179
src/main/java/de/serverpulse/bungee/commands/BungeeCommand.java
Normal 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) {}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/main/java/de/serverpulse/bungee/models/ServerData.java
Normal file
109
src/main/java/de/serverpulse/bungee/models/ServerData.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/main/java/de/serverpulse/bungee/utils/BungeeConfig.java
Normal file
103
src/main/java/de/serverpulse/bungee/utils/BungeeConfig.java
Normal 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)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
429
src/main/java/de/serverpulse/commands/ServerPulseCommand.java
Normal file
429
src/main/java/de/serverpulse/commands/ServerPulseCommand.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
446
src/main/java/de/serverpulse/database/DatabaseManager.java
Normal file
446
src/main/java/de/serverpulse/database/DatabaseManager.java
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
270
src/main/java/de/serverpulse/discord/DiscordWebhook.java
Normal file
270
src/main/java/de/serverpulse/discord/DiscordWebhook.java
Normal 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("_", " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/main/java/de/serverpulse/listeners/PlayerListener.java
Normal file
32
src/main/java/de/serverpulse/listeners/PlayerListener.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main/java/de/serverpulse/models/AlertSeverity.java
Normal file
13
src/main/java/de/serverpulse/models/AlertSeverity.java
Normal 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; }
|
||||||
|
}
|
||||||
34
src/main/java/de/serverpulse/models/EntitySnapshot.java
Normal file
34
src/main/java/de/serverpulse/models/EntitySnapshot.java
Normal 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; }
|
||||||
|
}
|
||||||
28
src/main/java/de/serverpulse/models/PerformanceSnapshot.java
Normal file
28
src/main/java/de/serverpulse/models/PerformanceSnapshot.java
Normal 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; }
|
||||||
|
}
|
||||||
212
src/main/java/de/serverpulse/monitoring/EntityMonitor.java
Normal file
212
src/main/java/de/serverpulse/monitoring/EntityMonitor.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
213
src/main/java/de/serverpulse/monitoring/PerformanceMonitor.java
Normal file
213
src/main/java/de/serverpulse/monitoring/PerformanceMonitor.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
200
src/main/java/de/serverpulse/monitoring/TrendAnalyzer.java
Normal file
200
src/main/java/de/serverpulse/monitoring/TrendAnalyzer.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/java/de/serverpulse/network/NetworkMessage.java
Normal file
21
src/main/java/de/serverpulse/network/NetworkMessage.java
Normal 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() {}
|
||||||
|
}
|
||||||
103
src/main/java/de/serverpulse/spigot/SpigotPlugin.java
Normal file
103
src/main/java/de/serverpulse/spigot/SpigotPlugin.java
Normal 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; }
|
||||||
|
}
|
||||||
219
src/main/java/de/serverpulse/spigot/alerts/AlertManager.java
Normal file
219
src/main/java/de/serverpulse/spigot/alerts/AlertManager.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
429
src/main/java/de/serverpulse/spigot/commands/SpigotCommand.java
Normal file
429
src/main/java/de/serverpulse/spigot/commands/SpigotCommand.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("_", " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/main/java/de/serverpulse/spigot/network/SpigotMessenger.java
Normal file
129
src/main/java/de/serverpulse/spigot/network/SpigotMessenger.java
Normal 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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/main/java/de/serverpulse/spigot/utils/MsgUtil.java
Normal file
99
src/main/java/de/serverpulse/spigot/utils/MsgUtil.java
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/main/java/de/serverpulse/spigot/utils/SpigotConfig.java
Normal file
76
src/main/java/de/serverpulse/spigot/utils/SpigotConfig.java
Normal 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; }
|
||||||
|
}
|
||||||
347
src/main/java/de/serverpulse/utils/ConfigManager.java
Normal file
347
src/main/java/de/serverpulse/utils/ConfigManager.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/main/java/de/serverpulse/utils/MessageUtil.java
Normal file
133
src/main/java/de/serverpulse/utils/MessageUtil.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/resources/bungee.yml
Normal file
12
src/main/resources/bungee.yml
Normal 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
|
||||||
169
src/main/resources/config.yml
Normal file
169
src/main/resources/config.yml
Normal 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."
|
||||||
28
src/main/resources/plugin.yml
Normal file
28
src/main/resources/plugin.yml
Normal 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 }
|
||||||
Reference in New Issue
Block a user