diff --git a/build.gradle.kts b/build.gradle.kts index cd70a13d..9fcd9098 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("java") id("com.gradleup.shadow") version "9.4.2" apply false + kotlin("jvm") version "2.3.20" apply false } val javaVersionsOverride = mapOf( @@ -27,6 +28,7 @@ subprojects { val example = project.name.startsWith("example") if (example) { + apply { plugin("org.jetbrains.kotlin.jvm") } if (project.path != ":fabric:example-mod" && project.path != ":neoforge:example-mod") { apply { plugin("com.gradleup.shadow") } } diff --git a/bukkit/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt b/bukkit/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt new file mode 100644 index 00000000..e790510b --- /dev/null +++ b/bukkit/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt @@ -0,0 +1,44 @@ +package com.example + +import dev.faststats.ErrorTracker +import dev.faststats.bukkit.BukkitContext +import dev.faststats.data.Metric +import org.bukkit.plugin.java.JavaPlugin +import java.util.concurrent.atomic.AtomicInteger + +class KotlinExamplePlugin : JavaPlugin() { + private val gameCount = AtomicInteger() + + private val context = BukkitContext.Factory(this, "YOUR_TOKEN_HERE") + .errorTrackerService(ERROR_TRACKER) + // .metrics(Metrics.Factory::create) // Define a minimal metrics instance without any custom metrics + .metrics { factory -> + factory + // Custom metrics require a corresponding data source in your project settings + .addMetric(Metric.number("game_count") { gameCount.get() }) + .addMetric(Metric.string("server_version") { "1.0.0" }) + + // #onFlush is invoked after successful metrics submission + // This is useful for cleaning up cached data + .onFlush { gameCount.set(0) } // reset game count on flush + + .create() + } + .create() + + override fun onEnable() { + context.ready() // start metrics and errors submission + } + + override fun onDisable() { + context.shutdown() // safely shut down configured services + } + + fun startGame() { + gameCount.incrementAndGet() + } + + companion object { + val ERROR_TRACKER: ErrorTracker = ErrorTracker.contextAware() + } +} diff --git a/bungeecord/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt b/bungeecord/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt new file mode 100644 index 00000000..339dd09e --- /dev/null +++ b/bungeecord/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt @@ -0,0 +1,44 @@ +package com.example + +import dev.faststats.ErrorTracker +import dev.faststats.bungee.BungeeContext +import dev.faststats.data.Metric +import net.md_5.bungee.api.plugin.Plugin +import java.util.concurrent.atomic.AtomicInteger + +class KotlinExamplePlugin : Plugin() { + private val gameCount = AtomicInteger() + + private val context = BungeeContext.Factory(this, "YOUR_TOKEN_HERE") + .errorTrackerService(ERROR_TRACKER) + // .metrics(Metrics.Factory::create) // Define a minimal metrics instance without any custom metrics + .metrics { factory -> + factory + // Custom metrics require a corresponding data source in your project settings + .addMetric(Metric.number("game_count") { gameCount.get() }) + .addMetric(Metric.string("server_version") { "1.0.0" }) + + // #onFlush is invoked after successful metrics submission + // This is useful for cleaning up cached data + .onFlush { gameCount.set(0) } // reset game count on flush + + .create() + } + .create() + + override fun onEnable() { + context.ready() // start metrics and errors submission + } + + override fun onDisable() { + context.shutdown() // safely shut down configured services + } + + fun startGame() { + gameCount.incrementAndGet() + } + + companion object { + val ERROR_TRACKER: ErrorTracker = ErrorTracker.contextAware() + } +} diff --git a/core/example/src/main/kotlin/dev/faststats/example/KotlinErrorTrackerExample.kt b/core/example/src/main/kotlin/dev/faststats/example/KotlinErrorTrackerExample.kt new file mode 100644 index 00000000..93538761 --- /dev/null +++ b/core/example/src/main/kotlin/dev/faststats/example/KotlinErrorTrackerExample.kt @@ -0,0 +1,60 @@ +package dev.faststats.example + +import dev.faststats.Attributes +import dev.faststats.ErrorTracker +import dev.faststats.FastStatsContext +import dev.faststats.SimpleContext +import java.lang.reflect.InvocationTargetException +import java.nio.file.AccessDeniedException + +object KotlinErrorTrackerExample { + // Context-aware: automatically tracks uncaught errors from the same class loader + val CONTEXT_AWARE: ErrorTracker = ErrorTracker.contextAware() // Filter expected noise before it is submitted + .ignoreError(InvocationTargetException::class.java, "Expected .* but got .*") + .ignoreError(AccessDeniedException::class.java) + + // Context-unaware: only tracks errors passed to trackError() manually + val CONTEXT_UNAWARE: ErrorTracker = + ErrorTracker.contextUnaware() // Replace sensitive values in error messages before submission + .anonymize("^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$", "[email hidden]") + .anonymize("Bearer [A-Za-z0-9._~+/=-]+", "Bearer [token hidden]") + .anonymize("AKIA[0-9A-Z]{16}", "[aws-key hidden]") + .anonymize("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "[uuid hidden]") + .anonymize("([?&](?:api_?key|token|secret)=)[^&\\s]+", "$1[redacted]") + + val CONTEXT: FastStatsContext = contextFactory + .errorTrackerService(CONTEXT_AWARE) // Set the global/internal error tracker + .create() + + init { + // Attributes on the global error tracker are attached to all reports + CONTEXT_AWARE.getAttributes() + .put("environment", "production") + .put("component", "global-error-handler") + + // Tracker-wide attributes are attached to every report submitted by this tracker + CONTEXT_UNAWARE.getAttributes() + .put("component", "manual-error-handler") + + // Register an additional tracker for submission + CONTEXT.errorTrackerService().orElseThrow().registerErrorTracker(CONTEXT_UNAWARE) + } + + fun manualTracking() { + try { + throw RuntimeException("Something went wrong!") + } catch (e: Exception) { + CONTEXT_UNAWARE.trackError(e) // Add additional attributes for more context + .attributes( + Attributes.empty() + .put("operation", "manualTracking") + .put("severity", "warning") + .put("retryable", false) + ) // Define whether the error was properly handled + .handled(false) + } + } + + private val contextFactory: SimpleContext.Factory<*, *> + get() = throw UnsupportedOperationException() +} diff --git a/core/example/src/main/kotlin/dev/faststats/example/KotlinFeatureFlagExample.kt b/core/example/src/main/kotlin/dev/faststats/example/KotlinFeatureFlagExample.kt new file mode 100644 index 00000000..49e2f485 --- /dev/null +++ b/core/example/src/main/kotlin/dev/faststats/example/KotlinFeatureFlagExample.kt @@ -0,0 +1,65 @@ +package dev.faststats.example + +import dev.faststats.* +import java.time.Duration +import java.util.function.Consumer + +object KotlinFeatureFlagExample { + val CONTEXT: FastStatsContext = contextFactory + // .featureFlagService(FeatureFlagService.Factory::create) // Define a feature flag service with default settings + .featureFlagService { factory: FeatureFlagService.Factory? -> + factory!! + .attributes( + Attributes.empty() // Define global attributes + .put("version", "1.2.3") + .put("java_version", System.getProperty("java.version")) + .put("java_vendor", System.getProperty("java.vendor")) + ) + .ttl(Duration.ofMinutes(10)) // Custom cache TTL for resolved flag values + .create() + }.create() + + val SERVICE: FeatureFlagService = CONTEXT.featureFlagService().orElseThrow() + + // Define flags with default values + val NEW_COMMANDS: FeatureFlag = SERVICE.define("new_commands", false) + val COMPRESSION: FeatureFlag = SERVICE.define("compression", "zstd") + + fun usage() { + // Async: waits for the server value to be fetched + NEW_COMMANDS.whenReady().thenAccept(Consumer { enabled: Boolean? -> + if (enabled == true) { + // register new commands + } + }) + + // Non-blocking: returns the cached value if present without triggering a fetch + COMPRESSION.getCached().ifPresent(Consumer { compression: String? -> + when (compression) { + "zstd" -> {} + "lz4" -> {} + else -> {} + } + }) + + // Refresh stale values explicitly when your code decides it is needed + if (COMPRESSION.isExpired()) { + COMPRESSION.fetch().thenAccept(Consumer { }) + } + + // Opt-in/out (requires allow_specific_opt_in on server) + NEW_COMMANDS.optIn().thenAccept(Consumer { updatedValue: Boolean? -> + if (updatedValue == true) { + // react to the updated server value + } + }) + NEW_COMMANDS.optOut().thenAccept(Consumer { updatedValue: Boolean? -> + if (!updatedValue!!) { + // react to the updated server value + } + }) + } + + private val contextFactory: SimpleContext.Factory<*, *> + get() = throw UnsupportedOperationException() +} diff --git a/core/example/src/main/kotlin/dev/faststats/example/KotlinMetricTypesExample.kt b/core/example/src/main/kotlin/dev/faststats/example/KotlinMetricTypesExample.kt new file mode 100644 index 00000000..45c679ae --- /dev/null +++ b/core/example/src/main/kotlin/dev/faststats/example/KotlinMetricTypesExample.kt @@ -0,0 +1,18 @@ +package dev.faststats.example + +import dev.faststats.data.Metric + +object KotlinMetricTypesExample { + // Single value metrics + val PLAYER_COUNT: Metric = Metric.number("player_count") { 42 } + val SERVER_VERSION: Metric = Metric.string("server_version") { "1.0.0" } + val MAINTENANCE_MODE: Metric = Metric.bool("maintenance_mode") { false } + + // Array metrics + val INSTALLED_PLUGINS: Metric> = Metric.stringArray("installed_plugins") { + arrayOf("WorldEdit", "Essentials") + } + val WORLDS: Metric> = Metric.stringArray("worlds") { + arrayOf("city", "farmworld", "farmworld_nether", "famrworld_end") + } +} diff --git a/fabric/example-mod/src/main/kotlin/com/example/KotlinExampleMod.kt b/fabric/example-mod/src/main/kotlin/com/example/KotlinExampleMod.kt new file mode 100644 index 00000000..38e25d12 --- /dev/null +++ b/fabric/example-mod/src/main/kotlin/com/example/KotlinExampleMod.kt @@ -0,0 +1,43 @@ +package com.example + +import dev.faststats.ErrorTracker +import dev.faststats.data.Metric +import dev.faststats.fabric.FabricContext +import net.fabricmc.api.ModInitializer +import java.util.concurrent.atomic.AtomicInteger + +class KotlinExampleMod : ModInitializer { + private val gameCount = AtomicInteger() + + private val context = FabricContext.Factory( + "example-mod", // your mod id as defined in fabric.mod.json + "YOUR_TOKEN_HERE", + ) + // .metrics(Metrics.Factory::create) // Define a minimal metrics instance without any custom metrics + .metrics { factory -> + factory + // Custom metrics require a corresponding data source in your project settings + .addMetric(Metric.number("game_count") { gameCount.get() }) + .addMetric(Metric.string("server_version") { "1.0.0" }) + + // #onFlush is invoked after successful metrics submission + // This is useful for cleaning up cached data + .onFlush { gameCount.set(0) } // reset game count on flush + + .create() + } + .errorTrackerService(ERROR_TRACKER) + .create() + + override fun onInitialize() { + // your actual logic + } + + fun startGame() { + gameCount.incrementAndGet() + } + + companion object { + val ERROR_TRACKER: ErrorTracker = ErrorTracker.contextAware() + } +} diff --git a/hytale/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt b/hytale/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt new file mode 100644 index 00000000..38872487 --- /dev/null +++ b/hytale/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt @@ -0,0 +1,45 @@ +package com.example + +import com.hypixel.hytale.server.core.plugin.JavaPlugin +import com.hypixel.hytale.server.core.plugin.JavaPluginInit +import dev.faststats.ErrorTracker +import dev.faststats.data.Metric +import dev.faststats.hytale.HytaleContext +import java.util.concurrent.atomic.AtomicInteger + +class KotlinExamplePlugin(init: JavaPluginInit) : JavaPlugin(init) { + private val gameCount = AtomicInteger() + + private val context = HytaleContext.Factory(this, "YOUR_TOKEN_HERE") + .errorTrackerService(ERROR_TRACKER) + // .metrics(Metrics.Factory::create) // Define a minimal metrics instance without any custom metrics + .metrics { factory -> + factory + // Custom metrics require a corresponding data source in your project settings + .addMetric(Metric.number("game_count") { gameCount.get() }) + .addMetric(Metric.string("server_version") { "1.0.0" }) + + // #onFlush is invoked after successful metrics submission + // This is useful for cleaning up cached data + .onFlush { gameCount.set(0) } // reset game count on flush + + .create() + } + .create() + + override fun setup() { + context.ready() // start metrics and errors submission + } + + override fun shutdown() { + context.shutdown() // safely shut down configured services + } + + fun startGame() { + gameCount.incrementAndGet() + } + + companion object { + val ERROR_TRACKER: ErrorTracker = ErrorTracker.contextAware() + } +} diff --git a/minestom/example-server/src/main/kotlin/com/example/KotlinExampleServer.kt b/minestom/example-server/src/main/kotlin/com/example/KotlinExampleServer.kt new file mode 100644 index 00000000..ff153e1a --- /dev/null +++ b/minestom/example-server/src/main/kotlin/com/example/KotlinExampleServer.kt @@ -0,0 +1,47 @@ +package com.example + +import dev.faststats.ErrorTracker +import dev.faststats.data.Metric +import dev.faststats.minestom.MinestomContext +import net.minestom.server.MinecraftServer +import java.util.concurrent.atomic.AtomicInteger + +object KotlinExampleServer { + val ERROR_TRACKER: ErrorTracker = ErrorTracker.contextAware() + private val gameCount = AtomicInteger() + + private val context = MinestomContext.Factory("YOUR_TOKEN_HERE") + .errorTrackerService(ERROR_TRACKER) + // .metrics(Metrics.Factory::create) // Define a minimal metrics instance without any custom metrics + .metrics { factory -> + factory + // Custom metrics require a corresponding data source in your project settings + .addMetric(Metric.number("game_count") { gameCount.get() }) + .addMetric(Metric.string("server_version") { "1.0.0" }) + + // #onFlush is invoked after successful metrics submission + // This is useful for cleaning up cached data + .onFlush { gameCount.set(0) } // reset game count on flush + + .create() + } + .create() + + @JvmStatic + fun main(args: Array) { + val server = MinecraftServer.init() + + server.start("0.0.0.0", 25565) + MinecraftServer.getSchedulerManager().buildShutdownTask { shutdown() } + + context.ready() // start metrics and errors submission + } + + fun shutdown() { + context.shutdown() // safely shut down configured services + } + + fun startGame() { + gameCount.incrementAndGet() + } +} diff --git a/neoforge/example-mod/src/main/kotlin/com/example/KotlinExampleMod.kt b/neoforge/example-mod/src/main/kotlin/com/example/KotlinExampleMod.kt new file mode 100644 index 00000000..b66c5691 --- /dev/null +++ b/neoforge/example-mod/src/main/kotlin/com/example/KotlinExampleMod.kt @@ -0,0 +1,34 @@ +package com.example + +import dev.faststats.ErrorTracker +import dev.faststats.data.Metric +import dev.faststats.neoforge.NeoForgeContext +import net.neoforged.fml.common.Mod +import java.util.concurrent.atomic.AtomicInteger + +@Mod("example_mod") +class KotlinExampleMod { + private val gameCount = AtomicInteger() + + private val context = NeoForgeContext.Factory( + "example_mod", + "YOUR_TOKEN_HERE", + ) + .metrics { factory -> + factory + .addMetric(Metric.number("game_count") { gameCount.get() }) + .addMetric(Metric.string("server_version") { "1.0.0" }) + .onFlush { gameCount.set(0) } + .create() + } + .errorTrackerService(ERROR_TRACKER) + .create() + + fun startGame() { + gameCount.incrementAndGet() + } + + companion object { + val ERROR_TRACKER: ErrorTracker = ErrorTracker.contextAware() + } +} diff --git a/nukkit/example-plugin/src/main/java/com/example/ExamplePlugin.kt b/nukkit/example-plugin/src/main/java/com/example/ExamplePlugin.kt new file mode 100644 index 00000000..afab6fe7 --- /dev/null +++ b/nukkit/example-plugin/src/main/java/com/example/ExamplePlugin.kt @@ -0,0 +1,44 @@ +package com.example + +import cn.nukkit.plugin.PluginBase +import dev.faststats.ErrorTracker +import dev.faststats.data.Metric +import dev.faststats.nukkit.NukkitContext +import java.util.concurrent.atomic.AtomicInteger + +class KotlinExamplePlugin : PluginBase() { + private val gameCount = AtomicInteger() + + private val context = NukkitContext.Factory(this, "YOUR_TOKEN_HERE") + .errorTrackerService(ERROR_TRACKER) + // .metrics(Metrics.Factory::create) // Define a minimal metrics instance without any custom metrics + .metrics { factory -> + factory + // Custom metrics require a corresponding data source in your project settings + .addMetric(Metric.number("game_count") { gameCount.get() }) + .addMetric(Metric.string("server_version") { "1.0.0" }) + + // #onFlush is invoked after successful metrics submission + // This is useful for cleaning up cached data + .onFlush { gameCount.set(0) } // reset game count on flush + + .create() + } + .create() + + override fun onEnable() { + context.ready() // start metrics and errors submission + } + + override fun onDisable() { + context.shutdown() // safely shut down configured services + } + + fun startGame() { + gameCount.incrementAndGet() + } + + companion object { + val ERROR_TRACKER: ErrorTracker = ErrorTracker.contextAware() + } +} diff --git a/sponge/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt b/sponge/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt new file mode 100644 index 00000000..e21b1d6d --- /dev/null +++ b/sponge/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt @@ -0,0 +1,57 @@ +package com.example + +import com.google.inject.Inject +import dev.faststats.ErrorTracker +import dev.faststats.data.Metric +import dev.faststats.sponge.SpongeContext +import org.spongepowered.api.Server +import org.spongepowered.api.event.Listener +import org.spongepowered.api.event.lifecycle.StartedEngineEvent +import org.spongepowered.api.event.lifecycle.StoppingEngineEvent +import org.spongepowered.plugin.builtin.jvm.Plugin +import java.util.concurrent.atomic.AtomicInteger + +@Plugin("example") +class KotlinExamplePlugin { + @Inject + private lateinit var contextBuilder: SpongeContext.Builder + + private val gameCount = AtomicInteger() + private var context: SpongeContext? = null + + @Listener + fun onServerStart(event: StartedEngineEvent) { + val context = contextBuilder + .token("YOUR_TOKEN_HERE") + .errorTrackerService(ERROR_TRACKER) + // .metrics(Metrics.Factory::create) // Define a minimal metrics instance without any custom metrics + .metrics { factory -> + factory + // Custom metrics require a corresponding data source in your project settings + .addMetric(Metric.number("game_count") { gameCount.get() }) + .addMetric(Metric.string("server_version") { "1.0.0" }) + + // #onFlush is invoked after successful metrics submission + // This is useful for cleaning up cached data + .onFlush { gameCount.set(0) } // reset game count on flush + + .create() + } + .create() + this.context = context + context.ready() // start metrics and errors submission + } + + @Listener + fun onServerStop(event: StoppingEngineEvent) { + context?.shutdown() // safely shut down configured services + } + + fun startGame() { + gameCount.incrementAndGet() + } + + companion object { + val ERROR_TRACKER: ErrorTracker = ErrorTracker.contextAware() + } +} diff --git a/velocity/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt b/velocity/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt new file mode 100644 index 00000000..2e10801d --- /dev/null +++ b/velocity/example-plugin/src/main/kotlin/com/example/KotlinExamplePlugin.kt @@ -0,0 +1,58 @@ +package com.example + +import com.google.inject.Inject +import com.velocitypowered.api.event.Subscribe +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent +import com.velocitypowered.api.plugin.Plugin +import dev.faststats.ErrorTracker +import dev.faststats.data.Metric +import dev.faststats.velocity.VelocityContext +import java.util.concurrent.atomic.AtomicInteger + +@Plugin( + id = "example", + name = "Example Plugin", + version = "1.0.0", + url = "https://example.com", + authors = ["Your Name"], +) +class KotlinExamplePlugin @Inject constructor(contextBuilder: VelocityContext.Builder) { + private val gameCount = AtomicInteger() + + private val context = contextBuilder + .token("YOUR_TOKEN_HERE") + .errorTrackerService(ERROR_TRACKER) + // .metrics(Metrics.Factory::create) // Define a minimal metrics instance without any custom metrics + .metrics { factory -> + factory + // Custom metrics require a corresponding data source in your project settings + .addMetric(Metric.number("game_count") { gameCount.get() }) + .addMetric(Metric.string("server_version") { "1.0.0" }) + + // #onFlush is invoked after successful metrics submission + // This is useful for cleaning up cached data + .onFlush { gameCount.set(0) } // reset game count on flush + + .create() + } + .create() + + @Subscribe + fun onProxyInitialize(event: ProxyInitializeEvent) { + context.ready() // start metrics and errors submission + } + + @Subscribe + fun onProxyStop(event: ProxyShutdownEvent) { + context.shutdown() // safely shut down configured services + } + + fun startGame() { + gameCount.incrementAndGet() + } + + companion object { + val ERROR_TRACKER: ErrorTracker = ErrorTracker.contextAware() + } +}