Note: there is intentionally no {@code onDeath}/{@code EntityDeathEvent} handler here. + * Kill events are still handled by the pre-existing {@code DeathEventLogger} to avoid + * double-logging the same kill as both a CombatEvent and a DeathEvent. + */ +public class CombatEventLogger extends AbstractEventLogger implements Listener { + + private static final long THROTTLE_MS = 1000L; + private final EventThrottle throttle = new EventThrottle(THROTTLE_MS); + + public CombatEventLogger(Properties props) { + super(props); + } + + /** Throttled per-victim: damage events can fire many times per second. */ + @EventHandler + public void onDamage(EntityDamageByEntityEvent event) { + if (!(event.getEntity() instanceof Player)) { + return; + } + Player victim = (Player) event.getEntity(); + if (!throttle.allow("dmg:" + victim.getName())) { + return; + } + LoggableCombatEvent loggable = new LoggableCombatEvent( + CombatAction.DAMAGE, victim.getWorld().getTime(), victim.getWorld().getName(), + locationAsPoint(victim.getLocation())); + loggable.setVictim(victim.getName()) + .setSource(event.getDamager().getType().toString()) + .setAmount(event.getFinalDamage()) + .setCause(event.getCause().toString()) + .setHealthRemaining(victim.getHealth()); + logAndSend(loggable); + } + + /** Throttled per-player: regen-based healing (e.g. saturation) can fire frequently. */ + @EventHandler + public void onRegainHealth(EntityRegainHealthEvent event) { + if (!(event.getEntity() instanceof Player)) { + return; + } + Player player = (Player) event.getEntity(); + if (!throttle.allow("heal:" + player.getName())) { + return; + } + LoggableCombatEvent loggable = new LoggableCombatEvent( + CombatAction.HEAL, player.getWorld().getTime(), player.getWorld().getName(), + locationAsPoint(player.getLocation())); + loggable.setVictim(player.getName()) + .setAmount(event.getAmount()) + .setCause(event.getRegainReason().toString()) + .setHealthRemaining(player.getHealth()); + logAndSend(loggable); + } + + /** Throttled per-player: food level changes frequently while eating, sprinting, etc. */ + @EventHandler + public void onFoodLevelChange(FoodLevelChangeEvent event) { + if (!(event.getEntity() instanceof Player)) { + return; + } + Player player = (Player) event.getEntity(); + if (!throttle.allow("food:" + player.getName())) { + return; + } + LoggableCombatEvent loggable = new LoggableCombatEvent( + CombatAction.HUNGER, player.getWorld().getTime(), player.getWorld().getName(), + locationAsPoint(player.getLocation())); + loggable.setVictim(player.getName()).setFoodLevel(event.getFoodLevel()); + logAndSend(loggable); + } + + /** Not throttled: respawn is a rare, deliberate-trigger event for a given player. */ + @EventHandler + public void onRespawn(PlayerRespawnEvent event) { + LoggableCombatEvent loggable = new LoggableCombatEvent( + CombatAction.RESPAWN, event.getPlayer().getWorld().getTime(), + event.getPlayer().getWorld().getName(), locationAsPoint(event.getRespawnLocation())); + loggable.setVictim(event.getPlayer().getName()); + logAndSend(loggable); + } +} diff --git a/spigot/src/main/java/com/splunk/spigot/eventloggers/ItemEventLogger.java b/spigot/src/main/java/com/splunk/spigot/eventloggers/ItemEventLogger.java new file mode 100644 index 0000000..60c13f5 --- /dev/null +++ b/spigot/src/main/java/com/splunk/spigot/eventloggers/ItemEventLogger.java @@ -0,0 +1,60 @@ +package com.splunk.spigot.eventloggers; + +import static com.splunk.spigot.LogToSplunkPlugin.locationAsPoint; + +import java.util.Properties; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.player.PlayerDropItemEvent; + +import com.splunk.sharedmc.event_loggers.AbstractEventLogger; +import com.splunk.sharedmc.loggable_events.LoggableItemEvent; +import com.splunk.sharedmc.loggable_events.LoggableItemEvent.ItemAction; +import com.splunk.sharedmc.util.EventThrottle; + +/** + * Logs item pickup and drop events. Pickup is throttled (it can fire rapidly, e.g. when + * picking up XP orbs or arrows); drop is not throttled since it is a deliberate, low + * frequency player action. + */ +public class ItemEventLogger extends AbstractEventLogger implements Listener { + + private final EventThrottle throttle = new EventThrottle(1000L); + + public ItemEventLogger(Properties props) { + super(props); + } + + @EventHandler + public void onPickup(EntityPickupItemEvent event) { + if (!(event.getEntity() instanceof Player)) { + return; + } + Player player = (Player) event.getEntity(); + if (!throttle.allow("pickup:" + player.getName())) { + return; + } + LoggableItemEvent loggable = new LoggableItemEvent( + ItemAction.PICKUP, player.getWorld().getTime(), player.getWorld().getName(), + locationAsPoint(player.getLocation())); + loggable.setPlayerName(player.getName()) + .setItem(event.getItem().getItemStack().getType().toString()) + .setQuantity(event.getItem().getItemStack().getAmount()); + logAndSend(loggable); + } + + @EventHandler + public void onDrop(PlayerDropItemEvent event) { + Player player = event.getPlayer(); + LoggableItemEvent loggable = new LoggableItemEvent( + ItemAction.DROP, player.getWorld().getTime(), player.getWorld().getName(), + locationAsPoint(player.getLocation())); + loggable.setPlayerName(player.getName()) + .setItem(event.getItemDrop().getItemStack().getType().toString()) + .setQuantity(event.getItemDrop().getItemStack().getAmount()); + logAndSend(loggable); + } +} diff --git a/spigot/src/main/java/com/splunk/spigot/eventloggers/PerformanceSampler.java b/spigot/src/main/java/com/splunk/spigot/eventloggers/PerformanceSampler.java new file mode 100644 index 0000000..dd369d8 --- /dev/null +++ b/spigot/src/main/java/com/splunk/spigot/eventloggers/PerformanceSampler.java @@ -0,0 +1,38 @@ +package com.splunk.spigot.eventloggers; + +import java.util.Properties; + +import org.bukkit.Bukkit; + +import com.splunk.sharedmc.loggable_events.LoggablePerformanceEvent; +import com.splunk.spigot.scheduling.ScheduledMetricLogger; + +/** + * Periodically samples server performance (online players, loaded chunks). Extends + * {@link ScheduledMetricLogger}, so this is polling-based rather than event-driven. + * + *
NOTE: {@code Bukkit.getTPS()} and {@code Bukkit.getAverageTickTime()} are + * Paper-only APIs and are not present on vanilla spigot-api (verified via + * {@code javap} against spigot-api 1.21.10 — no {@code getTPS}/{@code AverageTickTime} + * symbols found on {@code org.bukkit.Bukkit}). TPS/MSPT sampling is therefore omitted; + * only online_players and loaded_chunks, which are available on the Bukkit API, are + * sampled here. + */ +public class PerformanceSampler extends ScheduledMetricLogger { + + public PerformanceSampler(Properties props) { + super(props); + } + + @Override + protected void sample() { + LoggablePerformanceEvent e = new LoggablePerformanceEvent(0L); + e.setOnlinePlayers(Bukkit.getOnlinePlayers().size()); + int loaded = 0; + for (org.bukkit.World w : Bukkit.getWorlds()) { + loaded += w.getLoadedChunks().length; + } + e.setLoadedChunks(loaded); + logAndSend(e); + } +} diff --git a/spigot/src/main/java/com/splunk/spigot/eventloggers/PlayerEventLogger.java b/spigot/src/main/java/com/splunk/spigot/eventloggers/PlayerEventLogger.java index c68e170..bf75d26 100644 --- a/spigot/src/main/java/com/splunk/spigot/eventloggers/PlayerEventLogger.java +++ b/spigot/src/main/java/com/splunk/spigot/eventloggers/PlayerEventLogger.java @@ -13,6 +13,12 @@ import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerAdvancementDoneEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.event.player.PlayerGameModeChangeEvent; +import org.bukkit.event.player.PlayerBedEnterEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -57,8 +63,13 @@ public PlayerEventLogger(Properties props) { */ @EventHandler public void onPlayerConnect(PlayerLoginEvent event) { - logAndSend( - generateLoggablePlayerEvent(event, PlayerEventAction.PLAYER_CONNECT, null, event.getKickMessage())); + LoggablePlayerEvent loggable = + generateLoggablePlayerEvent(event, PlayerEventAction.PLAYER_CONNECT, null, event.getKickMessage()); + loggable.setPlayerUuid(event.getPlayer().getUniqueId().toString()); + if (isEnabled(ENABLE_SESSION_IP) && event.getAddress() != null) { + loggable.setPlayerIp(event.getAddress().getHostAddress()); + } + logAndSend(loggable); } /** @@ -119,4 +130,98 @@ private LoggablePlayerEvent generateLoggablePlayerEvent( return loggable; } + + /** + * Logs player chat messages to Splunk. + * + * @param event The captured event. + */ + @EventHandler + public void onPlayerChat(AsyncPlayerChatEvent event) { + logAndSend( + generateLoggablePlayerEvent(event, PlayerEventAction.CHAT, null, event.getMessage())); + } + + /** + * Logs player advancement unlocks to Splunk. + * + * @param event The captured event. + */ + @EventHandler + public void onPlayerAdvancementDone(PlayerAdvancementDoneEvent event) { + // We only want to log real achievements/advancements, not recipe unlocks. + String key = event.getAdvancement().getKey().toString(); + if (key.startsWith("minecraft:recipes/")) { + return; + } + logAndSend( + generateLoggablePlayerEvent(event, PlayerEventAction.ADVANCEMENT, null, key)); + } + + /** + * Logs player teleports to Splunk. Returns early via the + * {@code isEnabled(ENABLE_SESSION_DETAIL)} guard — this category is opt-in. + * + * @param event The captured event. + */ + @EventHandler + public void onTeleport(PlayerTeleportEvent event) { + if (!isEnabled(ENABLE_SESSION_DETAIL)) { + return; + } + LoggablePlayerEvent loggable = generateLoggablePlayerEvent( + event, PlayerEventAction.TELEPORT, event.getCause().toString(), null); + loggable.setFrom(locationAsPoint(event.getFrom())); + loggable.setTo(locationAsPoint(event.getTo())); + logAndSend(loggable); + } + + /** + * Logs player game mode changes to Splunk. Returns early via the + * {@code isEnabled(ENABLE_SESSION_DETAIL)} guard — this category is opt-in. + * + * @param event The captured event. + */ + @EventHandler + public void onGameModeChange(PlayerGameModeChangeEvent event) { + if (!isEnabled(ENABLE_SESSION_DETAIL)) { + return; + } + LoggablePlayerEvent loggable = generateLoggablePlayerEvent( + event, PlayerEventAction.GAMEMODE_CHANGE, null, null); + loggable.setGamemode(event.getNewGameMode().toString()); + logAndSend(loggable); + } + + /** + * Logs when a player enters a bed. Returns early via the + * {@code isEnabled(ENABLE_SESSION_DETAIL)} guard — this category is opt-in. + * + * @param event The captured event. + */ + @EventHandler + public void onBedEnter(PlayerBedEnterEvent event) { + if (!isEnabled(ENABLE_SESSION_DETAIL)) { + return; + } + LoggablePlayerEvent loggable = generateLoggablePlayerEvent( + event, PlayerEventAction.BED_ENTER, null, null); + logAndSend(loggable); + } + + /** + * Logs when a player changes worlds. Returns early via the + * {@code isEnabled(ENABLE_SESSION_DETAIL)} guard — this category is opt-in. + * + * @param event The captured event. + */ + @EventHandler + public void onWorldChange(PlayerChangedWorldEvent event) { + if (!isEnabled(ENABLE_SESSION_DETAIL)) { + return; + } + LoggablePlayerEvent loggable = generateLoggablePlayerEvent( + event, PlayerEventAction.WORLD_CHANGE, null, event.getFrom().getName()); + logAndSend(loggable); + } } diff --git a/spigot/src/main/java/com/splunk/spigot/eventloggers/ProgressionEventLogger.java b/spigot/src/main/java/com/splunk/spigot/eventloggers/ProgressionEventLogger.java new file mode 100644 index 0000000..07fb58f --- /dev/null +++ b/spigot/src/main/java/com/splunk/spigot/eventloggers/ProgressionEventLogger.java @@ -0,0 +1,80 @@ +package com.splunk.spigot.eventloggers; + +import java.util.Properties; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.enchantment.EnchantItemEvent; +import org.bukkit.event.inventory.CraftItemEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerExpChangeEvent; +import org.bukkit.event.player.PlayerFishEvent; +import org.bukkit.event.player.PlayerLevelChangeEvent; +import org.bukkit.entity.Player; + +import com.splunk.sharedmc.event_loggers.AbstractEventLogger; +import com.splunk.sharedmc.loggable_events.LoggableProgressionEvent; +import com.splunk.sharedmc.loggable_events.LoggableProgressionEvent.ProgressionAction; + +/** + * Logs progression and activity: XP, level, enchant, craft, fish, command. + */ +public class ProgressionEventLogger extends AbstractEventLogger implements Listener { + + public ProgressionEventLogger(Properties props) { + super(props); + } + + private LoggableProgressionEvent base(ProgressionAction action, Player player) { + LoggableProgressionEvent e = new LoggableProgressionEvent( + action, player.getWorld().getTime(), player.getWorld().getName()); + e.setPlayerName(player.getName()); + return e; + } + + @EventHandler + public void onExpChange(PlayerExpChangeEvent event) { + LoggableProgressionEvent e = base(ProgressionAction.EXP_CHANGE, event.getPlayer()); + e.setExpAmount(event.getAmount()); + logAndSend(e); + } + + @EventHandler + public void onLevelChange(PlayerLevelChangeEvent event) { + LoggableProgressionEvent e = base(ProgressionAction.LEVEL_CHANGE, event.getPlayer()); + e.setNewLevel(event.getNewLevel()); + logAndSend(e); + } + + @EventHandler + public void onEnchant(EnchantItemEvent event) { + LoggableProgressionEvent e = base(ProgressionAction.ENCHANT, event.getEnchanter()); + e.setDetail(event.getItem().getType().toString()); + logAndSend(e); + } + + @EventHandler + public void onCraft(CraftItemEvent event) { + if (!(event.getWhoClicked() instanceof Player)) { + return; + } + Player player = (Player) event.getWhoClicked(); + LoggableProgressionEvent e = base(ProgressionAction.CRAFT, player); + e.setDetail(event.getRecipe().getResult().getType().toString()); + logAndSend(e); + } + + @EventHandler + public void onFish(PlayerFishEvent event) { + LoggableProgressionEvent e = base(ProgressionAction.FISH, event.getPlayer()); + e.setDetail(event.getState().toString()); + logAndSend(e); + } + + @EventHandler + public void onCommand(PlayerCommandPreprocessEvent event) { + LoggableProgressionEvent e = base(ProgressionAction.COMMAND, event.getPlayer()); + e.setDetail(event.getMessage()); + logAndSend(e); + } +} diff --git a/spigot/src/main/java/com/splunk/spigot/eventloggers/ServerEventLogger.java b/spigot/src/main/java/com/splunk/spigot/eventloggers/ServerEventLogger.java new file mode 100644 index 0000000..41a61d6 --- /dev/null +++ b/spigot/src/main/java/com/splunk/spigot/eventloggers/ServerEventLogger.java @@ -0,0 +1,37 @@ +package com.splunk.spigot.eventloggers; + +import java.util.Properties; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.server.ServerLoadEvent; +import org.bukkit.event.weather.WeatherChangeEvent; + +import com.splunk.sharedmc.event_loggers.AbstractEventLogger; +import com.splunk.sharedmc.loggable_events.LoggableServerEvent; +import com.splunk.sharedmc.loggable_events.LoggableServerEvent.ServerAction; + +/** + * Logs server lifecycle and world-state events. + */ +public class ServerEventLogger extends AbstractEventLogger implements Listener { + + public ServerEventLogger(Properties props) { + super(props); + } + + @EventHandler + public void onServerLoad(ServerLoadEvent event) { + LoggableServerEvent e = new LoggableServerEvent(ServerAction.SERVER_START, 0L, null); + e.setMotd(event.getType().toString()); + logAndSend(e); + } + + @EventHandler + public void onWeatherChange(WeatherChangeEvent event) { + LoggableServerEvent e = new LoggableServerEvent( + ServerAction.WEATHER_CHANGE, event.getWorld().getTime(), event.getWorld().getName()); + e.setWeather(event.toWeatherState() ? "storm" : "clear"); + logAndSend(e); + } +} diff --git a/spigot/src/main/java/com/splunk/spigot/scheduling/ScheduledMetricLogger.java b/spigot/src/main/java/com/splunk/spigot/scheduling/ScheduledMetricLogger.java new file mode 100644 index 0000000..7d0cd1d --- /dev/null +++ b/spigot/src/main/java/com/splunk/spigot/scheduling/ScheduledMetricLogger.java @@ -0,0 +1,36 @@ +package com.splunk.spigot.scheduling; + +import java.util.Properties; + +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; + +import com.splunk.sharedmc.event_loggers.AbstractEventLogger; + +/** + * Base for loggers driven by the Bukkit scheduler rather than the event bus. + * Subclasses implement {@link #sample()}; {@link #start(Plugin, long)} schedules it. + */ +public abstract class ScheduledMetricLogger extends AbstractEventLogger { + + public ScheduledMetricLogger(Properties props) { + super(props); + } + + /** Called on each scheduled tick. Build and send the metric event(s) here. */ + protected abstract void sample(); + + /** Schedules {@link #sample()} every {@code intervalTicks} ticks. */ + public void start(Plugin plugin, long intervalTicks) { + new BukkitRunnable() { + @Override + public void run() { + try { + sample(); + } catch (Exception e) { + logger.warn("Scheduled metric sample failed", e); + } + } + }.runTaskTimer(plugin, intervalTicks, intervalTicks); + } +}