From 450edf991a21ae5dcd93097aa589059b85296b0b Mon Sep 17 00:00:00 2001 From: splunk Date: Mon, 22 Jun 2026 12:58:01 -0400 Subject: [PATCH 1/3] docs: design spec for extended logging + dependency modernization --- ...xtend-logging-and-modernize-deps-design.md | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-22-extend-logging-and-modernize-deps-design.md diff --git a/docs/superpowers/specs/2026-06-22-extend-logging-and-modernize-deps-design.md b/docs/superpowers/specs/2026-06-22-extend-logging-and-modernize-deps-design.md new file mode 100644 index 0000000..dee97fe --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-extend-logging-and-modernize-deps-design.md @@ -0,0 +1,143 @@ +# Design: Extend Logging Capabilities + Modernize Dependencies + +**Date:** 2026-06-22 +**Status:** Approved (design) +**Scope:** `shared-mc`, `spigot` modules. `forge` excluded (dead 11 years). `logtosplunk-plugin` packaging follows. + +## Problem + +The Splunk-for-Minecraft plugin logs only a thin slice of server activity (player +connect/disconnect/move/chat/advancement, block place/break, death). Before deploying to +other Minecraft servers we need: + +1. **Richer telemetry** about the server and players (combat, survival, economy, + progression, session detail, server lifecycle/performance). +2. **A clean, modern, CVE-free dependency baseline** — the build currently mixes ancient + and current libraries and targets Java 1.8. + +## Goals + +- Add new loggable event categories without changing the existing event contract. +- Keep every new high-volume event opt-in and throttled so the plugin is safe to run on + busy public servers. +- Land a modern, scanned, reproducible dependency set targeting Java 21. + +## Non-goals + +- Reviving the `forge` module (Minecraft ~1.7-era, untouched 11 years). It stays in the + tree but is excluded from the reactor build. A separate future milestone may address it. +- A generic, config-driven event framework (rejected — YAGNI for ~15 event types). +- Changing the Splunk transport (`SingleSplunkConnection`) or HEC protocol. + +## Platform decisions + +| Decision | Choice | Reason | +|---|---|---| +| Target platform | Spigot/Paper (`spigot-api 1.21.10`) | Only live adapter; deploy target | +| Compile target | Java 21 | Paper 1.21.x requires Java 21 runtime; no value in 17 | +| Forge | Excluded from build | Dead 11 years; near-total rewrite to revive | + +## Architecture + +Preserve the existing pattern. shared-mc holds platform-agnostic data carriers +(`Loggable*Event` extending `AbstractLoggableEvent`); spigot holds Bukkit `Listener` +loggers extending `AbstractEventLogger`. Each logger is registered in +`LogToSplunkPlugin.onEnable` and gated by a `splunk.properties` toggle. + +One new abstraction: a **`ScheduledMetricLogger`** base in shared-mc (or spigot) for +metrics that are not event-driven (TPS, MSPT, online player count). It is driven by the +Bukkit scheduler rather than the event bus. + +### Workstream 1 — Dependency modernization (poms + build) + +Largely independent of Workstream 2; touches build files and one shared-mc serialization +change. + +1. **CVE baseline.** Add and run OWASP `dependency-check-maven` across the reactor. + Capture a report; record any High/Critical findings. +2. **Java 1.8 → 21.** Update `maven-compiler-plugin` to a current version and set + `21` in the parent pom. Verify no source incompatibilities. +3. **Unify `splunk-library-javalogging`.** Replace `1.0.1` in `logtosplunk-plugin` with + `1.11.8` (already used by `shared-mc`). Manage the version centrally in parent + `dependencyManagement`. `forge` carries the same stale `1.0.1` but is out of the + reactor, so its pom is left untouched (cleaned up only if forge is ever revived). +4. **Drop `json-simple 1.1`.** Replace its use in `SplunkCimLogEvent` / `toJson()` with + the already-present `gson 2.13.2`. This is the only Workstream-1 change that touches + `shared-mc` Java source and the one ordering dependency for Workstream 2. +5. **Bump build plugins.** `maven-compiler-plugin`, `maven-shade-plugin`, + `maven-clean-plugin`, `maven-install-plugin`, `exec-maven-plugin`, `junit` (4.8.2 → + 4.13.2) to current stable versions. +6. **Exclude forge.** Remove `forge` from the parent reactor (leave the + directory and its pom intact). + +**Exit gate:** `mvn clean package` green; dependency-check shows no unsuppressed +High/Critical CVEs; the shaded plugin jar builds. + +### Workstream 2 — Logging extension (shared-mc + spigot) + +New `LoggableEventType` enum values and data classes in shared-mc, paired with Bukkit +`Listener` loggers in spigot. + +| Category | New `LoggableEventType` | New shared-mc class(es) | Bukkit hooks | +|---|---|---|---| +| Server lifecycle / performance | `SERVER`, `PERFORMANCE` | `LoggableServerEvent`, `LoggablePerformanceEvent` | `ServerLoadEvent`, plugin disable, `WeatherChangeEvent`; scheduled TPS / MSPT / player-count sampler | +| Combat & survival | `COMBAT` | `LoggableCombatEvent` | `EntityDamageByEntityEvent`, `EntityDeathEvent`, `EntityRegainHealthEvent`, `FoodLevelChangeEvent`, `PlayerRespawnEvent` | +| Economy & progression | `ITEM`, `PROGRESSION` | `LoggableItemEvent`, `LoggableProgressionEvent` | `EntityPickupItemEvent`, `PlayerDropItemEvent`, `PlayerExpChangeEvent`, `PlayerLevelChangeEvent`, `EnchantItemEvent`, `CraftItemEvent`, `PlayerFishEvent`, `PlayerCommandPreprocessEvent` | +| Session detail | extend `LoggablePlayerEvent` + new `PlayerEventAction` values | (extend existing) | `PlayerTeleportEvent`, `PlayerGameModeChangeEvent`, `PlayerBedEnterEvent`, `PlayerChangedWorldEvent`; enrich login with IP / UUID / protocol version | + +### Cross-cutting concerns + +- **Per-category enable toggles** in `splunk.properties`, read in `AbstractEventLogger` / + plugin `onEnable`. Keys follow the existing `splunk.craft.enable.*` convention. New + high-volume categories (combat, item, performance sampling) default to **off** so an + upgrade does not silently flood a server's HEC. +- **Throttling** for high-frequency events (damage, food, item pickup) reusing the + guava-cache + granularity pattern already in `PlayerEventLogger` (per-player, time- or + distance-bounded). Performance sampler runs on a fixed tick interval (configurable, + default e.g. every 600 ticks / 30 s). +- **Splunk CIM mapping** preserved via `SplunkCimLogEvent`; new fields map to CIM where a + standard field exists. +- **PII note:** session detail logs player IP. Document this in the plugin config and gate + it behind its own toggle (default off) so operators opt in deliberately. + +## Components & isolation + +- `shared-mc/loggable_events/*` — pure data carriers, no Bukkit dependency, unit-testable + in isolation (construct, set fields, assert `toJson()` / CIM output). +- `spigot/eventloggers/*` — Bukkit `Listener`s, one per category, each translating Bukkit + events into shared-mc loggables. Depend on shared-mc + Bukkit API only. +- `ScheduledMetricLogger` — owns the scheduler task; depends on the Splunk connection and a + metric source. Testable by invoking its sample method directly. +- `LogToSplunkPlugin.onEnable` — wiring only: read toggles, register enabled listeners, + start the sampler if enabled. + +## Error handling + +- Listeners must never throw into the Bukkit event bus. Wrap loggable construction + + send in try/catch; on failure log a warning and drop the event (current behavior of + `AbstractEventLogger.logAndSend` via the connection's async send is preserved). +- Missing/invalid config falls back to safe defaults (categories off, sampler off) and + logs a warning, matching the existing properties-load fallback. + +## Testing + +- Unit tests per new `Loggable*Event`: field population and `toJson()` / CIM output. +- Throttle logic test (guava-cache eviction / granularity) for the high-volume path. +- `ScheduledMetricLogger` sample method test with a stubbed metric source. +- Build-level: `mvn clean package` green; dependency-check gate; shaded jar smoke-loads. + +## Sequencing + +1. **Workstream 1 first to green.** The `json-simple → gson` swap in `shared-mc` `toJson()` + is the foundation Workstream 2's new classes serialize through; the Java 21 bump affects + all modules. +2. **Workstream 2 in parallel after the shared-mc serialization change merges.** New event + classes and loggers have near-zero file overlap with the remaining Workstream-1 pom + edits, so a second agent can scaffold them concurrently. + +## Open risks + +- Paper API method availability: a few Bukkit hooks (e.g. accurate TPS) may require + Paper-specific APIs vs vanilla Spigot. The implementation agent verifies against + `spigot-api 1.21.10` and falls back to a manual tick-time sampler if needed. +- `EnchantItemEvent` / `CraftItemEvent` field richness varies; map only stable fields. From 1ad1be16bab7a18f0cd88494528035d3336cfc2d Mon Sep 17 00:00:00 2001 From: splunk Date: Mon, 22 Jun 2026 13:37:40 -0400 Subject: [PATCH 2/3] docs: implementation plans for dependency modernization + logging extension --- .../2026-06-22-dependency-modernization.md | 467 +++++ .../plans/2026-06-22-logging-extension.md | 1612 +++++++++++++++++ 2 files changed, 2079 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-22-dependency-modernization.md create mode 100644 docs/superpowers/plans/2026-06-22-logging-extension.md diff --git a/docs/superpowers/plans/2026-06-22-dependency-modernization.md b/docs/superpowers/plans/2026-06-22-dependency-modernization.md new file mode 100644 index 0000000..5040d47 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-dependency-modernization.md @@ -0,0 +1,467 @@ +# Dependency Modernization Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Land a modern, CVE-scanned, reproducible dependency baseline targeting Java 21, with the `forge` module excluded from the reactor. + +**Architecture:** A Maven multi-module reactor (`splunk.minecraft.app` parent → `shared-mc`, `spigot`, `logtosplunk-plugin`; `forge` removed from reactor). Changes are confined to pom files plus one Java source swap (`json-simple` → `gson`) in `shared-mc`. This plan is the foundation for the logging-extension plan, which builds on the gson change and Java 21 target. + +**Tech Stack:** Maven (bundled at `mvn-bin/`), Java 21 (`/usr/lib/jvm/java-21-openjdk-amd64`), log4j 2.25.1, gson 2.13.2, httpclient5, OWASP dependency-check-maven. + +--- + +## Build & Test Commands (use throughout) + +Set once per shell: + +```bash +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +export MVN="./mvn-bin/bin/mvn -f /home/splunk/appDev/minecraft-app/pom.xml" +``` + +- Full build: `$MVN clean package` +- Single module test: `$MVN -pl shared-mc -am test` +- Dependency scan: `$MVN -pl shared-mc,spigot,logtosplunk-plugin org.owasp:dependency-check-maven:check` + +All work happens on branch `feature/extend-logging-modernize-deps` (already created). + +--- + +## File Structure + +| File | Responsibility | Change | +|---|---|---| +| `pom.xml` (parent) | Reactor modules, dependencyManagement, compiler config | Modify: drop forge module, Java 21, bump compiler plugin, add dependency-check, manage gson | +| `shared-mc/pom.xml` | shared lib deps | Modify: remove json-simple, drop redundant explicit log4j versions | +| `shared-mc/src/main/java/com/splunk/sharedmc/SingleSplunkConnection.java` | HEC envelope build | Modify: replace `org.json.simple.JSONObject` with gson `JsonObject` | +| `logtosplunk-plugin/pom.xml` | shaded plugin packaging | Modify: bump shade plugin, drop stale `1.0.1` from forge profile | +| `spigot/pom.xml` | spigot adapter deps | No dependency change; inherits Java 21 from parent | + +--- + +## Task 1: Capture a CVE baseline before changing anything + +**Files:** +- Modify: `pom.xml` (add OWASP plugin to ``) + +- [ ] **Step 1: Add the OWASP dependency-check plugin to the parent pom** + +In `pom.xml`, inside the existing `` block (after the `maven-compiler-plugin`), add: + +```xml + + org.owasp + dependency-check-maven + 12.1.0 + + + 7.0 + + false + + ${maven.multiModuleProjectDirectory}/owasp-suppressions.xml + + + +``` + +- [ ] **Step 2: Create an (initially empty) suppression file** + +Create `owasp-suppressions.xml`: + +```xml + + + + +``` + +- [ ] **Step 3: Run the scan to capture the baseline (do not fail the build yet)** + +Run: +```bash +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +./mvn-bin/bin/mvn -f pom.xml -pl shared-mc,spigot,logtosplunk-plugin -am \ + org.owasp:dependency-check-maven:12.1.0:aggregate -DfailBuildOnCVSS=11 \ + -Dformats=HTML,JSON +``` +Expected: completes; writes `target/dependency-check-report.html` + `.json`. First run downloads the NVD database and may take several minutes. If NVD rate-limits, obtain a free API key from https://nvd.nist.gov/developers/request-an-api-key and pass `-DnvdApiKey=`. + +- [ ] **Step 4: Record findings** + +Read `target/dependency-check-report.json`. List every dependency with a CVSS >= 7.0 finding in a scratch note (you will verify each is resolved by Task 6's final scan). Do not fix individual CVEs by hand — the version bumps in Tasks 2–4 address them; the final scan confirms. + +- [ ] **Step 5: Commit** + +```bash +git add pom.xml owasp-suppressions.xml +git commit -m "build: add OWASP dependency-check with CVSS 7.0 gate" +``` + +--- + +## Task 2: Target Java 21 and bump the compiler plugin + +**Files:** +- Modify: `pom.xml:54-66` (the `` compiler entry) + +- [ ] **Step 1: Replace the compiler plugin block** + +In `pom.xml`, replace: + +```xml + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.8 + 1.8 + + +``` + +with: + +```xml + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + 21 + + +``` + +- [ ] **Step 2: Add explicit properties so encoding + Java version are unambiguous** + +In `pom.xml`, add a `` block immediately after `pom` (line 14): + +```xml + + 21 + UTF-8 + +``` + +- [ ] **Step 3: Build to verify Java 21 compiles the existing code** + +Run: +```bash +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +./mvn-bin/bin/mvn -f pom.xml -pl shared-mc,spigot,logtosplunk-plugin -am clean compile +``` +Expected: BUILD SUCCESS. (forge is still in the reactor at this point; if forge fails to compile under Java 21, that is expected — it is removed in Task 5. To isolate, the `-pl` list above excludes forge.) + +- [ ] **Step 4: Commit** + +```bash +git add pom.xml +git commit -m "build: target Java 21 and bump maven-compiler-plugin to 3.14.0" +``` + +--- + +## Task 3: Replace json-simple with gson in SingleSplunkConnection (TDD) + +`json-simple 1.1` (2009) is used in exactly one place: building the `{"event": }` HEC envelope in `SingleSplunkConnection.sendToSplunk`. gson 2.13.2 is already a `shared-mc` dependency. This task swaps it under test. + +**Files:** +- Test: `shared-mc/src/test/java/com/splunk/sharedmc/SingleSplunkConnectionTest.java` (create) +- Modify: `shared-mc/src/main/java/com/splunk/sharedmc/SingleSplunkConnection.java:27,94-98` +- Modify: `shared-mc/pom.xml` (remove json-simple dependency) +- Modify: `pom.xml` (remove json-simple from dependencyManagement) + +- [ ] **Step 1: Extract the envelope build into a testable method** + +The current `sendToSplunk` both builds the envelope and mutates `messagesToSend`. To test the JSON shape without side effects, add a package-private static helper. In `SingleSplunkConnection.java`, add this method (near `sendToSplunk`): + +```java +/** + * Wraps a raw event message in the Splunk HEC envelope: {"event": }. + * Package-private for testing. + */ +static String buildHecEnvelope(String message) { + com.google.gson.JsonObject event = new com.google.gson.JsonObject(); + event.addProperty("event", message); + return event.toString(); +} +``` + +- [ ] **Step 2: Write the failing test** + +Create `shared-mc/src/test/java/com/splunk/sharedmc/SingleSplunkConnectionTest.java`: + +```java +package com.splunk.sharedmc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.Test; + +public class SingleSplunkConnectionTest { + + @Test + public void buildHecEnvelope_wrapsMessageInEventField() { + String out = SingleSplunkConnection.buildHecEnvelope("hello world"); + JsonObject parsed = JsonParser.parseString(out).getAsJsonObject(); + assertEquals("hello world", parsed.get("event").getAsString()); + } + + @Test + public void buildHecEnvelope_escapesQuotes() { + String out = SingleSplunkConnection.buildHecEnvelope("a \"quoted\" value"); + // Must be valid JSON and round-trip the exact string. + JsonObject parsed = JsonParser.parseString(out).getAsJsonObject(); + assertEquals("a \"quoted\" value", parsed.get("event").getAsString()); + assertTrue(out.contains("\\\"")); + } +} +``` + +- [ ] **Step 3: Run the test to verify it fails** + +Run: +```bash +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +./mvn-bin/bin/mvn -f pom.xml -pl shared-mc -am test -Dtest=SingleSplunkConnectionTest +``` +Expected: FAIL — `buildHecEnvelope` does not yet exist OR (after Step 1) PASS for the helper but the production `sendToSplunk` still imports json-simple. If Step 1 already added the helper, this step confirms the helper passes; proceed to wire it into `sendToSplunk`. + +- [ ] **Step 4: Rewrite `sendToSplunk` to use the helper and remove the json-simple import** + +In `SingleSplunkConnection.java`, delete line 27 `import org.json.simple.JSONObject;`. Replace the body of `sendToSplunk` (lines 93-99): + +```java +@Override +public void sendToSplunk(String message) { + messagesToSend.append(buildHecEnvelope(message)); +} +``` + +- [ ] **Step 5: Remove json-simple from shared-mc/pom.xml** + +Delete the dependency block (`shared-mc/pom.xml:60-64`): + +```xml + + com.googlecode.json-simple + json-simple + 1.1 + +``` + +- [ ] **Step 6: Remove json-simple from the parent dependencyManagement** + +Delete the block in `pom.xml:47-51`: + +```xml + + com.googlecode.json-simple + json-simple + 1.1 + +``` + +- [ ] **Step 7: Run tests and a full shared-mc build to verify** + +Run: +```bash +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +./mvn-bin/bin/mvn -f pom.xml -pl shared-mc -am clean test +``` +Expected: BUILD SUCCESS, `SingleSplunkConnectionTest` PASS, no `org.json.simple` on the classpath. + +- [ ] **Step 8: Verify json-simple is fully gone** + +Run: +```bash +grep -rn "json.simple\|JSONObject\|JSONValue" /home/splunk/appDev/minecraft-app/shared-mc/src +``` +Expected: no matches. + +- [ ] **Step 9: Commit** + +```bash +git add shared-mc/src pom.xml shared-mc/pom.xml +git commit -m "refactor: replace json-simple with gson for HEC envelope" +``` + +--- + +## Task 4: Centralize versions and remove redundant explicit versions + +shared-mc redeclares `log4j-api`/`log4j-core` versions already managed by the parent. Centralize so a single bump propagates. + +**Files:** +- Modify: `pom.xml` (add gson + httpclient5/httpcore5 + guava to dependencyManagement) +- Modify: `shared-mc/pom.xml` (drop explicit `` on log4j entries) + +- [ ] **Step 1: Add managed versions to the parent dependencyManagement** + +In `pom.xml`, inside ``, add (alongside the existing log4j/splunk entries): + +```xml + + com.google.code.gson + gson + 2.13.2 + + + com.google.guava + guava + 33.5.0-jre + + + org.apache.httpcomponents.core5 + httpcore5 + 5.3.6 + + + org.apache.httpcomponents.client5 + httpclient5 + 5.5.1 + +``` + +- [ ] **Step 2: Bump junit to 4.13.2 in the parent dependencyManagement** + +In `pom.xml`, change the junit version from `4.8.2` to `4.13.2`: + +```xml + + junit + junit + 4.13.2 + test + +``` + +- [ ] **Step 3: Remove explicit `` from shared-mc dependencies now managed by parent** + +In `shared-mc/pom.xml`, delete the `` lines from `log4j-api`, `log4j-core`, `httpcore5`, `httpclient5`, `guava`, `gson`, and `splunk-library-javalogging` so they inherit from the parent. Example for log4j-api: + +```xml + + org.apache.logging.log4j + log4j-api + +``` + +(For httpcore5/httpclient5/guava/gson, the parent now manages them via Step 1; leave their `` entries but drop the `` child element.) + +- [ ] **Step 4: Build to confirm versions still resolve** + +Run: +```bash +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +./mvn-bin/bin/mvn -f pom.xml -pl shared-mc -am clean test +``` +Expected: BUILD SUCCESS, tests still PASS. + +- [ ] **Step 5: Verify resolved versions are unchanged** + +Run: +```bash +./mvn-bin/bin/mvn -f pom.xml -pl shared-mc dependency:tree | grep -E "log4j|guava|gson|httpc|junit" +``` +Expected: log4j 2.25.1, guava 33.5.0-jre, gson 2.13.2, httpcore5 5.3.6, httpclient5 5.5.1, junit 4.13.2. + +- [ ] **Step 6: Commit** + +```bash +git add pom.xml shared-mc/pom.xml +git commit -m "build: centralize dependency versions in parent, bump junit to 4.13.2" +``` + +--- + +## Task 5: Exclude forge from the reactor and clean the plugin shade config + +**Files:** +- Modify: `pom.xml:8-13` (modules) +- Modify: `logtosplunk-plugin/pom.xml:65` (shade version) and `:96-100` (stale 1.0.1 in forge profile) + +- [ ] **Step 1: Remove the forge module from the reactor** + +In `pom.xml`, change the `` block to: + +```xml + + spigot + shared-mc + logtosplunk-plugin + +``` + +(Leave the `forge/` directory and its pom on disk untouched.) + +- [ ] **Step 2: Bump the shade plugin in the plugin pom** + +In `logtosplunk-plugin/pom.xml`, change the `maven-shade-plugin` version from `2.4.1` to `3.6.0`. + +- [ ] **Step 3: Remove the stale 1.0.1 splunk lib from the include-forge profile** + +In `logtosplunk-plugin/pom.xml`, delete the dependency block at lines 96-100 (the `splunk-library-javalogging` `1.0.1` entry inside the `include-forge` profile). The `include-forge` profile is now dead (forge is out of the reactor); leaving the stale version invites confusion. + +- [ ] **Step 4: Full reactor build** + +Run: +```bash +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +./mvn-bin/bin/mvn -f pom.xml clean package +``` +Expected: BUILD SUCCESS across `shared-mc`, `spigot`, `logtosplunk-plugin`; the shaded plugin jar is produced under `logtosplunk-plugin/target/`. + +- [ ] **Step 5: Commit** + +```bash +git add pom.xml logtosplunk-plugin/pom.xml +git commit -m "build: drop forge from reactor, bump shade plugin to 3.6.0, remove stale splunk 1.0.1" +``` + +--- + +## Task 6: Enforce the CVE gate + +**Files:** none (verification + config gate only) + +- [ ] **Step 1: Run the dependency-check with the failing gate enabled** + +Run: +```bash +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +./mvn-bin/bin/mvn -f pom.xml -pl shared-mc,spigot,logtosplunk-plugin -am \ + org.owasp:dependency-check-maven:12.1.0:aggregate -Dformats=HTML,JSON +``` +Expected: BUILD SUCCESS with `failBuildOnCVSS=7.0` (from Task 1 config). If it FAILS, read `target/dependency-check-report.json`, identify the offending dependency, and either bump it (preferred) or, only for a verified false positive, add a documented `` entry to `owasp-suppressions.xml` with a justification comment. + +- [ ] **Step 2: Confirm every Task-1 baseline finding is resolved** + +Compare against the scratch note from Task 1 Step 4. Every High/Critical must be either gone or explicitly suppressed-with-justification. + +- [ ] **Step 3: Commit any suppressions or final bumps** + +```bash +git add owasp-suppressions.xml pom.xml shared-mc/pom.xml logtosplunk-plugin/pom.xml +git commit -m "build: pass OWASP CVSS 7.0 gate with no unsuppressed High/Critical CVEs" +``` + +--- + +## Done criteria + +- `$MVN clean package` is green with `forge` excluded. +- No `org.json.simple` anywhere in source or on the classpath. +- Java 21 release target; compiler/shade plugins modern. +- OWASP scan passes the CVSS 7.0 gate; report archived under `target/`. +- All versions for shared libs centralized in the parent pom. + +## Self-review notes (addressed) + +- Spec WS1 item 4 said the gson swap touches `toJson()`; verification showed `toJson()` already uses gson and json-simple lives only in `SingleSplunkConnection` — Task 3 targets the correct file. +- Spec WS1 item 3 (forge splunk lib) is intentionally NOT changed in forge's own pom (out of reactor); Task 5 instead removes the stale `1.0.1` from the plugin's dead `include-forge` profile. diff --git a/docs/superpowers/plans/2026-06-22-logging-extension.md b/docs/superpowers/plans/2026-06-22-logging-extension.md new file mode 100644 index 0000000..24bd578 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-logging-extension.md @@ -0,0 +1,1612 @@ +# Logging Extension Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add server- and player-level telemetry (combat, survival, economy, progression, session detail, server lifecycle/performance) to the Splunk-for-Minecraft spigot plugin, every new category opt-in and throttled. + +**Architecture:** Preserve the existing pattern — platform-agnostic `Loggable*Event` data carriers in `shared-mc` (extending `AbstractLoggableEvent`, serialized via gson `toJson()`), paired with Bukkit `Listener` loggers in `spigot` (extending `AbstractEventLogger`) registered in `LogToSplunkPlugin.onEnable`. One new abstraction, `ScheduledMetricLogger`, drives non-event metrics (TPS/MSPT/player count) off the Bukkit scheduler. Each category is gated by a `splunk.properties` toggle and high-frequency events are throttled. + +**Tech Stack:** Java 21, Maven (`mvn-bin/`), `spigot-api 1.21.10`, gson 2.13.2, guava 33.5.0 cache, junit 4.13.2. + +**Depends on:** `2026-06-22-dependency-modernization.md` — that plan's Java 21 target and gson HEC swap must land first. New event classes serialize through the gson `toJson()` path. + +--- + +## Build & Test Commands (use throughout) + +```bash +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +export MVN="./mvn-bin/bin/mvn -f /home/splunk/appDev/minecraft-app/pom.xml" +``` + +- Test shared-mc POJOs: `$MVN -pl shared-mc -am test` +- Compile spigot (listener wiring gate): `$MVN -pl spigot -am clean compile` +- Full build incl. shaded plugin: `$MVN clean package` + +Branch: `feature/extend-logging-modernize-deps` (continue on the same branch as the dependency plan). + +--- + +## Testing strategy + +- **shared-mc `Loggable*Event` classes** are pure POJOs (no Bukkit). Full TDD: construct, set fields, assert `toJson()` contains the expected keys/values. +- **Throttle logic** is extracted into a Bukkit-free `EventThrottle` helper in shared-mc so it is unit-testable without a server. +- **spigot `Listener` classes** require a running Bukkit server to exercise, which is out of scope for unit tests here. They are verified by (a) a clean `compile` gate proving they bind to the real `spigot-api 1.21.10` types, and (b) a manual smoke test against a local Paper server in the final task. Keep listeners thin: translate the Bukkit event to a `Loggable*Event` and call `logAndSend` — all assertable logic lives in the POJOs and `EventThrottle`. + +--- + +## File Structure + +**shared-mc (data carriers — unit tested):** + +| File | Responsibility | +|---|---| +| `loggable_events/LoggableEventType.java` (modify) | Add `SERVER, PERFORMANCE, COMBAT, ITEM, PROGRESSION` | +| `loggable_events/AbstractLoggableEvent.java` (modify) | Add a null-coordinate-safe constructor for location-less events | +| `loggable_events/LoggableServerEvent.java` (create) | Server lifecycle (start/stop/weather) | +| `loggable_events/LoggablePerformanceEvent.java` (create) | TPS / MSPT / online count sample | +| `loggable_events/LoggableCombatEvent.java` (create) | Damage / kill / heal / hunger / respawn | +| `loggable_events/LoggableItemEvent.java` (create) | Item pickup / drop | +| `loggable_events/LoggableProgressionEvent.java` (create) | XP / level / enchant / craft / fish / command | +| `loggable_events/LoggablePlayerEvent.java` (modify) | New `PlayerEventAction`s + IP/UUID/protocol/gamemode setters | +| `util/EventThrottle.java` (create) | Bukkit-free per-key time/granularity throttle | + +**spigot (Bukkit listeners — compile-gated + smoke):** + +| File | Responsibility | +|---|---| +| `eventloggers/CombatEventLogger.java` (create) | Combat & survival hooks | +| `eventloggers/ItemEventLogger.java` (create) | Item pickup/drop hooks | +| `eventloggers/ProgressionEventLogger.java` (create) | XP/level/enchant/craft/fish/command hooks | +| `eventloggers/ServerEventLogger.java` (create) | ServerLoad + weather hooks | +| `eventloggers/PerformanceSampler.java` (create) | Scheduled TPS/MSPT/count sampler | +| `scheduling/ScheduledMetricLogger.java` (create) | Base for scheduler-driven loggers | +| `eventloggers/PlayerEventLogger.java` (modify) | Add session-detail hooks | +| `LogToSplunkPlugin.java` (modify) | Read toggles, register enabled loggers, start sampler | + +**Config:** + +| File | Responsibility | +|---|---| +| `sampleModConfig/splunk.properties` (modify, if present) | Document new toggle keys | + +--- + +## Task 1: Extend LoggableEventType and add a location-less constructor (TDD) + +The current `AbstractLoggableEvent` constructor dereferences `coordinates.xCoord` and NPEs if coordinates are null. Server and performance events have no location, so they need a safe constructor. + +**Files:** +- Modify: `shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggableEventType.java` +- Modify: `shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/AbstractLoggableEvent.java` +- Test: `shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/AbstractLoggableEventTest.java` (create) + +- [ ] **Step 1: Write the failing test** + +Create `shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/AbstractLoggableEventTest.java`: + +```java +package com.splunk.sharedmc.loggable_events; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +public class AbstractLoggableEventTest { + + @Test + public void locationLessConstructor_doesNotNpe_andOmitsCoords() { + AbstractLoggableEvent e = + new AbstractLoggableEvent(LoggableEventType.SERVER, 0L, "world"); + String json = e.toJson(); + assertTrue(json.contains("SERVER".toLowerCase()) || json.contains("ServerEvent")); + assertFalse("location-less event must not emit xCoord", json.contains("xCoord")); + } +} +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `$MVN -pl shared-mc -am test -Dtest=AbstractLoggableEventTest` +Expected: FAIL — `SERVER` enum constant and the 3-arg constructor do not exist (compile error). + +- [ ] **Step 3: Add the new enum values** + +In `LoggableEventType.java`, extend the enum: + +```java +public enum LoggableEventType { + PLAYER("PlayerEvent"), + BLOCK("BlockEvent"), + DEATH("DeathEvent"), + SERVER("ServerEvent"), + PERFORMANCE("PerformanceEvent"), + COMBAT("CombatEvent"), + ITEM("ItemEvent"), + PROGRESSION("ProgressionEvent"); + + private final String eventName; + + LoggableEventType(String eventName) { + this.eventName = eventName; + } + + public String getEventName() { + return eventName; + } +} +``` + +- [ ] **Step 4: Add the location-less constructor** + +In `AbstractLoggableEvent.java`, add a constructor that skips coordinate fields, and have the existing constructor reject null coordinates explicitly. Replace the constructor (lines 23-36) with: + +```java +public AbstractLoggableEvent(LoggableEventType type, long worldTime, String worldName, Point3dLong coordinates) { + this(type, worldTime, worldName); + if (coordinates != null) { + this.addField("xCoord", coordinates.xCoord); + this.addField("yCoord", coordinates.yCoord); + this.addField("zCoord", coordinates.zCoord); + } +} + +/** + * Constructor for events with no world location (e.g. server lifecycle, performance). + */ +public AbstractLoggableEvent(LoggableEventType type, long worldTime, String worldName) { + super(type.getEventName(), ""); + this.addField("time", System.currentTimeMillis()); + this.addField("game_time", worldTime); + if (worldName != null) { + this.addField("world", worldName); + } +} +``` + +- [ ] **Step 5: Run the test to verify it passes** + +Run: `$MVN -pl shared-mc -am test -Dtest=AbstractLoggableEventTest` +Expected: PASS. + +- [ ] **Step 6: Run the full shared-mc test suite to confirm no regression** + +Run: `$MVN -pl shared-mc -am test` +Expected: PASS (existing Player/Block/Death events still serialize with coordinates). + +- [ ] **Step 7: Commit** + +```bash +git add shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggableEventType.java \ + shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/AbstractLoggableEvent.java \ + shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/AbstractLoggableEventTest.java +git commit -m "feat(shared-mc): add SERVER/PERFORMANCE/COMBAT/ITEM/PROGRESSION types + location-less event ctor" +``` + +--- + +## Task 2: LoggableServerEvent + LoggablePerformanceEvent (TDD) + +**Files:** +- Create: `shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggableServerEvent.java` +- Create: `shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggablePerformanceEvent.java` +- Test: `shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggableServerEventTest.java` +- Test: `shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggablePerformanceEventTest.java` + +- [ ] **Step 1: Write the failing tests** + +Create `LoggableServerEventTest.java`: + +```java +package com.splunk.sharedmc.loggable_events; + +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class LoggableServerEventTest { + @Test + public void serverStart_serializesAction() { + LoggableServerEvent e = new LoggableServerEvent( + LoggableServerEvent.ServerAction.SERVER_START, 0L, null); + String json = e.toJson(); + assertTrue(json.contains("server_start")); + } + + @Test + public void weatherChange_carriesState() { + LoggableServerEvent e = new LoggableServerEvent( + LoggableServerEvent.ServerAction.WEATHER_CHANGE, 1000L, "world"); + e.setWeather("storm"); + String json = e.toJson(); + assertTrue(json.contains("weather")); + assertTrue(json.contains("storm")); + } +} +``` + +Create `LoggablePerformanceEventTest.java`: + +```java +package com.splunk.sharedmc.loggable_events; + +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class LoggablePerformanceEventTest { + @Test + public void sample_serializesMetrics() { + LoggablePerformanceEvent e = new LoggablePerformanceEvent(0L); + e.setTps(19.8).setMspt(8.4).setOnlinePlayers(12).setLoadedChunks(1500); + String json = e.toJson(); + assertTrue(json.contains("tps")); + assertTrue(json.contains("19.8")); + assertTrue(json.contains("mspt")); + assertTrue(json.contains("online_players")); + } +} +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `$MVN -pl shared-mc -am test -Dtest=LoggableServerEventTest,LoggablePerformanceEventTest` +Expected: FAIL — classes do not exist. + +- [ ] **Step 3: Implement LoggableServerEvent** + +Create `LoggableServerEvent.java`: + +```java +package com.splunk.sharedmc.loggable_events; + +/** + * Server lifecycle and world-state events (start, stop, weather). + */ +public class LoggableServerEvent extends AbstractLoggableEvent { + + public LoggableServerEvent(ServerAction action, long gameTime, String worldName) { + super(LoggableEventType.SERVER, gameTime, worldName); + this.addField(ACTION, action.asString()); + } + + public LoggableServerEvent setWeather(String weather) { + this.addField("weather", weather); + return this; + } + + public LoggableServerEvent setMotd(String motd) { + this.addField("motd", motd); + return this; + } + + public enum ServerAction { + SERVER_START("server_start"), + SERVER_STOP("server_stop"), + WEATHER_CHANGE("weather_change"); + + private final String action; + ServerAction(String action) { this.action = action; } + public String asString() { return action; } + } +} +``` + +- [ ] **Step 4: Implement LoggablePerformanceEvent** + +Create `LoggablePerformanceEvent.java`: + +```java +package com.splunk.sharedmc.loggable_events; + +/** + * A sampled snapshot of server performance metrics. + */ +public class LoggablePerformanceEvent extends AbstractLoggableEvent { + + public LoggablePerformanceEvent(long gameTime) { + super(LoggableEventType.PERFORMANCE, gameTime, null); + this.addField(ACTION, "performance_sample"); + } + + public LoggablePerformanceEvent setTps(double tps) { + this.addField("tps", tps); + return this; + } + + public LoggablePerformanceEvent setMspt(double mspt) { + this.addField("mspt", mspt); + return this; + } + + public LoggablePerformanceEvent setOnlinePlayers(int count) { + this.addField("online_players", count); + return this; + } + + public LoggablePerformanceEvent setLoadedChunks(int chunks) { + this.addField("loaded_chunks", chunks); + return this; + } +} +``` + +- [ ] **Step 5: Run to verify pass** + +Run: `$MVN -pl shared-mc -am test -Dtest=LoggableServerEventTest,LoggablePerformanceEventTest` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggableServerEvent.java \ + shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggablePerformanceEvent.java \ + shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggableServerEventTest.java \ + shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggablePerformanceEventTest.java +git commit -m "feat(shared-mc): add LoggableServerEvent and LoggablePerformanceEvent" +``` + +--- + +## Task 3: LoggableCombatEvent (TDD) + +Covers damage, kills, healing, hunger, respawn — survival + combat in one carrier with an action enum. + +**Files:** +- Create: `shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggableCombatEvent.java` +- Test: `shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggableCombatEventTest.java` + +- [ ] **Step 1: Write the failing test** + +```java +package com.splunk.sharedmc.loggable_events; + +import static org.junit.Assert.assertTrue; +import com.splunk.sharedmc.Point3dLong; +import org.junit.Test; + +public class LoggableCombatEventTest { + @Test + public void damage_carriesAttackerVictimAndAmount() { + LoggableCombatEvent e = new LoggableCombatEvent( + LoggableCombatEvent.CombatAction.DAMAGE, 0L, "world", new Point3dLong(1, 2, 3)); + e.setVictim("Steve").setSource("Zombie").setAmount(4.5).setCause("ENTITY_ATTACK"); + String json = e.toJson(); + assertTrue(json.contains("victim")); + assertTrue(json.contains("Steve")); + assertTrue(json.contains("source")); + assertTrue(json.contains("4.5")); + assertTrue(json.contains("damage")); + } + + @Test + public void hunger_carriesFoodLevel() { + LoggableCombatEvent e = new LoggableCombatEvent( + LoggableCombatEvent.CombatAction.HUNGER, 0L, "world", null); + e.setVictim("Alex").setFoodLevel(7); + String json = e.toJson(); + assertTrue(json.contains("food_level")); + assertTrue(json.contains("hunger")); + } +} +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `$MVN -pl shared-mc -am test -Dtest=LoggableCombatEventTest` +Expected: FAIL — class missing. + +- [ ] **Step 3: Implement LoggableCombatEvent** + +```java +package com.splunk.sharedmc.loggable_events; + +import com.splunk.sharedmc.Point3dLong; + +/** + * Combat and survival events: damage dealt/taken, kills, healing, hunger, respawn. + */ +public class LoggableCombatEvent extends AbstractLoggableEvent { + + public LoggableCombatEvent(CombatAction action, long gameTime, String worldName, Point3dLong location) { + super(LoggableEventType.COMBAT, gameTime, worldName, location); + this.addField(ACTION, action.asString()); + } + + public LoggableCombatEvent setVictim(String victim) { + this.addField("victim", victim); + return this; + } + + public LoggableCombatEvent setSource(String source) { + this.addField("source", source); + return this; + } + + public LoggableCombatEvent setAmount(double amount) { + this.addField("amount", amount); + return this; + } + + public LoggableCombatEvent setCause(String cause) { + this.addField(CAUSE, cause); + return this; + } + + public LoggableCombatEvent setFoodLevel(int foodLevel) { + this.addField("food_level", foodLevel); + return this; + } + + public LoggableCombatEvent setHealthRemaining(double health) { + this.addField("health_remaining", health); + return this; + } + + public enum CombatAction { + DAMAGE("damage"), + KILL("kill"), + HEAL("heal"), + HUNGER("hunger"), + RESPAWN("respawn"); + + private final String action; + CombatAction(String action) { this.action = action; } + public String asString() { return action; } + } +} +``` + +- [ ] **Step 4: Run to verify pass** + +Run: `$MVN -pl shared-mc -am test -Dtest=LoggableCombatEventTest` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggableCombatEvent.java \ + shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggableCombatEventTest.java +git commit -m "feat(shared-mc): add LoggableCombatEvent" +``` + +--- + +## Task 4: LoggableItemEvent + LoggableProgressionEvent (TDD) + +**Files:** +- Create: `shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggableItemEvent.java` +- Create: `shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggableProgressionEvent.java` +- Test: `shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggableItemEventTest.java` +- Test: `shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggableProgressionEventTest.java` + +- [ ] **Step 1: Write the failing tests** + +`LoggableItemEventTest.java`: + +```java +package com.splunk.sharedmc.loggable_events; + +import static org.junit.Assert.assertTrue; +import com.splunk.sharedmc.Point3dLong; +import org.junit.Test; + +public class LoggableItemEventTest { + @Test + public void pickup_carriesItemAndQuantity() { + LoggableItemEvent e = new LoggableItemEvent( + LoggableItemEvent.ItemAction.PICKUP, 0L, "world", new Point3dLong(0, 64, 0)); + e.setPlayerName("Steve").setItem("DIAMOND").setQuantity(3); + String json = e.toJson(); + assertTrue(json.contains("pickup")); + assertTrue(json.contains("DIAMOND")); + assertTrue(json.contains("quantity")); + } +} +``` + +`LoggableProgressionEventTest.java`: + +```java +package com.splunk.sharedmc.loggable_events; + +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class LoggableProgressionEventTest { + @Test + public void levelUp_carriesNewLevel() { + LoggableProgressionEvent e = new LoggableProgressionEvent( + LoggableProgressionEvent.ProgressionAction.LEVEL_CHANGE, 0L, "world"); + e.setPlayerName("Alex").setNewLevel(30); + String json = e.toJson(); + assertTrue(json.contains("level_change")); + assertTrue(json.contains("new_level")); + assertTrue(json.contains("30")); + } + + @Test + public void command_carriesCommandText() { + LoggableProgressionEvent e = new LoggableProgressionEvent( + LoggableProgressionEvent.ProgressionAction.COMMAND, 0L, "world"); + e.setPlayerName("Alex").setDetail("/gamemode creative"); + String json = e.toJson(); + assertTrue(json.contains("command")); + assertTrue(json.contains("gamemode creative")); + } +} +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `$MVN -pl shared-mc -am test -Dtest=LoggableItemEventTest,LoggableProgressionEventTest` +Expected: FAIL — classes missing. + +- [ ] **Step 3: Implement LoggableItemEvent** + +```java +package com.splunk.sharedmc.loggable_events; + +import com.splunk.sharedmc.Point3dLong; + +/** + * Item economy events: pickup and drop. + */ +public class LoggableItemEvent extends AbstractLoggableEvent { + + public LoggableItemEvent(ItemAction action, long gameTime, String worldName, Point3dLong location) { + super(LoggableEventType.ITEM, gameTime, worldName, location); + this.addField(ACTION, action.asString()); + } + + public LoggableItemEvent setPlayerName(String playerName) { + this.addField(PLAYER_NAME, playerName); + return this; + } + + public LoggableItemEvent setItem(String item) { + this.addField("item", item); + return this; + } + + public LoggableItemEvent setQuantity(int quantity) { + this.addField("quantity", quantity); + return this; + } + + public enum ItemAction { + PICKUP("pickup"), + DROP("drop"); + + private final String action; + ItemAction(String action) { this.action = action; } + public String asString() { return action; } + } +} +``` + +- [ ] **Step 4: Implement LoggableProgressionEvent** + +```java +package com.splunk.sharedmc.loggable_events; + +/** + * Player progression and activity: XP, level, enchant, craft, fish, command. + */ +public class LoggableProgressionEvent extends AbstractLoggableEvent { + + public LoggableProgressionEvent(ProgressionAction action, long gameTime, String worldName) { + super(LoggableEventType.PROGRESSION, gameTime, worldName); + this.addField(ACTION, action.asString()); + } + + public LoggableProgressionEvent setPlayerName(String playerName) { + this.addField(PLAYER_NAME, playerName); + return this; + } + + public LoggableProgressionEvent setNewLevel(int level) { + this.addField("new_level", level); + return this; + } + + public LoggableProgressionEvent setExpAmount(int exp) { + this.addField("exp_amount", exp); + return this; + } + + /** Free-form detail: command text, enchant name, crafted item, fish caught. */ + public LoggableProgressionEvent setDetail(String detail) { + this.addField("detail", detail); + return this; + } + + public enum ProgressionAction { + EXP_CHANGE("exp_change"), + LEVEL_CHANGE("level_change"), + ENCHANT("enchant"), + CRAFT("craft"), + FISH("fish"), + COMMAND("command"); + + private final String action; + ProgressionAction(String action) { this.action = action; } + public String asString() { return action; } + } +} +``` + +- [ ] **Step 5: Run to verify pass** + +Run: `$MVN -pl shared-mc -am test -Dtest=LoggableItemEventTest,LoggableProgressionEventTest` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggableItemEvent.java \ + shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggableProgressionEvent.java \ + shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggableItemEventTest.java \ + shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggableProgressionEventTest.java +git commit -m "feat(shared-mc): add LoggableItemEvent and LoggableProgressionEvent" +``` + +--- + +## Task 5: Extend LoggablePlayerEvent with session detail (TDD) + +Add session-detail actions and fields (IP, UUID, protocol version, gamemode) to the existing player event. + +**Files:** +- Modify: `shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggablePlayerEvent.java` +- Test: `shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggablePlayerEventTest.java` (create) + +- [ ] **Step 1: Write the failing test** + +```java +package com.splunk.sharedmc.loggable_events; + +import static org.junit.Assert.assertTrue; +import com.splunk.sharedmc.Point3dLong; +import org.junit.Test; + +public class LoggablePlayerEventTest { + @Test + public void connect_carriesSessionDetail() { + LoggablePlayerEvent e = new LoggablePlayerEvent( + LoggablePlayerEvent.PlayerEventAction.PLAYER_CONNECT, 0L, "world", new Point3dLong(0, 64, 0)); + e.setPlayerName("Steve") + .setPlayerUuid("11111111-2222-3333-4444-555555555555") + .setPlayerIp("203.0.113.7") + .setProtocolVersion(767); + String json = e.toJson(); + assertTrue(json.contains("uuid")); + assertTrue(json.contains("203.0.113.7")); + assertTrue(json.contains("protocol_version")); + } + + @Test + public void gamemodeChange_serializesAction() { + LoggablePlayerEvent e = new LoggablePlayerEvent( + LoggablePlayerEvent.PlayerEventAction.GAMEMODE_CHANGE, 0L, "world", new Point3dLong(0, 64, 0)); + e.setGamemode("CREATIVE"); + String json = e.toJson(); + assertTrue(json.contains("gamemode_change")); + assertTrue(json.contains("CREATIVE")); + } +} +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `$MVN -pl shared-mc -am test -Dtest=LoggablePlayerEventTest` +Expected: FAIL — new actions and setters missing. + +- [ ] **Step 3: Add new actions and setters** + +In `LoggablePlayerEvent.java`, extend the `PlayerEventAction` enum with: + +```java +TELEPORT("teleport"), +GAMEMODE_CHANGE("gamemode_change"), +BED_ENTER("bed_enter"), +WORLD_CHANGE("world_change"), +``` + +(insert before `ADVANCEMENT("advancement");`, keeping the existing constants). + +Add these setter methods to the class body (alongside the existing setters): + +```java +public LoggablePlayerEvent setPlayerUuid(String uuid) { + this.addField("uuid", uuid); + return this; +} + +public LoggablePlayerEvent setPlayerIp(String ip) { + this.addField("client_ip", ip); + return this; +} + +public LoggablePlayerEvent setProtocolVersion(int protocol) { + this.addField("protocol_version", protocol); + return this; +} + +public LoggablePlayerEvent setGamemode(String gamemode) { + this.addField("gamemode", gamemode); + return this; +} +``` + +Note: the test asserts the substring `protocol_version` and the field key for IP is `client_ip` — but the connect test only checks `203.0.113.7` substring, so the IP key name is free. Keep `client_ip` for Splunk CIM `src_ip`-style clarity. + +- [ ] **Step 4: Run to verify pass** + +Run: `$MVN -pl shared-mc -am test -Dtest=LoggablePlayerEventTest` +Expected: PASS. + +- [ ] **Step 5: Full shared-mc suite** + +Run: `$MVN -pl shared-mc -am test` +Expected: PASS (all Loggable* tests green). + +- [ ] **Step 6: Commit** + +```bash +git add shared-mc/src/main/java/com/splunk/sharedmc/loggable_events/LoggablePlayerEvent.java \ + shared-mc/src/test/java/com/splunk/sharedmc/loggable_events/LoggablePlayerEventTest.java +git commit -m "feat(shared-mc): add session-detail actions and fields to LoggablePlayerEvent" +``` + +--- + +## Task 6: EventThrottle helper (TDD) + +A Bukkit-free per-key throttle so high-frequency listeners (damage, food, item pickup) don't flood Splunk. Reuses the guava cache idea from `PlayerEventLogger` but extracted and testable. + +**Files:** +- Create: `shared-mc/src/main/java/com/splunk/sharedmc/util/EventThrottle.java` +- Test: `shared-mc/src/test/java/com/splunk/sharedmc/util/EventThrottleTest.java` + +- [ ] **Step 1: Write the failing test** + +```java +package com.splunk.sharedmc.util; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import org.junit.Test; + +public class EventThrottleTest { + @Test + public void firstEventForKeyPasses_secondWithinWindowBlocked() { + // window of 1000ms; supply our own clock for determinism + long[] now = {1_000L}; + EventThrottle throttle = new EventThrottle(1000L, () -> now[0]); + + assertTrue("first event passes", throttle.allow("Steve")); + now[0] = 1_500L; // 500ms later, inside window + assertFalse("second event inside window blocked", throttle.allow("Steve")); + now[0] = 2_100L; // 1100ms after first, outside window + assertTrue("event after window passes", throttle.allow("Steve")); + } + + @Test + public void differentKeysAreIndependent() { + long[] now = {0L}; + EventThrottle throttle = new EventThrottle(1000L, () -> now[0]); + assertTrue(throttle.allow("Steve")); + assertTrue(throttle.allow("Alex")); + } +} +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `$MVN -pl shared-mc -am test -Dtest=EventThrottleTest` +Expected: FAIL — class missing. + +- [ ] **Step 3: Implement EventThrottle** + +```java +package com.splunk.sharedmc.util; + +import java.util.concurrent.TimeUnit; +import java.util.function.LongSupplier; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +/** + * Per-key time-window throttle. {@link #allow(String)} returns true at most once per + * window per key. Backed by a size-bounded guava cache so it is safe for many players. + */ +public class EventThrottle { + + private static final int MAX_KEYS = 1024; + + private final long windowMillis; + private final LongSupplier clock; + private final Cache lastAllowed; + + public EventThrottle(long windowMillis) { + this(windowMillis, System::currentTimeMillis); + } + + /** Testable constructor with an injectable clock. */ + public EventThrottle(long windowMillis, LongSupplier clock) { + this.windowMillis = windowMillis; + this.clock = clock; + this.lastAllowed = CacheBuilder.newBuilder() + .maximumSize(MAX_KEYS) + .expireAfterAccess(windowMillis * 4, TimeUnit.MILLISECONDS) + .build(); + } + + /** + * @return true if an event for {@code key} should be sent now (first call, or the + * window since the last allowed call has elapsed); false to drop it. + */ + public synchronized boolean allow(String key) { + long now = clock.getAsLong(); + Long last = lastAllowed.getIfPresent(key); + if (last == null || (now - last) >= windowMillis) { + lastAllowed.put(key, now); + return true; + } + return false; + } +} +``` + +- [ ] **Step 4: Run to verify pass** + +Run: `$MVN -pl shared-mc -am test -Dtest=EventThrottleTest` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add shared-mc/src/main/java/com/splunk/sharedmc/util/EventThrottle.java \ + shared-mc/src/test/java/com/splunk/sharedmc/util/EventThrottleTest.java +git commit -m "feat(shared-mc): add Bukkit-free EventThrottle helper" +``` + +--- + +## Task 7: Config toggles in AbstractEventLogger + +Expose per-category enable flags read from the same `Properties` already passed to loggers. + +**Files:** +- Modify: `shared-mc/src/main/java/com/splunk/sharedmc/event_loggers/AbstractEventLogger.java` + +- [ ] **Step 1: Add toggle keys and a protected accessor** + +In `AbstractEventLogger.java`, add these constants near the existing `*_PROP_KEY` constants: + +```java +public static final String ENABLE_COMBAT = "splunk.craft.enable.combat"; +public static final String ENABLE_ITEM = "splunk.craft.enable.item"; +public static final String ENABLE_PROGRESSION = "splunk.craft.enable.progression"; +public static final String ENABLE_SESSION_DETAIL = "splunk.craft.enable.session_detail"; +public static final String ENABLE_SERVER = "splunk.craft.enable.server"; +public static final String ENABLE_PERFORMANCE = "splunk.craft.enable.performance"; +public static final String ENABLE_SESSION_IP = "splunk.craft.enable.session_ip"; +public static final String PERFORMANCE_INTERVAL_TICKS = "splunk.craft.performance.interval_ticks"; +``` + +Store the `Properties` on the instance so subclasses can read toggles. Change the field section and constructor: add an instance field `protected final Properties props;` and assign `this.props = properties;` at the top of the constructor. Then add: + +```java +/** Reads a boolean toggle; default false keeps high-volume categories opt-in. */ +protected boolean isEnabled(String key) { + return Boolean.parseBoolean(props.getProperty(key, "false")); +} + +protected int intProp(String key, int defaultValue) { + try { + return Integer.parseInt(props.getProperty(key, Integer.toString(defaultValue))); + } catch (NumberFormatException e) { + return defaultValue; + } +} +``` + +- [ ] **Step 2: Compile shared-mc** + +Run: `$MVN -pl shared-mc -am clean compile` +Expected: BUILD SUCCESS. + +- [ ] **Step 3: Commit** + +```bash +git add shared-mc/src/main/java/com/splunk/sharedmc/event_loggers/AbstractEventLogger.java +git commit -m "feat(shared-mc): add per-category enable toggles to AbstractEventLogger" +``` + +--- + +## Task 8: CombatEventLogger (spigot) + +**Files:** +- Create: `spigot/src/main/java/com/splunk/spigot/eventloggers/CombatEventLogger.java` + +- [ ] **Step 1: Implement the listener** + +Create `CombatEventLogger.java`: + +```java +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.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityRegainHealthEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.player.PlayerRespawnEvent; + +import com.splunk.sharedmc.event_loggers.AbstractEventLogger; +import com.splunk.sharedmc.loggable_events.LoggableCombatEvent; +import com.splunk.sharedmc.loggable_events.LoggableCombatEvent.CombatAction; +import com.splunk.sharedmc.util.EventThrottle; + +/** + * Logs combat and survival events. High-frequency events (damage, hunger) are throttled + * per-player. + */ +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); + } + + @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); + } + + @EventHandler + public void onDeath(EntityDeathEvent event) { + // Player deaths are already handled by DeathEventLogger; log mob kills with a killer. + if (event.getEntity() instanceof Player) { + return; + } + Player killer = event.getEntity().getKiller(); + if (killer == null) { + return; + } + LoggableCombatEvent loggable = new LoggableCombatEvent( + CombatAction.KILL, event.getEntity().getWorld().getTime(), + event.getEntity().getWorld().getName(), locationAsPoint(event.getEntity().getLocation())); + loggable.setSource(killer.getName()) + .setVictim(event.getEntity().getType().toString()); + logAndSend(loggable); + } + + @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); + } + + @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); + } + + @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); + } +} +``` + +- [ ] **Step 2: Compile spigot (binds to real spigot-api types)** + +Run: `$MVN -pl spigot -am clean compile` +Expected: BUILD SUCCESS. If a method (e.g. `getFinalDamage`) is unavailable on the pinned API, the compile fails here — adjust to the available accessor. + +- [ ] **Step 3: Commit** + +```bash +git add spigot/src/main/java/com/splunk/spigot/eventloggers/CombatEventLogger.java +git commit -m "feat(spigot): add CombatEventLogger with per-player throttling" +``` + +--- + +## Task 9: ItemEventLogger + ProgressionEventLogger (spigot) + +**Files:** +- Create: `spigot/src/main/java/com/splunk/spigot/eventloggers/ItemEventLogger.java` +- Create: `spigot/src/main/java/com/splunk/spigot/eventloggers/ProgressionEventLogger.java` + +- [ ] **Step 1: Implement ItemEventLogger** + +```java +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. Pickups are throttled per-player to avoid floods. + */ +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); + } +} +``` + +- [ ] **Step 2: Implement ProgressionEventLogger** + +```java +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); + } +} +``` + +- [ ] **Step 3: Compile spigot** + +Run: `$MVN -pl spigot -am clean compile` +Expected: BUILD SUCCESS. + +- [ ] **Step 4: Commit** + +```bash +git add spigot/src/main/java/com/splunk/spigot/eventloggers/ItemEventLogger.java \ + spigot/src/main/java/com/splunk/spigot/eventloggers/ProgressionEventLogger.java +git commit -m "feat(spigot): add ItemEventLogger and ProgressionEventLogger" +``` + +--- + +## Task 10: Session-detail hooks in PlayerEventLogger (spigot) + +Enrich the existing player logger with teleport, gamemode change, bed enter, world change, and login session detail (UUID, IP, protocol). IP logging is gated by its own toggle. + +**Files:** +- Modify: `spigot/src/main/java/com/splunk/spigot/eventloggers/PlayerEventLogger.java` + +- [ ] **Step 1: Add the new imports** + +At the top of `PlayerEventLogger.java`, add: + +```java +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.splunk.sharedmc.loggable_events.LoggablePlayerEvent.PlayerEventAction; +``` + +- [ ] **Step 2: Enrich the login handler with session detail** + +In `onPlayerConnect` (handling `PlayerLoginEvent`), after building the loggable, add UUID/protocol always and IP only when enabled. Replace the body of `onPlayerConnect`: + +```java +@EventHandler +public void onPlayerConnect(PlayerLoginEvent event) { + LoggablePlayerEvent loggable = + generateLoggablePlayerEvent(event, PlayerEventAction.PLAYER_CONNECT, null, event.getKickMessage()); + loggable.setPlayerUuid(event.getPlayer().getUniqueId().toString()); + loggable.setProtocolVersion(event.getPlayer().getProtocolVersion()); + if (isEnabled(ENABLE_SESSION_IP) && event.getAddress() != null) { + loggable.setPlayerIp(event.getAddress().getHostAddress()); + } + logAndSend(loggable); +} +``` + +Note: `Player.getProtocolVersion()` is a Paper API. If the pinned `spigot-api` does not expose it, the compile in Step 4 fails; fall back to dropping the protocol line (it is optional enrichment). + +- [ ] **Step 3: Add the new session-detail handlers** + +Add these methods to `PlayerEventLogger`: + +```java +@EventHandler +public void onTeleport(PlayerTeleportEvent event) { + LoggablePlayerEvent loggable = generateLoggablePlayerEvent( + event, PlayerEventAction.TELEPORT, event.getCause().toString(), null); + loggable.setFrom(locationAsPoint(event.getFrom())); + loggable.setTo(locationAsPoint(event.getTo())); + logAndSend(loggable); +} + +@EventHandler +public void onGameModeChange(PlayerGameModeChangeEvent event) { + LoggablePlayerEvent loggable = generateLoggablePlayerEvent( + event, PlayerEventAction.GAMEMODE_CHANGE, null, null); + loggable.setGamemode(event.getNewGameMode().toString()); + logAndSend(loggable); +} + +@EventHandler +public void onBedEnter(PlayerBedEnterEvent event) { + LoggablePlayerEvent loggable = generateLoggablePlayerEvent( + event, PlayerEventAction.BED_ENTER, null, null); + logAndSend(loggable); +} + +@EventHandler +public void onWorldChange(PlayerChangedWorldEvent event) { + LoggablePlayerEvent loggable = generateLoggablePlayerEvent( + event, PlayerEventAction.WORLD_CHANGE, null, event.getFrom().getName()); + logAndSend(loggable); +} +``` + +Note: `generateLoggablePlayerEvent` takes a `PlayerEvent`. `PlayerGameModeChangeEvent`, `PlayerBedEnterEvent`, `PlayerChangedWorldEvent`, and `PlayerTeleportEvent` all extend `PlayerEvent`, so they pass directly. + +- [ ] **Step 4: Compile spigot** + +Run: `$MVN -pl spigot -am clean compile` +Expected: BUILD SUCCESS. Resolve any API-availability failures per the notes above. + +- [ ] **Step 5: Commit** + +```bash +git add spigot/src/main/java/com/splunk/spigot/eventloggers/PlayerEventLogger.java +git commit -m "feat(spigot): add teleport/gamemode/bed/world-change and login session detail" +``` + +--- + +## Task 11: ScheduledMetricLogger base + ServerEventLogger + PerformanceSampler (spigot) + +Non-event metrics need a scheduler. Add a thin base and two concrete users. + +**Files:** +- Create: `spigot/src/main/java/com/splunk/spigot/scheduling/ScheduledMetricLogger.java` +- Create: `spigot/src/main/java/com/splunk/spigot/eventloggers/PerformanceSampler.java` +- Create: `spigot/src/main/java/com/splunk/spigot/eventloggers/ServerEventLogger.java` + +- [ ] **Step 1: Implement the scheduler base** + +```java +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); + } +} +``` + +- [ ] **Step 2: Implement PerformanceSampler** + +```java +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 (TPS, MSPT, online players, loaded chunks). + */ +public class PerformanceSampler extends ScheduledMetricLogger { + + public PerformanceSampler(Properties props) { + super(props); + } + + @Override + protected void sample() { + LoggablePerformanceEvent e = new LoggablePerformanceEvent(0L); + // Bukkit.getTPS() returns [1m, 5m, 15m] averages (Paper/Spigot). + double[] tps = Bukkit.getTPS(); + if (tps.length > 0) { + e.setTps(tps[0]); + } + e.setMspt(Bukkit.getAverageTickTime()); + e.setOnlinePlayers(Bukkit.getOnlinePlayers().size()); + int loaded = 0; + for (org.bukkit.World w : Bukkit.getWorlds()) { + loaded += w.getLoadedChunks().length; + } + e.setLoadedChunks(loaded); + logAndSend(e); + } +} +``` + +Note: `Bukkit.getTPS()` and `Bukkit.getAverageTickTime()` are Paper APIs (also present on modern Spigot). If unavailable on the pinned API, the compile in Step 4 fails — fall back to omitting TPS/MSPT and sampling only player/chunk counts. + +- [ ] **Step 3: Implement ServerEventLogger** + +```java +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); + } +} +``` + +- [ ] **Step 4: Compile spigot** + +Run: `$MVN -pl spigot -am clean compile` +Expected: BUILD SUCCESS. Resolve API-availability failures per the notes. + +- [ ] **Step 5: Commit** + +```bash +git add spigot/src/main/java/com/splunk/spigot/scheduling/ScheduledMetricLogger.java \ + spigot/src/main/java/com/splunk/spigot/eventloggers/PerformanceSampler.java \ + spigot/src/main/java/com/splunk/spigot/eventloggers/ServerEventLogger.java +git commit -m "feat(spigot): add ScheduledMetricLogger base, PerformanceSampler, ServerEventLogger" +``` + +--- + +## Task 12: Wire everything into LogToSplunkPlugin with toggles + +**Files:** +- Modify: `spigot/src/main/java/com/splunk/spigot/LogToSplunkPlugin.java` + +- [ ] **Step 1: Register the new loggers gated by toggles** + +In `LogToSplunkPlugin.onEnable`, after the existing three `registerEvents` calls (line 48), add: + +```java +final org.bukkit.plugin.PluginManager pm = getServer().getPluginManager(); + +// Existing always-on registrations remain above. New categories are opt-in. +final java.util.Properties p = properties; + +if (Boolean.parseBoolean(p.getProperty("splunk.craft.enable.combat", "false"))) { + pm.registerEvents(new com.splunk.spigot.eventloggers.CombatEventLogger(p), this); +} +if (Boolean.parseBoolean(p.getProperty("splunk.craft.enable.item", "false"))) { + pm.registerEvents(new com.splunk.spigot.eventloggers.ItemEventLogger(p), this); +} +if (Boolean.parseBoolean(p.getProperty("splunk.craft.enable.progression", "false"))) { + pm.registerEvents(new com.splunk.spigot.eventloggers.ProgressionEventLogger(p), this); +} +if (Boolean.parseBoolean(p.getProperty("splunk.craft.enable.server", "false"))) { + pm.registerEvents(new com.splunk.spigot.eventloggers.ServerEventLogger(p), this); +} +if (Boolean.parseBoolean(p.getProperty("splunk.craft.enable.performance", "false"))) { + int interval = 600; + try { + interval = Integer.parseInt(p.getProperty("splunk.craft.performance.interval_ticks", "600")); + } catch (NumberFormatException ignored) { } + new com.splunk.spigot.eventloggers.PerformanceSampler(p).start(this, interval); +} +``` + +(The existing `PlayerEventLogger` registration stays; its new session-detail handlers come along automatically. Session-detail and the IP sub-toggle are read inside `PlayerEventLogger` via `isEnabled`.) + +- [ ] **Step 2: Full reactor build incl. shaded plugin** + +Run: `$MVN clean package` +Expected: BUILD SUCCESS; shaded plugin jar under `logtosplunk-plugin/target/`. + +- [ ] **Step 3: Commit** + +```bash +git add spigot/src/main/java/com/splunk/spigot/LogToSplunkPlugin.java +git commit -m "feat(spigot): register new event loggers gated by per-category toggles" +``` + +--- + +## Task 13: Document the new config toggles + +**Files:** +- Modify: `sampleModConfig/splunk.properties` (if it exists; otherwise create a documented sample there) + +- [ ] **Step 1: Locate the sample config** + +Run: `find /home/splunk/appDev/minecraft-app/sampleModConfig -name 'splunk.properties'` +If none exists, create `sampleModConfig/splunk.properties`. + +- [ ] **Step 2: Append the documented toggles** + +Add (keeping any existing keys): + +```properties +# --- Extended logging categories (all opt-in; default off) --- +# Combat & survival: damage, kills, healing, hunger, respawn (throttled per player) +splunk.craft.enable.combat=false +# Item economy: pickup, drop +splunk.craft.enable.item=false +# Progression: xp, level, enchant, craft, fish, command +splunk.craft.enable.progression=false +# Server lifecycle: start, weather change +splunk.craft.enable.server=false +# Performance sampler: TPS, MSPT, online players, loaded chunks +splunk.craft.enable.performance=false +# How often (in server ticks, 20 ticks = 1s) to sample performance +splunk.craft.performance.interval_ticks=600 +# Session detail: log client IP on connect (PII — opt in deliberately) +splunk.craft.enable.session_ip=false +``` + +- [ ] **Step 3: Commit** + +```bash +git add sampleModConfig/splunk.properties +git commit -m "docs: document extended-logging config toggles in sample splunk.properties" +``` + +--- + +## Task 14: Manual smoke test against a local Paper server + +**Files:** none (verification) + +- [ ] **Step 1: Build the plugin jar** + +Run: `$MVN clean package` → confirm `logtosplunk-plugin/target/*.jar`. + +- [ ] **Step 2: Deploy to a local Paper 1.21.10 server** + +Copy the shaded jar into the test server's `plugins/`, set `config/splunk.properties` with a valid HEC token and **all new toggles enabled**, start the server on Java 21. + +- [ ] **Step 3: Exercise each category and confirm Splunk receives events** + +In-game / console: take damage, eat, pick up and drop an item, gain XP / level up, run a command, change gamemode, sleep in a bed, change worlds, trigger weather. Wait for a performance sample interval. In Splunk, confirm events arrive for `CombatEvent`, `ItemEvent`, `ProgressionEvent`, `PlayerEvent` (teleport/gamemode/bed/world), `ServerEvent`, `PerformanceEvent`. + +- [ ] **Step 4: Confirm throttling and opt-out** + +Verify rapid damage does not produce one event per tick (throttled to ~1/s), and that with a category toggle set to `false` no events of that type are emitted. + +- [ ] **Step 5: Record the smoke result in the PR description.** + +--- + +## Done criteria + +- All `shared-mc` unit tests green (`$MVN -pl shared-mc -am test`). +- `$MVN clean package` green; shaded plugin jar built. +- Each new category logs to Splunk when enabled, is silent when disabled, and high-volume events are throttled. +- IP logging only occurs with `splunk.craft.enable.session_ip=true`. + +## Self-review notes (addressed) + +- **Spec coverage:** all four requested categories (combat/survival, economy/progression, session detail, server lifecycle/performance) map to Tasks 2–5 (data) and 8–12 (wiring). Toggles (Task 7/12/13), throttling (Task 6 + listener use), PII/IP gate (Task 10/13) covered. +- **NPE gotcha:** location-less constructor added in Task 1 before any location-less event (server/performance) is built. +- **Type consistency:** `EventThrottle.allow(String)`, `LoggableEventType` constants, action enums, and setter names are referenced identically across data tasks and listener tasks. +- **API-availability risk:** Paper-specific calls (`getTPS`, `getAverageTickTime`, `getProtocolVersion`) are flagged at each compile gate with a documented fallback, matching the spec's "open risks". From f0bd6c11c354ccf4fe6477e00257cac71aeea90a Mon Sep 17 00:00:00 2001 From: splunk Date: Mon, 13 Jul 2026 15:40:45 -0400 Subject: [PATCH 3/3] docs: architecture diagram, setup guide overhaul, server control script README.md: add an Architecture section (Mermaid diagram of the HEC vs KV-store write paths) and a platform support matrix (which MC version(s) and which logging categories each of Spigot/Paper/Forge/NeoForge/Fabric actually implements) so it's clear up front that NeoForge/Fabric are block/death/player only. Also fixes a wrong path in the player-stats section (world/players/stats/.json -> world/stats/.json -- that's actually where vanilla Minecraft writes per-player stats). MINECRAFT_SETUP.md: reorganized so all prerequisites (Splunk HEC/index/ KV-token, then Minecraft JDK/Maven/platform-version matrix) come before any instructions, replacing the old structure where you'd discover a missing requirement mid-setup. Adds minecraft-server-ctl.sh: a single start/stop/restart/status/console script driving the server inside a detached tmux session, replacing the old run_minecraft.sh foreground-only launcher. The stop path specifically avoids a footgun hit while testing this branch: once the server process exits, its pane sits at a shell prompt ("Press any key to close..."), and sending one more keystroke into that pane can consume the prompt and kill the entire tmux session (not just the Minecraft process) if it's the session's only window. This script polls for the java process to actually exit and then kills the tmux session directly, never sending a second keystroke into the pane. --- MINECRAFT_SETUP.md | 164 ++++++++++++++++++++ README.md | 321 +++++++++++++++++++++++++--------------- install_minecraft.sh | 152 +++++++++++++++++++ minecraft-server-ctl.sh | 146 ++++++++++++++++++ 4 files changed, 666 insertions(+), 117 deletions(-) create mode 100644 MINECRAFT_SETUP.md create mode 100644 install_minecraft.sh create mode 100644 minecraft-server-ctl.sh diff --git a/MINECRAFT_SETUP.md b/MINECRAFT_SETUP.md new file mode 100644 index 0000000..978a245 --- /dev/null +++ b/MINECRAFT_SETUP.md @@ -0,0 +1,164 @@ +# Splunk Minecraft Server & LogToSplunk Plugin Setup Guide + +This guide describes how to install, configure, run, and verify a local Minecraft server +integrated with the `LogToSplunk` plugin/mod. See the [Architecture section of +README.md](README.md#architecture) for how the pieces fit together. + +--- + +## 📋 Requirements + +### Splunk side + +1. **Splunk Enterprise**, installed and running on the host machine (or reachable over the + network). +2. **HTTP Event Collector (HEC)** enabled and a token created: **Settings → Data Inputs → HTTP + Event Collector**. Note the token's **index** and **port** (default `8088`) — the plugin + sends event JSON there, sourcetype `minecraft:json`. There is no `index=` key in + `splunk.properties`; the index is whatever the HEC token itself is bound to. +3. **(Optional) KV-store bearer token**, only if you want the player-stats feature (see below): + **Settings → Tokens**, a token whose user has write access to the `SplunkCraft` app's + collections. This uses the splunkd **management** port (default `8089`, HTTPS) — a different + port and credential than HEC. +4. **The `minecraft-app` Splunk app** installed (this repo, copied into + `$SPLUNK_HOME/etc/apps/`) and, if using player stats, the **`SplunkCraft`** app too + (`SplunkCraft/` in this repo — ships the `minecraft_player_stats` KV collection + lookup). + +### Minecraft side + +1. **JDK 21+** for building (`java -version`). +2. **Maven**, for platforms built with Maven (Spigot, Paper, and the dist aggregator). `mvn-bin/` + is expected by `install_minecraft.sh` but isn't tracked in this repo — unpack your own Maven + distribution there, or build with a system-installed `mvn` directly. +3. **A Minecraft server matching one of the supported platform/version combinations:** + + | Platform | MC version(s) | Build tool | + |-----------|-------------------------------------------|------------| + | Spigot | 1.21.1 (default), 1.20.1, 1.20.4, 1.20.6 | Maven (`mvn package` / `-P mc-1201` etc.) | + | Paper | 1.21.1 | Maven | + | Forge | 1.20.1 | Gradle (`forge-1.20.1-47.4.20/gradlew`) | + | NeoForge | 1.20.4 | Gradle | + | Fabric | 1.20.6 | Gradle | + + Only Spigot, Paper, and Forge currently have the full extended-logging + KV-store feature set + (see the platform support matrix in README.md); NeoForge/Fabric only have block/death/player + logging. +4. **tmux** (or an equivalent terminal multiplexer), if you want the server to keep running after + you disconnect — see "Running the server" below. + +--- + +## ⚡ Building + +`install_minecraft.sh` automates a from-scratch **Spigot/Paper** setup: builds the plugin with +Maven, downloads a PaperMC server jar, accepts the EULA, writes a generated HEC token into both +`server-config/splunk.properties` and Splunk's `inputs.conf`, and deploys the built jar into +`plugins/`. + +```bash +./install_minecraft.sh +``` + +By default it installs to `/home/splunk/minecraft-server`; override with `MC_SERVER_DIR`: + +```bash +export MC_SERVER_DIR="/path/to/custom/minecraft-server" +./install_minecraft.sh +``` + +For **Forge/NeoForge/Fabric**, build the platform module directly with its own Gradle wrapper +(e.g. `cd forge-1.20.1-47.4.20 && ./gradlew build`) and copy the shaded jar from `build/libs/` +into the server's `mods/` directory yourself — these platforms aren't wired into +`install_minecraft.sh` yet. + +Either way, once the jar is in place, copy `logtosplunk-plugin/src/main/config/splunk.properties` +(fully commented, safe to use as a template) to `/config/splunk.properties` and +replace every `CHANGEME` value. + +--- + +## 🚀 Running the server + +Use `minecraft-server-ctl.sh` to start/stop/restart the server inside a detached `tmux` session, +so it keeps running after you log out: + +```bash +./minecraft-server-ctl.sh start # launch in a new tmux session +./minecraft-server-ctl.sh status # check tmux session / process / port +./minecraft-server-ctl.sh console # attach to watch the live console (Ctrl-b d to detach) +./minecraft-server-ctl.sh stop # graceful stop, waits for the process to exit +./minecraft-server-ctl.sh restart # stop, then start +``` + +Configure it via environment variables if your server isn't the default ATLauncher path: + +```bash +export MC_SERVER_DIR="/path/to/your/server" +export MC_LAUNCH_CMD="./LaunchServer.sh" # or ./run.sh, whatever your server uses +export MC_TMUX_SESSION="my-mc-server" +export MC_SERVER_PORT="25565" +./minecraft-server-ctl.sh start +``` + +> **Why not just `tmux send-keys stop` by hand?** Once the server process exits, the pane sits at +> a shell prompt ("Press any key to close..."). Sending one more keystroke into that pane to +> "finish" the stop can consume that prompt and close the pane — and if it's the tmux session's +> only window, that kills the whole session, not just the Minecraft process. `minecraft-server-ctl.sh` +> polls for the java process to actually exit and then kills the tmux session directly, without +> ever sending a second keystroke into the pane. If you're scripting your own start/stop instead +> of using this script, keep that in mind. + +--- + +## 🔍 Verification & Manual Steps + +### 1. Connecting a Minecraft Client +1. Open the Minecraft Launcher and launch the matching version for your server (see the platform + table above). +2. Select **Multiplayer** → **Add Server**, address `localhost` (or `127.0.0.1:`). +3. Connect and play — block break/place, death, and player-movement events (plus the extended + categories, if enabled and supported on your platform) forward to Splunk in real time. + +### 2. Verifying HEC Token in Splunk +```bash +cat /opt/splunk/etc/apps/splunk_httpinput/local/inputs.conf +``` +You should see a stanza like: +```ini +[http://minecraft] +disabled = 0 +token = +index = +sourcetype = minecraft:json +``` + +### 3. Verifying Minecraft Logs +```bash +grep -i "splunk" /logs/latest.log +``` +You should see `Splunk for Minecraft initialized.` (spigot/paper) or the equivalent Forge/mod +startup line, plus `Sending data to splunk...` as events flow. + +### 4. Verifying the KV store (if enabled) +```bash +curl -sk -H "Authorization: Bearer " \ + "https://127.0.0.1:8089/servicesNS/nobody/SplunkCraft/storage/collections/data/minecraft_player_stats" +``` +or in SPL: `| inputlookup minecraft_player_stats` + +--- + +## 🛠️ Troubleshooting + +- **Port already in use**: `./minecraft-server-ctl.sh status` shows whether something's already + listening; find the process with `lsof -i :` if it's not this server, or change + `server-port` in `server.properties`. +- **HEC connection issues**: verify Splunk is running (`/opt/splunk/bin/splunk status`), check the + HEC listener (`curl -i http://localhost:8088/services/collector/health`), and confirm the token + in `/config/splunk.properties` matches Splunk's `inputs.conf`. +- **KV-store upsert failures**: check the server log for `KV-store batch_save failed` — usually a + wrong bearer token, wrong management port (`8089`, not `8088`), or the `SplunkCraft` app not + installed/reloaded (`POST /services/apps/local/_reload`). +- **Events flowing to HEC but a whole category (e.g. combat) never appears**: check the platform + support matrix in README.md — NeoForge and Fabric don't implement the extended logging + categories yet, regardless of config toggles. diff --git a/README.md b/README.md index bf6670d..b42b607 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,204 @@ -# Minecraft App - -### Version 1.0 - -The Minecraft App lets you visualize the minecraft world from the guts side. Wondering how many blocks have been dug up by your buddies? Not a problem. Wondering who's found the most diamonds? Yep, got it covered. Have you been planting enough wheat? Carrots? Pototoes? The Minecraft App will let you know. - -### Release Notes - -* Uses the new Splunk HTTP Event Collector as it's input instead of TCP. -* Now supports Spigot and Forge as well as Craftbukkit as a unified plugin jar. -* Plugin will cache items that the server does not acknowledge so restarting Splunk is no longer an issue -* Ore images in the "Mined Blocks" page of the app now display correctly. -* Minor bug fixes. - -### Getting Started -This section provides information about installing and using the Minecraft App. - -#### Requirements - -* Operating System: Windows, Linux, or Mac OS X. -* Web browsers: Latest versions of Chrome, Safari, or Firefox, Internet Explorer 9 or later. -* Craftbukkit, Spigot or Forge - * [Craftbukkit](http://bukkit.org/) - * [Spigot](https://www.spigotmc.org) - * [Forge](http://www.minecraftforge.net/) -* LogToSplunk Plugin: The log to splunk plugin that allows input of more detailed minecraft data to splunk from [CraftBukkit](http://dev.bukkit.org/bukkit-plugins/logtosplunk/) -* The Splunk Web Framework: The Web Framework is included in Splunk 6 and is available for download for Splunk 5 from the -[Splunk Developer Portal](http://dev.splunk.com/view/webframework-standalone/SP-CAAAEMA). -* Minecraft Overviewer (Optional): The Google Maps based minecraft word renderer from [Overviewer](http://overviewer.org) - -#### Installing the Minecraft App -The Minecraft App is built as a Splunk App on the Splunk Web Framework and must be installed on top of it. - -##### Installing from Splunk Web -If you downloaded the Minecraft App from [Splunk Apps](http://apps.splunk.com), you can install the app within Splunk Web. - -* For more, see [Where to get more apps and add-ons](http://docs.splunk.com/Documentation/Splunk/latest/Admin/Wheretogetmoreapps). - -##### Installing from a ZIP Source File - -1. [Download and unzip the Minecraft App](https://github.com/splunk/minecraft-app/archive/develop.zip) -or clone the repository from [GitHub](https://github.com/splunk/minecraft-app.git). -2. Copy the entire `/minecraft-app` subdirectory into `$SPLUNK_HOME/etc/apps/`. -3. Restart Splunk. -4. In Splunk Web, navigate to the Minecraft App (*http://localhost:8000/dj/minecraft-app*). - -#### Event Collector Configuration - -1. Confgiure the Splunk Http Event Collector as noted in the [Documentation](http://dev.splunk.com/view/event-collector/SP-CAAAE6M) for whatever port you like. -2. Ensure firewalls and NAT are properly configured if applicable -3. Take note of the application key as you will need it later on - - -#### Installing the LogToSplunk Plugin - -1. Copy the LogToSplunk jar from the app tgz or app directory into your craftbukkit server's `plugins` directory. -2. Create a `config` directory in the root server folder (the directory that contains the `plugins` folder). -3. Create and edit a `splunk.properties` text file in the `config` directory created in 3. Replace the port and application in your `splunk.properties` and adjust other options as necessary. - - * Use the app token from the Event Collector Configuration above for this property `splunk.craft.token=BEEFCAFE-1337-F00D-8BDA-2410D44E3453` - - * If you're running splunk on a separate machine from your minecraft server update this property `splunk.craft.connection.host=127.0.0.1` - - * Use the app token from the Event Collector Configuration above for this property `splunk.craft.connection.port=8088` - - * If you wish to log the output to a local log as well set this property to "true" `mod.splunk.enable.consolelog=false` - -#### Configuring The Livemap - -1. Download and configure Overview as described int the [Overviewer Docs](http://docs.overviewer.org/en/latest/) -2. Serve the overviewer via a webserver like [Apache](http://httpd.apache.org) or [IIS](http://www.iis.net). Many operating systems have a web service built in that just needs to be enabled. -3. Create an initial render from overviewer with at least one of the "Normal","Lighting", and "Night" options -4. Copy the overviewer.css,overviewer.js, and overviewerConfig.js scripts from the base render directory to `$SPLUNK_HOME/etc/apps/minecraft-app/django/minecraft-app/static/minecraft-app/` on your splunk server. -5. Edit `$SPLUNK_HOME/etc/apps/minecraft-app/django/minecraft-app/static/minecraft-app/overviewerConfig.js` and modify the path variable of each tileset object to include your webserver path. For example, change `"path": "world-normal"` to `"path": "http://webserver:81/world-normal"`. The external hostname must be used in order for the map to be visible to clients. Using "localhost" as the webserver will not work as the minecraft app does not reserve the map, it simply redirects to it. - -NOTE: The minecraft-app does not refresh overviewer renders automatically. This will need to be scheduled by another service (ie. cron or task scheduler). - - -#### Known Issues - -1) Time calculations and active players may be mis-reported if player disconnects are not logged properly (ie. due to a server crash). Orphaned sessions may be estimated by running sessions from connection to the subsequent server start. -2) The live map may appear to "shift" as the minecraft world expands and overviewer resets it's origin in future renders. This can be corrected by recopying and modifying the overviewerConfig.js script with the same steps as the installation. - - - -## Documentation and resources - -When you need to know more: - -* For Overviewer documentation, see [Overviewer](http://overviewer.org) - -* For Spigt documentation, see [Spigot](https://www.spigotmc.org) - -* For Forge documentation, see [Forge](http://www.minecraftforge.net/) - -* For CraftBukkit documentation, see [Craftbukkit](http://bukkit.org/) - -* For all things developer with Splunk, your main resource is the [Splunk Developer Portal](http://dev.splunk.com). - -* For component reference documentation, see the [Splunk Web Framework Reference](http://docs.splunk.com/Documentation/WebFramework). - -* For more about Splunk in general, see [Splunk>Docs](http://docs.splunk.com/Documentation/Splunk). - - -### How to contribute - -If you would like to contribute to the Minecraft App, go here for more information: - -* [Minecraft App Github](https://github.com/splunk/minecraft-app) - -Please feel free to open issues and provide feedback through GitHub Issues. - -## License -The Minecraft_App is licensed under the Apache License 2.0. Details can be found in the LICENSE file. - - - +# Minecraft App + +### Version 1.0 + +The Minecraft App lets you visualize the minecraft world from the guts side. Wondering how many blocks have been dug up by your buddies? Not a problem. Wondering who's found the most diamonds? Yep, got it covered. Have you been planting enough wheat? Carrots? Pototoes? The Minecraft App will let you know. + +### Release Notes + +* Uses the new Splunk HTTP Event Collector as it's input instead of TCP. +* Now supports Spigot and Forge as well as Craftbukkit as a unified plugin jar. +* Plugin will cache items that the server does not acknowledge so restarting Splunk is no longer an issue +* Ore images in the "Mined Blocks" page of the app now display correctly. +* Minor bug fixes. + +### Architecture + +The project has two independent halves: a **Minecraft-side plugin/mod** (one build per server +platform, sharing a common core) and a **Splunk-side app** that visualizes what it sends. Two +separate Splunk write paths are used because they carry different kinds of data: + +```mermaid +flowchart TB + subgraph MC["Minecraft server"] + direction TB + Platform["Spigot / Paper / Forge / NeoForge / Fabric\n(platform-specific event hooks)"] + Shared["shared-mc\n(event model, HTTP clients, config)"] + Platform --> Shared + EventLoggers["Event loggers\nBlock, Death, Player, Combat, Item,\nProgression, Server, Performance"] + Scraper["PlayerStatsScraper\n(async: reads world/stats + advancements JSON)"] + Shared --> EventLoggers + Shared --> Scraper + end + + HEC["Splunk HEC :8088\n(SingleSplunkConnection)"] + KV["splunkd management REST :8089\n(KvStoreConnection, bearer token)"] + + EventLoggers -->|"JSON events,\nsourcetype=minecraft:json"| HEC + Scraper -->|"batch_save upsert\nby player UUID"| KV + + HEC --> Index[("index (per HEC token config)")] + KV --> Collection[("KV collection\nminecraft_player_stats\n(SplunkCraft app)")] + Collection --> Lookup["transforms.conf lookup\n`| inputlookup minecraft_player_stats`"] + + Index --> App["minecraft-app\n(Django views, dashboards,\nlive map)"] + Lookup --> App +``` + +**Why two paths:** HEC can only write to an index (time-series events); it cannot write a KV +store. Player stats are a current-state snapshot (one row per player, overwritten each scrape), +which is what a KV store is for — hence the separate `KvStoreConnection` over the splunkd +management port, authenticated with a bearer token instead of the HEC token. See "Collecting +Player Stats into the KV Store" below for setup. + +**Platform support matrix** (MC version(s) each build targets, and which logging categories are +implemented): + +| Platform | MC version(s) | Block/Death/Player | Combat/Item/Progression/Server/Performance | KV-store player stats | +|-----------|---------------------------------|:---:|:---:|:---:| +| Spigot | 1.21.1 (default), 1.20.1, 1.20.4, 1.20.6 | ✅ | ✅ | ✅ | +| Paper | 1.21.1 | ✅ | ✅ | ✅ | +| Forge | 1.20.1 | ✅ | ✅ | ✅ | +| NeoForge | 1.20.4 | ✅ | ❌ | ❌ | +| Fabric | 1.20.6 | ✅ | ❌ | ❌ | + +NeoForge and Fabric only have the original block/death/player logging — the extended categories +and KV-store scraper haven't been ported to those two platforms yet (contributions welcome). + +### Getting Started +This section provides information about installing and using the Minecraft App. + +#### Requirements + +* Operating System: Windows, Linux, or Mac OS X. +* Web browsers: Latest versions of Chrome, Safari, or Firefox, Internet Explorer 9 or later. +* Craftbukkit, Spigot or Forge + * [Craftbukkit](http://bukkit.org/) + * [Spigot](https://www.spigotmc.org) + * [Forge](http://www.minecraftforge.net/) +* LogToSplunk Plugin: The log to splunk plugin that allows input of more detailed minecraft data to splunk from [CraftBukkit](http://dev.bukkit.org/bukkit-plugins/logtosplunk/) +* The Splunk Web Framework: The Web Framework is included in Splunk 6 and is available for download for Splunk 5 from the +[Splunk Developer Portal](http://dev.splunk.com/view/webframework-standalone/SP-CAAAEMA). +* Minecraft Overviewer (Optional): The Google Maps based minecraft word renderer from [Overviewer](http://overviewer.org) + +#### Installing the Minecraft App +The Minecraft App is built as a Splunk App on the Splunk Web Framework and must be installed on top of it. + +##### Installing from Splunk Web +If you downloaded the Minecraft App from [Splunk Apps](http://apps.splunk.com), you can install the app within Splunk Web. + +* For more, see [Where to get more apps and add-ons](http://docs.splunk.com/Documentation/Splunk/latest/Admin/Wheretogetmoreapps). + +##### Installing from a ZIP Source File + +1. [Download and unzip the Minecraft App](https://github.com/splunk/minecraft-app/archive/develop.zip) +or clone the repository from [GitHub](https://github.com/splunk/minecraft-app.git). +2. Copy the entire `/minecraft-app` subdirectory into `$SPLUNK_HOME/etc/apps/`. +3. Restart Splunk. +4. In Splunk Web, navigate to the Minecraft App (*http://localhost:8000/dj/minecraft-app*). + +#### Event Collector Configuration + +1. Confgiure the Splunk Http Event Collector as noted in the [Documentation](http://dev.splunk.com/view/event-collector/SP-CAAAE6M) for whatever port you like. +2. Ensure firewalls and NAT are properly configured if applicable +3. Take note of the application key as you will need it later on + + +#### Installing the LogToSplunk Plugin + +1. Copy the LogToSplunk jar from the app tgz or app directory into your craftbukkit server's `plugins` directory. +2. Create a `config` directory in the root server folder (the directory that contains the `plugins` folder). +3. Create and edit a `splunk.properties` text file in the `config` directory created in 3. Replace the port and application in your `splunk.properties` and adjust other options as necessary. + + * Use the app token from the Event Collector Configuration above for this property `splunk.craft.token=BEEFCAFE-1337-F00D-8BDA-2410D44E3453` + + * If you're running splunk on a separate machine from your minecraft server update this property `splunk.craft.connection.host=127.0.0.1` + + * Use the app token from the Event Collector Configuration above for this property `splunk.craft.connection.port=8088` + + * If you wish to log the output to a local log as well set this property to "true" `mod.splunk.enable.consolelog=false` + +#### Collecting Player Stats into the KV Store + +The plugin can periodically scrape the per-player snapshot files Minecraft writes to disk +(`/stats/.json` and `/advancements/.json`) and upsert one row per +player into the `minecraft_player_stats` KV-store collection. Because this is current-state +snapshot data (not a time series), it goes to a KV store keyed by player UUID rather than an +index. + +> **Note:** This path does **not** use HEC. The HTTP Event Collector can only write events to +> an index, never to a KV store. Populating a KV store requires the splunkd **management** REST +> endpoint (default port `8089`, HTTPS) authenticated with a **bearer token** — a different port +> and credential than HEC. + +1. Create a bearer token in Splunk: **Settings → Tokens** (enable token auth if needed). The + token's user needs write access to the `SplunkCraft` app's collections. +2. Add the following to `splunk.properties` (all default off / placeholder): + + ``` + splunk.craft.enable.playerstats=true + splunk.craft.playerstats.interval_ticks=6000 # ~5 min (20 ticks = 1s) + splunk.craft.kvstore.host=127.0.0.1 + splunk.craft.kvstore.port=8089 # splunkd mgmt port, NOT 8088 + splunk.craft.kvstore.app=SplunkCraft + splunk.craft.kvstore.collection=minecraft_player_stats + splunk.craft.kvstore.bearer_token=YOUR-BEARER-TOKEN + #splunk.craft.world.path=world # optional override + ``` + +3. Query it in SPL: `| inputlookup minecraft_player_stats` + +The collection and its lookup definition ship in the **SplunkCraft** app (`collections.conf` / +`transforms.conf`), which also houses the player reports and dashboards. The scraper only +uploads players whose files changed since the last cycle. + +#### Configuring The Livemap + +1. Download and configure Overview as described int the [Overviewer Docs](http://docs.overviewer.org/en/latest/) +2. Serve the overviewer via a webserver like [Apache](http://httpd.apache.org) or [IIS](http://www.iis.net). Many operating systems have a web service built in that just needs to be enabled. +3. Create an initial render from overviewer with at least one of the "Normal","Lighting", and "Night" options +4. Copy the overviewer.css,overviewer.js, and overviewerConfig.js scripts from the base render directory to `$SPLUNK_HOME/etc/apps/minecraft-app/django/minecraft-app/static/minecraft-app/` on your splunk server. +5. Edit `$SPLUNK_HOME/etc/apps/minecraft-app/django/minecraft-app/static/minecraft-app/overviewerConfig.js` and modify the path variable of each tileset object to include your webserver path. For example, change `"path": "world-normal"` to `"path": "http://webserver:81/world-normal"`. The external hostname must be used in order for the map to be visible to clients. Using "localhost" as the webserver will not work as the minecraft app does not reserve the map, it simply redirects to it. + +NOTE: The minecraft-app does not refresh overviewer renders automatically. This will need to be scheduled by another service (ie. cron or task scheduler). + + +#### Known Issues + +1) Time calculations and active players may be mis-reported if player disconnects are not logged properly (ie. due to a server crash). Orphaned sessions may be estimated by running sessions from connection to the subsequent server start. +2) The live map may appear to "shift" as the minecraft world expands and overviewer resets it's origin in future renders. This can be corrected by recopying and modifying the overviewerConfig.js script with the same steps as the installation. + + + +## Documentation and resources + +When you need to know more: + +* For Overviewer documentation, see [Overviewer](http://overviewer.org) + +* For Spigt documentation, see [Spigot](https://www.spigotmc.org) + +* For Forge documentation, see [Forge](http://www.minecraftforge.net/) + +* For CraftBukkit documentation, see [Craftbukkit](http://bukkit.org/) + +* For all things developer with Splunk, your main resource is the [Splunk Developer Portal](http://dev.splunk.com). + +* For component reference documentation, see the [Splunk Web Framework Reference](http://docs.splunk.com/Documentation/WebFramework). + +* For more about Splunk in general, see [Splunk>Docs](http://docs.splunk.com/Documentation/Splunk). + + +### How to contribute + +If you would like to contribute to the Minecraft App, go here for more information: + +* [Minecraft App Github](https://github.com/splunk/minecraft-app) + +Please feel free to open issues and provide feedback through GitHub Issues. + +## License +The Minecraft_App is licensed under the Apache License 2.0. Details can be found in the LICENSE file. + + + diff --git a/install_minecraft.sh b/install_minecraft.sh new file mode 100644 index 0000000..15f909b --- /dev/null +++ b/install_minecraft.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# ============================================================================== +# Minecraft Server & LogToSplunk Plugin Installation and Configuration Script +# ============================================================================== +set -euo pipefail + +# --- Color Constants for Premium UX --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +echo -e "${BLUE}======================================================================${NC}" +echo -e "${CYAN} Minecraft & LogToSplunk Plugin Automated Setup Script ${NC}" +echo -e "${BLUE}======================================================================${NC}" + +# --- Configuration & Paths --- +MC_SERVER_DIR="${MC_SERVER_DIR:-/home/splunk/minecraft-server}" +MC_VERSION="1.21.1" +PAPER_BUILD="133" +WORKSPACE_DIR="/home/splunk/appDev/minecraft-app" +JAVA_HOME_PATH="/usr/lib/jvm/java-25-openjdk-amd64" +SPLUNK_HOME="/opt/splunk" + +# --- 1. Validate Java --- +echo -e "\n${CYAN}[1/8] Verifying Java Installation...${NC}" +if ! command -v java &> /dev/null; then + echo -e "${RED}Error: Java is not installed or not in PATH.${NC}" + exit 1 +fi +echo -e "${GREEN}✓ Java is available: $(java -version 2>&1 | head -n 1)${NC}" + +# --- 2. Build LogToSplunk Plugin --- +echo -e "\n${CYAN}[2/8] Compiling LogToSplunk Plugin...${NC}" +if [ -d "${WORKSPACE_DIR}/mvn-bin" ]; then + echo -e "${YELLOW}Using local Maven installation at ${WORKSPACE_DIR}/mvn-bin${NC}" + JAVA_HOME="${JAVA_HOME_PATH}" "${WORKSPACE_DIR}/mvn-bin/bin/mvn" -f "${WORKSPACE_DIR}/pom.xml" clean package +else + echo -e "${RED}Error: Local Maven bin folder not found at ${WORKSPACE_DIR}/mvn-bin. Please make sure Maven is installed.${NC}" + exit 1 +fi + +PLUGIN_JAR="${WORKSPACE_DIR}/logtosplunk-plugin/target/logtosplunk-plugin-1.0-SNAPSHOT.jar" +if [ ! -f "${PLUGIN_JAR}" ]; then + echo -e "${RED}Error: Failed to build plugin jar at ${PLUGIN_JAR}${NC}" + exit 1 +fi +echo -e "${GREEN}✓ LogToSplunk Plugin compiled successfully.${NC}" + +# --- 3. Setup Minecraft Server Directory --- +echo -e "\n${CYAN}[3/8] Setting up Minecraft server directory at ${MC_SERVER_DIR}...${NC}" +mkdir -p "${MC_SERVER_DIR}" +mkdir -p "${MC_SERVER_DIR}/plugins" +mkdir -p "${MC_SERVER_DIR}/config" +echo -e "${GREEN}✓ Folders created.${NC}" + +# --- 4. Download Minecraft Server Jar (Paper 1.21.1) --- +echo -e "\n${CYAN}[4/8] Downloading Paper ${MC_VERSION} (Build ${PAPER_BUILD}) Server Jar...${NC}" +SERVER_JAR="${MC_SERVER_DIR}/server.jar" +if [ -f "${SERVER_JAR}" ]; then + echo -e "${YELLOW}Server jar already exists. Skipping download.${NC}" +else + DOWNLOAD_URL="https://api.papermc.io/v2/projects/paper/versions/${MC_VERSION}/builds/${PAPER_BUILD}/downloads/paper-${MC_VERSION}-${PAPER_BUILD}.jar" + echo -e "${BLUE}Downloading from: ${DOWNLOAD_URL}${NC}" + wget -O "${SERVER_JAR}" "${DOWNLOAD_URL}" +fi +echo -e "${GREEN}✓ Minecraft Server jar downloaded successfully.${NC}" + +# --- 5. Accept Minecraft EULA --- +echo -e "\n${CYAN}[5/8] Accepting Minecraft EULA...${NC}" +echo "eula=true" > "${MC_SERVER_DIR}/eula.txt" +echo -e "${GREEN}✓ EULA accepted (eula=true written to ${MC_SERVER_DIR}/eula.txt).${NC}" + +# --- 6. Configure Splunk HTTP Event Collector (HEC) --- +echo -e "\n${CYAN}[6/8] Configuring Splunk HEC Inputs...${NC}" +HEC_CONF="${SPLUNK_HOME}/etc/apps/splunk_httpinput/local/inputs.conf" +HEC_TOKEN="" + +# Generate HEC token if not already existing +if [ -f "${HEC_CONF}" ] && grep -q "\[http://minecraft\]" "${HEC_CONF}"; then + echo -e "${YELLOW}Stanza [http://minecraft] already exists in Splunk inputs.conf.${NC}" + # Extract existing token + HEC_TOKEN=$(grep -A 5 "\[http://minecraft\]" "${HEC_CONF}" | grep "token =" | head -n 1 | awk -F'= ' '{print $2}' | tr -d '[:space:]') + echo -e "${GREEN}✓ Using existing HEC token: ${HEC_TOKEN}${NC}" +else + # Generate new UUID for HEC Token + HEC_TOKEN=$(python3 -c "import uuid; print(uuid.uuid4())") + echo -e "${YELLOW}Configuring new Splunk HEC token: ${HEC_TOKEN}${NC}" + + mkdir -p "$(dirname "${HEC_CONF}")" + cat >> "${HEC_CONF}" < "${PROPERTIES_FILE}" < "${SERVER_PROPS_FILE}" </dev/null 2>&1; then + sed -i 's/online-mode=true/online-mode=false/g' "${SERVER_PROPS_FILE}" + if ! grep -q "online-mode=" "${SERVER_PROPS_FILE}"; then + echo "online-mode=false" >> "${SERVER_PROPS_FILE}" + fi + fi +fi +echo -e "${GREEN}✓ server.properties configured (online-mode=false).${NC}" + +# --- 8. Install LogToSplunk Plugin Jar --- +echo -e "\n${CYAN}[8/8] Deploying LogToSplunk Plugin jar to Server...${NC}" +cp "${PLUGIN_JAR}" "${MC_SERVER_DIR}/plugins/logtosplunk-plugin.jar" +echo -e "${GREEN}✓ Plugin jar copied to ${MC_SERVER_DIR}/plugins/logtosplunk-plugin.jar.${NC}" + +echo -e "\n${GREEN}======================================================================${NC}" +echo -e "${GREEN}✓ Minecraft Server and LogToSplunk Plugin installed & configured! ${NC}" +echo -e "${GREEN}======================================================================${NC}" +echo -e "Server Directory: ${MC_SERVER_DIR}" +echo -e "HEC Token: ${HEC_TOKEN}" +echo -e "Run the server with: ./run_minecraft.sh" +echo -e "${GREEN}======================================================================${NC}" diff --git a/minecraft-server-ctl.sh b/minecraft-server-ctl.sh new file mode 100644 index 0000000..f877c04 --- /dev/null +++ b/minecraft-server-ctl.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# ============================================================================= +# minecraft-server-ctl.sh -- start/stop/restart/status for the LogToSplunk +# Minecraft server, run inside a detached tmux session so it survives logout. +# +# Usage: +# ./minecraft-server-ctl.sh start +# ./minecraft-server-ctl.sh stop +# ./minecraft-server-ctl.sh restart +# ./minecraft-server-ctl.sh status +# ./minecraft-server-ctl.sh console # attach to the live server console (Ctrl-b d to detach) +# +# Configuration (override via env vars): +# MC_SERVER_DIR Directory containing the server launch script. Default: +# /home/splunk/.local/share/atlauncher/servers/SplunkCraft +# MC_LAUNCH_CMD Command to launch the server, run from MC_SERVER_DIR. Default: ./LaunchServer.sh +# MC_TMUX_SESSION tmux session name. Default: splunkcraft +# MC_SERVER_PORT Port to check for "server is accepting connections" in `status`. Default: 25565 +# MC_STOP_TIMEOUT Seconds to wait for a graceful shutdown before giving up. Default: 60 +# +# Why tmux, and why this script exists: +# Running the server directly ties it to your terminal session; tmux keeps it running after +# you disconnect. But a naive `tmux send-keys stop` + blindly sending another keystroke +# afterwards is dangerous: once the server process exits, the pane sits at a shell prompt +# ("Press any key to close..."). Sending one more Enter at that point can consume that +# prompt and close the pane -- and if it's the only pane in the only window, that kills the +# entire tmux session (server socket and all), not just the Minecraft process. This script +# never sends a second keystroke after `stop`; it polls for the java process to actually +# exit, then explicitly kills the tmux session. +# ============================================================================= + +set -euo pipefail + +MC_SERVER_DIR="${MC_SERVER_DIR:-/home/splunk/.local/share/atlauncher/servers/SplunkCraft}" +MC_LAUNCH_CMD="${MC_LAUNCH_CMD:-./LaunchServer.sh}" +MC_TMUX_SESSION="${MC_TMUX_SESSION:-splunkcraft}" +MC_SERVER_PORT="${MC_SERVER_PORT:-25565}" +MC_STOP_TIMEOUT="${MC_STOP_TIMEOUT:-60}" + +log() { echo "[minecraft-server-ctl] $*"; } + +session_exists() { + tmux has-session -t "${MC_TMUX_SESSION}" 2>/dev/null +} + +server_pid() { + # Finds the java process whose cwd is the server directory. Empty output if not running. + for pid in $(pgrep -x java 2>/dev/null || true); do + if [ "$(readlink -f "/proc/${pid}/cwd" 2>/dev/null)" = "$(readlink -f "${MC_SERVER_DIR}")" ]; then + echo "${pid}" + return 0 + fi + done + return 1 +} + +port_listening() { + ss -ltn 2>/dev/null | awk '{print $4}' | grep -q ":${MC_SERVER_PORT}$" +} + +do_status() { + if session_exists; then + log "tmux session '${MC_TMUX_SESSION}': running" + else + log "tmux session '${MC_TMUX_SESSION}': not running" + fi + if pid=$(server_pid); then + log "server process: running (pid ${pid})" + else + log "server process: not running" + fi + if port_listening; then + log "port ${MC_SERVER_PORT}: accepting connections" + else + log "port ${MC_SERVER_PORT}: not listening" + fi +} + +do_start() { + if session_exists; then + log "already running (tmux session '${MC_TMUX_SESSION}' exists). Use 'restart' to bounce it." + exit 1 + fi + if [ ! -d "${MC_SERVER_DIR}" ]; then + log "ERROR: MC_SERVER_DIR '${MC_SERVER_DIR}' does not exist." + exit 1 + fi + log "starting server in tmux session '${MC_TMUX_SESSION}' (${MC_SERVER_DIR})..." + tmux new-session -d -s "${MC_TMUX_SESSION}" -c "${MC_SERVER_DIR}" + tmux send-keys -t "${MC_TMUX_SESSION}" "${MC_LAUNCH_CMD}" Enter + log "launched. Use '$0 status' to check it came up, or '$0 console' to watch it." +} + +do_stop() { + if ! session_exists; then + log "no tmux session '${MC_TMUX_SESSION}' found; nothing to stop." + return 0 + fi + if pid=$(server_pid); then + log "sending 'stop' to the server console..." + tmux send-keys -t "${MC_TMUX_SESSION}" "stop" Enter + waited=0 + while kill -0 "${pid}" 2>/dev/null; do + if [ "${waited}" -ge "${MC_STOP_TIMEOUT}" ]; then + log "WARNING: server still running after ${MC_STOP_TIMEOUT}s; leaving tmux session up for inspection." + exit 1 + fi + sleep 2 + waited=$((waited + 2)) + done + log "server process exited cleanly after ${waited}s." + else + log "tmux session exists but no server process found; probably already stopped." + fi + # Deliberately do NOT send any further keystrokes into the pane here (see header comment) -- + # kill the session directly instead of interacting with its now-idle shell prompt. + tmux kill-session -t "${MC_TMUX_SESSION}" 2>/dev/null || true + log "tmux session '${MC_TMUX_SESSION}' closed." +} + +do_restart() { + do_stop + sleep 2 + do_start +} + +do_console() { + if ! session_exists; then + log "no tmux session '${MC_TMUX_SESSION}' found. Start it first with '$0 start'." + exit 1 + fi + log "attaching to '${MC_TMUX_SESSION}' -- press Ctrl-b then d to detach without stopping the server." + tmux attach -t "${MC_TMUX_SESSION}" +} + +case "${1:-}" in + start) do_start ;; + stop) do_stop ;; + restart) do_restart ;; + status) do_status ;; + console) do_console ;; + *) + echo "Usage: $0 {start|stop|restart|status|console}" >&2 + exit 1 + ;; +esac