Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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") }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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<Boolean> = SERVICE.define("new_commands", false)
val COMPRESSION: FeatureFlag<String> = 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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.faststats.example

import dev.faststats.data.Metric

object KotlinMetricTypesExample {
// Single value metrics
val PLAYER_COUNT: Metric<Number> = Metric.number("player_count") { 42 }
val SERVER_VERSION: Metric<String> = Metric.string("server_version") { "1.0.0" }
val MAINTENANCE_MODE: Metric<Boolean> = Metric.bool("maintenance_mode") { false }

// Array metrics
val INSTALLED_PLUGINS: Metric<Array<String>> = Metric.stringArray("installed_plugins") {
arrayOf("WorldEdit", "Essentials")
}
val WORLDS: Metric<Array<String>> = Metric.stringArray("worlds") {
arrayOf("city", "farmworld", "farmworld_nether", "famrworld_end")
}
}
43 changes: 43 additions & 0 deletions fabric/example-mod/src/main/kotlin/com/example/KotlinExampleMod.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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<String>) {
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()
}
}
Loading