From 3495bb703ecdd99b0272982be5a1998afd424ed3 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 17 Apr 2026 23:20:39 +0200 Subject: [PATCH 01/40] Refactor feature flags --- .../main/java/com/example/ExamplePlugin.java | 55 +---- .../main/java/com/example/ExamplePlugin.java | 42 ++-- core/example/build.gradle.kts | 3 + .../example/ErrorTrackerExample.java | 29 +++ .../faststats/example/FeatureFlagExample.java | 66 +++++ .../faststats/example/MetricTypesExample.java | 14 ++ .../faststats/example/SettingsExample.java | 17 ++ .../java/dev/faststats/core/ErrorTracker.java | 2 +- .../main/java/dev/faststats/core/Metrics.java | 61 ++--- .../java/dev/faststats/core/Settings.java | 120 +++++++++ .../dev/faststats/core/SimpleMetrics.java | 74 +++--- .../dev/faststats/core/SimpleSettings.java | 44 ++++ .../dev/faststats/core/flags/Attributes.java | 113 +++++++++ .../dev/faststats/core/flags/FeatureFlag.java | 141 +++++++++++ .../core/flags/FeatureFlagService.java | 169 +++++++++++++ .../core/flags/SimpleAttributes.java | 34 +++ .../core/flags/SimpleFeatureFlag.java | 103 ++++++++ .../core/flags/SimpleFeatureFlagService.java | 227 ++++++++++++++++++ core/src/main/java/module-info.java | 1 + .../test/java/dev/faststats/MockMetrics.java | 7 +- .../src/main/java/com/example/ExampleMod.java | 37 +-- .../dev/faststats/fabric/FabricMetrics.java | 5 +- gradle.properties | 2 +- .../main/java/com/example/ExamplePlugin.java | 37 +-- settings.gradle.kts | 1 + .../main/java/com/example/ExamplePlugin.java | 38 +-- .../main/java/com/example/ExamplePlugin.java | 38 +-- .../velocity/VelocityMetricsImpl.java | 5 +- 28 files changed, 1196 insertions(+), 289 deletions(-) create mode 100644 core/example/build.gradle.kts create mode 100644 core/example/src/main/java/dev/faststats/example/ErrorTrackerExample.java create mode 100644 core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java create mode 100644 core/example/src/main/java/dev/faststats/example/MetricTypesExample.java create mode 100644 core/example/src/main/java/dev/faststats/example/SettingsExample.java create mode 100644 core/src/main/java/dev/faststats/core/Settings.java create mode 100644 core/src/main/java/dev/faststats/core/SimpleSettings.java create mode 100644 core/src/main/java/dev/faststats/core/flags/Attributes.java create mode 100644 core/src/main/java/dev/faststats/core/flags/FeatureFlag.java create mode 100644 core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java create mode 100644 core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java create mode 100644 core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java create mode 100644 core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java diff --git a/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java b/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java index 2b1d9cf..296adec 100644 --- a/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -2,54 +2,28 @@ import dev.faststats.bukkit.BukkitMetrics; import dev.faststats.core.ErrorTracker; +import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import org.bukkit.plugin.java.JavaPlugin; -import java.lang.reflect.InvocationTargetException; -import java.net.URI; -import java.nio.file.AccessDeniedException; import java.util.concurrent.atomic.AtomicInteger; -public class ExamplePlugin extends JavaPlugin { - // context-aware error tracker, automatically tracks errors in the same class loader - public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware() - // Ignore specific errors and messages - .ignoreError(InvocationTargetException.class, "Expected .* but got .*") // Ignored an error with a message - .ignoreError(AccessDeniedException.class); // Ignored a specific error type - - // context-unaware error tracker, does not automatically track errors - public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware() - // Anonymize error messages if required - .anonymize("^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$", "[email hidden]") // Email addresses - .anonymize("Bearer [A-Za-z0-9._~+/=-]+", "Bearer [token hidden]") // Bearer tokens in error messages - .anonymize("AKIA[0-9A-Z]{16}", "[aws-key hidden]") // AWS access key IDs - .anonymize("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "[uuid hidden]") // UUIDs (e.g. session/user IDs) - .anonymize("([?&](?:api_?key|token|secret)=)[^&\\s]+", "$1[redacted]"); // API keys in query strings - +public final class ExamplePlugin extends JavaPlugin { private final AtomicInteger gameCount = new AtomicInteger(); private final BukkitMetrics metrics = BukkitMetrics.factory() - .url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only - - // Custom example metrics - // For this to work you have to create a corresponding data source in your project settings first - .addMetric(Metric.number("example_metric", () -> 42)) + // Custom metrics require a corresponding data source in your project settings .addMetric(Metric.number("game_count", gameCount::get)) - .addMetric(Metric.string("example_string", () -> "Hello, World!")) - .addMetric(Metric.bool("example_boolean", () -> true)) - .addMetric(Metric.stringArray("example_string_array", () -> new String[]{"Option 1", "Option 2"})) - .addMetric(Metric.numberArray("example_number_array", () -> new Number[]{1, 2, 3})) - .addMetric(Metric.booleanArray("example_boolean_array", () -> new Boolean[]{true, false})) + .addMetric(Metric.string("server_version", () -> "1.0.0")) - // Attach an error tracker - // This must be enabled in the project settings - .errorTracker(ERROR_TRACKER) + // Error tracking must be enabled in the project settings + .errorTracker(ErrorTracker.contextAware()) - .onFlush(() -> gameCount.set(0)) // Reset game count on flush + // #onFlush is invoked after successful metrics submission + // This is useful for cleaning up cached data + .onFlush(() -> gameCount.set(0)) // reset game count on flush - .debug(true) // Enable debug mode for development and testing - - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project + .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project .create(this); @Override @@ -62,15 +36,6 @@ public void onDisable() { metrics.shutdown(); // safely shut down metrics submission } - public void doSomethingWrong() { - try { - // Do something that might throw an error - throw new RuntimeException("Something went wrong!"); - } catch (final Exception e) { - CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e); - } - } - public void startGame() { gameCount.incrementAndGet(); } diff --git a/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java b/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java index 37d245a..0c9a1f8 100644 --- a/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -3,37 +3,28 @@ import dev.faststats.bungee.BungeeMetrics; import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; +import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import net.md_5.bungee.api.plugin.Plugin; -import java.net.URI; +import java.util.concurrent.atomic.AtomicInteger; public class ExamplePlugin extends Plugin { - // context-aware error tracker, automatically tracks errors in the same class loader - public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware(); - - // context-unaware error tracker, does not automatically track errors - public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware(); + private final AtomicInteger gameCount = new AtomicInteger(); private final Metrics metrics = BungeeMetrics.factory() - .url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only - - // Custom example metrics - // For this to work you have to create a corresponding data source in your project settings first - .addMetric(Metric.number("example_metric", () -> 42)) - .addMetric(Metric.string("example_string", () -> "Hello, World!")) - .addMetric(Metric.bool("example_boolean", () -> true)) - .addMetric(Metric.stringArray("example_string_array", () -> new String[]{"Option 1", "Option 2"})) - .addMetric(Metric.numberArray("example_number_array", () -> new Number[]{1, 2, 3})) - .addMetric(Metric.booleanArray("example_boolean_array", () -> new Boolean[]{true, false})) + // 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")) - // Attach an error tracker - // This must be enabled in the project settings - .errorTracker(ERROR_TRACKER) + // Error tracking must be enabled in the project settings + .errorTracker(ErrorTracker.contextAware()) - .debug(true) // Enable debug mode for development and testing + // #onFlush is invoked after successful metrics submission + // This is useful for cleaning up cached data + .onFlush(() -> gameCount.set(0)) // reset game count on flush - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project + .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project .create(this); @Override @@ -41,12 +32,7 @@ public void onDisable() { metrics.shutdown(); // safely shut down metrics submission } - public void doSomethingWrong() { - try { - // Do something that might throw an error - throw new RuntimeException("Something went wrong!"); - } catch (final Exception e) { - CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e); - } + public void startGame() { + gameCount.incrementAndGet(); } } diff --git a/core/example/build.gradle.kts b/core/example/build.gradle.kts new file mode 100644 index 0000000..eca1ab1 --- /dev/null +++ b/core/example/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + implementation(project(":core")) +} diff --git a/core/example/src/main/java/dev/faststats/example/ErrorTrackerExample.java b/core/example/src/main/java/dev/faststats/example/ErrorTrackerExample.java new file mode 100644 index 0000000..aa8d643 --- /dev/null +++ b/core/example/src/main/java/dev/faststats/example/ErrorTrackerExample.java @@ -0,0 +1,29 @@ +package dev.faststats.example; + +import dev.faststats.core.ErrorTracker; + +import java.lang.reflect.InvocationTargetException; +import java.nio.file.AccessDeniedException; + +public final class ErrorTrackerExample { + // Context-aware: automatically tracks uncaught errors from the same class loader + public static final ErrorTracker CONTEXT_AWARE = ErrorTracker.contextAware() + .ignoreError(InvocationTargetException.class, "Expected .* but got .*") + .ignoreError(AccessDeniedException.class); + + // Context-unaware: only tracks errors passed to trackError() manually + public static final ErrorTracker CONTEXT_UNAWARE = ErrorTracker.contextUnaware() + .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]"); + + public static void manualTracking() { + try { + throw new RuntimeException("Something went wrong!"); + } catch (final Exception e) { + CONTEXT_UNAWARE.trackError(e); + } + } +} diff --git a/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java b/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java new file mode 100644 index 0000000..fb0c49d --- /dev/null +++ b/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java @@ -0,0 +1,66 @@ +package dev.faststats.example; + +import dev.faststats.core.Settings; +import dev.faststats.core.flags.Attributes; +import dev.faststats.core.flags.FeatureFlag; +import dev.faststats.core.flags.FeatureFlagService; + +import java.time.Duration; +import java.time.Instant; + +public final class FeatureFlagExample { + private static final Settings SETTINGS = Settings.withToken("YOUR_TOKEN_HERE"); + + public static final FeatureFlagService SERVICE = FeatureFlagService.factory() + .settings(SETTINGS) + .ttl(Duration.ofMinutes(10)) + .attributes(Attributes.create() + .put("version", "1.2.3") + .put("java_version", System.getProperty("java.version")) + .put("java_vendor", System.getProperty("java.vendor"))) + .create(); + + // Define flags with default values + public static final FeatureFlag NEW_COMMANDS = SERVICE.define("new_commands", false); + public static final FeatureFlag COMPRESSION = SERVICE.define("compression", "zstd"); + + public static void usage() { + // Async: waits for the server value to be fetched + NEW_COMMANDS.whenReady().thenAccept(enabled -> { + if (enabled) { + // register new commands + } + }); + + // Non-blocking: returns the cached value if present without triggering a fetch + COMPRESSION.getCached().ifPresent(compression -> { + switch (compression) { + case "zstd": + // default compression + break; + case "lz4": + // experimental compression + break; + default: + break; + } + }); + + // Refresh stale values explicitly when your code decides it is needed + if (COMPRESSION.getExpiration().filter(Instant.now()::isAfter).isPresent()) { + COMPRESSION.fetch(); + } + + // Opt-in/out (requires allow_specific_opt_in on server) + NEW_COMMANDS.optIn().thenAccept(updatedValue -> { + if (updatedValue) { + // react to the updated server value + } + }); + NEW_COMMANDS.optOut().thenAccept(updatedValue -> { + if (!updatedValue) { + // react to the updated server value + } + }); + } +} diff --git a/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java b/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java new file mode 100644 index 0000000..066b5b4 --- /dev/null +++ b/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java @@ -0,0 +1,14 @@ +package dev.faststats.example; + +import dev.faststats.core.data.Metric; + +public final class MetricTypesExample { + // Single value metrics + public static final Metric PLAYER_COUNT = Metric.number("player_count", () -> 42); + public static final Metric SERVER_VERSION = Metric.string("server_version", () -> "1.0.0"); + public static final Metric MAINTENANCE_MODE = Metric.bool("maintenance_mode", () -> false); + + // Array metrics + public static final Metric INSTALLED_PLUGINS = Metric.stringArray("installed_plugins", () -> new String[]{"WorldEdit", "Essentials"}); + public static final Metric WORLDS = Metric.stringArray("worlds", () -> new String[]{"city", "farmworld", "farmworld_nether", "famrworld_end"}); +} diff --git a/core/example/src/main/java/dev/faststats/example/SettingsExample.java b/core/example/src/main/java/dev/faststats/example/SettingsExample.java new file mode 100644 index 0000000..6dd9bd2 --- /dev/null +++ b/core/example/src/main/java/dev/faststats/example/SettingsExample.java @@ -0,0 +1,17 @@ +package dev.faststats.example; + +import dev.faststats.core.Settings; + +import java.net.URI; + +public final class SettingsExample { + // Recommended: create settings with just a token + public static final Settings SETTINGS = Settings.withToken("YOUR_TOKEN_HERE"); + + // Or use the factory for full control + public static final Settings ALL_SETTINGS = Settings.factory() + .url(URI.create("https://metrics.example.com/v1/collect")) // only for different metrics servers (mainly for testing) + .debug(true) // Enable debug mode for development and testing + .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project + .create(); +} diff --git a/core/src/main/java/dev/faststats/core/ErrorTracker.java b/core/src/main/java/dev/faststats/core/ErrorTracker.java index 1fe010d..a02b928 100644 --- a/core/src/main/java/dev/faststats/core/ErrorTracker.java +++ b/core/src/main/java/dev/faststats/core/ErrorTracker.java @@ -47,7 +47,7 @@ static ErrorTracker contextAware() { * @see #trackError(Throwable) * @since 0.10.0 */ - @Contract(value = " -> new") + @Contract(value = " -> new", pure = true) static ErrorTracker contextUnaware() { return new SimpleErrorTracker(); } diff --git a/core/src/main/java/dev/faststats/core/Metrics.java b/core/src/main/java/dev/faststats/core/Metrics.java index 7a60ede..4dc2f25 100644 --- a/core/src/main/java/dev/faststats/core/Metrics.java +++ b/core/src/main/java/dev/faststats/core/Metrics.java @@ -1,10 +1,10 @@ package dev.faststats.core; import dev.faststats.core.data.Metric; +import dev.faststats.core.flags.FeatureFlagService; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import java.net.URI; import java.util.Optional; import java.util.UUID; @@ -15,14 +15,13 @@ */ public interface Metrics { /** - * Get the token used to authenticate with the metrics server and identify the project. + * Get the SDK-wide settings for this metrics instance. * - * @return the metrics token - * @since 0.1.0 + * @return the settings + * @since 0.23.0 */ - @Token @Contract(pure = true) - String getToken(); + Settings getSettings(); /** * Get the error tracker for this metrics instance. @@ -33,6 +32,15 @@ public interface Metrics { @Contract(pure = true) Optional getErrorTracker(); + /** + * Get the feature flag service for this metrics instance. + * + * @return the feature flag service + * @since 0.23.0 + */ + @Contract(pure = true) + Optional getFeatureFlagService(); + /** * Get the metrics configuration. * @@ -49,6 +57,7 @@ public interface Metrics { *

* No-op in most implementations. * + * @apiNote Refer to your {@code Metrics} provider's documentation. * @since 0.14.0 */ default void ready() { @@ -108,44 +117,24 @@ interface Factory> { F errorTracker(ErrorTracker tracker); /** - * Enables or disabled debug mode for this metrics instance. - *

- * If {@link Config#debug()} is enabled, debug logging will be enabled for all metrics instances, - * including this one, regardless of this setting. - *

- * This is only meant for development and testing and should not be enabled in production. + * Sets the feature flag service for this metrics instance. * - * @param enabled whether debug mode is enabled + * @param service the feature flag service * @return the metrics factory - * @since 0.1.0 + * @since 0.23.0 */ @Contract(mutates = "this") - F debug(boolean enabled); + F featureFlagService(FeatureFlagService service); /** - * Sets the token used to authenticate with the metrics server and identify the project. - *

- * This token can be found in the settings of your project under "Your API Token". + * Sets the SDK-wide settings for this metrics instance. * - * @param token the metrics token + * @param settings the settings * @return the metrics factory - * @throws IllegalArgumentException if the token does not match the {@link Token#PATTERN} - * @since 0.1.0 - */ - @Contract(mutates = "this") - F token(@Token String token) throws IllegalArgumentException; - - /** - * Sets the metrics server URL. - *

- * This is only required for self-hosted metrics servers. - * - * @param url the metrics server URL - * @return the metrics factory - * @since 0.1.0 + * @since 0.23.0 */ @Contract(mutates = "this") - F url(URI url); + F settings(Settings settings); /** * Creates a new metrics instance. @@ -154,8 +143,8 @@ interface Factory> { * * @param object a required object as defined by the implementation * @return the metrics instance - * @throws IllegalStateException if the token is not specified - * @see #token(String) + * @throws IllegalStateException if the settings are not specified + * @see #settings(Settings) * @since 0.1.0 */ @Async.Schedule diff --git a/core/src/main/java/dev/faststats/core/Settings.java b/core/src/main/java/dev/faststats/core/Settings.java new file mode 100644 index 0000000..45ff172 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/Settings.java @@ -0,0 +1,120 @@ +package dev.faststats.core; + +import org.jetbrains.annotations.Contract; + +import java.net.URI; + +/** + * SDK-wide settings shared across all FastStats services. + * + * @since 0.23.0 + */ +public sealed interface Settings permits SimpleSettings { + /** + * Creates a new {@link Settings} instance with the given token. + *

+ * This token can be found in the settings of your project under "Your API Token". + * It is used to authenticate with the server and identify the project. + * + * @param token the token + * @return the new settings + * @since 0.23.0 + */ + @Contract(value = "_ -> new", pure = true) + static Settings withToken(@Token final String token) { + return factory().token(token).create(); + } + + /** + * Create a new factory for building {@link Settings}. + * + * @return a new factory + * @since 0.23.0 + */ + @Contract(value = " -> new", pure = true) + static Factory factory() { + return new SimpleSettings.Factory(); + } + + /** + * The token used to authenticate with the server and identify the project. + * + * @return the token + * @since 0.23.0 + */ + @Token + @Contract(pure = true) + String token(); + + /** + * The server URL. + * + * @return the server URL + * @since 0.23.0 + */ + @Contract(pure = true) + URI url(); + + /** + * Whether debug logging is enabled. + * + * @return {@code true} if debug logging is enabled, {@code false} otherwise + * @since 0.23.0 + */ + @Contract(pure = true) + boolean debug(); + + /** + * A factory for creating {@link Settings} instances. + * + * @since 0.23.0 + */ + sealed interface Factory permits SimpleSettings.Factory { + /** + * Sets the token used to authenticate with the server and identify the project. + *

+ * This token can be found in the settings of your project under "Your API Token". + * + * @param token the token + * @return the factory + * @throws IllegalArgumentException if the token does not match the {@link Token#PATTERN} + * @since 0.23.0 + */ + @Contract(mutates = "this") + Factory token(@Token String token) throws IllegalArgumentException; + + /** + * Sets the server URL. + *

+ * This is only required for self-hosted servers. + * + * @param url the server URL + * @return the factory + * @since 0.23.0 + */ + @Contract(mutates = "this") + Factory url(URI url); + + /** + * Enables or disables debug logging. + *

+ * This is only meant for development and testing and should not be enabled in production. + * + * @param enabled whether debug logging is enabled + * @return the factory + * @since 0.23.0 + */ + @Contract(mutates = "this") + Factory debug(boolean enabled); + + /** + * Creates a new {@link Settings} instance. + * + * @return the settings + * @throws IllegalStateException if the token is not specified + * @since 0.23.0 + */ + @Contract(value = " -> new", pure = true) + Settings create() throws IllegalStateException; + } +} diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 7dbfd5a..1d6183b 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -2,6 +2,7 @@ import com.google.gson.JsonObject; import dev.faststats.core.data.Metric; +import dev.faststats.core.flags.FeatureFlagService; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.MustBeInvokedByOverriders; @@ -12,7 +13,6 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.net.ConnectException; -import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpRequest; @@ -43,10 +43,10 @@ public abstract class SimpleMetrics implements Metrics { private final Set> metrics; private final Config config; - private final @Token String token; + private final Settings settings; private final @Nullable ErrorTracker tracker; private final @Nullable Runnable flush; - private final URI url; + private final @Nullable FeatureFlagService flagService; private final boolean debug; private final String SDK_NAME; @@ -65,17 +65,16 @@ public abstract class SimpleMetrics implements Metrics { } @Contract(mutates = "io") - @SuppressWarnings("PatternValidation") protected SimpleMetrics(final Factory factory, final Config config) throws IllegalStateException { - if (factory.token == null) throw new IllegalStateException("Token must be specified"); + if (factory.settings == null) throw new IllegalStateException("Settings must be specified"); this.config = config; + this.settings = factory.settings; this.metrics = config.additionalMetrics ? Set.copyOf(factory.metrics) : Set.of(); - this.debug = factory.debug || Boolean.getBoolean("faststats.debug") || config.debug(); - this.token = factory.token; + this.debug = settings.debug() || Boolean.getBoolean("faststats.debug") || config.debug(); this.tracker = config.errorTracking ? factory.tracker : null; this.flush = factory.flush; - this.url = factory.url; + this.flagService = factory.flagService; } @Contract(mutates = "io") @@ -87,23 +86,17 @@ protected SimpleMetrics(final Factory factory, final Path config) throws I protected SimpleMetrics( final Config config, final Set> metrics, - @Token final String token, + final Settings settings, @Nullable final ErrorTracker tracker, - @Nullable final Runnable flush, - final URI url, - final boolean debug + @Nullable final Runnable flush ) { - if (!token.matches(Token.PATTERN)) { - throw new IllegalArgumentException("Invalid token '" + token + "', must match '" + Token.PATTERN + "'"); - } - this.metrics = config.additionalMetrics ? Set.copyOf(metrics) : Set.of(); this.config = config; - this.debug = debug; - this.token = token; + this.debug = settings.debug(); + this.settings = settings; this.tracker = tracker; this.flush = flush; - this.url = url; + this.flagService = null; } protected String getOnboardingMessage() { @@ -205,13 +198,13 @@ private boolean submitNow() throws IOException { .POST(HttpRequest.BodyPublishers.ofByteArray(compressed)) .header("Content-Encoding", "gzip") .header("Content-Type", "application/octet-stream") - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + settings.token()) .header("User-Agent", "FastStats Metrics " + SDK_NAME + "/" + SDK_VERSION) .timeout(Duration.ofSeconds(3)) - .uri(url) + .uri(settings.url()) .build(); - info("Sending metrics to: " + url); + info("Sending metrics to: " + settings.url()); try { final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(UTF_8)); final var statusCode = response.statusCode(); @@ -232,9 +225,9 @@ private boolean submitNow() throws IOException { warn("Received unexpected response from metrics server: " + statusCode + " (" + body + ")"); } } catch (final HttpConnectTimeoutException t) { - error("Metrics submission timed out after 3 seconds: " + url, null); + error("Metrics submission timed out after 3 seconds: " + settings.url(), null); } catch (final ConnectException t) { - error("Failed to connect to metrics server: " + url, null); + error("Failed to connect to metrics server: " + settings.url(), null); } catch (final Throwable t) { error("Failed to submit metrics", t); } @@ -287,8 +280,8 @@ protected JsonObject createData() { } @Override - public @Token String getToken() { - return token; + public Settings getSettings() { + return settings; } @Override @@ -296,6 +289,11 @@ public Optional getErrorTracker() { return Optional.ofNullable(tracker); } + @Override + public Optional getFeatureFlagService() { + return Optional.ofNullable(flagService); + } + @Override public Metrics.Config getConfig() { return config; @@ -324,6 +322,7 @@ protected void info(final String message) { @Override public void shutdown() { + if (flagService != null) flagService.shutdown(); getErrorTracker().ifPresent(ErrorTracker::detachErrorContext); if (executor != null) try { info("Shutting down metrics submission"); @@ -340,11 +339,10 @@ public void shutdown() { public abstract static class Factory> implements Metrics.Factory { private final Set> metrics = new HashSet<>(0); - private URI url = URI.create("https://metrics.faststats.dev/v1/collect"); private @Nullable ErrorTracker tracker; + private @Nullable FeatureFlagService flagService; private @Nullable Runnable flush; - private @Nullable String token; - private boolean debug = false; + private @Nullable Settings settings; @Override @SuppressWarnings("unchecked") @@ -369,25 +367,15 @@ public F errorTracker(final ErrorTracker tracker) { @Override @SuppressWarnings("unchecked") - public F debug(final boolean enabled) { - this.debug = enabled; - return (F) this; - } - - @Override - @SuppressWarnings("unchecked") - public F token(@Token final String token) throws IllegalArgumentException { - if (!token.matches(Token.PATTERN)) { - throw new IllegalArgumentException("Invalid token '" + token + "', must match '" + Token.PATTERN + "'"); - } - this.token = token; + public F featureFlagService(final FeatureFlagService service) { + this.flagService = service; return (F) this; } @Override @SuppressWarnings("unchecked") - public F url(final URI url) { - this.url = url; + public F settings(final Settings settings) { + this.settings = settings; return (F) this; } } diff --git a/core/src/main/java/dev/faststats/core/SimpleSettings.java b/core/src/main/java/dev/faststats/core/SimpleSettings.java new file mode 100644 index 0000000..534a670 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/SimpleSettings.java @@ -0,0 +1,44 @@ +package dev.faststats.core; + +import org.jspecify.annotations.Nullable; + +import java.net.URI; + +record SimpleSettings(@Token String token, URI url, boolean debug) implements Settings { + + static final class Factory implements Settings.Factory { + private static final URI DEFAULT_URL = URI.create("https://metrics.faststats.dev/v1/collect"); + + private URI url = DEFAULT_URL; + private @Nullable String token; + private boolean debug = false; + + @Override + public Settings.Factory token(@Token final String token) throws IllegalArgumentException { + if (!token.matches(Token.PATTERN)) { + throw new IllegalArgumentException("Invalid token '" + token + "', must match '" + Token.PATTERN + "'"); + } + this.token = token; + return this; + } + + @Override + public Settings.Factory url(final URI url) { + this.url = url; + return this; + } + + @Override + public Settings.Factory debug(final boolean enabled) { + this.debug = enabled; + return this; + } + + @Override + @SuppressWarnings("PatternValidation") + public Settings create() throws IllegalStateException { + if (token == null) throw new IllegalStateException("Token must be specified"); + return new SimpleSettings(token, url, debug); + } + } +} diff --git a/core/src/main/java/dev/faststats/core/flags/Attributes.java b/core/src/main/java/dev/faststats/core/flags/Attributes.java new file mode 100644 index 0000000..ba0c776 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/flags/Attributes.java @@ -0,0 +1,113 @@ +package dev.faststats.core.flags; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.Nullable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Mutable key-value attributes for feature flag targeting. + *

+ * Attributes are sent to the server on each flag fetch + * so that targeting rules can be evaluated server-side. + * + * @since 0.23.0 + */ +public sealed interface Attributes permits SimpleAttributes { + /** + * Create new empty attributes. + * + * @return new attributes + * @since 0.23.0 + */ + @Contract(value = " -> new", pure = true) + static Attributes create() { + return new SimpleAttributes(new ConcurrentHashMap<>()); + } + + /** + * Create new attributes by copying entries from the given source. + * + * @param attributes the source attributes to copy + * @return new attributes containing the copied entries + * @since 0.23.0 + */ + @Contract(value = "_ -> new", pure = true) + static Attributes copyOf(final Attributes attributes) { + final var entries = ((SimpleAttributes) attributes).attributes(); + return new SimpleAttributes(new ConcurrentHashMap<>(entries)); + } + + /** + * Set a string value. + * + * @param key the key + * @param value the value + * @return these attributes + * @since 0.23.0 + */ + @Contract(value = "_, _ -> this", mutates = "this") + Attributes put(String key, String value); + + /** + * Set a number value. + * + * @param key the key + * @param value the value + * @return these attributes + * @since 0.23.0 + */ + @Contract(value = "_, _ -> this", mutates = "this") + Attributes put(String key, Number value); + + /** + * Set a boolean value. + * + * @param key the key + * @param value the value + * @return these attributes + * @since 0.23.0 + */ + @Contract(value = "_, _ -> this", mutates = "this") + Attributes put(String key, boolean value); + + /** + * Remove a value. + * + * @param key the key + * @return these attributes + * @since 0.23.0 + */ + @Contract(value = "_ -> this", mutates = "this") + Attributes remove(String key); + + /** + * Return an unmodifiable view of all attribute entries. + * + * @return unmodifiable map of attribute entries + * @since 0.23.0 + */ + @Unmodifiable + @Contract(pure = true) + Map entries(); + + /** + * Create new attributes by merging two attribute sets. + *

+ * If both contain the same key, the value from {@code second} takes precedence. + * + * @param first the first attributes + * @param second the second attributes, takes precedence on conflicts + * @return new merged attributes + * @since 0.23.0 + */ + @Contract(value = "_, _ -> new", pure = true) + static Attributes join(@Nullable final Attributes first, @Nullable final Attributes second) { + final var attributes = new ConcurrentHashMap(); + if (first instanceof final SimpleAttributes simple) attributes.putAll(simple.attributes()); + if (second instanceof final SimpleAttributes simple) attributes.putAll(simple.attributes()); + return new SimpleAttributes(attributes); + } +} diff --git a/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java b/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java new file mode 100644 index 0000000..e2f275d --- /dev/null +++ b/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java @@ -0,0 +1,141 @@ +package dev.faststats.core.flags; + +import org.jetbrains.annotations.Contract; + +import java.time.Instant; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * A feature flag. + *

+ * Feature flags are defined via {@link FeatureFlagService#define} and are bound to + * the service's cache and lifecycle. + * + * @param the flag value type + * @since 0.23.0 + */ +public sealed interface FeatureFlag permits SimpleFeatureFlag { + /** + * Get the flag identifier. + * + * @return the flag id + * @since 0.23.0 + */ + @Contract(pure = true) + String getId(); + + /** + * Returns the class representing the value type of this flag. + * + * @return the value type class + * @since 0.23.0 + */ + @Contract(pure = true) + Class getType(); + + /** + * Get the current cached flag value. + *

+ * This method is non-blocking and never performs a network request. It + * returns {@link Optional#empty()} until a value has been fetched and + * stored locally. + * + * @return the cached value, if present + * @since 0.23.0 + */ + @Contract(pure = true) + Optional getCached(); + + /** + * Get the expiration time for the current cached value. + *

+ * If no value has been cached yet, this returns {@link Optional#empty()}. + * The returned timestamp indicates when the cached value should be treated + * as stale according to the configured TTL. + * + * @return the expiration time of the cached value, if present + * @see #isValid() + * @since 0.23.0 + */ + @Contract(pure = true) + Optional getExpiration(); + + /** + * Returns whether the current cached value is still valid. + *

+ * A value is valid when it is cached and its configured TTL has not yet + * expired. This method is non-blocking and never performs a network + * request. + * + * @return {@code true} if a non-expired cached value is available + * @see #getExpiration() + * @since 0.23.0 + */ + @Contract(pure = true) + boolean isValid(); + + /** + * Return a future that completes with the flag value once it is ready. + *

+ * If the value is valid according to {@link #isValid()}, + * the returned future completes immediately. Otherwise, a new fetch is + * performed and the future completes when the response arrives. + * + * @return a future completing with the flag value + * @see #fetch() + * @since 0.23.0 + */ + CompletableFuture whenReady(); + + /** + * Force a fresh fetch of the flag value from the server. + *

+ * Unlike {@link #whenReady()}, this always performs a server request. + * + * @return a future completing with the latest server value + * @since 0.23.0 + */ + @Contract(mutates = "this") + CompletableFuture fetch(); + + /** + * Request that the server opt in to this flag, then invalidate the local + * value and fetch the current server value again. + *

+ * This sends a {@code POST /v1/flag/opt-in} request. The server determines + * the resulting flag value based on its own conditions. + *

+ * The returned future completes with the updated value after the local + * cache has been reset and the follow-up fetch finishes. + * + * @return a future completing with the updated flag value + * @since 0.23.0 + */ + @Contract(mutates = "this") + CompletableFuture optIn(); + + /** + * Request that the server opt out of this flag, then invalidate the local + * value and fetch the current server value again. + *

+ * This sends a {@code POST /v1/flag/opt-out} request. + *

+ * The returned future completes with the updated value after the local + * cache has been reset and the follow-up fetch finishes. + * + * @return a future completing with the updated flag value + * @since 0.23.0 + */ + @Contract(mutates = "this") + CompletableFuture optOut(); + + /** + * Get the default value for this flag. + * + * @return the default value + * @since 0.23.0 + */ + @Contract(pure = true) + T getDefaultValue(); +} diff --git a/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java new file mode 100644 index 0000000..cebbed4 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java @@ -0,0 +1,169 @@ +package dev.faststats.core.flags; + +import dev.faststats.core.Metrics; +import dev.faststats.core.Settings; +import org.jetbrains.annotations.Contract; + +import java.time.Duration; + +/** + * A service for managing feature flags. + *

+ * Create an instance using the {@link Factory} and pass it to the metrics factory + * via {@link Metrics.Factory#featureFlagService(FeatureFlagService)}. + * + * @since 0.23.0 + */ +public sealed interface FeatureFlagService permits SimpleFeatureFlagService { + /** + * Create a new {@link FeatureFlagService} with the given settings and default options. + * + * @param settings the SDK-wide settings + * @return a new feature flag service + * @since 0.23.0 + */ + @Contract(value = "_ -> new", pure = true) + static FeatureFlagService create(final Settings settings) { + return factory().settings(settings).create(); + } + + /** + * Create a new factory for building a {@link FeatureFlagService}. + * + * @return a new factory + * @since 0.23.0 + */ + @Contract(value = " -> new", pure = true) + static Factory factory() { + return new SimpleFeatureFlagService.Factory(); + } + + /** + * Define a boolean feature flag. + * + * @param id the flag identifier + * @param defaultValue the default value + * @return the feature flag + * @since 0.23.0 + */ + @Contract(value = "_, _ -> new", pure = true) + FeatureFlag define(String id, boolean defaultValue); + + /** + * Define a boolean feature flag with per-flag targeting attributes. + * + * @param id the flag identifier + * @param defaultValue the default value + * @param attributes the per-flag targeting attributes, merged with the service attributes + * @return the feature flag + * @since 0.23.0 + */ + @Contract(value = "_, _, _ -> new", pure = true) + FeatureFlag define(String id, boolean defaultValue, Attributes attributes); + + /** + * Define a string feature flag. + * + * @param id the flag identifier + * @param defaultValue the default value + * @return the feature flag + * @since 0.23.0 + */ + @Contract(value = "_, _ -> new", pure = true) + FeatureFlag define(String id, String defaultValue); + + /** + * Define a string feature flag with per-flag targeting attributes. + * + * @param id the flag identifier + * @param defaultValue the default value + * @param attributes the per-flag targeting attributes, merged with the service attributes + * @return the feature flag + * @since 0.23.0 + */ + @Contract(value = "_, _, _ -> new", pure = true) + FeatureFlag define(String id, String defaultValue, Attributes attributes); + + /** + * Define a number feature flag. + * + * @param id the flag identifier + * @param defaultValue the default value + * @return the feature flag + * @since 0.23.0 + */ + @Contract(value = "_, _ -> new", pure = true) + FeatureFlag define(String id, Number defaultValue); + + /** + * Define a number feature flag with per-flag targeting attributes. + * + * @param id the flag identifier + * @param defaultValue the default value + * @param attributes the per-flag targeting attributes, merged with the service attributes + * @return the feature flag + * @since 0.23.0 + */ + @Contract(value = "_, _, _ -> new", pure = true) + FeatureFlag define(String id, Number defaultValue, Attributes attributes); + + /** + * Shuts down the feature flag service. + * + * @since 0.23.0 + */ + @Contract(mutates = "this") + void shutdown(); + + /** + * A factory for creating {@link FeatureFlagService} instances. + * + * @since 0.23.0 + */ + interface Factory { + /** + * Sets the cache time-to-live for flag values. + *

+ * This TTL determines the staleness window reported by + * {@link FeatureFlag#getExpiration()}. Expired cached values remain + * readable until they are explicitly refreshed or invalidated. + * + * @param ttl the cache time-to-live + * @return the factory + * @since 0.23.0 + */ + @Contract(mutates = "this") + Factory ttl(Duration ttl); + + /** + * Sets the global targeting attributes for all flags created by this service. + * + * @param attributes the targeting attributes + * @return the factory + * @since 0.23.0 + */ + @Contract(mutates = "this") + Factory attributes(Attributes attributes); + + /** + * Sets the SDK-wide settings for this feature flag service. + * + * @param settings the settings + * @return the factory + * @since 0.23.0 + */ + @Contract(mutates = "this") + Factory settings(Settings settings); + + /** + * Creates a new {@link FeatureFlagService} instance. + * + * @return the feature flag service + * @throws IllegalStateException if the settings are not specified + * @see #settings(Settings) + * @since 0.23.0 + */ + @Contract(value = " -> new", pure = true) + FeatureFlagService create() throws IllegalStateException; + } +} diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java b/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java new file mode 100644 index 0000000..84d1ca4 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java @@ -0,0 +1,34 @@ +package dev.faststats.core.flags; + +import java.util.Map; + +record SimpleAttributes(Map attributes) implements Attributes { + @Override + public Attributes put(final String key, final String value) { + attributes.put(key, value); + return this; + } + + @Override + public Attributes put(final String key, final Number value) { + attributes.put(key, value); + return this; + } + + @Override + public Attributes put(final String key, final boolean value) { + attributes.put(key, value); + return this; + } + + @Override + public Attributes remove(final String key) { + attributes.remove(key); + return this; + } + + @Override + public Map entries() { + return Map.copyOf(attributes); + } +} diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java new file mode 100644 index 0000000..5743f63 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java @@ -0,0 +1,103 @@ +package dev.faststats.core.flags; + +import org.jspecify.annotations.Nullable; + +import java.time.Instant; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +final class SimpleFeatureFlag implements FeatureFlag { + private final SimpleFeatureFlagService service; + + private final String id; + private final T defaultValue; + private final @Nullable Attributes attributes; + + SimpleFeatureFlag( + final String id, + final T defaultValue, + final @Nullable Attributes attributes, + final SimpleFeatureFlagService service + ) { + this.id = id; + this.defaultValue = defaultValue; + this.attributes = attributes; + this.service = service; + service.fetch(this); + } + + @Override + public String getId() { + return id; + } + + @Override + @SuppressWarnings("unchecked") + public Class getType() { + return (Class) defaultValue.getClass(); + } + + @Override + public Optional getCached() { + return service.get(this); + } + + @Override + public Optional getExpiration() { + return service.getExpiration(this); + } + + @Override + public boolean isValid() { + return service.isValid(this); + } + + @Override + public CompletableFuture whenReady() { + return service.whenReady(this); + } + + @Override + public CompletableFuture fetch() { + return service.fetch(this); + } + + @Override + public CompletableFuture optIn() { + return service.optIn(this); + } + + @Override + public CompletableFuture optOut() { + return service.optOut(this); + } + + @Override + public T getDefaultValue() { + return defaultValue; + } + + @Nullable Attributes attributes() { + return attributes; + } + + @Override + public boolean equals(@Nullable final Object o) { + if (o == null || getClass() != o.getClass()) return false; + final SimpleFeatureFlag that = (SimpleFeatureFlag) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return "SimpleFeatureFlag{" + + "id='" + id + '\'' + + '}'; + } +} diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java new file mode 100644 index 0000000..d091dd2 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -0,0 +1,227 @@ +package dev.faststats.core.flags; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import dev.faststats.core.Settings; +import org.jspecify.annotations.Nullable; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +final class SimpleFeatureFlagService implements FeatureFlagService { + private static final Gson GSON = new Gson(); + + private final Settings settings; + private final @Nullable Attributes attributes; + private final Duration ttl; + + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(3)) + .build(); + private final Map cache = new ConcurrentHashMap<>(); + private final Map fetchTimes = new ConcurrentHashMap<>(); + private final Map> fetchesInProgress = new ConcurrentHashMap<>(); + + SimpleFeatureFlagService( + final Settings settings, + final @Nullable Attributes attributes, + final Duration ttl + ) { + this.settings = settings; + this.attributes = attributes; + this.ttl = ttl; + } + + @SuppressWarnings("unchecked") + Optional get(final SimpleFeatureFlag flag) { + final var cached = cache.get(flag.getId()); + return Optional.ofNullable((T) cached); + } + + @SuppressWarnings("unchecked") + CompletableFuture whenReady(final SimpleFeatureFlag flag) { + final var cached = cache.get(flag.getId()); + if (cached != null && !isExpired(flag)) { + return CompletableFuture.completedFuture((T) cached); + } + return fetch(flag); + } + + @SuppressWarnings("unchecked") + CompletableFuture fetch(final SimpleFeatureFlag flag) { + return (CompletableFuture) fetchesInProgress.computeIfAbsent(flag.getId(), ignored -> createFetch(flag)); + } + + CompletableFuture optIn(final SimpleFeatureFlag flag) { + return sendOptRequest(flag, "/v1/flag/opt-in"); + } + + CompletableFuture optOut(final SimpleFeatureFlag flag) { + return sendOptRequest(flag, "/v1/flag/opt-out"); + } + + private CompletableFuture sendOptRequest(final SimpleFeatureFlag flag, final String path) { + invalidate(flag); + + final var requestBody = new JsonObject(); + requestBody.addProperty("projectToken", settings.token()); + requestBody.addProperty("serverId", UUID.randomUUID().toString()); // todo: read from config + requestBody.addProperty("flag", flag.getId()); + + final var request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(requestBody))) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + settings.token()) + .timeout(Duration.ofSeconds(3)) + .uri(settings.url().resolve(path)) + .build(); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenCompose(response -> { + if (response.statusCode() < 200 || response.statusCode() >= 300) { + return CompletableFuture.failedFuture(new IllegalStateException( + "Feature flag opt request failed with status " + response.statusCode() + )); + } + return fetch(flag); + }); + } + + void invalidate(final SimpleFeatureFlag flag) { + final var id = flag.getId(); + cache.remove(id); + fetchTimes.remove(id); + } + + Optional getExpiration(final SimpleFeatureFlag flag) { + final var lastFetch = fetchTimes.get(flag.getId()); + if (lastFetch == null) return Optional.empty(); + return Optional.of(Instant.ofEpochMilli(lastFetch).plus(ttl)); + } + + boolean isValid(final SimpleFeatureFlag flag) { + return cache.containsKey(flag.getId()) && !isExpired(flag); + } + + boolean isExpired(final SimpleFeatureFlag flag) { + final var lastFetch = fetchTimes.get(flag.getId()); + if (lastFetch == null) return true; + return System.currentTimeMillis() - lastFetch > ttl.toMillis(); + } + + private CompletableFuture createFetch(final SimpleFeatureFlag flag) { + final var requestBody = new JsonObject(); + requestBody.addProperty("flag", flag.getId()); + + final var mergedAttributes = Attributes.join(attributes, flag.attributes()); + // todo: drop gson + requestBody.add("attributes", GSON.toJsonTree(mergedAttributes)); + + final var request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(requestBody))) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + settings.token()) + .timeout(Duration.ofSeconds(3)) + .uri(settings.url().resolve("/flags")) + .build(); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(response -> { + if (response.statusCode() >= 200 && response.statusCode() < 300) { + // todo: replace gson with safer read + final var body = GSON.fromJson(response.body(), JsonObject.class); + final var value = body.get("value"); + if (value != null && !value.isJsonNull()) { + cache.put(flag.getId(), toValue(value)); + fetchTimes.put(flag.getId(), System.currentTimeMillis()); + return flag.getType().cast(cache.get(flag.getId())); + } + } + return flag.getDefaultValue(); + }).whenComplete((ignored, throwable) -> fetchesInProgress.remove(flag.getId())); + } + + private Object toValue(final JsonElement element) { + if (element.isJsonPrimitive()) { + final var primitive = element.getAsJsonPrimitive(); + if (primitive.isBoolean()) return primitive.getAsBoolean(); + if (primitive.isNumber()) return primitive.getAsNumber(); + return primitive.getAsString(); + } // todo: guarantee for primitives? + return element.toString(); + } + + @Override + public FeatureFlag define(final String id, final boolean defaultValue) { + return new SimpleFeatureFlag<>(id, defaultValue, null, this); + } + + @Override + public FeatureFlag define(final String id, final boolean defaultValue, final Attributes attributes) { + return new SimpleFeatureFlag<>(id, defaultValue, attributes, this); + } + + @Override + public FeatureFlag define(final String id, final String defaultValue) { + return new SimpleFeatureFlag<>(id, defaultValue, null, this); + } + + @Override + public FeatureFlag define(final String id, final String defaultValue, final Attributes attributes) { + return new SimpleFeatureFlag<>(id, defaultValue, attributes, this); + } + + @Override + public FeatureFlag define(final String id, final Number defaultValue) { + return new SimpleFeatureFlag<>(id, defaultValue, null, this); + } + + @Override + public FeatureFlag define(final String id, final Number defaultValue, final Attributes attributes) { + return new SimpleFeatureFlag<>(id, defaultValue, attributes, this); + } + + @Override + public void shutdown() { + cache.clear(); + fetchTimes.clear(); + fetchesInProgress.clear(); + } + + static final class Factory implements FeatureFlagService.Factory { + private Duration ttl = Duration.ofMinutes(5); + private @Nullable Settings settings; + private @Nullable Attributes attributes; + + @Override + public FeatureFlagService.Factory ttl(final Duration ttl) { + this.ttl = ttl; + return this; + } + + @Override + public FeatureFlagService.Factory attributes(final Attributes attributes) { + this.attributes = attributes; + return this; + } + + @Override + public FeatureFlagService.Factory settings(final Settings settings) { + this.settings = settings; + return this; + } + + @Override + public FeatureFlagService create() throws IllegalStateException { + if (settings == null) throw new IllegalStateException("Settings must be specified"); + return new SimpleFeatureFlagService(settings, attributes, ttl); + } + } +} diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 612834e..e635f81 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -3,6 +3,7 @@ @NullMarked module dev.faststats.core { exports dev.faststats.core.data; + exports dev.faststats.core.flags; exports dev.faststats.core; requires com.google.gson; diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index 3d9df9e..01cc1be 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -2,6 +2,7 @@ import com.google.gson.JsonObject; import dev.faststats.core.ErrorTracker; +import dev.faststats.core.Settings; import dev.faststats.core.SimpleMetrics; import dev.faststats.core.Token; import org.jspecify.annotations.NullMarked; @@ -14,7 +15,11 @@ @NullMarked public final class MockMetrics extends SimpleMetrics { public MockMetrics(final UUID serverId, @Token final String token, @Nullable final ErrorTracker tracker, final boolean debug) { - super(new Config(serverId, true, debug, true, true, false, false), Set.of(), token, tracker, null, URI.create("http://localhost:5000/v1/collect"), debug); + super(new Config(serverId, true, debug, true, true, false, false), Set.of(), Settings.factory() + .url(URI.create("http://localhost:5000/v1/collect")) + .token(token) + .debug(debug) + .create(), tracker, null); } @Override diff --git a/fabric/example-mod/src/main/java/com/example/ExampleMod.java b/fabric/example-mod/src/main/java/com/example/ExampleMod.java index 47d8eae..78b4431 100644 --- a/fabric/example-mod/src/main/java/com/example/ExampleMod.java +++ b/fabric/example-mod/src/main/java/com/example/ExampleMod.java @@ -2,49 +2,22 @@ import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; +import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import dev.faststats.fabric.FabricMetrics; import net.fabricmc.api.ModInitializer; -import java.net.URI; - public class ExampleMod implements ModInitializer { - // context-aware error tracker, automatically tracks errors in the same class loader - public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware(); - - // context-unaware error tracker, does not automatically track errors - public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware(); - private final Metrics metrics = FabricMetrics.factory() - .url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only - - // Custom example metrics - // For this to work you have to create a corresponding data source in your project settings first + // Custom metrics require a corresponding data source in your project settings .addMetric(Metric.number("example_metric", () -> 42)) - .addMetric(Metric.string("example_string", () -> "Hello, World!")) - .addMetric(Metric.bool("example_boolean", () -> true)) - .addMetric(Metric.stringArray("example_string_array", () -> new String[]{"Option 1", "Option 2"})) - .addMetric(Metric.numberArray("example_number_array", () -> new Number[]{1, 2, 3})) - .addMetric(Metric.booleanArray("example_boolean_array", () -> new Boolean[]{true, false})) - // Attach an error tracker - // This must be enabled in the project settings - .errorTracker(ERROR_TRACKER) + // Error tracking must be enabled in the project settings + .errorTracker(ErrorTracker.contextAware()) - .debug(true) // Enable debug mode for development and testing - - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project + .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project .create("example-mod"); // your mod id as defined in fabric.mod.json - public void doSomethingWrong() { - try { - // Do something that might throw an error - throw new RuntimeException("Something went wrong!"); - } catch (final Exception e) { - CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e); - } - } - @Override public void onInitialize() { } diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java b/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java index f6ce2df..2df30b7 100644 --- a/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java +++ b/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java @@ -1,6 +1,7 @@ package dev.faststats.fabric; import dev.faststats.core.Metrics; +import dev.faststats.core.Settings; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; @@ -29,9 +30,9 @@ interface Factory extends Metrics.Factory { * * @param modId the mod id * @return the metrics instance - * @throws IllegalStateException if the token is not specified + * @throws IllegalStateException if the settings are not specified * @throws IllegalArgumentException if the mod is not found - * @see #token(String) + * @see #settings(Settings) * @since 0.12.0 */ @Override diff --git a/gradle.properties b/gradle.properties index edf5dab..4433e21 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.22.0 +version=0.23.0 diff --git a/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java b/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java index a215487..88921ae 100644 --- a/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -4,37 +4,19 @@ import com.hypixel.hytale.server.core.plugin.JavaPluginInit; import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; +import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import dev.faststats.hytale.HytaleMetrics; -import java.net.URI; - public class ExamplePlugin extends JavaPlugin { - // context-aware error tracker, automatically tracks errors in the same class loader - public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware(); - - // context-unaware error tracker, does not automatically track errors - public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware(); - private final Metrics metrics = HytaleMetrics.factory() - .url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only - - // Custom example metrics - // For this to work you have to create a corresponding data source in your project settings first + // Custom metrics require a corresponding data source in your project settings .addMetric(Metric.number("example_metric", () -> 42)) - .addMetric(Metric.string("example_string", () -> "Hello, World!")) - .addMetric(Metric.bool("example_boolean", () -> true)) - .addMetric(Metric.stringArray("example_string_array", () -> new String[]{"Option 1", "Option 2"})) - .addMetric(Metric.numberArray("example_number_array", () -> new Number[]{1, 2, 3})) - .addMetric(Metric.booleanArray("example_boolean_array", () -> new Boolean[]{true, false})) - // Attach an error tracker - // This must be enabled in the project settings - .errorTracker(ERROR_TRACKER) + // Error tracking must be enabled in the project settings + .errorTracker(ErrorTracker.contextAware()) - .debug(true) // Enable debug mode for development and testing - - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project + .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project .create(this); public ExamplePlugin(final JavaPluginInit init) { @@ -45,13 +27,4 @@ public ExamplePlugin(final JavaPluginInit init) { protected void shutdown() { metrics.shutdown(); // safely shut down metrics submission } - - public void doSomethingWrong() { - try { - // Do something that might throw an error - throw new RuntimeException("Something went wrong!"); - } catch (final Exception e) { - CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e); - } - } } diff --git a/settings.gradle.kts b/settings.gradle.kts index e67c2ef..d944992 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,7 @@ include("bukkit:example-plugin") include("bungeecord") include("bungeecord:example-plugin") include("core") +include("core:example") include("fabric") include("fabric:example-mod") include("hytale") diff --git a/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java b/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java index 3c43e7d..920bb3e 100644 --- a/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -3,6 +3,7 @@ import com.google.inject.Inject; import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; +import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import dev.faststats.sponge.SpongeMetrics; import org.jspecify.annotations.Nullable; @@ -13,17 +14,8 @@ import org.spongepowered.plugin.PluginContainer; import org.spongepowered.plugin.builtin.jvm.Plugin; -import java.net.URI; - - @Plugin("example") public class ExamplePlugin { - // context-aware error tracker, automatically tracks errors in the same class loader - public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware(); - - // context-unaware error tracker, does not automatically track errors - public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware(); - private @Inject PluginContainer pluginContainer; private @Inject SpongeMetrics.Factory factory; @@ -32,24 +24,13 @@ public class ExamplePlugin { @Listener public void onServerStart(final StartedEngineEvent event) { this.metrics = factory - .url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only - - // Custom example metrics - // For this to work you have to create a corresponding data source in your project settings first + // Custom metrics require a corresponding data source in your project settings .addMetric(Metric.number("example_metric", () -> 42)) - .addMetric(Metric.string("example_string", () -> "Hello, World!")) - .addMetric(Metric.bool("example_boolean", () -> true)) - .addMetric(Metric.stringArray("example_string_array", () -> new String[]{"Option 1", "Option 2"})) - .addMetric(Metric.numberArray("example_number_array", () -> new Number[]{1, 2, 3})) - .addMetric(Metric.booleanArray("example_boolean_array", () -> new Boolean[]{true, false})) - - // Attach an error tracker - // This must be enabled in the project settings - .errorTracker(ERROR_TRACKER) - .debug(true) // Enable debug mode for development and testing + // Error tracking must be enabled in the project settings + .errorTracker(ErrorTracker.contextAware()) - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project + .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project .create(pluginContainer); } @@ -57,13 +38,4 @@ public void onServerStart(final StartedEngineEvent event) { public void onServerStop(final StoppingEngineEvent event) { if (metrics != null) metrics.shutdown(); // safely shut down metrics submission } - - public void doSomethingWrong() { - try { - // Do something that might throw an error - throw new RuntimeException("Something went wrong!"); - } catch (final Exception e) { - CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e); - } - } } diff --git a/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java b/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java index b852d12..69afdc4 100644 --- a/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -7,22 +7,14 @@ import com.velocitypowered.api.plugin.Plugin; import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; +import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import dev.faststats.velocity.VelocityMetrics; import org.jspecify.annotations.Nullable; -import java.net.URI; - - @Plugin(id = "example", name = "Example Plugin", version = "1.0.0", url = "https://example.com", authors = {"Your Name"}) public class ExamplePlugin { - // context-aware error tracker, automatically tracks errors in the same class loader - public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware(); - - // context-unaware error tracker, does not automatically track errors - public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware(); - private final VelocityMetrics.Factory metricsFactory; private @Nullable Metrics metrics = null; @@ -34,24 +26,13 @@ public ExamplePlugin(final VelocityMetrics.Factory factory) { @Subscribe public void onProxyInitialize(final ProxyInitializeEvent event) { this.metrics = metricsFactory - .url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only - - // Custom example metrics - // For this to work you have to create a corresponding data source in your project settings first + // Custom metrics require a corresponding data source in your project settings .addMetric(Metric.number("example_metric", () -> 42)) - .addMetric(Metric.string("example_string", () -> "Hello, World!")) - .addMetric(Metric.bool("example_boolean", () -> true)) - .addMetric(Metric.stringArray("example_string_array", () -> new String[]{"Option 1", "Option 2"})) - .addMetric(Metric.numberArray("example_number_array", () -> new Number[]{1, 2, 3})) - .addMetric(Metric.booleanArray("example_boolean_array", () -> new Boolean[]{true, false})) - - // Attach an error tracker - // This must be enabled in the project settings - .errorTracker(ERROR_TRACKER) - .debug(true) // Enable debug mode for development and testing + // Error tracking must be enabled in the project settings + .errorTracker(ErrorTracker.contextAware()) - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project + .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project .create(this); } @@ -59,13 +40,4 @@ public void onProxyInitialize(final ProxyInitializeEvent event) { public void onProxyStop(final ProxyShutdownEvent event) { if (metrics != null) metrics.shutdown(); // safely shut down metrics submission } - - public void doSomethingWrong() { - try { - // Do something that might throw an error - throw new RuntimeException("Something went wrong!"); - } catch (final Exception e) { - CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e); - } - } } diff --git a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java index 0c64cf8..70cc8d3 100644 --- a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java +++ b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java @@ -5,6 +5,7 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; import dev.faststats.core.Metrics; +import dev.faststats.core.Settings; import dev.faststats.core.SimpleMetrics; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; @@ -79,9 +80,9 @@ public Factory(final ProxyServer server, final Logger logger, @DataDirectory fin * * @param plugin the plugin instance * @return the metrics instance - * @throws IllegalStateException if the token is not specified + * @throws IllegalStateException if the settings are not specified * @throws IllegalArgumentException if the given object is not a valid plugin - * @see #token(String) + * @see #settings(Settings) * @since 0.1.0 */ @Override From 1fc446e619eeca6456cb98ce48c24aa50f9b9569 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 13:18:45 +0200 Subject: [PATCH 02/40] Split metrics and flags url into separate values --- .../java/dev/faststats/core/Settings.java | 21 +++++++++++++++---- .../dev/faststats/core/SimpleMetrics.java | 9 ++++---- .../dev/faststats/core/SimpleSettings.java | 19 ++++++++++------- .../core/flags/SimpleFeatureFlagService.java | 16 +++++++------- .../test/java/dev/faststats/MockMetrics.java | 3 ++- 5 files changed, 45 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/Settings.java b/core/src/main/java/dev/faststats/core/Settings.java index 45ff172..7e0b02c 100644 --- a/core/src/main/java/dev/faststats/core/Settings.java +++ b/core/src/main/java/dev/faststats/core/Settings.java @@ -47,13 +47,22 @@ static Factory factory() { String token(); /** - * The server URL. + * The metrics server URL. * - * @return the server URL + * @return the metrics server URL * @since 0.23.0 */ @Contract(pure = true) - URI url(); + URI metricsUrl(); + + /** + * The flags server URL. + * + * @return the flags server URL + * @since 0.23.0 + */ + @Contract(pure = true) + URI flagsUrl(); /** * Whether debug logging is enabled. @@ -93,7 +102,11 @@ sealed interface Factory permits SimpleSettings.Factory { * @since 0.23.0 */ @Contract(mutates = "this") - Factory url(URI url); + Factory metricsServer(URI url); // todo: rethink naming + + // todo: add docs + @Contract(mutates = "this") + Factory flagsServer(URI url); // todo: rethink naming /** * Enables or disables debug logging. diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 1d6183b..e9e0d12 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -194,6 +194,7 @@ private boolean submitNow() throws IOException { final var compressed = byteOutput.toByteArray(); info("Compressed size: " + compressed.length + " bytes"); + final var url = settings.metricsUrl().resolve("/collect"); final var request = HttpRequest.newBuilder() .POST(HttpRequest.BodyPublishers.ofByteArray(compressed)) .header("Content-Encoding", "gzip") @@ -201,10 +202,10 @@ private boolean submitNow() throws IOException { .header("Authorization", "Bearer " + settings.token()) .header("User-Agent", "FastStats Metrics " + SDK_NAME + "/" + SDK_VERSION) .timeout(Duration.ofSeconds(3)) - .uri(settings.url()) + .uri(url) .build(); - info("Sending metrics to: " + settings.url()); + info("Sending metrics to: " + url); try { final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(UTF_8)); final var statusCode = response.statusCode(); @@ -225,9 +226,9 @@ private boolean submitNow() throws IOException { warn("Received unexpected response from metrics server: " + statusCode + " (" + body + ")"); } } catch (final HttpConnectTimeoutException t) { - error("Metrics submission timed out after 3 seconds: " + settings.url(), null); + error("Metrics submission timed out after 3 seconds: " + url, null); } catch (final ConnectException t) { - error("Failed to connect to metrics server: " + settings.url(), null); + error("Failed to connect to metrics server: " + url, null); } catch (final Throwable t) { error("Failed to submit metrics", t); } diff --git a/core/src/main/java/dev/faststats/core/SimpleSettings.java b/core/src/main/java/dev/faststats/core/SimpleSettings.java index 534a670..5f01d2e 100644 --- a/core/src/main/java/dev/faststats/core/SimpleSettings.java +++ b/core/src/main/java/dev/faststats/core/SimpleSettings.java @@ -4,12 +4,11 @@ import java.net.URI; -record SimpleSettings(@Token String token, URI url, boolean debug) implements Settings { +record SimpleSettings(@Token String token, URI metricsUrl, URI flagsUrl, boolean debug) implements Settings { static final class Factory implements Settings.Factory { - private static final URI DEFAULT_URL = URI.create("https://metrics.faststats.dev/v1/collect"); - - private URI url = DEFAULT_URL; + private URI metricsUrl = URI.create("https://metrics.faststats.dev/v1"); + private URI flagsUrl = URI.create("https://flags.faststats.dev/v1"); private @Nullable String token; private boolean debug = false; @@ -23,8 +22,14 @@ public Settings.Factory token(@Token final String token) throws IllegalArgumentE } @Override - public Settings.Factory url(final URI url) { - this.url = url; + public Settings.Factory metricsServer(final URI url) { + this.metricsUrl = url; + return this; + } + + @Override + public Settings.Factory flagsServer(final URI url) { + this.flagsUrl = url; return this; } @@ -38,7 +43,7 @@ public Settings.Factory debug(final boolean enabled) { @SuppressWarnings("PatternValidation") public Settings create() throws IllegalStateException { if (token == null) throw new IllegalStateException("Token must be specified"); - return new SimpleSettings(token, url, debug); + return new SimpleSettings(token, metricsUrl, flagsUrl, debug); } } } diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index d091dd2..0e29609 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -20,13 +20,15 @@ final class SimpleFeatureFlagService implements FeatureFlagService { private static final Gson GSON = new Gson(); + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(3)) + .version(HttpClient.Version.HTTP_1_1) + .build(); + private final Settings settings; private final @Nullable Attributes attributes; private final Duration ttl; - private final HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(3)) - .build(); private final Map cache = new ConcurrentHashMap<>(); private final Map fetchTimes = new ConcurrentHashMap<>(); private final Map> fetchesInProgress = new ConcurrentHashMap<>(); @@ -62,11 +64,11 @@ CompletableFuture fetch(final SimpleFeatureFlag flag) { } CompletableFuture optIn(final SimpleFeatureFlag flag) { - return sendOptRequest(flag, "/v1/flag/opt-in"); + return sendOptRequest(flag, "/opt-in"); } CompletableFuture optOut(final SimpleFeatureFlag flag) { - return sendOptRequest(flag, "/v1/flag/opt-out"); + return sendOptRequest(flag, "/opt-out"); } private CompletableFuture sendOptRequest(final SimpleFeatureFlag flag, final String path) { @@ -82,7 +84,7 @@ private CompletableFuture sendOptRequest(final SimpleFeatureFlag flag, .header("Content-Type", "application/json") .header("Authorization", "Bearer " + settings.token()) .timeout(Duration.ofSeconds(3)) - .uri(settings.url().resolve(path)) + .uri(settings.flagsUrl().resolve(path)) .build(); return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenCompose(response -> { @@ -130,7 +132,7 @@ private CompletableFuture createFetch(final SimpleFeatureFlag flag) { .header("Content-Type", "application/json") .header("Authorization", "Bearer " + settings.token()) .timeout(Duration.ofSeconds(3)) - .uri(settings.url().resolve("/flags")) + .uri(settings.flagsUrl().resolve("/check")) .build(); return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(response -> { diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index 01cc1be..4ff0999 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -16,7 +16,8 @@ public final class MockMetrics extends SimpleMetrics { public MockMetrics(final UUID serverId, @Token final String token, @Nullable final ErrorTracker tracker, final boolean debug) { super(new Config(serverId, true, debug, true, true, false, false), Set.of(), Settings.factory() - .url(URI.create("http://localhost:5000/v1/collect")) + .metricsServer(URI.create("http://localhost:5000/v1")) + .flagsServer(URI.create("http://localhost:5001/v1")) .token(token) .debug(debug) .create(), tracker, null); From 6f508d76bfaf48e0d1eada70d57ac42278a25470 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 13:19:23 +0200 Subject: [PATCH 03/40] Throw on non-finite numbers --- core/src/main/java/dev/faststats/core/flags/Attributes.java | 3 ++- .../main/java/dev/faststats/core/flags/SimpleAttributes.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/flags/Attributes.java b/core/src/main/java/dev/faststats/core/flags/Attributes.java index ba0c776..0a161a1 100644 --- a/core/src/main/java/dev/faststats/core/flags/Attributes.java +++ b/core/src/main/java/dev/faststats/core/flags/Attributes.java @@ -57,10 +57,11 @@ static Attributes copyOf(final Attributes attributes) { * @param key the key * @param value the value * @return these attributes + * @throws IllegalArgumentException if the given value is not {@link Double#isFinite(double) finite} * @since 0.23.0 */ @Contract(value = "_, _ -> this", mutates = "this") - Attributes put(String key, Number value); + Attributes put(String key, Number value) throws IllegalArgumentException; /** * Set a boolean value. diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java b/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java index 84d1ca4..c9cffd8 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java @@ -11,7 +11,8 @@ public Attributes put(final String key, final String value) { @Override public Attributes put(final String key, final Number value) { - attributes.put(key, value); + if (!Double.isFinite(value.doubleValue())) throw new IllegalArgumentException("Value must be finite"); + attributes.put(key, new JsonPrimitive(value)); return this; } From adcc65adc6ee3b31d1e35505850550c6e7345ca6 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 13:21:12 +0200 Subject: [PATCH 04/40] Replace Object with JsonPrimitive in Attributes --- .../dev/faststats/core/flags/Attributes.java | 17 +++++------------ .../faststats/core/flags/SimpleAttributes.java | 13 ++++++++----- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/flags/Attributes.java b/core/src/main/java/dev/faststats/core/flags/Attributes.java index 0a161a1..ccd79b1 100644 --- a/core/src/main/java/dev/faststats/core/flags/Attributes.java +++ b/core/src/main/java/dev/faststats/core/flags/Attributes.java @@ -1,11 +1,11 @@ package dev.faststats.core.flags; +import com.google.gson.JsonPrimitive; import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.Unmodifiable; import org.jspecify.annotations.Nullable; -import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; /** * Mutable key-value attributes for feature flag targeting. @@ -84,15 +84,8 @@ static Attributes copyOf(final Attributes attributes) { @Contract(value = "_ -> this", mutates = "this") Attributes remove(String key); - /** - * Return an unmodifiable view of all attribute entries. - * - * @return unmodifiable map of attribute entries - * @since 0.23.0 - */ - @Unmodifiable - @Contract(pure = true) - Map entries(); + // todo: add docs + void forEachPrimitive(BiConsumer action); /** * Create new attributes by merging two attribute sets. @@ -106,7 +99,7 @@ static Attributes copyOf(final Attributes attributes) { */ @Contract(value = "_, _ -> new", pure = true) static Attributes join(@Nullable final Attributes first, @Nullable final Attributes second) { - final var attributes = new ConcurrentHashMap(); + final var attributes = new ConcurrentHashMap(); if (first instanceof final SimpleAttributes simple) attributes.putAll(simple.attributes()); if (second instanceof final SimpleAttributes simple) attributes.putAll(simple.attributes()); return new SimpleAttributes(attributes); diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java b/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java index c9cffd8..a137801 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java @@ -1,11 +1,14 @@ package dev.faststats.core.flags; +import com.google.gson.JsonPrimitive; + import java.util.Map; +import java.util.function.BiConsumer; -record SimpleAttributes(Map attributes) implements Attributes { +record SimpleAttributes(Map attributes) implements Attributes { @Override public Attributes put(final String key, final String value) { - attributes.put(key, value); + attributes.put(key, new JsonPrimitive(value)); return this; } @@ -18,7 +21,7 @@ public Attributes put(final String key, final Number value) { @Override public Attributes put(final String key, final boolean value) { - attributes.put(key, value); + attributes.put(key, new JsonPrimitive(value)); return this; } @@ -29,7 +32,7 @@ public Attributes remove(final String key) { } @Override - public Map entries() { - return Map.copyOf(attributes); + public void forEachPrimitive(final BiConsumer action) { + attributes.forEach(action); } } From a62722ae08314311c21d7c0ddf833edafbc6a2dc Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 13:21:28 +0200 Subject: [PATCH 05/40] Add Type enum to FeatureFlags --- .../dev/faststats/core/flags/FeatureFlag.java | 16 +++++++++++++- .../core/flags/SimpleFeatureFlag.java | 21 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java b/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java index e2f275d..23cf853 100644 --- a/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java +++ b/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java @@ -25,6 +25,15 @@ public sealed interface FeatureFlag permits SimpleFeatureFlag { @Contract(pure = true) String getId(); + /** + * Returns the type representing the value type of this flag. + * + * @return the value type class + * @since 0.23.0 + */ + @Contract(pure = true) + Type getType(); + /** * Returns the class representing the value type of this flag. * @@ -32,7 +41,7 @@ public sealed interface FeatureFlag permits SimpleFeatureFlag { * @since 0.23.0 */ @Contract(pure = true) - Class getType(); + Class getTypeClass(); /** * Get the current cached flag value. @@ -138,4 +147,9 @@ public sealed interface FeatureFlag permits SimpleFeatureFlag { */ @Contract(pure = true) T getDefaultValue(); + + // todo: add docs + enum Type { + STRING, BOOLEAN, NUMBER + } } diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java index 5743f63..b7f4670 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java @@ -13,6 +13,7 @@ final class SimpleFeatureFlag implements FeatureFlag { private final String id; private final T defaultValue; private final @Nullable Attributes attributes; + private final Type type; SimpleFeatureFlag( final String id, @@ -24,6 +25,13 @@ final class SimpleFeatureFlag implements FeatureFlag { this.defaultValue = defaultValue; this.attributes = attributes; this.service = service; + if (defaultValue instanceof final String string) { + this.type = Type.STRING; + } else if (defaultValue instanceof final Number number) { + this.type = Type.NUMBER; + } else if (defaultValue instanceof final Boolean bool) { + this.type = Type.BOOLEAN; + } else throw new IllegalArgumentException("Unsupported type: " + defaultValue.getClass().getName()); service.fetch(this); } @@ -32,10 +40,19 @@ public String getId() { return id; } + @Override + public Type getType() { + return type; + } + @Override @SuppressWarnings("unchecked") - public Class getType() { - return (Class) defaultValue.getClass(); + public Class getTypeClass() { + return (Class) switch (type) { + case STRING -> String.class; + case NUMBER -> Number.class; + case BOOLEAN -> Boolean.class; + }; } @Override From 9b06da979d92033eb35a4c9fe7361143f6103eba Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 13:22:24 +0200 Subject: [PATCH 06/40] Refactor SimpleFeatureFlagService --- .../core/flags/SimpleFeatureFlagService.java | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index 0e29609..c944996 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -3,6 +3,9 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; import dev.faststats.core.Settings; import org.jspecify.annotations.Nullable; @@ -45,17 +48,14 @@ final class SimpleFeatureFlagService implements FeatureFlagService { @SuppressWarnings("unchecked") Optional get(final SimpleFeatureFlag flag) { - final var cached = cache.get(flag.getId()); - return Optional.ofNullable((T) cached); + return Optional.ofNullable((T) cache.get(flag.getId())); } @SuppressWarnings("unchecked") CompletableFuture whenReady(final SimpleFeatureFlag flag) { final var cached = cache.get(flag.getId()); - if (cached != null && !isExpired(flag)) { - return CompletableFuture.completedFuture((T) cached); - } - return fetch(flag); + if (cached == null || isExpired(flag)) return fetch(flag); + return CompletableFuture.completedFuture((T) cached); } @SuppressWarnings("unchecked") @@ -72,8 +72,6 @@ CompletableFuture optOut(final SimpleFeatureFlag flag) { } private CompletableFuture sendOptRequest(final SimpleFeatureFlag flag, final String path) { - invalidate(flag); - final var requestBody = new JsonObject(); requestBody.addProperty("projectToken", settings.token()); requestBody.addProperty("serverId", UUID.randomUUID().toString()); // todo: read from config @@ -97,12 +95,6 @@ private CompletableFuture sendOptRequest(final SimpleFeatureFlag flag, }); } - void invalidate(final SimpleFeatureFlag flag) { - final var id = flag.getId(); - cache.remove(id); - fetchTimes.remove(id); - } - Optional getExpiration(final SimpleFeatureFlag flag) { final var lastFetch = fetchTimes.get(flag.getId()); if (lastFetch == null) return Optional.empty(); @@ -121,11 +113,14 @@ boolean isExpired(final SimpleFeatureFlag flag) { private CompletableFuture createFetch(final SimpleFeatureFlag flag) { final var requestBody = new JsonObject(); - requestBody.addProperty("flag", flag.getId()); + requestBody.addProperty("projectToken", settings.token()); + requestBody.addProperty("serverId", UUID.randomUUID().toString()); // todo: read from config + requestBody.addProperty("key", flag.getId()); - final var mergedAttributes = Attributes.join(attributes, flag.attributes()); - // todo: drop gson - requestBody.add("attributes", GSON.toJsonTree(mergedAttributes)); + final var attributes = new JsonObject(); + if (this.attributes != null) this.attributes.forEachPrimitive(attributes::add); + if (flag.attributes() != null) flag.attributes().forEachPrimitive(attributes::add); + if (!attributes.isEmpty()) requestBody.add("attributes", attributes); final var request = HttpRequest.newBuilder() .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(requestBody))) @@ -136,28 +131,34 @@ private CompletableFuture createFetch(final SimpleFeatureFlag flag) { .build(); return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(response -> { - if (response.statusCode() >= 200 && response.statusCode() < 300) { - // todo: replace gson with safer read - final var body = GSON.fromJson(response.body(), JsonObject.class); - final var value = body.get("value"); - if (value != null && !value.isJsonNull()) { - cache.put(flag.getId(), toValue(value)); - fetchTimes.put(flag.getId(), System.currentTimeMillis()); - return flag.getType().cast(cache.get(flag.getId())); - } + try { + final var body = JsonParser.parseString(response.body()); + + if (response.statusCode() < 200 && response.statusCode() >= 300) + throw new IllegalStateException("Unexpected response status: %s (%s)".formatted(response.statusCode(), body)); + + final var value = getValue(flag, body); + cache.put(flag.getId(), value); + fetchTimes.put(flag.getId(), System.currentTimeMillis()); + return value; + } catch (final JsonParseException e) { + throw new IllegalStateException("Unexpected response body: %s (%s)".formatted(response.body(), response.statusCode()), e); } - return flag.getDefaultValue(); }).whenComplete((ignored, throwable) -> fetchesInProgress.remove(flag.getId())); } - private Object toValue(final JsonElement element) { - if (element.isJsonPrimitive()) { - final var primitive = element.getAsJsonPrimitive(); - if (primitive.isBoolean()) return primitive.getAsBoolean(); - if (primitive.isNumber()) return primitive.getAsNumber(); - return primitive.getAsString(); - } // todo: guarantee for primitives? - return element.toString(); + @SuppressWarnings("unchecked") + private static T getValue(final SimpleFeatureFlag flag, final JsonElement body) { + if (!(body instanceof final JsonObject object)) + throw new IllegalStateException("Unexpected JSON response: " + body); + if (!(object.get("value") instanceof final JsonPrimitive primitive)) + throw new IllegalStateException("Missing or invalid 'value' in JSON response: " + body); + + return (T) switch (flag.getType()) { + case STRING -> primitive.getAsString(); + case NUMBER -> primitive.getAsNumber(); + case BOOLEAN -> primitive.getAsBoolean(); + }; } @Override From 7682962ddd8859def32f863f00f69af3c7df9171 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 13:22:47 +0200 Subject: [PATCH 07/40] Generalize terms in onboarding message and default config --- core/src/main/java/dev/faststats/core/SimpleMetrics.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index e9e0d12..3bf2693 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -101,7 +101,7 @@ protected SimpleMetrics( protected String getOnboardingMessage() { return """ - This plugin uses FastStats to collect anonymous usage statistics. + This piece of software uses FastStats to collect anonymous usage statistics. No personal or identifying information is ever collected. To opt out, set 'enabled=false' in the metrics configuration file. Learn more at: https://faststats.dev/info @@ -392,7 +392,7 @@ public record Config( ) implements Metrics.Config { public static final String DEFAULT_COMMENT = """ - FastStats (https://faststats.dev) collects anonymous usage statistics for plugin developers. + FastStats (https://faststats.dev) collects anonymous usage statistics for developers. # This helps developers understand how their projects are used in the real world. # # No IP addresses, player data, or personal information is collected. @@ -400,9 +400,9 @@ public record Config( # # Enabling metrics has no noticeable performance impact. # Keeping metrics enabled is recommended, but you can opt out by setting - # 'enabled=false' in plugins/faststats/config.properties. + # 'enabled=false' in faststats/config.properties. # - # If you suspect a plugin is collecting personal data or bypassing the "enabled" option, + # If you suspect a developer is collecting personal data or bypassing the "enabled" option, # please report it at: https://faststats.dev/abuse # # For more information, visit: https://faststats.dev/info From fc4d8f3147bf147abb3e1f8bc3aef21da775db4c Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 15:28:13 +0200 Subject: [PATCH 08/40] Refactored logging --- .../faststats/bukkit/BukkitMetricsImpl.java | 17 ------ .../faststats/bungee/BungeeMetricsImpl.java | 20 ------- .../dev/faststats/core/SimpleMetrics.java | 58 +++++++++++-------- core/src/main/java/module-info.java | 1 + .../test/java/dev/faststats/MockMetrics.java | 16 ----- .../faststats/fabric/FabricMetricsImpl.java | 18 ------ .../faststats/hytale/HytaleMetricsImpl.java | 14 ++--- hytale/src/main/java/module-info.java | 1 + .../minestom/MinestomMetricsImpl.java | 20 ------- .../faststats/nukkit/NukkitMetricsImpl.java | 17 ------ .../faststats/sponge/SpongeMetricsImpl.java | 20 ------- .../velocity/VelocityMetricsImpl.java | 19 ------ 12 files changed, 41 insertions(+), 180 deletions(-) diff --git a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java index 58e30e5..4afb46f 100644 --- a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java +++ b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java @@ -5,12 +5,10 @@ import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.Nullable; import java.nio.file.Path; import java.util.Optional; import java.util.function.Supplier; -import java.util.logging.Level; final class BukkitMetricsImpl extends SimpleMetrics implements BukkitMetrics { private final Plugin plugin; @@ -81,21 +79,6 @@ private int getPlayerCount() { } } - @Override - protected void printError(final String message, @Nullable final Throwable throwable) { - plugin.getLogger().log(Level.SEVERE, message, throwable); - } - - @Override - protected void printInfo(final String message) { - plugin.getLogger().info(message); - } - - @Override - protected void printWarning(final String message) { - plugin.getLogger().warning(message); - } - @Override public void ready() { if (getErrorTracker().isPresent()) try { diff --git a/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java b/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java index 34fd29f..ba70bc0 100644 --- a/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java +++ b/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java @@ -7,14 +7,10 @@ import net.md_5.bungee.api.plugin.Plugin; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.Nullable; import java.nio.file.Path; -import java.util.logging.Level; -import java.util.logging.Logger; final class BungeeMetricsImpl extends SimpleMetrics implements BungeeMetrics { - private final Logger logger; private final ProxyServer server; private final Plugin plugin; @@ -23,7 +19,6 @@ final class BungeeMetricsImpl extends SimpleMetrics implements BungeeMetrics { private BungeeMetricsImpl(final Factory factory, final Plugin plugin, final Path config) throws IllegalStateException { super(factory, config); - this.logger = plugin.getLogger(); this.server = plugin.getProxy(); this.plugin = plugin; @@ -39,21 +34,6 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", server.getName()); } - @Override - protected void printError(final String message, @Nullable final Throwable throwable) { - logger.log(Level.SEVERE, message, throwable); - } - - @Override - protected void printInfo(final String message) { - logger.info(message); - } - - @Override - protected void printWarning(final String message) { - logger.warning(message); - } - static final class Factory extends SimpleMetrics.Factory implements BungeeMetrics.Factory { @Override public Metrics create(final Plugin plugin) throws IllegalStateException { diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 3bf2693..09e4b98 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -3,6 +3,7 @@ import com.google.gson.JsonObject; import dev.faststats.core.data.Metric; import dev.faststats.core.flags.FeatureFlagService; +import org.intellij.lang.annotations.PrintFormat; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.MustBeInvokedByOverriders; @@ -30,11 +31,16 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiPredicate; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; import java.util.zip.GZIPOutputStream; import static java.nio.charset.StandardCharsets.UTF_8; public abstract class SimpleMetrics implements Metrics { + protected final Logger logger = Logger.getLogger(getClass().getName()); + private static final URI defaultUrl = URI.create("https://metrics.faststats.dev/v1/collect"); private final HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .version(HttpClient.Version.HTTP_1_1) @@ -124,6 +130,7 @@ protected void startSubmitting() { startSubmitting(getInitialDelay(), getPeriod(), TimeUnit.MILLISECONDS); } + @SuppressWarnings("PatternValidation") private void startSubmitting(final long initialDelay, final long period, final TimeUnit unit) { if (Boolean.getBoolean("faststats.first-run")) { info("Skipping metrics submission due to first-run flag"); @@ -136,9 +143,9 @@ private void startSubmitting(final long initialDelay, final long period, final T final var split = getOnboardingMessage().split("\n"); for (final var s : split) if (s.length() > separatorLength) separatorLength = s.length(); - printInfo("-".repeat(separatorLength)); - for (final var s : split) printInfo(s); - printInfo("-".repeat(separatorLength)); + info("-".repeat(separatorLength)); + for (final var s : split) info(s); + info("-".repeat(separatorLength)); System.setProperty("faststats.first-run", "true"); if (!config.externallyManaged()) return; @@ -183,7 +190,7 @@ private boolean submitNow() throws IOException { final var data = createData().toString(); final var bytes = data.getBytes(UTF_8); - info("Uncompressed data: " + data); + info("Uncompressed data: %s", data); try (final var byteOutput = new ByteArrayOutputStream(); final var output = new GZIPOutputStream(byteOutput)) { @@ -192,7 +199,7 @@ private boolean submitNow() throws IOException { output.finish(); final var compressed = byteOutput.toByteArray(); - info("Compressed size: " + compressed.length + " bytes"); + info("Compressed size: %s bytes", compressed.length); final var url = settings.metricsUrl().resolve("/collect"); final var request = HttpRequest.newBuilder() @@ -212,23 +219,23 @@ private boolean submitNow() throws IOException { final var body = response.body(); if (statusCode >= 200 && statusCode < 300) { - info("Metrics submitted with status code: " + statusCode + " (" + body + ")"); + info("Metrics submitted with status code: %s (%s)", statusCode, body); getErrorTracker().map(SimpleErrorTracker.class::cast).ifPresent(SimpleErrorTracker::clear); if (flush != null) flush.run(); return true; } else if (statusCode >= 300 && statusCode < 400) { - warn("Received redirect response from metrics server: " + statusCode + " (" + body + ")"); + warn("Received redirect response from metrics server: %s (%s)", statusCode, body); } else if (statusCode >= 400 && statusCode < 500) { - error("Submitted invalid request to metrics server: " + statusCode + " (" + body + ")", null); + error("Submitted invalid request to metrics server: %s (%s)", null, statusCode, body); } else if (statusCode >= 500 && statusCode < 600) { - error("Received server error response from metrics server: " + statusCode + " (" + body + ")", null); + error("Received server error response from metrics server: %s (%s)", null, statusCode, body); } else { - warn("Received unexpected response from metrics server: " + statusCode + " (" + body + ")"); + warn("Received unexpected response from metrics server: %s (%s)", statusCode, body); } } catch (final HttpConnectTimeoutException t) { - error("Metrics submission timed out after 3 seconds: " + url, null); + error("Metrics submission timed out after 3 seconds: %s", null, defaultUrl); } catch (final ConnectException t) { - error("Failed to connect to metrics server: " + url, null); + error("Failed to connect to metrics server: %s", null, defaultUrl); } catch (final Throwable t) { error("Failed to submit metrics", t); } @@ -265,7 +272,7 @@ protected JsonObject createData() { try { metric.getData().ifPresent(element -> metrics.add(metric.getId(), element)); } catch (final Throwable t) { - error("Failed to build metric data: " + metric.getId(), t); + error("Failed to build metric data: %s", t, metric.getId()); getErrorTracker().ifPresent(tracker -> tracker.trackError(t)); } }); @@ -303,23 +310,26 @@ public Metrics.Config getConfig() { @Contract(mutates = "param1") protected abstract void appendDefaultData(JsonObject metrics); - protected void error(final String message, @Nullable final Throwable throwable) { - if (debug) printError("[" + getClass().getName() + "]: " + message, throwable); + protected void error(@PrintFormat final String message, @Nullable final Throwable throwable, @Nullable final Object... args) { + if (throwable != null) { + if (!logger.isLoggable(Level.SEVERE)) return; + final var logRecord = new LogRecord(Level.SEVERE, message.formatted(args)); + logRecord.setThrown(throwable); + logger.log(logRecord); + } else log(Level.SEVERE, message, args); } - protected void warn(final String message) { - if (debug) printWarning("[" + getClass().getName() + "]: " + message); + protected void log(final Level level, @PrintFormat final String message, @Nullable final Object... args) { + logger.log(level, () -> message.formatted(args)); } - protected void info(final String message) { - if (debug) printInfo("[" + getClass().getName() + "]: " + message); + protected void info(@PrintFormat final String message, @Nullable final Object... args) { + log(Level.INFO, message, args); } - protected abstract void printError(String message, @Nullable Throwable throwable); - - protected abstract void printInfo(String message); - - protected abstract void printWarning(String message); + protected void warn(@PrintFormat final String message, @Nullable final Object... args) { + log(Level.WARNING, message, args); + } @Override public void shutdown() { diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index e635f81..fce66ce 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -7,6 +7,7 @@ exports dev.faststats.core; requires com.google.gson; + requires java.logging; requires java.net.http; requires static org.jetbrains.annotations; diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index 4ff0999..44d5977 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -23,22 +23,6 @@ public MockMetrics(final UUID serverId, @Token final String token, @Nullable fin .create(), tracker, null); } - @Override - protected void printError(final String message, @Nullable final Throwable throwable) { - System.err.println(message); - if (throwable != null) throwable.printStackTrace(System.err); - } - - @Override - protected void printInfo(final String message) { - System.out.println(message); - } - - @Override - protected void printWarning(final String message) { - System.out.println(message); - } - @Override public JsonObject createData() { return super.createData(); diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java b/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java index ba48e45..5195fa7 100644 --- a/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java +++ b/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java @@ -10,15 +10,12 @@ import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.nio.file.Path; import java.util.Optional; import java.util.function.Supplier; final class FabricMetricsImpl extends SimpleMetrics implements FabricMetrics { - private final Logger logger = LoggerFactory.getLogger("FastStats"); private final ModContainer mod; private @Nullable MinecraftServer server; @@ -47,21 +44,6 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", "Fabric"); } - @Override - protected void printError(final String message, @Nullable final Throwable throwable) { - logger.error(message, throwable); - } - - @Override - protected void printInfo(final String message) { - logger.info(message); - } - - @Override - protected void printWarning(final String message) { - logger.warn(message); - } - private Optional tryOrEmpty(final Supplier supplier) { try { return Optional.of(supplier.get()); diff --git a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java index d837202..6049c9b 100644 --- a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java +++ b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java @@ -12,6 +12,7 @@ import org.jspecify.annotations.Nullable; import java.nio.file.Path; +import java.util.logging.Level; final class HytaleMetricsImpl extends SimpleMetrics implements HytaleMetrics { private final HytaleLogger logger; @@ -33,18 +34,13 @@ protected void appendDefaultData(final JsonObject metrics) { } @Override - protected void printError(final String message, @Nullable final Throwable throwable) { - logger.atSevere().log(message, throwable); + protected void error(final String message, @Nullable final Throwable throwable, @Nullable final Object... args) { + if (super.logger.isLoggable(Level.SEVERE)) logger.atSevere().withCause(throwable).logVarargs(message, args); } @Override - protected void printInfo(final String message) { - logger.atInfo().log(message); - } - - @Override - protected void printWarning(final String message) { - logger.atWarning().log(message); + protected void log(final Level level, final String message, @Nullable final Object... args) { + if (super.logger.isLoggable(level)) logger.at(level).logVarargs(message, args); } static final class Factory extends SimpleMetrics.Factory implements HytaleMetrics.Factory { diff --git a/hytale/src/main/java/module-info.java b/hytale/src/main/java/module-info.java index a091bad..1b3293d 100644 --- a/hytale/src/main/java/module-info.java +++ b/hytale/src/main/java/module-info.java @@ -6,6 +6,7 @@ requires com.google.gson; requires dev.faststats.core; + requires java.logging; requires static org.jetbrains.annotations; requires static org.jspecify; diff --git a/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java b/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java index 217f194..91bc04e 100644 --- a/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java +++ b/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java @@ -8,15 +8,10 @@ import net.minestom.server.MinecraftServer; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.nio.file.Path; final class MinestomMetricsImpl extends SimpleMetrics implements MinestomMetrics { - private final Logger logger = LoggerFactory.getLogger(MinestomMetricsImpl.class); - @Async.Schedule @Contract(mutates = "io") private MinestomMetricsImpl(final Factory factory, final Path config) throws IllegalStateException { @@ -33,21 +28,6 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", "Minestom"); } - @Override - protected void printError(final String message, @Nullable final Throwable throwable) { - logger.error(message, throwable); - } - - @Override - protected void printInfo(final String message) { - logger.info(message); - } - - @Override - protected void printWarning(final String message) { - logger.warn(message); - } - @Override public void ready() { getErrorTracker().ifPresent(this::registerExceptionHandler); diff --git a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java index 1316d89..c9c8eac 100644 --- a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java +++ b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java @@ -15,7 +15,6 @@ import java.util.function.Supplier; final class NukkitMetricsImpl extends SimpleMetrics implements NukkitMetrics { - private final Logger logger; private final Server server; private final PluginBase plugin; @@ -24,7 +23,6 @@ final class NukkitMetricsImpl extends SimpleMetrics implements NukkitMetrics { private NukkitMetricsImpl(final Factory factory, final PluginBase plugin, final Path config) throws IllegalStateException { super(factory, config); - this.logger = plugin.getLogger(); this.server = plugin.getServer(); this.plugin = plugin; @@ -40,21 +38,6 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", server.getName()); } - @Override - protected void printError(final String message, @Nullable final Throwable throwable) { - logger.error(message, throwable); - } - - @Override - protected void printInfo(final String message) { - logger.info(message); - } - - @Override - protected void printWarning(final String message) { - logger.warning(message); - } - private Optional tryOrEmpty(final Supplier supplier) { try { return Optional.of(supplier.get()); diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java index b029e84..006b491 100644 --- a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java @@ -6,7 +6,6 @@ import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.Nullable; import org.spongepowered.api.Platform; import org.spongepowered.api.Sponge; import org.spongepowered.plugin.PluginContainer; @@ -31,7 +30,6 @@ final class SpongeMetricsImpl extends SimpleMetrics implements SpongeMetrics { # For more information, visit: https://faststats.dev/info """; - private final Logger logger; private final PluginContainer plugin; @Async.Schedule @@ -44,10 +42,7 @@ private SpongeMetricsImpl( ) throws IllegalStateException { super(factory, SimpleMetrics.Config.read(config, COMMENT, true, Sponge.metricsConfigManager() .effectiveCollectionState(plugin).asBoolean())); - - this.logger = logger; this.plugin = plugin; - startSubmitting(); } @@ -70,21 +65,6 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", Sponge.platform().container(Platform.Component.IMPLEMENTATION).metadata().id()); } - @Override - protected void printError(final String message, @Nullable final Throwable throwable) { - logger.error(message, throwable); - } - - @Override - protected void printInfo(final String message) { - logger.info(message); - } - - @Override - protected void printWarning(final String message) { - logger.warn(message); - } - static class Factory extends SimpleMetrics.Factory { protected final Logger logger; protected final Path dataDirectory; diff --git a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java index 70cc8d3..d082fe8 100644 --- a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java +++ b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java @@ -5,17 +5,14 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; import dev.faststats.core.Metrics; -import dev.faststats.core.Settings; import dev.faststats.core.SimpleMetrics; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import java.nio.file.Path; final class VelocityMetricsImpl extends SimpleMetrics implements VelocityMetrics { - private final Logger logger; private final ProxyServer server; private final PluginContainer plugin; @@ -30,7 +27,6 @@ private VelocityMetricsImpl( ) throws IllegalStateException { super(factory, config); - this.logger = logger; this.server = server; this.plugin = plugin; @@ -47,21 +43,6 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", server.getVersion().getName()); } - @Override - protected void printError(final String message, @Nullable final Throwable throwable) { - logger.error(message, throwable); - } - - @Override - protected void printInfo(final String message) { - logger.info(message); - } - - @Override - protected void printWarning(final String message) { - logger.warn(message); - } - static class Factory extends SimpleMetrics.Factory { protected final Logger logger; protected final Path dataDirectory; From e5be6b3a29a1e37c92cf1730798bedbd017d8f20 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 15:43:19 +0200 Subject: [PATCH 09/40] Removed settings and ability to define metrics URL and debug --- .../main/java/com/example/ExamplePlugin.java | 3 +- .../main/java/com/example/ExamplePlugin.java | 3 +- .../faststats/example/FeatureFlagExample.java | 15 +- .../faststats/example/SettingsExample.java | 17 --- .../main/java/dev/faststats/core/Metrics.java | 24 ++-- .../java/dev/faststats/core/Settings.java | 133 ------------------ .../dev/faststats/core/SimpleMetrics.java | 59 +++++--- .../dev/faststats/core/SimpleSettings.java | 49 ------- .../core/flags/FeatureFlagService.java | 98 +++++-------- .../core/flags/SimpleFeatureFlagService.java | 63 +++------ .../test/java/dev/faststats/MockMetrics.java | 8 +- .../src/main/java/com/example/ExampleMod.java | 3 +- .../dev/faststats/fabric/FabricMetrics.java | 5 +- .../main/java/com/example/ExamplePlugin.java | 3 +- .../main/java/com/example/ExamplePlugin.java | 3 +- .../main/java/com/example/ExamplePlugin.java | 3 +- .../velocity/VelocityMetricsImpl.java | 4 +- 17 files changed, 129 insertions(+), 364 deletions(-) delete mode 100644 core/example/src/main/java/dev/faststats/example/SettingsExample.java delete mode 100644 core/src/main/java/dev/faststats/core/Settings.java delete mode 100644 core/src/main/java/dev/faststats/core/SimpleSettings.java diff --git a/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java b/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java index 296adec..90884f3 100644 --- a/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -2,7 +2,6 @@ import dev.faststats.bukkit.BukkitMetrics; import dev.faststats.core.ErrorTracker; -import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import org.bukkit.plugin.java.JavaPlugin; @@ -23,7 +22,7 @@ public final class ExamplePlugin extends JavaPlugin { // This is useful for cleaning up cached data .onFlush(() -> gameCount.set(0)) // reset game count on flush - .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project + .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create(this); @Override diff --git a/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java b/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java index 0c9a1f8..223a64d 100644 --- a/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -3,7 +3,6 @@ import dev.faststats.bungee.BungeeMetrics; import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; -import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import net.md_5.bungee.api.plugin.Plugin; @@ -24,7 +23,7 @@ public class ExamplePlugin extends Plugin { // This is useful for cleaning up cached data .onFlush(() -> gameCount.set(0)) // reset game count on flush - .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project + .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create(this); @Override diff --git a/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java b/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java index fb0c49d..bae91d8 100644 --- a/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java +++ b/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java @@ -1,6 +1,5 @@ package dev.faststats.example; -import dev.faststats.core.Settings; import dev.faststats.core.flags.Attributes; import dev.faststats.core.flags.FeatureFlag; import dev.faststats.core.flags.FeatureFlagService; @@ -9,16 +8,14 @@ import java.time.Instant; public final class FeatureFlagExample { - private static final Settings SETTINGS = Settings.withToken("YOUR_TOKEN_HERE"); - - public static final FeatureFlagService SERVICE = FeatureFlagService.factory() - .settings(SETTINGS) - .ttl(Duration.ofMinutes(10)) - .attributes(Attributes.create() + public static final FeatureFlagService SERVICE = FeatureFlagService.create( + "YOUR_TOKEN_HERE", // token can be found in the settings of your project + Attributes.create() .put("version", "1.2.3") .put("java_version", System.getProperty("java.version")) - .put("java_vendor", System.getProperty("java.vendor"))) - .create(); + .put("java_vendor", System.getProperty("java.vendor")), + Duration.ofMinutes(10) + ); // Define flags with default values public static final FeatureFlag NEW_COMMANDS = SERVICE.define("new_commands", false); diff --git a/core/example/src/main/java/dev/faststats/example/SettingsExample.java b/core/example/src/main/java/dev/faststats/example/SettingsExample.java deleted file mode 100644 index 6dd9bd2..0000000 --- a/core/example/src/main/java/dev/faststats/example/SettingsExample.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.faststats.example; - -import dev.faststats.core.Settings; - -import java.net.URI; - -public final class SettingsExample { - // Recommended: create settings with just a token - public static final Settings SETTINGS = Settings.withToken("YOUR_TOKEN_HERE"); - - // Or use the factory for full control - public static final Settings ALL_SETTINGS = Settings.factory() - .url(URI.create("https://metrics.example.com/v1/collect")) // only for different metrics servers (mainly for testing) - .debug(true) // Enable debug mode for development and testing - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project - .create(); -} diff --git a/core/src/main/java/dev/faststats/core/Metrics.java b/core/src/main/java/dev/faststats/core/Metrics.java index 4dc2f25..04beb85 100644 --- a/core/src/main/java/dev/faststats/core/Metrics.java +++ b/core/src/main/java/dev/faststats/core/Metrics.java @@ -15,13 +15,14 @@ */ public interface Metrics { /** - * Get the SDK-wide settings for this metrics instance. + * Get the token used to authenticate with the metrics server and identify the project. * - * @return the settings - * @since 0.23.0 + * @return the metrics token + * @since 0.1.0 */ + @Token @Contract(pure = true) - Settings getSettings(); + String getToken(); /** * Get the error tracker for this metrics instance. @@ -127,14 +128,17 @@ interface Factory> { F featureFlagService(FeatureFlagService service); /** - * Sets the SDK-wide settings for this metrics instance. + * Sets the token used to authenticate with the metrics server and identify the project. + *

+ * This token can be found in the settings of your project under "Your API Token". * - * @param settings the settings + * @param token the metrics token * @return the metrics factory - * @since 0.23.0 + * @throws IllegalArgumentException if the token does not match the {@link Token#PATTERN} + * @since 0.1.0 */ @Contract(mutates = "this") - F settings(Settings settings); + F token(@Token String token) throws IllegalArgumentException; /** * Creates a new metrics instance. @@ -143,8 +147,8 @@ interface Factory> { * * @param object a required object as defined by the implementation * @return the metrics instance - * @throws IllegalStateException if the settings are not specified - * @see #settings(Settings) + * @throws IllegalStateException if the token is not specified + * @see #token(String) * @since 0.1.0 */ @Async.Schedule diff --git a/core/src/main/java/dev/faststats/core/Settings.java b/core/src/main/java/dev/faststats/core/Settings.java deleted file mode 100644 index 7e0b02c..0000000 --- a/core/src/main/java/dev/faststats/core/Settings.java +++ /dev/null @@ -1,133 +0,0 @@ -package dev.faststats.core; - -import org.jetbrains.annotations.Contract; - -import java.net.URI; - -/** - * SDK-wide settings shared across all FastStats services. - * - * @since 0.23.0 - */ -public sealed interface Settings permits SimpleSettings { - /** - * Creates a new {@link Settings} instance with the given token. - *

- * This token can be found in the settings of your project under "Your API Token". - * It is used to authenticate with the server and identify the project. - * - * @param token the token - * @return the new settings - * @since 0.23.0 - */ - @Contract(value = "_ -> new", pure = true) - static Settings withToken(@Token final String token) { - return factory().token(token).create(); - } - - /** - * Create a new factory for building {@link Settings}. - * - * @return a new factory - * @since 0.23.0 - */ - @Contract(value = " -> new", pure = true) - static Factory factory() { - return new SimpleSettings.Factory(); - } - - /** - * The token used to authenticate with the server and identify the project. - * - * @return the token - * @since 0.23.0 - */ - @Token - @Contract(pure = true) - String token(); - - /** - * The metrics server URL. - * - * @return the metrics server URL - * @since 0.23.0 - */ - @Contract(pure = true) - URI metricsUrl(); - - /** - * The flags server URL. - * - * @return the flags server URL - * @since 0.23.0 - */ - @Contract(pure = true) - URI flagsUrl(); - - /** - * Whether debug logging is enabled. - * - * @return {@code true} if debug logging is enabled, {@code false} otherwise - * @since 0.23.0 - */ - @Contract(pure = true) - boolean debug(); - - /** - * A factory for creating {@link Settings} instances. - * - * @since 0.23.0 - */ - sealed interface Factory permits SimpleSettings.Factory { - /** - * Sets the token used to authenticate with the server and identify the project. - *

- * This token can be found in the settings of your project under "Your API Token". - * - * @param token the token - * @return the factory - * @throws IllegalArgumentException if the token does not match the {@link Token#PATTERN} - * @since 0.23.0 - */ - @Contract(mutates = "this") - Factory token(@Token String token) throws IllegalArgumentException; - - /** - * Sets the server URL. - *

- * This is only required for self-hosted servers. - * - * @param url the server URL - * @return the factory - * @since 0.23.0 - */ - @Contract(mutates = "this") - Factory metricsServer(URI url); // todo: rethink naming - - // todo: add docs - @Contract(mutates = "this") - Factory flagsServer(URI url); // todo: rethink naming - - /** - * Enables or disables debug logging. - *

- * This is only meant for development and testing and should not be enabled in production. - * - * @param enabled whether debug logging is enabled - * @return the factory - * @since 0.23.0 - */ - @Contract(mutates = "this") - Factory debug(boolean enabled); - - /** - * Creates a new {@link Settings} instance. - * - * @return the settings - * @throws IllegalStateException if the token is not specified - * @since 0.23.0 - */ - @Contract(value = " -> new", pure = true) - Settings create() throws IllegalStateException; - } -} diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 09e4b98..c5e146d 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -14,6 +14,8 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.net.ConnectException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpRequest; @@ -41,19 +43,24 @@ public abstract class SimpleMetrics implements Metrics { protected final Logger logger = Logger.getLogger(getClass().getName()); private static final URI defaultUrl = URI.create("https://metrics.faststats.dev/v1/collect"); + + private static final String SDK_NAME; + private static final String SDK_VERSION; + private static final String BUILD_ID; + private final HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .version(HttpClient.Version.HTTP_1_1) .build(); private @Nullable ScheduledExecutorService executor = null; + private final URI url; private final Set> metrics; private final Config config; - private final Settings settings; + private final @Token String token; private final @Nullable ErrorTracker tracker; private final @Nullable Runnable flush; private final @Nullable FeatureFlagService flagService; - private final boolean debug; private final String SDK_NAME; private final String SDK_VERSION; @@ -71,16 +78,29 @@ public abstract class SimpleMetrics implements Metrics { } @Contract(mutates = "io") + @SuppressWarnings("PatternValidation") protected SimpleMetrics(final Factory factory, final Config config) throws IllegalStateException { - if (factory.settings == null) throw new IllegalStateException("Settings must be specified"); + if (factory.token == null) throw new IllegalStateException("Token must be specified"); this.config = config; - this.settings = factory.settings; + this.token = factory.token; this.metrics = config.additionalMetrics ? Set.copyOf(factory.metrics) : Set.of(); - this.debug = settings.debug() || Boolean.getBoolean("faststats.debug") || config.debug(); + final var debug = config.debug() || Boolean.getBoolean("faststats.debug"); + this.logger.setFilter(record -> debug || record.getLevel().equals(Level.CONFIG)); this.tracker = config.errorTracking ? factory.tracker : null; this.flush = factory.flush; this.flagService = factory.flagService; + this.url = getMetricsServerUrl(); + } + + private URI getMetricsServerUrl() { + final var property = System.getProperty("faststats.metrics-server"); + try { + return property != null ? new URI(property) : defaultUrl; + } catch (final URISyntaxException e) { + error("Failed to parse metrics server url: %s", e, property); + return defaultUrl; + } } @Contract(mutates = "io") @@ -92,16 +112,19 @@ protected SimpleMetrics(final Factory factory, final Path config) throws I protected SimpleMetrics( final Config config, final Set> metrics, - final Settings settings, + @Token final String token, @Nullable final ErrorTracker tracker, - @Nullable final Runnable flush + @Nullable final Runnable flush, + final URI url, + final boolean debug ) { this.metrics = config.additionalMetrics ? Set.copyOf(metrics) : Set.of(); this.config = config; - this.debug = settings.debug(); - this.settings = settings; + this.logger.setLevel(debug ? Level.ALL : Level.OFF); + this.token = token; this.tracker = tracker; this.flush = flush; + this.url = url; this.flagService = null; } @@ -201,18 +224,17 @@ private boolean submitNow() throws IOException { final var compressed = byteOutput.toByteArray(); info("Compressed size: %s bytes", compressed.length); - final var url = settings.metricsUrl().resolve("/collect"); final var request = HttpRequest.newBuilder() .POST(HttpRequest.BodyPublishers.ofByteArray(compressed)) .header("Content-Encoding", "gzip") .header("Content-Type", "application/octet-stream") - .header("Authorization", "Bearer " + settings.token()) + .header("Authorization", "Bearer " + token) .header("User-Agent", "FastStats Metrics " + SDK_NAME + "/" + SDK_VERSION) .timeout(Duration.ofSeconds(3)) .uri(url) .build(); - info("Sending metrics to: " + url); + info("Sending metrics to: %s", url); try { final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(UTF_8)); final var statusCode = response.statusCode(); @@ -288,8 +310,8 @@ protected JsonObject createData() { } @Override - public Settings getSettings() { - return settings; + public @Token String getToken() { + return token; } @Override @@ -353,7 +375,7 @@ public abstract static class Factory> impleme private @Nullable ErrorTracker tracker; private @Nullable FeatureFlagService flagService; private @Nullable Runnable flush; - private @Nullable Settings settings; + private @Nullable String token; @Override @SuppressWarnings("unchecked") @@ -385,8 +407,11 @@ public F featureFlagService(final FeatureFlagService service) { @Override @SuppressWarnings("unchecked") - public F settings(final Settings settings) { - this.settings = settings; + public F token(@Token final String token) throws IllegalArgumentException { + if (!token.matches(Token.PATTERN)) { + throw new IllegalArgumentException("Invalid token '" + token + "', must match '" + Token.PATTERN + "'"); + } + this.token = token; return (F) this; } } diff --git a/core/src/main/java/dev/faststats/core/SimpleSettings.java b/core/src/main/java/dev/faststats/core/SimpleSettings.java deleted file mode 100644 index 5f01d2e..0000000 --- a/core/src/main/java/dev/faststats/core/SimpleSettings.java +++ /dev/null @@ -1,49 +0,0 @@ -package dev.faststats.core; - -import org.jspecify.annotations.Nullable; - -import java.net.URI; - -record SimpleSettings(@Token String token, URI metricsUrl, URI flagsUrl, boolean debug) implements Settings { - - static final class Factory implements Settings.Factory { - private URI metricsUrl = URI.create("https://metrics.faststats.dev/v1"); - private URI flagsUrl = URI.create("https://flags.faststats.dev/v1"); - private @Nullable String token; - private boolean debug = false; - - @Override - public Settings.Factory token(@Token final String token) throws IllegalArgumentException { - if (!token.matches(Token.PATTERN)) { - throw new IllegalArgumentException("Invalid token '" + token + "', must match '" + Token.PATTERN + "'"); - } - this.token = token; - return this; - } - - @Override - public Settings.Factory metricsServer(final URI url) { - this.metricsUrl = url; - return this; - } - - @Override - public Settings.Factory flagsServer(final URI url) { - this.flagsUrl = url; - return this; - } - - @Override - public Settings.Factory debug(final boolean enabled) { - this.debug = enabled; - return this; - } - - @Override - @SuppressWarnings("PatternValidation") - public Settings create() throws IllegalStateException { - if (token == null) throw new IllegalStateException("Token must be specified"); - return new SimpleSettings(token, metricsUrl, flagsUrl, debug); - } - } -} diff --git a/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java index cebbed4..63e5417 100644 --- a/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java @@ -1,41 +1,61 @@ package dev.faststats.core.flags; -import dev.faststats.core.Metrics; -import dev.faststats.core.Settings; +import dev.faststats.core.Token; import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.Nullable; import java.time.Duration; /** * A service for managing feature flags. *

- * Create an instance using the {@link Factory} and pass it to the metrics factory - * via {@link Metrics.Factory#featureFlagService(FeatureFlagService)}. + * Use one of the static {@code create} methods to construct a service instance. * * @since 0.23.0 */ public sealed interface FeatureFlagService permits SimpleFeatureFlagService { /** - * Create a new {@link FeatureFlagService} with the given settings and default options. + * Creates a feature flag service for the given environment token + * and a default cache TTL of five minutes. * - * @param settings the SDK-wide settings + * @param token the environment token * @return a new feature flag service + * @see #create(String, Attributes) * @since 0.23.0 */ @Contract(value = "_ -> new", pure = true) - static FeatureFlagService create(final Settings settings) { - return factory().settings(settings).create(); + static FeatureFlagService create(@Token final String token) { + return create(token, null); } /** - * Create a new factory for building a {@link FeatureFlagService}. + * Creates a feature flag service for the given environment token + * and global targeting attributes with a default cache TTL of five minutes. * - * @return a new factory + * @param token the environment token + * @param attributes the global targeting attributes + * @return a new feature flag service + * @see #create(String, Attributes, Duration) + * @since 0.23.0 + */ + @Contract(value = "_, _ -> new", pure = true) + static FeatureFlagService create(@Token final String token, @Nullable final Attributes attributes) { + return create(token, attributes, Duration.ofMinutes(5)); + } + + /** + * Creates a feature flag service for the given environment token, + * global targeting attributes, and cache TTL. + * + * @param token the environment token + * @param attributes the global targeting attributes + * @param ttl the cache time-to-live for resolved flag values + * @return a new feature flag service * @since 0.23.0 */ - @Contract(value = " -> new", pure = true) - static Factory factory() { - return new SimpleFeatureFlagService.Factory(); + @Contract(value = "_, _, _ -> new", pure = true) + static FeatureFlagService create(@Token final String token, @Nullable final Attributes attributes, final Duration ttl) { + return new SimpleFeatureFlagService(token, attributes, ttl); } /** @@ -114,56 +134,4 @@ static Factory factory() { */ @Contract(mutates = "this") void shutdown(); - - /** - * A factory for creating {@link FeatureFlagService} instances. - * - * @since 0.23.0 - */ - interface Factory { - /** - * Sets the cache time-to-live for flag values. - *

- * This TTL determines the staleness window reported by - * {@link FeatureFlag#getExpiration()}. Expired cached values remain - * readable until they are explicitly refreshed or invalidated. - * - * @param ttl the cache time-to-live - * @return the factory - * @since 0.23.0 - */ - @Contract(mutates = "this") - Factory ttl(Duration ttl); - - /** - * Sets the global targeting attributes for all flags created by this service. - * - * @param attributes the targeting attributes - * @return the factory - * @since 0.23.0 - */ - @Contract(mutates = "this") - Factory attributes(Attributes attributes); - - /** - * Sets the SDK-wide settings for this feature flag service. - * - * @param settings the settings - * @return the factory - * @since 0.23.0 - */ - @Contract(mutates = "this") - Factory settings(Settings settings); - - /** - * Creates a new {@link FeatureFlagService} instance. - * - * @return the feature flag service - * @throws IllegalStateException if the settings are not specified - * @see #settings(Settings) - * @since 0.23.0 - */ - @Contract(value = " -> new", pure = true) - FeatureFlagService create() throws IllegalStateException; - } } diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index c944996..68c3a61 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -6,9 +6,11 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; -import dev.faststats.core.Settings; +import dev.faststats.core.Token; import org.jspecify.annotations.Nullable; +import java.net.URI; +import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -22,28 +24,41 @@ final class SimpleFeatureFlagService implements FeatureFlagService { private static final Gson GSON = new Gson(); + private static final URI defaultUrl = URI.create("https://flags.faststats.dev/v1"); private final HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .version(HttpClient.Version.HTTP_1_1) .build(); - private final Settings settings; + private final @Token String token; private final @Nullable Attributes attributes; private final Duration ttl; + private final URI url; private final Map cache = new ConcurrentHashMap<>(); private final Map fetchTimes = new ConcurrentHashMap<>(); private final Map> fetchesInProgress = new ConcurrentHashMap<>(); SimpleFeatureFlagService( - final Settings settings, + final @Token String token, final @Nullable Attributes attributes, final Duration ttl ) { - this.settings = settings; + this.token = token; this.attributes = attributes; this.ttl = ttl; + this.url = getFlagsServerUrl(); + } + + private URI getFlagsServerUrl() { + final var property = System.getProperty("faststats.flags-server"); + try { + return property != null ? new URI(property) : defaultUrl; + } catch (final URISyntaxException e) { + //error("Failed to parse flags server url: %s", e, property); // todo: recover + return defaultUrl; + } } @SuppressWarnings("unchecked") @@ -73,16 +88,15 @@ CompletableFuture optOut(final SimpleFeatureFlag flag) { private CompletableFuture sendOptRequest(final SimpleFeatureFlag flag, final String path) { final var requestBody = new JsonObject(); - requestBody.addProperty("projectToken", settings.token()); requestBody.addProperty("serverId", UUID.randomUUID().toString()); // todo: read from config requestBody.addProperty("flag", flag.getId()); final var request = HttpRequest.newBuilder() .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(requestBody))) .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + settings.token()) + .header("Authorization", "Bearer " + token) .timeout(Duration.ofSeconds(3)) - .uri(settings.flagsUrl().resolve(path)) + .uri(url.resolve(path)) .build(); return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenCompose(response -> { @@ -113,7 +127,6 @@ boolean isExpired(final SimpleFeatureFlag flag) { private CompletableFuture createFetch(final SimpleFeatureFlag flag) { final var requestBody = new JsonObject(); - requestBody.addProperty("projectToken", settings.token()); requestBody.addProperty("serverId", UUID.randomUUID().toString()); // todo: read from config requestBody.addProperty("key", flag.getId()); @@ -125,9 +138,9 @@ private CompletableFuture createFetch(final SimpleFeatureFlag flag) { final var request = HttpRequest.newBuilder() .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(requestBody))) .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + settings.token()) + .header("Authorization", "Bearer " + token) .timeout(Duration.ofSeconds(3)) - .uri(settings.flagsUrl().resolve("/check")) + .uri(url.resolve("/check")) .build(); return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(response -> { @@ -197,34 +210,4 @@ public void shutdown() { fetchTimes.clear(); fetchesInProgress.clear(); } - - static final class Factory implements FeatureFlagService.Factory { - private Duration ttl = Duration.ofMinutes(5); - private @Nullable Settings settings; - private @Nullable Attributes attributes; - - @Override - public FeatureFlagService.Factory ttl(final Duration ttl) { - this.ttl = ttl; - return this; - } - - @Override - public FeatureFlagService.Factory attributes(final Attributes attributes) { - this.attributes = attributes; - return this; - } - - @Override - public FeatureFlagService.Factory settings(final Settings settings) { - this.settings = settings; - return this; - } - - @Override - public FeatureFlagService create() throws IllegalStateException { - if (settings == null) throw new IllegalStateException("Settings must be specified"); - return new SimpleFeatureFlagService(settings, attributes, ttl); - } - } } diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index 44d5977..fec3062 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -2,7 +2,6 @@ import com.google.gson.JsonObject; import dev.faststats.core.ErrorTracker; -import dev.faststats.core.Settings; import dev.faststats.core.SimpleMetrics; import dev.faststats.core.Token; import org.jspecify.annotations.NullMarked; @@ -15,12 +14,7 @@ @NullMarked public final class MockMetrics extends SimpleMetrics { public MockMetrics(final UUID serverId, @Token final String token, @Nullable final ErrorTracker tracker, final boolean debug) { - super(new Config(serverId, true, debug, true, true, false, false), Set.of(), Settings.factory() - .metricsServer(URI.create("http://localhost:5000/v1")) - .flagsServer(URI.create("http://localhost:5001/v1")) - .token(token) - .debug(debug) - .create(), tracker, null); + super(new Config(serverId, true, debug, true, true, false, false), Set.of(), token, tracker, null, URI.create("http://localhost:5000/v1/collect"), debug); } @Override diff --git a/fabric/example-mod/src/main/java/com/example/ExampleMod.java b/fabric/example-mod/src/main/java/com/example/ExampleMod.java index 78b4431..779163a 100644 --- a/fabric/example-mod/src/main/java/com/example/ExampleMod.java +++ b/fabric/example-mod/src/main/java/com/example/ExampleMod.java @@ -2,7 +2,6 @@ import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; -import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import dev.faststats.fabric.FabricMetrics; import net.fabricmc.api.ModInitializer; @@ -15,7 +14,7 @@ public class ExampleMod implements ModInitializer { // Error tracking must be enabled in the project settings .errorTracker(ErrorTracker.contextAware()) - .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project + .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create("example-mod"); // your mod id as defined in fabric.mod.json @Override diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java b/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java index 2df30b7..f6ce2df 100644 --- a/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java +++ b/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java @@ -1,7 +1,6 @@ package dev.faststats.fabric; import dev.faststats.core.Metrics; -import dev.faststats.core.Settings; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; @@ -30,9 +29,9 @@ interface Factory extends Metrics.Factory { * * @param modId the mod id * @return the metrics instance - * @throws IllegalStateException if the settings are not specified + * @throws IllegalStateException if the token is not specified * @throws IllegalArgumentException if the mod is not found - * @see #settings(Settings) + * @see #token(String) * @since 0.12.0 */ @Override diff --git a/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java b/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java index 88921ae..98a7958 100644 --- a/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -4,7 +4,6 @@ import com.hypixel.hytale.server.core.plugin.JavaPluginInit; import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; -import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import dev.faststats.hytale.HytaleMetrics; @@ -16,7 +15,7 @@ public class ExamplePlugin extends JavaPlugin { // Error tracking must be enabled in the project settings .errorTracker(ErrorTracker.contextAware()) - .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project + .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create(this); public ExamplePlugin(final JavaPluginInit init) { diff --git a/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java b/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java index 920bb3e..bb6caab 100644 --- a/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -3,7 +3,6 @@ import com.google.inject.Inject; import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; -import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import dev.faststats.sponge.SpongeMetrics; import org.jspecify.annotations.Nullable; @@ -30,7 +29,7 @@ public void onServerStart(final StartedEngineEvent event) { // Error tracking must be enabled in the project settings .errorTracker(ErrorTracker.contextAware()) - .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project + .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create(pluginContainer); } diff --git a/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java b/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java index 69afdc4..29d136d 100644 --- a/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -7,7 +7,6 @@ import com.velocitypowered.api.plugin.Plugin; import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; -import dev.faststats.core.Settings; import dev.faststats.core.data.Metric; import dev.faststats.velocity.VelocityMetrics; import org.jspecify.annotations.Nullable; @@ -32,7 +31,7 @@ public void onProxyInitialize(final ProxyInitializeEvent event) { // Error tracking must be enabled in the project settings .errorTracker(ErrorTracker.contextAware()) - .settings(Settings.withToken("YOUR_TOKEN_HERE")) // token can be found in the settings of your project + .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create(this); } diff --git a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java index d082fe8..dbfb60b 100644 --- a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java +++ b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java @@ -61,9 +61,9 @@ public Factory(final ProxyServer server, final Logger logger, @DataDirectory fin * * @param plugin the plugin instance * @return the metrics instance - * @throws IllegalStateException if the settings are not specified + * @throws IllegalStateException if the token is not specified * @throws IllegalArgumentException if the given object is not a valid plugin - * @see #settings(Settings) + * @see #token(String) * @since 0.1.0 */ @Override From f4f548e278e0f3d8c9626bb5d49bfdeb86d0514c Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 15:43:40 +0200 Subject: [PATCH 10/40] Document FeatureFlags --- .../dev/faststats/core/flags/FeatureFlag.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java b/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java index 23cf853..77532a1 100644 --- a/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java +++ b/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java @@ -36,6 +36,9 @@ public sealed interface FeatureFlag permits SimpleFeatureFlag { /** * Returns the class representing the value type of this flag. + *

+ * This always returns exactly one of {@link String}.class, + * {@link Number}.class, or {@link Boolean}.class, matching {@link #getType()}. * * @return the value type class * @since 0.23.0 @@ -148,8 +151,31 @@ public sealed interface FeatureFlag permits SimpleFeatureFlag { @Contract(pure = true) T getDefaultValue(); - // todo: add docs + /** + * Supported value types for feature flags. + * + * @since 0.23.0 + */ enum Type { - STRING, BOOLEAN, NUMBER + /** + * A string-valued flag. + * + * @since 0.23.0 + */ + STRING, + + /** + * A boolean-valued flag. + * + * @since 0.23.0 + */ + BOOLEAN, + + /** + * A numeric flag. + * + * @since 0.23.0 + */ + NUMBER } } From e983d253a110eb40e5ed62fb3f72276dc9bf7fe1 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 15:43:48 +0200 Subject: [PATCH 11/40] Document Attributes#forEachPrimitive --- .../src/main/java/dev/faststats/core/flags/Attributes.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/dev/faststats/core/flags/Attributes.java b/core/src/main/java/dev/faststats/core/flags/Attributes.java index ccd79b1..ad08cb8 100644 --- a/core/src/main/java/dev/faststats/core/flags/Attributes.java +++ b/core/src/main/java/dev/faststats/core/flags/Attributes.java @@ -84,7 +84,12 @@ static Attributes copyOf(final Attributes attributes) { @Contract(value = "_ -> this", mutates = "this") Attributes remove(String key); - // todo: add docs + /** + * Visit each stored attribute as its underlying JSON primitive value. + * + * @param action the action to invoke for each key-value pair + * @since 0.23.0 + */ void forEachPrimitive(BiConsumer action); /** From ba59d499567bc30d61af397cc104c810777bd7e9 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 15:44:12 +0200 Subject: [PATCH 12/40] Use correct url --- core/src/main/java/dev/faststats/core/SimpleMetrics.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index c5e146d..8cb8d69 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -255,9 +255,9 @@ private boolean submitNow() throws IOException { warn("Received unexpected response from metrics server: %s (%s)", statusCode, body); } } catch (final HttpConnectTimeoutException t) { - error("Metrics submission timed out after 3 seconds: %s", null, defaultUrl); + error("Metrics submission timed out after 3 seconds: %s", null, url); } catch (final ConnectException t) { - error("Failed to connect to metrics server: %s", null, defaultUrl); + error("Failed to connect to metrics server: %s", null, url); } catch (final Throwable t) { error("Failed to submit metrics", t); } From 24786f9167a124705bf55e5bd72e23d14504e749 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 15:44:32 +0200 Subject: [PATCH 13/40] Make SDK properties static --- .../java/dev/faststats/core/SimpleMetrics.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 8cb8d69..67e7987 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -62,19 +62,15 @@ public abstract class SimpleMetrics implements Metrics { private final @Nullable Runnable flush; private final @Nullable FeatureFlagService flagService; - private final String SDK_NAME; - private final String SDK_VERSION; - private final String BUILD_ID; - - { + static { final var properties = new Properties(); - try (final var stream = getClass().getResourceAsStream("/META-INF/faststats.properties")) { + try (final var stream = SimpleMetrics.class.getClassLoader().getResourceAsStream("/META-INF/faststats.properties")) { if (stream != null) properties.load(stream); } catch (final IOException ignored) { } - this.SDK_NAME = properties.getProperty("name", "unknown"); - this.SDK_VERSION = properties.getProperty("version", "unknown"); - this.BUILD_ID = properties.getProperty("build-id", "unknown"); + SDK_NAME = properties.getProperty("name", "unknown"); + SDK_VERSION = properties.getProperty("version", "unknown"); + BUILD_ID = properties.getProperty("build-id", "unknown"); } @Contract(mutates = "io") From efc352a6eac336fc99bca2293c625d721845c9e4 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 16:02:33 +0200 Subject: [PATCH 14/40] Add minimal logger api --- .../faststats/bukkit/BukkitMetricsImpl.java | 2 +- .../dev/faststats/core/SimpleMetrics.java | 110 ++++++------------ .../dev/faststats/core/internal/Logger.java | 27 +++++ .../core/internal/LoggerFactory.java | 16 +++ .../faststats/core/internal/SimpleLogger.java | 45 +++++++ .../core/internal/SimpleLoggerFactory.java | 8 ++ .../faststats/core/internal/package-info.java | 4 + core/src/main/java/module-info.java | 5 +- 8 files changed, 142 insertions(+), 75 deletions(-) create mode 100644 core/src/main/java/dev/faststats/core/internal/Logger.java create mode 100644 core/src/main/java/dev/faststats/core/internal/LoggerFactory.java create mode 100644 core/src/main/java/dev/faststats/core/internal/SimpleLogger.java create mode 100644 core/src/main/java/dev/faststats/core/internal/SimpleLoggerFactory.java create mode 100644 core/src/main/java/dev/faststats/core/internal/package-info.java diff --git a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java index 4afb46f..4222d8f 100644 --- a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java +++ b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java @@ -74,7 +74,7 @@ private int getPlayerCount() { try { return plugin.getServer().getOnlinePlayers().size(); } catch (final Throwable t) { - error("Failed to get player count", t); + logger.error("Failed to get player count", t); return 0; } } diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 67e7987..fd4d956 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -3,7 +3,9 @@ import com.google.gson.JsonObject; import dev.faststats.core.data.Metric; import dev.faststats.core.flags.FeatureFlagService; -import org.intellij.lang.annotations.PrintFormat; +import dev.faststats.core.internal.Constants; +import dev.faststats.core.internal.Logger; +import dev.faststats.core.internal.LoggerFactory; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.MustBeInvokedByOverriders; @@ -34,20 +36,14 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiPredicate; import java.util.logging.Level; -import java.util.logging.LogRecord; -import java.util.logging.Logger; import java.util.zip.GZIPOutputStream; import static java.nio.charset.StandardCharsets.UTF_8; public abstract class SimpleMetrics implements Metrics { - protected final Logger logger = Logger.getLogger(getClass().getName()); + protected final Logger logger = LoggerFactory.factory().getLogger(getClass().getName()); private static final URI defaultUrl = URI.create("https://metrics.faststats.dev/v1/collect"); - private static final String SDK_NAME; - private static final String SDK_VERSION; - private static final String BUILD_ID; - private final HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .version(HttpClient.Version.HTTP_1_1) @@ -62,17 +58,6 @@ public abstract class SimpleMetrics implements Metrics { private final @Nullable Runnable flush; private final @Nullable FeatureFlagService flagService; - static { - final var properties = new Properties(); - try (final var stream = SimpleMetrics.class.getClassLoader().getResourceAsStream("/META-INF/faststats.properties")) { - if (stream != null) properties.load(stream); - } catch (final IOException ignored) { - } - SDK_NAME = properties.getProperty("name", "unknown"); - SDK_VERSION = properties.getProperty("version", "unknown"); - BUILD_ID = properties.getProperty("build-id", "unknown"); - } - @Contract(mutates = "io") @SuppressWarnings("PatternValidation") protected SimpleMetrics(final Factory factory, final Config config) throws IllegalStateException { @@ -94,7 +79,7 @@ private URI getMetricsServerUrl() { try { return property != null ? new URI(property) : defaultUrl; } catch (final URISyntaxException e) { - error("Failed to parse metrics server url: %s", e, property); + logger.error("Failed to parse metrics server url: %s", e, property); return defaultUrl; } } @@ -152,7 +137,7 @@ protected void startSubmitting() { @SuppressWarnings("PatternValidation") private void startSubmitting(final long initialDelay, final long period, final TimeUnit unit) { if (Boolean.getBoolean("faststats.first-run")) { - info("Skipping metrics submission due to first-run flag"); + logger.info("Skipping metrics submission due to first-run flag"); return; } @@ -162,9 +147,9 @@ private void startSubmitting(final long initialDelay, final long period, final T final var split = getOnboardingMessage().split("\n"); for (final var s : split) if (s.length() > separatorLength) separatorLength = s.length(); - info("-".repeat(separatorLength)); - for (final var s : split) info(s); - info("-".repeat(separatorLength)); + logger.info("-".repeat(separatorLength)); + for (final var s : split) logger.info(s); + logger.info("-".repeat(separatorLength)); System.setProperty("faststats.first-run", "true"); if (!config.externallyManaged()) return; @@ -173,22 +158,22 @@ private void startSubmitting(final long initialDelay, final long period, final T final var enabled = Boolean.parseBoolean(System.getProperty("faststats.enabled", "true")); if (!config.enabled() || !enabled) { - warn("Metrics disabled, not starting submission"); + logger.warn("Metrics disabled, not starting submission"); return; } if (isSubmitting()) { - warn("Metrics already submitting, not starting again"); + logger.warn("Metrics already submitting, not starting again"); return; } - this.executor = Executors.newSingleThreadScheduledExecutor(runnable -> { + this.executor = Executors.newSingleThreadScheduledExecutor(runnable -> { // todo: SINGLE THREAD??? what was i smoking? final var thread = new Thread(runnable, "metrics-submitter"); thread.setDaemon(true); return thread; }); - info("Starting metrics submission"); + logger.info("Starting metrics submission"); executor.scheduleAtFixedRate(this::submit, Math.max(0, initialDelay), Math.max(1000, period), unit); } @@ -200,7 +185,7 @@ public boolean submit() { try { return submitNow(); } catch (final Throwable t) { - error("Failed to submit metrics", t); + logger.error("Failed to submit metrics", t); return false; } } @@ -209,7 +194,7 @@ private boolean submitNow() throws IOException { final var data = createData().toString(); final var bytes = data.getBytes(UTF_8); - info("Uncompressed data: %s", data); + logger.info("Uncompressed data: %s", data); try (final var byteOutput = new ByteArrayOutputStream(); final var output = new GZIPOutputStream(byteOutput)) { @@ -218,55 +203,55 @@ private boolean submitNow() throws IOException { output.finish(); final var compressed = byteOutput.toByteArray(); - info("Compressed size: %s bytes", compressed.length); + logger.info("Compressed size: %s bytes", compressed.length); final var request = HttpRequest.newBuilder() .POST(HttpRequest.BodyPublishers.ofByteArray(compressed)) .header("Content-Encoding", "gzip") .header("Content-Type", "application/octet-stream") .header("Authorization", "Bearer " + token) - .header("User-Agent", "FastStats Metrics " + SDK_NAME + "/" + SDK_VERSION) + .header("User-Agent", "FastStats Metrics " + Constants.SDK_NAME + "/" + Constants.SDK_VERSION) .timeout(Duration.ofSeconds(3)) .uri(url) .build(); - info("Sending metrics to: %s", url); + logger.info("Sending metrics to: %s", url); try { final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(UTF_8)); final var statusCode = response.statusCode(); final var body = response.body(); if (statusCode >= 200 && statusCode < 300) { - info("Metrics submitted with status code: %s (%s)", statusCode, body); + logger.info("Metrics submitted with status code: %s (%s)", statusCode, body); getErrorTracker().map(SimpleErrorTracker.class::cast).ifPresent(SimpleErrorTracker::clear); if (flush != null) flush.run(); return true; } else if (statusCode >= 300 && statusCode < 400) { - warn("Received redirect response from metrics server: %s (%s)", statusCode, body); + logger.warn("Received redirect response from metrics server: %s (%s)", statusCode, body); } else if (statusCode >= 400 && statusCode < 500) { - error("Submitted invalid request to metrics server: %s (%s)", null, statusCode, body); + logger.error("Submitted invalid request to metrics server: %s (%s)", null, statusCode, body); } else if (statusCode >= 500 && statusCode < 600) { - error("Received server error response from metrics server: %s (%s)", null, statusCode, body); + logger.error("Received server error response from metrics server: %s (%s)", null, statusCode, body); } else { - warn("Received unexpected response from metrics server: %s (%s)", statusCode, body); + logger.warn("Received unexpected response from metrics server: %s (%s)", statusCode, body); } } catch (final HttpConnectTimeoutException t) { - error("Metrics submission timed out after 3 seconds: %s", null, url); + logger.error("Metrics submission timed out after 3 seconds: %s", null, url); } catch (final ConnectException t) { - error("Failed to connect to metrics server: %s", null, url); + logger.error("Failed to connect to metrics server: %s", null, url); } catch (final Throwable t) { - error("Failed to submit metrics", t); + logger.error("Failed to submit metrics", t); } return false; } } - private final String javaVendor = System.getProperty("java.vendor"); - private final String javaVersion = System.getProperty("java.version"); - private final String osArch = System.getProperty("os.arch"); - private final String osName = System.getProperty("os.name"); - private final String osVersion = System.getProperty("os.version"); - private final int coreCount = Runtime.getRuntime().availableProcessors(); + private static final String javaVendor = System.getProperty("java.vendor"); + private static final String javaVersion = System.getProperty("java.version"); + private static final String osArch = System.getProperty("os.arch"); + private static final String osName = System.getProperty("os.name"); + private static final String osVersion = System.getProperty("os.version"); + private static final int coreCount = Runtime.getRuntime().availableProcessors(); protected JsonObject createData() { final var data = new JsonObject(); @@ -282,7 +267,7 @@ protected JsonObject createData() { try { appendDefaultData(metrics); } catch (final Throwable t) { - error("Failed to append default data", t); + logger.error("Failed to append default data", t); getErrorTracker().ifPresent(tracker -> tracker.trackError(t)); } @@ -290,7 +275,7 @@ protected JsonObject createData() { try { metric.getData().ifPresent(element -> metrics.add(metric.getId(), element)); } catch (final Throwable t) { - error("Failed to build metric data: %s", t, metric.getId()); + logger.error("Failed to build metric data: %s", t, metric.getId()); getErrorTracker().ifPresent(tracker -> tracker.trackError(t)); } }); @@ -299,7 +284,7 @@ protected JsonObject createData() { data.add("data", metrics); getErrorTracker().map(SimpleErrorTracker.class::cast) - .map(tracker -> tracker.getData(BUILD_ID)) + .map(tracker -> tracker.getData(Constants.BUILD_ID)) .filter(errors -> !errors.isEmpty()) .ifPresent(errors -> data.add("errors", errors)); return data; @@ -328,39 +313,18 @@ public Metrics.Config getConfig() { @Contract(mutates = "param1") protected abstract void appendDefaultData(JsonObject metrics); - protected void error(@PrintFormat final String message, @Nullable final Throwable throwable, @Nullable final Object... args) { - if (throwable != null) { - if (!logger.isLoggable(Level.SEVERE)) return; - final var logRecord = new LogRecord(Level.SEVERE, message.formatted(args)); - logRecord.setThrown(throwable); - logger.log(logRecord); - } else log(Level.SEVERE, message, args); - } - - protected void log(final Level level, @PrintFormat final String message, @Nullable final Object... args) { - logger.log(level, () -> message.formatted(args)); - } - - protected void info(@PrintFormat final String message, @Nullable final Object... args) { - log(Level.INFO, message, args); - } - - protected void warn(@PrintFormat final String message, @Nullable final Object... args) { - log(Level.WARNING, message, args); - } - @Override public void shutdown() { if (flagService != null) flagService.shutdown(); getErrorTracker().ifPresent(ErrorTracker::detachErrorContext); if (executor != null) try { - info("Shutting down metrics submission"); + logger.info("Shutting down metrics submission"); executor.shutdown(); getErrorTracker().map(SimpleErrorTracker.class::cast) .filter(SimpleErrorTracker::needsFlushing) .ifPresent(ignored -> submit()); } catch (final Throwable t) { - error("Failed to submit metrics on shutdown", t); + logger.error("Failed to submit metrics on shutdown", t); } finally { executor = null; } diff --git a/core/src/main/java/dev/faststats/core/internal/Logger.java b/core/src/main/java/dev/faststats/core/internal/Logger.java new file mode 100644 index 0000000..2a44988 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/internal/Logger.java @@ -0,0 +1,27 @@ +package dev.faststats.core.internal; + +import org.intellij.lang.annotations.PrintFormat; +import org.jspecify.annotations.Nullable; + +import java.util.logging.Filter; +import java.util.logging.Level; + +public interface Logger { + void setLevel(Level level); + + boolean isLoggable(Level level); + + void setFilter(@Nullable Filter filter); + + void error(@PrintFormat final String message, @Nullable final Throwable throwable, @Nullable final Object... args); + + void log(final Level level, @PrintFormat final String message, @Nullable final Object... args); + + default void info(@PrintFormat final String message, @Nullable final Object... args) { + log(Level.INFO, message, args); + } + + default void warn(@PrintFormat final String message, @Nullable final Object... args) { + log(Level.WARNING, message, args); + } +} diff --git a/core/src/main/java/dev/faststats/core/internal/LoggerFactory.java b/core/src/main/java/dev/faststats/core/internal/LoggerFactory.java new file mode 100644 index 0000000..64b63c2 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/internal/LoggerFactory.java @@ -0,0 +1,16 @@ +package dev.faststats.core.internal; + +import java.util.ServiceLoader; + +public interface LoggerFactory { + static LoggerFactory factory() { + final class Holder { + private static final LoggerFactory INSTANCE = ServiceLoader.load(LoggerFactory.class) + .findFirst() + .orElseGet(SimpleLoggerFactory::new); + } + return Holder.INSTANCE; + } + + Logger getLogger(String name); +} diff --git a/core/src/main/java/dev/faststats/core/internal/SimpleLogger.java b/core/src/main/java/dev/faststats/core/internal/SimpleLogger.java new file mode 100644 index 0000000..f6e6b78 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/internal/SimpleLogger.java @@ -0,0 +1,45 @@ +package dev.faststats.core.internal; + +import org.jspecify.annotations.Nullable; + +import java.util.logging.Filter; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +class SimpleLogger implements Logger { + private final java.util.logging.Logger logger; + + public SimpleLogger(final String name) { + this.logger = java.util.logging.Logger.getLogger(name); + } + + @Override + public void setLevel(final Level level) { + logger.setLevel(level); + } + + @Override + public boolean isLoggable(final Level level) { + return logger.isLoggable(level); + } + + @Override + public void setFilter(@Nullable final Filter filter) { + logger.setFilter(filter); + } + + @Override + public void error(final String message, @Nullable final Throwable throwable, @Nullable final Object... args) { + if (throwable != null) { + if (!logger.isLoggable(Level.SEVERE)) return; + final var logRecord = new LogRecord(Level.SEVERE, message.formatted(args)); + logRecord.setThrown(throwable); + logger.log(logRecord); + } else log(Level.SEVERE, message, args); + } + + @Override + public void log(final Level level, final String message, @Nullable final Object... args) { + logger.log(level, () -> message.formatted(args)); + } +} diff --git a/core/src/main/java/dev/faststats/core/internal/SimpleLoggerFactory.java b/core/src/main/java/dev/faststats/core/internal/SimpleLoggerFactory.java new file mode 100644 index 0000000..aab3ef8 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/internal/SimpleLoggerFactory.java @@ -0,0 +1,8 @@ +package dev.faststats.core.internal; + +final class SimpleLoggerFactory implements LoggerFactory { + @Override + public Logger getLogger(final String name) { + return new SimpleLogger(name); + } +} diff --git a/core/src/main/java/dev/faststats/core/internal/package-info.java b/core/src/main/java/dev/faststats/core/internal/package-info.java new file mode 100644 index 0000000..93362ed --- /dev/null +++ b/core/src/main/java/dev/faststats/core/internal/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package dev.faststats.core.internal; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index fce66ce..e665b64 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -4,6 +4,7 @@ module dev.faststats.core { exports dev.faststats.core.data; exports dev.faststats.core.flags; + exports dev.faststats.core.internal; exports dev.faststats.core; requires com.google.gson; @@ -12,4 +13,6 @@ requires static org.jetbrains.annotations; requires static org.jspecify; -} \ No newline at end of file + + uses dev.faststats.core.internal.LoggerFactory; +} From aa162ded81b9098d4710ab944f3dc4c8c509061c Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 16:02:46 +0200 Subject: [PATCH 15/40] Extracts constants to its own class --- .../faststats/core/internal/Constants.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 core/src/main/java/dev/faststats/core/internal/Constants.java diff --git a/core/src/main/java/dev/faststats/core/internal/Constants.java b/core/src/main/java/dev/faststats/core/internal/Constants.java new file mode 100644 index 0000000..b379046 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/internal/Constants.java @@ -0,0 +1,23 @@ +package dev.faststats.core.internal; + +import dev.faststats.core.SimpleMetrics; + +import java.io.IOException; +import java.util.Properties; + +public final class Constants { + public static final String SDK_NAME; + public static final String SDK_VERSION; + public static final String BUILD_ID; + + static { + final var properties = new Properties(); + try (final var stream = SimpleMetrics.class.getClassLoader().getResourceAsStream("/META-INF/faststats.properties")) { + if (stream != null) properties.load(stream); + } catch (final IOException ignored) { + } + SDK_NAME = properties.getProperty("name", "unknown"); + SDK_VERSION = properties.getProperty("version", "unknown"); + BUILD_ID = properties.getProperty("build-id", "unknown"); + } +} From 012fc3253525ad389d3c0f188062e8cfdc93f8b3 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 16:16:56 +0200 Subject: [PATCH 16/40] Add dedicated Hytale logger --- .../faststats/hytale/HytaleMetricsImpl.java | 20 +------ .../faststats/hytale/logger/HytaleLogger.java | 53 +++++++++++++++++++ .../hytale/logger/HytaleLoggerFactory.java | 8 +++ hytale/src/main/java/module-info.java | 4 +- .../dev.faststats.core.internal.LoggerFactory | 1 + 5 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 hytale/src/main/java/dev/faststats/hytale/logger/HytaleLogger.java create mode 100644 hytale/src/main/java/dev/faststats/hytale/logger/HytaleLoggerFactory.java create mode 100644 hytale/src/main/resources/META-INF/services/dev.faststats.core.internal.LoggerFactory diff --git a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java index 6049c9b..8a6cba2 100644 --- a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java +++ b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java @@ -1,7 +1,6 @@ package dev.faststats.hytale; import com.google.gson.JsonObject; -import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.server.core.HytaleServer; import com.hypixel.hytale.server.core.plugin.JavaPlugin; import com.hypixel.hytale.server.core.universe.Universe; @@ -9,19 +8,14 @@ import dev.faststats.core.SimpleMetrics; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.Nullable; import java.nio.file.Path; -import java.util.logging.Level; final class HytaleMetricsImpl extends SimpleMetrics implements HytaleMetrics { - private final HytaleLogger logger; - @Async.Schedule @Contract(mutates = "io") - private HytaleMetricsImpl(final Factory factory, final HytaleLogger logger, final Path config) throws IllegalStateException { + private HytaleMetricsImpl(final Factory factory, final Path config) throws IllegalStateException { super(factory, config); - this.logger = logger; startSubmitting(); } @@ -33,22 +27,12 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", "Hytale"); } - @Override - protected void error(final String message, @Nullable final Throwable throwable, @Nullable final Object... args) { - if (super.logger.isLoggable(Level.SEVERE)) logger.atSevere().withCause(throwable).logVarargs(message, args); - } - - @Override - protected void log(final Level level, final String message, @Nullable final Object... args) { - if (super.logger.isLoggable(level)) logger.at(level).logVarargs(message, args); - } - static final class Factory extends SimpleMetrics.Factory implements HytaleMetrics.Factory { @Override public Metrics create(final JavaPlugin plugin) throws IllegalStateException { final var mods = plugin.getDataDirectory().toAbsolutePath().getParent(); final var config = mods.resolve("faststats").resolve("config.properties"); - return new HytaleMetricsImpl(this, plugin.getLogger(), config); + return new HytaleMetricsImpl(this, config); } } } diff --git a/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLogger.java b/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLogger.java new file mode 100644 index 0000000..3b9ae5c --- /dev/null +++ b/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLogger.java @@ -0,0 +1,53 @@ +package dev.faststats.hytale.logger; + +import org.intellij.lang.annotations.PrintFormat; +import org.jspecify.annotations.Nullable; + +import java.util.function.Predicate; +import java.util.logging.Level; + +final class HytaleLogger implements dev.faststats.core.internal.Logger { + private final com.hypixel.hytale.logger.HytaleLogger logger; + private volatile @Nullable Predicate filter; + + HytaleLogger(final String name) { + this.logger = com.hypixel.hytale.logger.HytaleLogger.get(name); + } + + @Override + public void setLevel(final Level level) { + logger.setLevel(level); + } + + @Override + public boolean isLoggable(final Level level) { + final var loggerLevel = logger.getLevel(); + if (level.intValue() < loggerLevel.intValue()) return false; + + final var currentFilter = filter; + return currentFilter != null && currentFilter.test(level); + } + + @Override + public void setFilter(@Nullable final Predicate filter) { + this.filter = filter; + } + + @Override + public void error(@PrintFormat final String message, @Nullable final Throwable throwable, @Nullable final Object... args) { + if (!isLoggable(Level.SEVERE)) return; + + final var api = logger.atSevere(); + if (throwable != null) { + api.withCause(throwable).logVarargs(message, args); + return; + } + api.logVarargs(message, args); + } + + @Override + public void log(final Level level, @PrintFormat final String message, @Nullable final Object... args) { + if (!isLoggable(level)) return; + logger.at(level).logVarargs(message, args); + } +} diff --git a/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLoggerFactory.java b/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLoggerFactory.java new file mode 100644 index 0000000..81d2cce --- /dev/null +++ b/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLoggerFactory.java @@ -0,0 +1,8 @@ +package dev.faststats.hytale.logger; + +public final class HytaleLoggerFactory implements dev.faststats.core.internal.LoggerFactory { + @Override + public dev.faststats.core.internal.Logger getLogger(final String name) { + return new HytaleLogger(name); + } +} diff --git a/hytale/src/main/java/module-info.java b/hytale/src/main/java/module-info.java index 1b3293d..ce31562 100644 --- a/hytale/src/main/java/module-info.java +++ b/hytale/src/main/java/module-info.java @@ -10,4 +10,6 @@ requires static org.jetbrains.annotations; requires static org.jspecify; -} \ No newline at end of file + + provides dev.faststats.core.internal.LoggerFactory with dev.faststats.hytale.logger.HytaleLoggerFactory; +} diff --git a/hytale/src/main/resources/META-INF/services/dev.faststats.core.internal.LoggerFactory b/hytale/src/main/resources/META-INF/services/dev.faststats.core.internal.LoggerFactory new file mode 100644 index 0000000..9affb6b --- /dev/null +++ b/hytale/src/main/resources/META-INF/services/dev.faststats.core.internal.LoggerFactory @@ -0,0 +1 @@ +dev.faststats.hytale.logger.HytaleLoggerFactory From 53c997e8171a8f7feb7621ebea3ed83fc288bcca Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 16:17:12 +0200 Subject: [PATCH 17/40] Use custom filter predicate --- core/src/main/java/dev/faststats/core/SimpleMetrics.java | 2 +- core/src/main/java/dev/faststats/core/internal/Logger.java | 4 ++-- .../main/java/dev/faststats/core/internal/SimpleLogger.java | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index fd4d956..486c0e4 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -67,7 +67,7 @@ protected SimpleMetrics(final Factory factory, final Config config) throws this.token = factory.token; this.metrics = config.additionalMetrics ? Set.copyOf(factory.metrics) : Set.of(); final var debug = config.debug() || Boolean.getBoolean("faststats.debug"); - this.logger.setFilter(record -> debug || record.getLevel().equals(Level.CONFIG)); + this.logger.setFilter(level -> debug || level.equals(Level.CONFIG)); this.tracker = config.errorTracking ? factory.tracker : null; this.flush = factory.flush; this.flagService = factory.flagService; diff --git a/core/src/main/java/dev/faststats/core/internal/Logger.java b/core/src/main/java/dev/faststats/core/internal/Logger.java index 2a44988..d5fd1d9 100644 --- a/core/src/main/java/dev/faststats/core/internal/Logger.java +++ b/core/src/main/java/dev/faststats/core/internal/Logger.java @@ -3,7 +3,7 @@ import org.intellij.lang.annotations.PrintFormat; import org.jspecify.annotations.Nullable; -import java.util.logging.Filter; +import java.util.function.Predicate; import java.util.logging.Level; public interface Logger { @@ -11,7 +11,7 @@ public interface Logger { boolean isLoggable(Level level); - void setFilter(@Nullable Filter filter); + void setFilter(@Nullable Predicate filter); void error(@PrintFormat final String message, @Nullable final Throwable throwable, @Nullable final Object... args); diff --git a/core/src/main/java/dev/faststats/core/internal/SimpleLogger.java b/core/src/main/java/dev/faststats/core/internal/SimpleLogger.java index f6e6b78..37d96ed 100644 --- a/core/src/main/java/dev/faststats/core/internal/SimpleLogger.java +++ b/core/src/main/java/dev/faststats/core/internal/SimpleLogger.java @@ -2,7 +2,7 @@ import org.jspecify.annotations.Nullable; -import java.util.logging.Filter; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.LogRecord; @@ -24,8 +24,8 @@ public boolean isLoggable(final Level level) { } @Override - public void setFilter(@Nullable final Filter filter) { - logger.setFilter(filter); + public void setFilter(@Nullable final Predicate filter) { + logger.setFilter(filter != null ? record -> filter.test(record.getLevel()) : null); } @Override From fcc26bd6091d6a1b2b6b45424d48d070b15823a2 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 16:17:50 +0200 Subject: [PATCH 18/40] Move logger below metrics server url --- core/src/main/java/dev/faststats/core/SimpleMetrics.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 486c0e4..b3cf239 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -41,9 +41,10 @@ import static java.nio.charset.StandardCharsets.UTF_8; public abstract class SimpleMetrics implements Metrics { - protected final Logger logger = LoggerFactory.factory().getLogger(getClass().getName()); private static final URI defaultUrl = URI.create("https://metrics.faststats.dev/v1/collect"); + protected final Logger logger = LoggerFactory.factory().getLogger(getClass().getName()); + private final HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .version(HttpClient.Version.HTTP_1_1) From 6d2378610de80dc3ee77488143ccff6c3e3e238d Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 16:18:24 +0200 Subject: [PATCH 19/40] Removed unused imports --- .../src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java index c9c8eac..80bec0d 100644 --- a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java +++ b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java @@ -2,13 +2,11 @@ import cn.nukkit.Server; import cn.nukkit.plugin.PluginBase; -import cn.nukkit.utils.Logger; import com.google.gson.JsonObject; import dev.faststats.core.Metrics; import dev.faststats.core.SimpleMetrics; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.Nullable; import java.nio.file.Path; import java.util.Optional; From ee4acd3031c3e9fbe7d8bc46b4ed85f7fd7a6ede Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 17:09:51 +0200 Subject: [PATCH 20/40] Replace Gson#toJson with toString --- .../dev/faststats/core/flags/SimpleFeatureFlagService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index 68c3a61..91dc53d 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -92,7 +92,7 @@ private CompletableFuture sendOptRequest(final SimpleFeatureFlag flag, requestBody.addProperty("flag", flag.getId()); final var request = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(requestBody))) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + token) .timeout(Duration.ofSeconds(3)) @@ -136,7 +136,7 @@ private CompletableFuture createFetch(final SimpleFeatureFlag flag) { if (!attributes.isEmpty()) requestBody.add("attributes", attributes); final var request = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(requestBody))) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + token) .timeout(Duration.ofSeconds(3)) From 89557314af9f0a5ed25945e1a7d243e8a202577e Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 17:10:09 +0200 Subject: [PATCH 21/40] Add logger to feature flag service --- .../dev/faststats/core/flags/SimpleFeatureFlagService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index 91dc53d..ae0a276 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -1,12 +1,13 @@ package dev.faststats.core.flags; -import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import dev.faststats.core.Token; +import dev.faststats.core.internal.Logger; +import dev.faststats.core.internal.LoggerFactory; import org.jspecify.annotations.Nullable; import java.net.URI; @@ -23,7 +24,7 @@ import java.util.concurrent.ConcurrentHashMap; final class SimpleFeatureFlagService implements FeatureFlagService { - private static final Gson GSON = new Gson(); + private static final Logger logger = LoggerFactory.factory().getLogger(SimpleFeatureFlagService.class.getName()); private static final URI defaultUrl = URI.create("https://flags.faststats.dev/v1"); private final HttpClient httpClient = HttpClient.newBuilder() @@ -56,7 +57,7 @@ private URI getFlagsServerUrl() { try { return property != null ? new URI(property) : defaultUrl; } catch (final URISyntaxException e) { - //error("Failed to parse flags server url: %s", e, property); // todo: recover + logger.error("Failed to parse flags server url: %s", e, property); return defaultUrl; } } From 6be7deb730f0af3aa918fcc69fcc2a9cd397bebe Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 17:12:17 +0200 Subject: [PATCH 22/40] Decouple config from Metrics interface --- .../main/java/dev/faststats/core/Config.java | 60 ++++++++ .../main/java/dev/faststats/core/Metrics.java | 55 -------- .../java/dev/faststats/core/SimpleConfig.java | 119 ++++++++++++++++ .../dev/faststats/core/SimpleMetrics.java | 130 ++---------------- .../test/java/dev/faststats/MockMetrics.java | 3 +- .../faststats/sponge/SpongeMetricsImpl.java | 3 +- 6 files changed, 194 insertions(+), 176 deletions(-) create mode 100644 core/src/main/java/dev/faststats/core/Config.java create mode 100644 core/src/main/java/dev/faststats/core/SimpleConfig.java diff --git a/core/src/main/java/dev/faststats/core/Config.java b/core/src/main/java/dev/faststats/core/Config.java new file mode 100644 index 0000000..1fedf52 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/Config.java @@ -0,0 +1,60 @@ +package dev.faststats.core; + +import org.jetbrains.annotations.Contract; + +import java.util.UUID; + +/** + * A representation of the metrics configuration. + * + * @since 0.23.0 + */ +public sealed interface Config permits SimpleConfig { + /** + * The server id. + * + * @return the server id + * @since 0.23.0 + */ + @Contract(pure = true) + UUID serverId(); + + /** + * Whether metrics submission is enabled. + *

+ * Bypassing this setting may get your project banned from FastStats.
+ * Users have to be able to opt out from metrics submission. + * + * @return {@code true} if metrics submission is enabled, {@code false} otherwise + * @since 0.23.0 + */ + @Contract(pure = true) + boolean enabled(); + + /** + * Whether error tracking is enabled across all metrics instances. + * + * @return {@code true} if error tracking is enabled, {@code false} otherwise + * @since 0.23.0 + */ + @Contract(pure = true) + boolean errorTracking(); + + /** + * Whether additional metrics are enabled across all metrics instances. + * + * @return {@code true} if additional metrics are enabled, {@code false} otherwise + * @since 0.23.0 + */ + @Contract(pure = true) + boolean additionalMetrics(); + + /** + * Whether debug logging is enabled across all metrics instances. + * + * @return {@code true} if debug logging is enabled, {@code false} otherwise + * @since 0.23.0 + */ + @Contract(pure = true) + boolean debug(); +} diff --git a/core/src/main/java/dev/faststats/core/Metrics.java b/core/src/main/java/dev/faststats/core/Metrics.java index 04beb85..d5b65d2 100644 --- a/core/src/main/java/dev/faststats/core/Metrics.java +++ b/core/src/main/java/dev/faststats/core/Metrics.java @@ -6,7 +6,6 @@ import org.jetbrains.annotations.Contract; import java.util.Optional; -import java.util.UUID; /** * Metrics interface. @@ -156,58 +155,4 @@ interface Factory> { Metrics create(T object) throws IllegalStateException; } - /** - * A representation of the metrics configuration. - * - * @since 0.1.0 - */ - sealed interface Config permits SimpleMetrics.Config { - /** - * The server id. - * - * @return the server id - * @since 0.1.0 - */ - @Contract(pure = true) - UUID serverId(); - - /** - * Whether metrics submission is enabled. - *

- * Bypassing this setting may get your project banned from FastStats.
- * Users have to be able to opt out from metrics submission. - * - * @return {@code true} if metrics submission is enabled, {@code false} otherwise - * @since 0.1.0 - */ - @Contract(pure = true) - boolean enabled(); - - /** - * Whether error tracking is enabled across all metrics instances. - * - * @return {@code true} if error tracking is enabled, {@code false} otherwise - * @since 0.11.0 - */ - @Contract(pure = true) - boolean errorTracking(); - - /** - * Whether additional metrics are enabled across all metrics instances. - * - * @return {@code true} if additional metrics are enabled, {@code false} otherwise - * @since 0.11.0 - */ - @Contract(pure = true) - boolean additionalMetrics(); - - /** - * Whether debug logging is enabled across all metrics instances. - * - * @return {@code true} if debug logging is enabled, {@code false} otherwise - * @since 0.1.0 - */ - @Contract(pure = true) - boolean debug(); - } } diff --git a/core/src/main/java/dev/faststats/core/SimpleConfig.java b/core/src/main/java/dev/faststats/core/SimpleConfig.java new file mode 100644 index 0000000..fdb7c11 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/SimpleConfig.java @@ -0,0 +1,119 @@ +package dev.faststats.core; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiPredicate; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@ApiStatus.Internal +public record SimpleConfig( + UUID serverId, + boolean additionalMetrics, + boolean debug, + boolean enabled, + boolean errorTracking, + boolean firstRun, + boolean externallyManaged +) implements Config { + + public static final String DEFAULT_COMMENT = """ + FastStats (https://faststats.dev) collects anonymous usage statistics for developers. + # This helps developers understand how their projects are used in the real world. + # + # No IP addresses, player data, or personal information is collected. + # The server ID below is randomly generated and can be regenerated at any time. + # + # Enabling metrics has no noticeable performance impact. + # Keeping metrics enabled is recommended, but you can opt out by setting + # 'enabled=false' in faststats/config.properties. + # + # If you suspect a developer is collecting personal data or bypassing the "enabled" option, + # please report it at: https://faststats.dev/abuse + # + # For more information, visit: https://faststats.dev/info + """; + + @Contract(mutates = "io") + public static SimpleConfig read(final Path file) throws RuntimeException { + return read(file, DEFAULT_COMMENT, false, false); + } + + @Contract(mutates = "io") + public static SimpleConfig read(final Path file, final String comment, final boolean externallyManaged, final boolean externallyEnabled) throws RuntimeException { + final var properties = readOrEmpty(file); + final var firstRun = properties.isEmpty(); + final var saveConfig = new AtomicBoolean(firstRun); + + final var serverId = properties.map(object -> object.getProperty("serverId")).map(string -> { + try { + final var trimmed = string.trim(); + final var corrected = trimmed.length() > 36 ? trimmed.substring(0, 36) : trimmed; + if (!corrected.equals(string)) saveConfig.set(true); + return UUID.fromString(corrected); + } catch (final IllegalArgumentException e) { + saveConfig.set(true); + return UUID.randomUUID(); + } + }).orElseGet(() -> { + saveConfig.set(true); + return UUID.randomUUID(); + }); + + final BiPredicate predicate = (key, defaultValue) -> { + return properties.map(object -> object.getProperty(key)).map(Boolean::parseBoolean).orElseGet(() -> { + saveConfig.set(true); + return defaultValue; + }); + }; + + final var enabled = externallyManaged ? externallyEnabled : predicate.test("enabled", true); + final var errorTracking = predicate.test("submitErrors", true); + final var additionalMetrics = predicate.test("submitAdditionalMetrics", true); + final var debug = predicate.test("debug", false); + + if (saveConfig.get()) try { + save(file, externallyManaged, comment, serverId, enabled, errorTracking, additionalMetrics, debug); + } catch (final IOException e) { + throw new RuntimeException("Failed to save metrics config", e); + } + + return new SimpleConfig(serverId, additionalMetrics, debug, enabled, errorTracking, firstRun, externallyManaged); + } + + private static Optional readOrEmpty(final Path file) throws RuntimeException { + if (!Files.isRegularFile(file)) return Optional.empty(); + try (final var reader = Files.newBufferedReader(file, UTF_8)) { + final var properties = new Properties(); + properties.load(reader); + return Optional.of(properties); + } catch (final IOException e) { + throw new RuntimeException("Failed to read metrics config", e); + } + } + + private static void save(final Path file, final boolean externallyManaged, final String comment, final UUID serverId, final boolean enabled, final boolean errorTracking, final boolean additionalMetrics, final boolean debug) throws IOException { + Files.createDirectories(file.getParent()); + try (final var out = Files.newOutputStream(file); + final var writer = new OutputStreamWriter(out, UTF_8)) { + final var properties = new Properties(); + + properties.setProperty("serverId", serverId.toString()); + if (!externallyManaged) properties.setProperty("enabled", Boolean.toString(enabled)); + properties.setProperty("submitErrors", Boolean.toString(errorTracking)); + properties.setProperty("submitAdditionalMetrics", Boolean.toString(additionalMetrics)); + properties.setProperty("debug", Boolean.toString(debug)); + + properties.store(writer, comment); + } + } +} diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index b3cf239..f8fc599 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -3,6 +3,7 @@ import com.google.gson.JsonObject; import dev.faststats.core.data.Metric; import dev.faststats.core.flags.FeatureFlagService; +import dev.faststats.core.internal.ConfigProvider; import dev.faststats.core.internal.Constants; import dev.faststats.core.internal.Logger; import dev.faststats.core.internal.LoggerFactory; @@ -14,7 +15,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStreamWriter; import java.net.ConnectException; import java.net.URI; import java.net.URISyntaxException; @@ -22,19 +22,13 @@ import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Duration; import java.util.HashSet; import java.util.Optional; -import java.util.Properties; import java.util.Set; -import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.BiPredicate; import java.util.logging.Level; import java.util.zip.GZIPOutputStream; @@ -53,7 +47,7 @@ public abstract class SimpleMetrics implements Metrics { private final URI url; private final Set> metrics; - private final Config config; + private final SimpleConfig config; private final @Token String token; private final @Nullable ErrorTracker tracker; private final @Nullable Runnable flush; @@ -64,12 +58,12 @@ public abstract class SimpleMetrics implements Metrics { protected SimpleMetrics(final Factory factory, final Config config) throws IllegalStateException { if (factory.token == null) throw new IllegalStateException("Token must be specified"); - this.config = config; + this.config = (SimpleConfig) config; this.token = factory.token; - this.metrics = config.additionalMetrics ? Set.copyOf(factory.metrics) : Set.of(); + this.metrics = config.additionalMetrics() ? Set.copyOf(factory.metrics) : Set.of(); final var debug = config.debug() || Boolean.getBoolean("faststats.debug"); this.logger.setFilter(level -> debug || level.equals(Level.CONFIG)); - this.tracker = config.errorTracking ? factory.tracker : null; + this.tracker = config.errorTracking() ? factory.tracker : null; this.flush = factory.flush; this.flagService = factory.flagService; this.url = getMetricsServerUrl(); @@ -86,8 +80,8 @@ private URI getMetricsServerUrl() { } @Contract(mutates = "io") - protected SimpleMetrics(final Factory factory, final Path config) throws IllegalStateException { - this(factory, Config.read(config)); + protected SimpleMetrics(final Factory factory) throws IllegalStateException { + this(factory, SimpleConfig.read(ConfigProvider.provider().getConfigPath().resolve("config.properties"))); } @VisibleForTesting @@ -100,8 +94,8 @@ protected SimpleMetrics( final URI url, final boolean debug ) { - this.metrics = config.additionalMetrics ? Set.copyOf(metrics) : Set.of(); - this.config = config; + this.metrics = config.additionalMetrics() ? Set.copyOf(metrics) : Set.of(); + this.config = (SimpleConfig) config; this.logger.setLevel(debug ? Level.ALL : Level.OFF); this.token = token; this.tracker = tracker; @@ -142,8 +136,7 @@ private void startSubmitting(final long initialDelay, final long period, final T return; } - if (config.firstRun) { - + if (config.firstRun()) { var separatorLength = 0; final var split = getOnboardingMessage().split("\n"); for (final var s : split) if (s.length() > separatorLength) separatorLength = s.length(); @@ -307,7 +300,7 @@ public Optional getFeatureFlagService() { } @Override - public Metrics.Config getConfig() { + public dev.faststats.core.Config getConfig() { return config; } @@ -377,105 +370,4 @@ public F token(@Token final String token) throws IllegalArgumentException { } } - public record Config( - UUID serverId, - boolean additionalMetrics, - boolean debug, - boolean enabled, - boolean errorTracking, - boolean firstRun, - boolean externallyManaged - ) implements Metrics.Config { - - public static final String DEFAULT_COMMENT = """ - FastStats (https://faststats.dev) collects anonymous usage statistics for developers. - # This helps developers understand how their projects are used in the real world. - # - # No IP addresses, player data, or personal information is collected. - # The server ID below is randomly generated and can be regenerated at any time. - # - # Enabling metrics has no noticeable performance impact. - # Keeping metrics enabled is recommended, but you can opt out by setting - # 'enabled=false' in faststats/config.properties. - # - # If you suspect a developer is collecting personal data or bypassing the "enabled" option, - # please report it at: https://faststats.dev/abuse - # - # For more information, visit: https://faststats.dev/info - """; - - @Contract(mutates = "io") - public static Config read(final Path file) throws RuntimeException { - return read(file, DEFAULT_COMMENT, false, false); - } - - @Contract(mutates = "io") - public static Config read(final Path file, final String comment, final boolean externallyManaged, final boolean externallyEnabled) throws RuntimeException { - final var properties = readOrEmpty(file); - final var firstRun = properties.isEmpty(); - final var saveConfig = new AtomicBoolean(firstRun); - - final var serverId = properties.map(object -> object.getProperty("serverId")).map(string -> { - try { - final var trimmed = string.trim(); - final var corrected = trimmed.length() > 36 ? trimmed.substring(0, 36) : trimmed; - if (!corrected.equals(string)) saveConfig.set(true); - return UUID.fromString(corrected); - } catch (final IllegalArgumentException e) { - saveConfig.set(true); - return UUID.randomUUID(); - } - }).orElseGet(() -> { - saveConfig.set(true); - return UUID.randomUUID(); - }); - - final BiPredicate predicate = (key, defaultValue) -> { - return properties.map(object -> object.getProperty(key)).map(Boolean::parseBoolean).orElseGet(() -> { - saveConfig.set(true); - return defaultValue; - }); - }; - - final var enabled = externallyManaged ? externallyEnabled : predicate.test("enabled", true); - final var errorTracking = predicate.test("submitErrors", true); - final var additionalMetrics = predicate.test("submitAdditionalMetrics", true); - final var debug = predicate.test("debug", false); - - if (saveConfig.get()) try { - save(file, externallyManaged, comment, serverId, enabled, errorTracking, additionalMetrics, debug); - } catch (final IOException e) { - throw new RuntimeException("Failed to save metrics config", e); - } - - return new Config(serverId, additionalMetrics, debug, enabled, errorTracking, firstRun, externallyManaged); - } - - private static Optional readOrEmpty(final Path file) throws RuntimeException { - if (!Files.isRegularFile(file)) return Optional.empty(); - try (final var reader = Files.newBufferedReader(file, UTF_8)) { - final var properties = new Properties(); - properties.load(reader); - return Optional.of(properties); - } catch (final IOException e) { - throw new RuntimeException("Failed to read metrics config", e); - } - } - - private static void save(final Path file, final boolean externallyManaged, final String comment, final UUID serverId, final boolean enabled, final boolean errorTracking, final boolean additionalMetrics, final boolean debug) throws IOException { - Files.createDirectories(file.getParent()); - try (final var out = Files.newOutputStream(file); - final var writer = new OutputStreamWriter(out, UTF_8)) { - final var properties = new Properties(); - - properties.setProperty("serverId", serverId.toString()); - if (!externallyManaged) properties.setProperty("enabled", Boolean.toString(enabled)); - properties.setProperty("submitErrors", Boolean.toString(errorTracking)); - properties.setProperty("submitAdditionalMetrics", Boolean.toString(additionalMetrics)); - properties.setProperty("debug", Boolean.toString(debug)); - - properties.store(writer, comment); - } - } - } } diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index fec3062..87494bd 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -2,6 +2,7 @@ import com.google.gson.JsonObject; import dev.faststats.core.ErrorTracker; +import dev.faststats.core.SimpleConfig; import dev.faststats.core.SimpleMetrics; import dev.faststats.core.Token; import org.jspecify.annotations.NullMarked; @@ -14,7 +15,7 @@ @NullMarked public final class MockMetrics extends SimpleMetrics { public MockMetrics(final UUID serverId, @Token final String token, @Nullable final ErrorTracker tracker, final boolean debug) { - super(new Config(serverId, true, debug, true, true, false, false), Set.of(), token, tracker, null, URI.create("http://localhost:5000/v1/collect"), debug); + super(new SimpleConfig(serverId, true, debug, true, true, false, false), Set.of(), token, tracker, null, URI.create("http://localhost:5000/v1/collect"), debug); } @Override diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java index 006b491..5aea55f 100644 --- a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java @@ -2,6 +2,7 @@ import com.google.gson.JsonObject; import dev.faststats.core.Metrics; +import dev.faststats.core.SimpleConfig; import dev.faststats.core.SimpleMetrics; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Async; @@ -40,7 +41,7 @@ private SpongeMetricsImpl( final PluginContainer plugin, final Path config ) throws IllegalStateException { - super(factory, SimpleMetrics.Config.read(config, COMMENT, true, Sponge.metricsConfigManager() + super(factory, SimpleConfig.read(config, COMMENT, true, Sponge.metricsConfigManager() .effectiveCollectionState(plugin).asBoolean())); this.plugin = plugin; startSubmitting(); From dc9c7da750d737dda8530c7b07b821c8ee1a3115 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 17:18:29 +0200 Subject: [PATCH 23/40] Undo happy little accident :) --- core/src/main/java/dev/faststats/core/SimpleMetrics.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index f8fc599..1adcdbe 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -3,7 +3,6 @@ import com.google.gson.JsonObject; import dev.faststats.core.data.Metric; import dev.faststats.core.flags.FeatureFlagService; -import dev.faststats.core.internal.ConfigProvider; import dev.faststats.core.internal.Constants; import dev.faststats.core.internal.Logger; import dev.faststats.core.internal.LoggerFactory; @@ -22,6 +21,7 @@ import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.file.Path; import java.time.Duration; import java.util.HashSet; import java.util.Optional; @@ -80,8 +80,8 @@ private URI getMetricsServerUrl() { } @Contract(mutates = "io") - protected SimpleMetrics(final Factory factory) throws IllegalStateException { - this(factory, SimpleConfig.read(ConfigProvider.provider().getConfigPath().resolve("config.properties"))); + protected SimpleMetrics(final Factory factory, final Path config) throws IllegalStateException { + this(factory, SimpleConfig.read(config)); } @VisibleForTesting From aceb872feb9e2d8c5c84e27a2de32e9ed9203866 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 20:25:45 +0200 Subject: [PATCH 24/40] Add info comments to example --- .../main/java/dev/faststats/example/FeatureFlagExample.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java b/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java index bae91d8..1461d99 100644 --- a/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java +++ b/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java @@ -10,11 +10,11 @@ public final class FeatureFlagExample { public static final FeatureFlagService SERVICE = FeatureFlagService.create( "YOUR_TOKEN_HERE", // token can be found in the settings of your project - Attributes.create() + Attributes.create() // Define global attributes .put("version", "1.2.3") .put("java_version", System.getProperty("java.version")) .put("java_vendor", System.getProperty("java.vendor")), - Duration.ofMinutes(10) + Duration.ofMinutes(10) // Custom cache TTL for resolved flag values ); // Define flags with default values From 100684feee24b6bd2d1df52e35b953e13eec6af7 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 20:26:11 +0200 Subject: [PATCH 25/40] Throw on negative ttl --- .../main/java/dev/faststats/core/flags/FeatureFlagService.java | 3 ++- .../dev/faststats/core/flags/SimpleFeatureFlagService.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java index 63e5417..f4d5fd5 100644 --- a/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java @@ -51,10 +51,11 @@ static FeatureFlagService create(@Token final String token, @Nullable final Attr * @param attributes the global targeting attributes * @param ttl the cache time-to-live for resolved flag values * @return a new feature flag service + * @throws IllegalArgumentException if the TTL is negative * @since 0.23.0 */ @Contract(value = "_, _, _ -> new", pure = true) - static FeatureFlagService create(@Token final String token, @Nullable final Attributes attributes, final Duration ttl) { + static FeatureFlagService create(@Token final String token, @Nullable final Attributes attributes, final Duration ttl) throws IllegalArgumentException { return new SimpleFeatureFlagService(token, attributes, ttl); } diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index ae0a276..024946e 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -45,7 +45,8 @@ final class SimpleFeatureFlagService implements FeatureFlagService { final @Token String token, final @Nullable Attributes attributes, final Duration ttl - ) { + ) throws IllegalArgumentException { + if (ttl.isNegative()) throw new IllegalArgumentException("TTL cannot be negative"); this.token = token; this.attributes = attributes; this.ttl = ttl; From dd34a5518c022f867ea3f6358759b3cb6573febd Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 20:28:26 +0200 Subject: [PATCH 26/40] Add attributes and TTL getters --- .../core/flags/FeatureFlagService.java | 21 +++++++++++++++++++ .../core/flags/SimpleFeatureFlagService.java | 10 +++++++++ 2 files changed, 31 insertions(+) diff --git a/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java index f4d5fd5..d576a1b 100644 --- a/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java @@ -5,6 +5,7 @@ import org.jspecify.annotations.Nullable; import java.time.Duration; +import java.util.Optional; /** * A service for managing feature flags. @@ -128,6 +129,26 @@ static FeatureFlagService create(@Token final String token, @Nullable final Attr @Contract(value = "_, _, _ -> new", pure = true) FeatureFlag define(String id, Number defaultValue, Attributes attributes); + /** + * Returns the global targeting attributes configured for this service. + *

+ * These attributes apply to every flag defined by the service and are + * merged with any per-flag attributes supplied during definition. + * + * @return the global targeting attributes, if configured + * @since 0.23.0 + */ + @Contract(pure = true) + Optional getAttributes(); + + /** + * Returns the cache time-to-live used for resolved flag values. + * + * @return the configured cache time-to-live + * @since 0.23.0 + */ + Duration getTTL(); + /** * Shuts down the feature flag service. * diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index 024946e..c153e43 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -206,6 +206,16 @@ public FeatureFlag define(final String id, final Number defaultValue, fi return new SimpleFeatureFlag<>(id, defaultValue, attributes, this); } + @Override + public Optional getAttributes() { + return Optional.ofNullable(attributes); + } + + @Override + public Duration getTTL() { + return ttl; + } + @Override public void shutdown() { cache.clear(); From 32cff12e4fbd89982f395022a264e3feb8521cdc Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 20:35:51 +0200 Subject: [PATCH 27/40] Refactor URL retrieval --- .../java/dev/faststats/core/SimpleMetrics.java | 11 ++++------- .../core/flags/SimpleFeatureFlagService.java | 14 ++++++-------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 1adcdbe..37d93af 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -35,9 +35,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; public abstract class SimpleMetrics implements Metrics { - private static final URI defaultUrl = URI.create("https://metrics.faststats.dev/v1/collect"); - - protected final Logger logger = LoggerFactory.factory().getLogger(getClass().getName()); + protected final Logger logger = LoggerFactory.factory().getLogger(getClass()); private final HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) @@ -71,12 +69,12 @@ protected SimpleMetrics(final Factory factory, final Config config) throws private URI getMetricsServerUrl() { final var property = System.getProperty("faststats.metrics-server"); - try { - return property != null ? new URI(property) : defaultUrl; + if (property != null) try { + return new URI(property); } catch (final URISyntaxException e) { logger.error("Failed to parse metrics server url: %s", e, property); - return defaultUrl; } + return URI.create("https://metrics.faststats.dev/v1/collect"); } @Contract(mutates = "io") @@ -101,7 +99,6 @@ protected SimpleMetrics( this.tracker = tracker; this.flush = flush; this.url = url; - this.flagService = null; } protected String getOnboardingMessage() { diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index c153e43..5d26985 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -24,8 +24,8 @@ import java.util.concurrent.ConcurrentHashMap; final class SimpleFeatureFlagService implements FeatureFlagService { - private static final Logger logger = LoggerFactory.factory().getLogger(SimpleFeatureFlagService.class.getName()); - private static final URI defaultUrl = URI.create("https://flags.faststats.dev/v1"); + private static final Logger logger = LoggerFactory.factory().getLogger(SimpleFeatureFlagService.class); + private static final URI url = getFlagsServerUrl(); private final HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) @@ -35,7 +35,6 @@ final class SimpleFeatureFlagService implements FeatureFlagService { private final @Token String token; private final @Nullable Attributes attributes; private final Duration ttl; - private final URI url; private final Map cache = new ConcurrentHashMap<>(); private final Map fetchTimes = new ConcurrentHashMap<>(); @@ -50,17 +49,16 @@ final class SimpleFeatureFlagService implements FeatureFlagService { this.token = token; this.attributes = attributes; this.ttl = ttl; - this.url = getFlagsServerUrl(); } - private URI getFlagsServerUrl() { + private static URI getFlagsServerUrl() { final var property = System.getProperty("faststats.flags-server"); - try { - return property != null ? new URI(property) : defaultUrl; + if (property != null) try { + return new URI(property); } catch (final URISyntaxException e) { logger.error("Failed to parse flags server url: %s", e, property); - return defaultUrl; } + return URI.create("https://flags.faststats.dev/v1"); } @SuppressWarnings("unchecked") From 10bec1abc486025ada7eaa7f99327f18a84567d5 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 20:36:08 +0200 Subject: [PATCH 28/40] Add `getLogger(Class)` overload --- .../main/java/dev/faststats/core/internal/LoggerFactory.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/main/java/dev/faststats/core/internal/LoggerFactory.java b/core/src/main/java/dev/faststats/core/internal/LoggerFactory.java index 64b63c2..5a4a1af 100644 --- a/core/src/main/java/dev/faststats/core/internal/LoggerFactory.java +++ b/core/src/main/java/dev/faststats/core/internal/LoggerFactory.java @@ -12,5 +12,9 @@ final class Holder { return Holder.INSTANCE; } + default Logger getLogger(final Class clazz) { + return getLogger(clazz.getName()); + } + Logger getLogger(String name); } From 7925c678d5143dbd13fb5e3b4203b7366f49fc7d Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 20:36:35 +0200 Subject: [PATCH 29/40] Decouple metrics and feature flags --- .../main/java/dev/faststats/core/Metrics.java | 20 ------------------- .../dev/faststats/core/SimpleMetrics.java | 17 ---------------- 2 files changed, 37 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/Metrics.java b/core/src/main/java/dev/faststats/core/Metrics.java index d5b65d2..d931cc2 100644 --- a/core/src/main/java/dev/faststats/core/Metrics.java +++ b/core/src/main/java/dev/faststats/core/Metrics.java @@ -1,7 +1,6 @@ package dev.faststats.core; import dev.faststats.core.data.Metric; -import dev.faststats.core.flags.FeatureFlagService; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; @@ -32,15 +31,6 @@ public interface Metrics { @Contract(pure = true) Optional getErrorTracker(); - /** - * Get the feature flag service for this metrics instance. - * - * @return the feature flag service - * @since 0.23.0 - */ - @Contract(pure = true) - Optional getFeatureFlagService(); - /** * Get the metrics configuration. * @@ -116,16 +106,6 @@ interface Factory> { @Contract(mutates = "this") F errorTracker(ErrorTracker tracker); - /** - * Sets the feature flag service for this metrics instance. - * - * @param service the feature flag service - * @return the metrics factory - * @since 0.23.0 - */ - @Contract(mutates = "this") - F featureFlagService(FeatureFlagService service); - /** * Sets the token used to authenticate with the metrics server and identify the project. *

diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 37d93af..91a0d24 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -2,7 +2,6 @@ import com.google.gson.JsonObject; import dev.faststats.core.data.Metric; -import dev.faststats.core.flags.FeatureFlagService; import dev.faststats.core.internal.Constants; import dev.faststats.core.internal.Logger; import dev.faststats.core.internal.LoggerFactory; @@ -49,7 +48,6 @@ public abstract class SimpleMetrics implements Metrics { private final @Token String token; private final @Nullable ErrorTracker tracker; private final @Nullable Runnable flush; - private final @Nullable FeatureFlagService flagService; @Contract(mutates = "io") @SuppressWarnings("PatternValidation") @@ -63,7 +61,6 @@ protected SimpleMetrics(final Factory factory, final Config config) throws this.logger.setFilter(level -> debug || level.equals(Level.CONFIG)); this.tracker = config.errorTracking() ? factory.tracker : null; this.flush = factory.flush; - this.flagService = factory.flagService; this.url = getMetricsServerUrl(); } @@ -291,11 +288,6 @@ public Optional getErrorTracker() { return Optional.ofNullable(tracker); } - @Override - public Optional getFeatureFlagService() { - return Optional.ofNullable(flagService); - } - @Override public dev.faststats.core.Config getConfig() { return config; @@ -306,7 +298,6 @@ public dev.faststats.core.Config getConfig() { @Override public void shutdown() { - if (flagService != null) flagService.shutdown(); getErrorTracker().ifPresent(ErrorTracker::detachErrorContext); if (executor != null) try { logger.info("Shutting down metrics submission"); @@ -324,7 +315,6 @@ public void shutdown() { public abstract static class Factory> implements Metrics.Factory { private final Set> metrics = new HashSet<>(0); private @Nullable ErrorTracker tracker; - private @Nullable FeatureFlagService flagService; private @Nullable Runnable flush; private @Nullable String token; @@ -349,13 +339,6 @@ public F errorTracker(final ErrorTracker tracker) { return (F) this; } - @Override - @SuppressWarnings("unchecked") - public F featureFlagService(final FeatureFlagService service) { - this.flagService = service; - return (F) this; - } - @Override @SuppressWarnings("unchecked") public F token(@Token final String token) throws IllegalArgumentException { From 9fb8104872b846910dacdba0811f05b20baf3355 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 20:59:46 +0200 Subject: [PATCH 30/40] Cancel all running fetches on shutdown --- .../dev/faststats/core/flags/SimpleFeatureFlagService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index 5d26985..8597a56 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -216,8 +216,9 @@ public Duration getTTL() { @Override public void shutdown() { - cache.clear(); - fetchTimes.clear(); + fetchesInProgress.values().forEach(fetch -> fetch.cancel(true)); fetchesInProgress.clear(); + fetchTimes.clear(); + cache.clear(); } } From 2c8d212c89889a9e82ab209bf522e00d123e45b8 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 21:46:24 +0200 Subject: [PATCH 31/40] Retrieve server id from config --- .../faststats/core/flags/SimpleFeatureFlagService.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index 8597a56..f361279 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -5,6 +5,8 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; +import dev.faststats.core.Config; +import dev.faststats.core.SimpleConfig; import dev.faststats.core.Token; import dev.faststats.core.internal.Logger; import dev.faststats.core.internal.LoggerFactory; @@ -15,11 +17,11 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.file.Path; import java.time.Duration; import java.time.Instant; import java.util.Map; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -31,6 +33,7 @@ final class SimpleFeatureFlagService implements FeatureFlagService { .connectTimeout(Duration.ofSeconds(3)) .version(HttpClient.Version.HTTP_1_1) .build(); + private final Config config = SimpleConfig.read(Path.of("plugins/faststats")); // todo: DI config or just server id? private final @Token String token; private final @Nullable Attributes attributes; @@ -88,7 +91,7 @@ CompletableFuture optOut(final SimpleFeatureFlag flag) { private CompletableFuture sendOptRequest(final SimpleFeatureFlag flag, final String path) { final var requestBody = new JsonObject(); - requestBody.addProperty("serverId", UUID.randomUUID().toString()); // todo: read from config + requestBody.addProperty("serverId", config.serverId().toString()); requestBody.addProperty("flag", flag.getId()); final var request = HttpRequest.newBuilder() @@ -127,7 +130,7 @@ boolean isExpired(final SimpleFeatureFlag flag) { private CompletableFuture createFetch(final SimpleFeatureFlag flag) { final var requestBody = new JsonObject(); - requestBody.addProperty("serverId", UUID.randomUUID().toString()); // todo: read from config + requestBody.addProperty("serverId", config.serverId().toString()); requestBody.addProperty("key", flag.getId()); final var attributes = new JsonObject(); From 9ed05021fc8f32544687f5779fc93ad0f5158e28 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 21:46:31 +0200 Subject: [PATCH 32/40] Unseal config --- core/src/main/java/dev/faststats/core/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/dev/faststats/core/Config.java b/core/src/main/java/dev/faststats/core/Config.java index 1fedf52..f1e39f5 100644 --- a/core/src/main/java/dev/faststats/core/Config.java +++ b/core/src/main/java/dev/faststats/core/Config.java @@ -9,7 +9,7 @@ * * @since 0.23.0 */ -public sealed interface Config permits SimpleConfig { +public interface Config { /** * The server id. * From f4f9a69df80efd1448a1fc342e2ea66568f04a6c Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 21:46:45 +0200 Subject: [PATCH 33/40] Update config comment --- core/src/main/java/dev/faststats/core/SimpleConfig.java | 5 ++--- .../main/java/dev/faststats/sponge/SpongeMetricsImpl.java | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleConfig.java b/core/src/main/java/dev/faststats/core/SimpleConfig.java index fdb7c11..b515c03 100644 --- a/core/src/main/java/dev/faststats/core/SimpleConfig.java +++ b/core/src/main/java/dev/faststats/core/SimpleConfig.java @@ -27,15 +27,14 @@ public record SimpleConfig( ) implements Config { public static final String DEFAULT_COMMENT = """ - FastStats (https://faststats.dev) collects anonymous usage statistics for developers. + FastStats (https://faststats.dev) collects anonymous usage statistics. # This helps developers understand how their projects are used in the real world. # # No IP addresses, player data, or personal information is collected. # The server ID below is randomly generated and can be regenerated at any time. # # Enabling metrics has no noticeable performance impact. - # Keeping metrics enabled is recommended, but you can opt out by setting - # 'enabled=false' in faststats/config.properties. + # Keeping metrics enabled is recommended, but you can opt out by setting 'enabled=false'. # # If you suspect a developer is collecting personal data or bypassing the "enabled" option, # please report it at: https://faststats.dev/abuse diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java index 5aea55f..c47e4bf 100644 --- a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java @@ -15,7 +15,7 @@ final class SpongeMetricsImpl extends SimpleMetrics implements SpongeMetrics { public static final String COMMENT = """ - FastStats (https://faststats.dev) collects anonymous usage statistics for plugin developers. + FastStats (https://faststats.dev) collects anonymous usage statistics. # This helps developers understand how their projects are used in the real world. # # No IP addresses, player data, or personal information is collected. @@ -25,7 +25,7 @@ final class SpongeMetricsImpl extends SimpleMetrics implements SpongeMetrics { # Enabling metrics is recommended, you can do so in the Sponge metrics.config, # by setting the "global-state" property to "TRUE". # - # If you suspect a plugin is collecting personal data or bypassing the Sponge config, + # If you suspect a developer is collecting personal data or bypassing the Sponge config, # please report it at: https://faststats.dev/abuse # # For more information, visit: https://faststats.dev/info From 95e1aabf2b95a9b69ed9a9aa26aa746e9ee948b1 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 21:48:24 +0200 Subject: [PATCH 34/40] Very elegant but sounds stupid --- core/src/main/java/dev/faststats/core/SimpleMetrics.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 91a0d24..6953b21 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -100,7 +100,7 @@ protected SimpleMetrics( protected String getOnboardingMessage() { return """ - This piece of software uses FastStats to collect anonymous usage statistics. + This plugin uses FastStats to collect anonymous usage statistics. No personal or identifying information is ever collected. To opt out, set 'enabled=false' in the metrics configuration file. Learn more at: https://faststats.dev/info From 70af53f22c2f99f7424d56e3399973208a43cf2e Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 21:49:01 +0200 Subject: [PATCH 35/40] Prepare for config impl extraction --- .../dev/faststats/core/SimpleMetrics.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 6953b21..1354034 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -44,7 +44,7 @@ public abstract class SimpleMetrics implements Metrics { private final URI url; private final Set> metrics; - private final SimpleConfig config; + private final Config config; private final @Token String token; private final @Nullable ErrorTracker tracker; private final @Nullable Runnable flush; @@ -54,7 +54,7 @@ public abstract class SimpleMetrics implements Metrics { protected SimpleMetrics(final Factory factory, final Config config) throws IllegalStateException { if (factory.token == null) throw new IllegalStateException("Token must be specified"); - this.config = (SimpleConfig) config; + this.config = config; this.token = factory.token; this.metrics = config.additionalMetrics() ? Set.copyOf(factory.metrics) : Set.of(); final var debug = config.debug() || Boolean.getBoolean("faststats.debug"); @@ -90,7 +90,7 @@ protected SimpleMetrics( final boolean debug ) { this.metrics = config.additionalMetrics() ? Set.copyOf(metrics) : Set.of(); - this.config = (SimpleConfig) config; + this.config = config; this.logger.setLevel(debug ? Level.ALL : Level.OFF); this.token = token; this.tracker = tracker; @@ -124,10 +124,11 @@ protected void startSubmitting() { } @SuppressWarnings("PatternValidation") - private void startSubmitting(final long initialDelay, final long period, final TimeUnit unit) { + protected boolean preSubmissionStart() { + /* if (Boolean.getBoolean("faststats.first-run")) { logger.info("Skipping metrics submission due to first-run flag"); - return; + return false; } if (config.firstRun()) { @@ -140,8 +141,14 @@ private void startSubmitting(final long initialDelay, final long period, final T logger.info("-".repeat(separatorLength)); System.setProperty("faststats.first-run", "true"); - if (!config.externallyManaged()) return; + if (!config.externallyManaged()) return false; } + */ + return true; // todo: move to config module? + } + + private void startSubmitting(final long initialDelay, final long period, final TimeUnit unit) { + if (!preSubmissionStart()) return; final var enabled = Boolean.parseBoolean(System.getProperty("faststats.enabled", "true")); From f82feb72cd6e7a749990dc439883e9806ca12602 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 21:49:09 +0200 Subject: [PATCH 36/40] todo --- core/src/main/java/dev/faststats/core/SimpleErrorTracker.java | 2 +- .../src/main/java/dev/faststats/fabric/FabricMetricsImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java b/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java index 0b2b757..3a72d7d 100644 --- a/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java +++ b/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java @@ -60,7 +60,7 @@ public void trackError(final Throwable error, final boolean handled) { try { if (isIgnored(error, Collections.newSetFromMap(new IdentityHashMap<>()))) return; final var compiled = ErrorHelper.compile(error, null, handled, anonymizationEntries); - final var hashed = MurmurHash3.hash(compiled); + final var hashed = MurmurHash3.hash(compiled); // todo: replace with minimization and normalization algorithm if (collected.compute(hashed, (k, v) -> { return v == null ? 1 : v + 1; }) > 1) return; diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java b/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java index 5195fa7..35b443b 100644 --- a/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java +++ b/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java @@ -27,7 +27,7 @@ private FabricMetricsImpl(final Factory factory, final ModContainer mod, final P this.mod = mod; - ServerLifecycleEvents.SERVER_STARTED.register(server -> { + ServerLifecycleEvents.SERVER_STARTED.register(server -> { // todo: client support this.server = server; startSubmitting(); }); From c3de6819622881f810b7553f88d81a711f36594b Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 22:15:53 +0200 Subject: [PATCH 37/40] Extract config impl to separate module --- bukkit/build.gradle.kts | 1 + .../java/dev/faststats/bukkit/BukkitMetricsImpl.java | 3 ++- bukkit/src/main/java/module-info.java | 3 ++- bungeecord/build.gradle.kts | 1 + .../java/dev/faststats/bungee/BungeeMetricsImpl.java | 3 ++- bungeecord/src/main/java/module-info.java | 3 ++- config/build.gradle.kts | 3 +++ .../main/java/dev/faststats/config}/SimpleConfig.java | 3 ++- config/src/main/java/module-info.java | 11 +++++++++++ core/build.gradle.kts | 1 + .../main/java/dev/faststats/core/SimpleMetrics.java | 6 ------ .../core/flags/SimpleFeatureFlagService.java | 11 +++++------ core/src/test/java/dev/faststats/MockMetrics.java | 2 +- fabric/build.gradle.kts | 1 + .../java/dev/faststats/fabric/FabricMetricsImpl.java | 3 ++- fabric/src/main/java/module-info.java | 3 ++- hytale/build.gradle.kts | 1 + .../java/dev/faststats/hytale/HytaleMetricsImpl.java | 3 ++- hytale/src/main/java/module-info.java | 1 + minestom/build.gradle.kts | 3 ++- .../dev/faststats/minestom/MinestomMetricsImpl.java | 3 ++- minestom/src/main/java/module-info.java | 3 ++- nukkit/build.gradle.kts | 1 + .../java/dev/faststats/nukkit/NukkitMetricsImpl.java | 3 ++- nukkit/src/main/java/module-info.java | 3 ++- settings.gradle.kts | 1 + sponge/build.gradle.kts | 1 + .../java/dev/faststats/sponge/SpongeMetricsImpl.java | 2 +- sponge/src/main/java/module-info.java | 3 ++- velocity/build.gradle.kts | 1 + .../dev/faststats/velocity/VelocityMetricsImpl.java | 3 ++- velocity/src/main/java/module-info.java | 3 ++- 32 files changed, 63 insertions(+), 30 deletions(-) create mode 100644 config/build.gradle.kts rename {core/src/main/java/dev/faststats/core => config/src/main/java/dev/faststats/config}/SimpleConfig.java (98%) create mode 100644 config/src/main/java/module-info.java diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index 10ff4af..dce2e74 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -14,5 +14,6 @@ configurations.compileClasspath { dependencies { api(project(":core")) + implementation(project(":config")) compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") } diff --git a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java index 4222d8f..584fd79 100644 --- a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java +++ b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java @@ -1,6 +1,7 @@ package dev.faststats.bukkit; import com.google.gson.JsonObject; +import dev.faststats.config.SimpleConfig; import dev.faststats.core.SimpleMetrics; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.Async; @@ -21,7 +22,7 @@ final class BukkitMetricsImpl extends SimpleMetrics implements BukkitMetrics { @Contract(mutates = "io") @SuppressWarnings({"deprecation", "Convert2MethodRef"}) private BukkitMetricsImpl(final Factory factory, final Plugin plugin, final Path config) throws IllegalStateException { - super(factory, config); + super(factory, SimpleConfig.read(config)); this.plugin = plugin; final var server = plugin.getServer(); diff --git a/bukkit/src/main/java/module-info.java b/bukkit/src/main/java/module-info.java index afc285b..d8eced3 100644 --- a/bukkit/src/main/java/module-info.java +++ b/bukkit/src/main/java/module-info.java @@ -5,10 +5,11 @@ exports dev.faststats.bukkit; requires com.google.gson; + requires dev.faststats.config; requires dev.faststats.core; requires java.logging; requires org.bukkit; requires static org.jetbrains.annotations; requires static org.jspecify; -} \ No newline at end of file +} diff --git a/bungeecord/build.gradle.kts b/bungeecord/build.gradle.kts index ed20e02..7bf56fa 100644 --- a/bungeecord/build.gradle.kts +++ b/bungeecord/build.gradle.kts @@ -6,5 +6,6 @@ repositories { dependencies { api(project(":core")) + implementation(project(":config")) compileOnly("net.md-5:bungeecord-api:26.1-R0.1-SNAPSHOT") } diff --git a/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java b/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java index ba70bc0..ec21011 100644 --- a/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java +++ b/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java @@ -1,6 +1,7 @@ package dev.faststats.bungee; import com.google.gson.JsonObject; +import dev.faststats.config.SimpleConfig; import dev.faststats.core.Metrics; import dev.faststats.core.SimpleMetrics; import net.md_5.bungee.api.ProxyServer; @@ -17,7 +18,7 @@ final class BungeeMetricsImpl extends SimpleMetrics implements BungeeMetrics { @Async.Schedule @Contract(mutates = "io") private BungeeMetricsImpl(final Factory factory, final Plugin plugin, final Path config) throws IllegalStateException { - super(factory, config); + super(factory, SimpleConfig.read(config)); this.server = plugin.getProxy(); this.plugin = plugin; diff --git a/bungeecord/src/main/java/module-info.java b/bungeecord/src/main/java/module-info.java index 5764d13..8380b46 100644 --- a/bungeecord/src/main/java/module-info.java +++ b/bungeecord/src/main/java/module-info.java @@ -5,9 +5,10 @@ exports dev.faststats.bungee; requires com.google.gson; + requires dev.faststats.config; requires dev.faststats.core; requires java.logging; requires static org.jetbrains.annotations; requires static org.jspecify; -} \ No newline at end of file +} diff --git a/config/build.gradle.kts b/config/build.gradle.kts new file mode 100644 index 0000000..e762f00 --- /dev/null +++ b/config/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + compileOnly(project(":core")) +} diff --git a/core/src/main/java/dev/faststats/core/SimpleConfig.java b/config/src/main/java/dev/faststats/config/SimpleConfig.java similarity index 98% rename from core/src/main/java/dev/faststats/core/SimpleConfig.java rename to config/src/main/java/dev/faststats/config/SimpleConfig.java index b515c03..7657623 100644 --- a/core/src/main/java/dev/faststats/core/SimpleConfig.java +++ b/config/src/main/java/dev/faststats/config/SimpleConfig.java @@ -1,5 +1,6 @@ -package dev.faststats.core; +package dev.faststats.config; +import dev.faststats.core.Config; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; diff --git a/config/src/main/java/module-info.java b/config/src/main/java/module-info.java new file mode 100644 index 0000000..a37c649 --- /dev/null +++ b/config/src/main/java/module-info.java @@ -0,0 +1,11 @@ +import org.jspecify.annotations.NullMarked; + +@NullMarked +module dev.faststats.config { + exports dev.faststats.config; + + requires dev.faststats.core; + + requires static org.jetbrains.annotations; + requires static org.jspecify; +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index c87a367..92b7c0a 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -3,6 +3,7 @@ dependencies { compileOnlyApi("org.jetbrains:annotations:26.1.0") compileOnlyApi("org.jspecify:jspecify:1.0.0") + testImplementation(project(":config")) testImplementation("com.google.code.gson:gson:2.13.2") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation(platform("org.junit:junit-bom:6.1.0-M1")) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 1354034..b1501a3 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -20,7 +20,6 @@ import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.nio.file.Path; import java.time.Duration; import java.util.HashSet; import java.util.Optional; @@ -74,11 +73,6 @@ private URI getMetricsServerUrl() { return URI.create("https://metrics.faststats.dev/v1/collect"); } - @Contract(mutates = "io") - protected SimpleMetrics(final Factory factory, final Path config) throws IllegalStateException { - this(factory, SimpleConfig.read(config)); - } - @VisibleForTesting protected SimpleMetrics( final Config config, diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java index f361279..c439117 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java @@ -5,8 +5,6 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; -import dev.faststats.core.Config; -import dev.faststats.core.SimpleConfig; import dev.faststats.core.Token; import dev.faststats.core.internal.Logger; import dev.faststats.core.internal.LoggerFactory; @@ -17,11 +15,11 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.nio.file.Path; import java.time.Duration; import java.time.Instant; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -33,7 +31,7 @@ final class SimpleFeatureFlagService implements FeatureFlagService { .connectTimeout(Duration.ofSeconds(3)) .version(HttpClient.Version.HTTP_1_1) .build(); - private final Config config = SimpleConfig.read(Path.of("plugins/faststats")); // todo: DI config or just server id? + private final UUID serverId; private final @Token String token; private final @Nullable Attributes attributes; @@ -52,6 +50,7 @@ final class SimpleFeatureFlagService implements FeatureFlagService { this.token = token; this.attributes = attributes; this.ttl = ttl; + this.serverId = UUID.randomUUID(); // todo: DI somehow } private static URI getFlagsServerUrl() { @@ -91,7 +90,7 @@ CompletableFuture optOut(final SimpleFeatureFlag flag) { private CompletableFuture sendOptRequest(final SimpleFeatureFlag flag, final String path) { final var requestBody = new JsonObject(); - requestBody.addProperty("serverId", config.serverId().toString()); + requestBody.addProperty("serverId", serverId.toString()); requestBody.addProperty("flag", flag.getId()); final var request = HttpRequest.newBuilder() @@ -130,7 +129,7 @@ boolean isExpired(final SimpleFeatureFlag flag) { private CompletableFuture createFetch(final SimpleFeatureFlag flag) { final var requestBody = new JsonObject(); - requestBody.addProperty("serverId", config.serverId().toString()); + requestBody.addProperty("serverId", serverId.toString()); requestBody.addProperty("key", flag.getId()); final var attributes = new JsonObject(); diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index 87494bd..2a82ac1 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -1,8 +1,8 @@ package dev.faststats; import com.google.gson.JsonObject; +import dev.faststats.config.SimpleConfig; import dev.faststats.core.ErrorTracker; -import dev.faststats.core.SimpleConfig; import dev.faststats.core.SimpleMetrics; import dev.faststats.core.Token; import org.jspecify.annotations.NullMarked; diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index d9caf07..4b1a38f 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { api(project(":core")) + implementation(project(":config")) mappings(loom.officialMojangMappings()) minecraft("com.mojang:minecraft:1.21.11") modCompileOnly("net.fabricmc.fabric-api:fabric-api:0.139.4+1.21.11") diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java b/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java index 35b443b..93d90c1 100644 --- a/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java +++ b/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java @@ -1,6 +1,7 @@ package dev.faststats.fabric; import com.google.gson.JsonObject; +import dev.faststats.config.SimpleConfig; import dev.faststats.core.Metrics; import dev.faststats.core.SimpleMetrics; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; @@ -23,7 +24,7 @@ final class FabricMetricsImpl extends SimpleMetrics implements FabricMetrics { @Async.Schedule @Contract(mutates = "io") private FabricMetricsImpl(final Factory factory, final ModContainer mod, final Path config) throws IllegalStateException { - super(factory, config); + super(factory, SimpleConfig.read(config)); this.mod = mod; diff --git a/fabric/src/main/java/module-info.java b/fabric/src/main/java/module-info.java index c6601e4..d4d6554 100644 --- a/fabric/src/main/java/module-info.java +++ b/fabric/src/main/java/module-info.java @@ -5,10 +5,11 @@ exports dev.faststats.fabric; requires com.google.gson; + requires dev.faststats.config; requires dev.faststats.core; requires net.fabricmc.loader; requires org.slf4j; requires static org.jetbrains.annotations; requires static org.jspecify; -} \ No newline at end of file +} diff --git a/hytale/build.gradle.kts b/hytale/build.gradle.kts index fdada47..f7c947f 100644 --- a/hytale/build.gradle.kts +++ b/hytale/build.gradle.kts @@ -6,5 +6,6 @@ repositories { dependencies { api(project(":core")) + implementation(project(":config")) compileOnly("com.hypixel.hytale:Server:2026.04.17-c2d518cc9") } diff --git a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java index 8a6cba2..1cd5c95 100644 --- a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java +++ b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java @@ -4,6 +4,7 @@ import com.hypixel.hytale.server.core.HytaleServer; import com.hypixel.hytale.server.core.plugin.JavaPlugin; import com.hypixel.hytale.server.core.universe.Universe; +import dev.faststats.config.SimpleConfig; import dev.faststats.core.Metrics; import dev.faststats.core.SimpleMetrics; import org.jetbrains.annotations.Async; @@ -15,7 +16,7 @@ final class HytaleMetricsImpl extends SimpleMetrics implements HytaleMetrics { @Async.Schedule @Contract(mutates = "io") private HytaleMetricsImpl(final Factory factory, final Path config) throws IllegalStateException { - super(factory, config); + super(factory, SimpleConfig.read(config)); startSubmitting(); } diff --git a/hytale/src/main/java/module-info.java b/hytale/src/main/java/module-info.java index ce31562..5fa387f 100644 --- a/hytale/src/main/java/module-info.java +++ b/hytale/src/main/java/module-info.java @@ -5,6 +5,7 @@ exports dev.faststats.hytale; requires com.google.gson; + requires dev.faststats.config; requires dev.faststats.core; requires java.logging; diff --git a/minestom/build.gradle.kts b/minestom/build.gradle.kts index dead07b..caebdea 100644 --- a/minestom/build.gradle.kts +++ b/minestom/build.gradle.kts @@ -1,4 +1,5 @@ dependencies { api(project(":core")) + implementation(project(":config")) compileOnly("net.minestom:minestom:2026.04.13-1.21.11") -} \ No newline at end of file +} diff --git a/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java b/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java index 91bc04e..6dc6e41 100644 --- a/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java +++ b/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java @@ -1,6 +1,7 @@ package dev.faststats.minestom; import com.google.gson.JsonObject; +import dev.faststats.config.SimpleConfig; import dev.faststats.core.ErrorTracker; import dev.faststats.core.Metrics; import dev.faststats.core.SimpleMetrics; @@ -15,7 +16,7 @@ final class MinestomMetricsImpl extends SimpleMetrics implements MinestomMetrics @Async.Schedule @Contract(mutates = "io") private MinestomMetricsImpl(final Factory factory, final Path config) throws IllegalStateException { - super(factory, config); + super(factory, SimpleConfig.read(config)); startSubmitting(); } diff --git a/minestom/src/main/java/module-info.java b/minestom/src/main/java/module-info.java index 629f4d5..ff84749 100644 --- a/minestom/src/main/java/module-info.java +++ b/minestom/src/main/java/module-info.java @@ -5,10 +5,11 @@ exports dev.faststats.minestom; requires com.google.gson; + requires dev.faststats.config; requires dev.faststats.core; requires net.minestom.server; requires org.slf4j; requires static org.jetbrains.annotations; requires static org.jspecify; -} \ No newline at end of file +} diff --git a/nukkit/build.gradle.kts b/nukkit/build.gradle.kts index af632e8..d4a090a 100644 --- a/nukkit/build.gradle.kts +++ b/nukkit/build.gradle.kts @@ -7,5 +7,6 @@ repositories { dependencies { api(project(":core")) + implementation(project(":config")) compileOnly("cn.nukkit:nukkit:1.0-SNAPSHOT") } diff --git a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java index 80bec0d..9d6037b 100644 --- a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java +++ b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java @@ -3,6 +3,7 @@ import cn.nukkit.Server; import cn.nukkit.plugin.PluginBase; import com.google.gson.JsonObject; +import dev.faststats.config.SimpleConfig; import dev.faststats.core.Metrics; import dev.faststats.core.SimpleMetrics; import org.jetbrains.annotations.Async; @@ -19,7 +20,7 @@ final class NukkitMetricsImpl extends SimpleMetrics implements NukkitMetrics { @Async.Schedule @Contract(mutates = "io") private NukkitMetricsImpl(final Factory factory, final PluginBase plugin, final Path config) throws IllegalStateException { - super(factory, config); + super(factory, SimpleConfig.read(config)); this.server = plugin.getServer(); this.plugin = plugin; diff --git a/nukkit/src/main/java/module-info.java b/nukkit/src/main/java/module-info.java index b7b0b2b..1b104b5 100644 --- a/nukkit/src/main/java/module-info.java +++ b/nukkit/src/main/java/module-info.java @@ -5,8 +5,9 @@ exports dev.faststats.nukkit; requires com.google.gson; + requires dev.faststats.config; requires dev.faststats.core; requires static org.jetbrains.annotations; requires static org.jspecify; -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index d944992..402a4cd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,7 @@ include("bukkit") include("bukkit:example-plugin") include("bungeecord") include("bungeecord:example-plugin") +include("config") include("core") include("core:example") include("fabric") diff --git a/sponge/build.gradle.kts b/sponge/build.gradle.kts index 06fbed9..ee394e7 100644 --- a/sponge/build.gradle.kts +++ b/sponge/build.gradle.kts @@ -6,5 +6,6 @@ repositories { dependencies { api(project(":core")) + implementation(project(":config")) compileOnly("org.spongepowered:spongeapi:8.3.0-SNAPSHOT") } diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java index c47e4bf..1db17d6 100644 --- a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java @@ -1,8 +1,8 @@ package dev.faststats.sponge; import com.google.gson.JsonObject; +import dev.faststats.config.SimpleConfig; import dev.faststats.core.Metrics; -import dev.faststats.core.SimpleConfig; import dev.faststats.core.SimpleMetrics; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Async; diff --git a/sponge/src/main/java/module-info.java b/sponge/src/main/java/module-info.java index b01a156..8eb72fc 100644 --- a/sponge/src/main/java/module-info.java +++ b/sponge/src/main/java/module-info.java @@ -6,8 +6,9 @@ requires com.google.gson; requires com.google.guice; + requires dev.faststats.config; requires dev.faststats.core; requires static org.jetbrains.annotations; requires static org.jspecify; -} \ No newline at end of file +} diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 74da85b..ef8247d 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -4,5 +4,6 @@ repositories { dependencies { api(project(":core")) + implementation(project(":config")) compileOnly("com.velocitypowered:velocity-api:3.5.0-SNAPSHOT") } diff --git a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java index dbfb60b..fadfa55 100644 --- a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java +++ b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java @@ -4,6 +4,7 @@ import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; +import dev.faststats.config.SimpleConfig; import dev.faststats.core.Metrics; import dev.faststats.core.SimpleMetrics; import org.jetbrains.annotations.Async; @@ -25,7 +26,7 @@ private VelocityMetricsImpl( final Path config, final PluginContainer plugin ) throws IllegalStateException { - super(factory, config); + super(factory, SimpleConfig.read(config)); this.server = server; this.plugin = plugin; diff --git a/velocity/src/main/java/module-info.java b/velocity/src/main/java/module-info.java index 2855dcb..77b01de 100644 --- a/velocity/src/main/java/module-info.java +++ b/velocity/src/main/java/module-info.java @@ -7,9 +7,10 @@ requires com.google.gson; requires com.google.guice; requires com.velocitypowered.api; + requires dev.faststats.config; requires dev.faststats.core; requires org.slf4j; requires static org.jetbrains.annotations; requires static org.jspecify; -} \ No newline at end of file +} From 4cd2c0e1a13a159f2edf4b1bc3d2804b708f930e Mon Sep 17 00:00:00 2001 From: david Date: Sun, 19 Apr 2026 22:17:20 +0200 Subject: [PATCH 38/40] Update plugin application code --- build.gradle.kts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 668852d..8949609 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,14 +17,16 @@ val javaVersionsOverride = mapOf( val defaultJavaVersion = 17 subprojects { - apply(plugin = "java") - apply(plugin = "java-library") + apply { + plugin("java") + plugin("java-library") + } - val example = project.name.startsWith("example") - if (example) { - apply(plugin = "com.gradleup.shadow") + val noPublish = project.name.startsWith("example") || project.name != "config" + if (noPublish) { + apply { plugin("com.gradleup.shadow") } } else { - apply(plugin = "maven-publish") + apply { plugin("maven-publish") } } group = "dev.faststats.metrics" @@ -94,7 +96,7 @@ subprojects { } afterEvaluate { - if (example) return@afterEvaluate + if (noPublish) return@afterEvaluate extensions.configure { publications.create("maven") { artifactId = project.name From 1f25e80a21ff02367c65fb143dd4fd8c709986d4 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 20 Apr 2026 19:33:06 +0200 Subject: [PATCH 39/40] Refactor config handling --- .../faststats/bukkit/BukkitMetricsImpl.java | 5 + .../faststats/bungee/BungeeMetricsImpl.java | 5 + .../dev/faststats/config/SimpleConfig.java | 64 +++++--- config/src/main/java/module-info.java | 1 + .../dev/faststats/core/SimpleMetrics.java | 35 +---- .../test/java/dev/faststats/MockMetrics.java | 7 +- .../faststats/fabric/FabricMetricsImpl.java | 5 + .../faststats/hytale/HytaleMetricsImpl.java | 5 + .../minestom/MinestomMetricsImpl.java | 5 + .../faststats/nukkit/NukkitMetricsImpl.java | 5 + sponge/build.gradle.kts | 1 - .../dev/faststats/sponge/SpongeConfig.java | 141 ++++++++++++++++++ .../faststats/sponge/SpongeMetricsImpl.java | 29 +--- sponge/src/main/java/module-info.java | 2 +- .../velocity/VelocityMetricsImpl.java | 5 + 15 files changed, 230 insertions(+), 85 deletions(-) create mode 100644 sponge/src/main/java/dev/faststats/sponge/SpongeConfig.java diff --git a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java index 584fd79..0168505 100644 --- a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java +++ b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java @@ -62,6 +62,11 @@ private boolean isProxyOnlineMode() { return settings.getBoolean("bungeecord") && proxies.getBoolean("bungee-cord.online-mode"); } + @Override + protected boolean preSubmissionStart() { + return ((SimpleConfig) getConfig()).preSubmissionStart(); + } + @Override protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("minecraft_version", minecraftVersion); diff --git a/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java b/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java index ec21011..e71045e 100644 --- a/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java +++ b/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java @@ -26,6 +26,11 @@ private BungeeMetricsImpl(final Factory factory, final Plugin plugin, final Path startSubmitting(); } + @Override + protected boolean preSubmissionStart() { + return ((SimpleConfig) getConfig()).preSubmissionStart(); + } + @Override protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("online_mode", server.getConfig().isOnlineMode()); diff --git a/config/src/main/java/dev/faststats/config/SimpleConfig.java b/config/src/main/java/dev/faststats/config/SimpleConfig.java index 7657623..e50548c 100644 --- a/config/src/main/java/dev/faststats/config/SimpleConfig.java +++ b/config/src/main/java/dev/faststats/config/SimpleConfig.java @@ -1,6 +1,7 @@ package dev.faststats.config; import dev.faststats.core.Config; +import dev.faststats.core.internal.LoggerFactory; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; @@ -13,6 +14,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiPredicate; +import java.util.logging.Level; import static java.nio.charset.StandardCharsets.UTF_8; @@ -23,11 +25,10 @@ public record SimpleConfig( boolean debug, boolean enabled, boolean errorTracking, - boolean firstRun, - boolean externallyManaged + boolean firstRun ) implements Config { - public static final String DEFAULT_COMMENT = """ + public static final String COMMENT = """ FastStats (https://faststats.dev) collects anonymous usage statistics. # This helps developers understand how their projects are used in the real world. # @@ -42,14 +43,17 @@ public record SimpleConfig( # # For more information, visit: https://faststats.dev/info """; + private static final String ONBOARDING_MESSAGE = """ + This plugin uses FastStats to collect anonymous usage statistics. + No personal or identifying information is ever collected. + To opt out, set 'enabled=false' in the metrics configuration file. + Learn more at: https://faststats.dev/info + + Since this is your first start with FastStats, metrics submission will not start + until you restart the server to allow you to opt out if you prefer."""; @Contract(mutates = "io") public static SimpleConfig read(final Path file) throws RuntimeException { - return read(file, DEFAULT_COMMENT, false, false); - } - - @Contract(mutates = "io") - public static SimpleConfig read(final Path file, final String comment, final boolean externallyManaged, final boolean externallyEnabled) throws RuntimeException { final var properties = readOrEmpty(file); final var firstRun = properties.isEmpty(); final var saveConfig = new AtomicBoolean(firstRun); @@ -76,18 +80,30 @@ public static SimpleConfig read(final Path file, final String comment, final boo }); }; - final var enabled = externallyManaged ? externallyEnabled : predicate.test("enabled", true); + final var enabled = predicate.test("enabled", true); final var errorTracking = predicate.test("submitErrors", true); final var additionalMetrics = predicate.test("submitAdditionalMetrics", true); final var debug = predicate.test("debug", false); if (saveConfig.get()) try { - save(file, externallyManaged, comment, serverId, enabled, errorTracking, additionalMetrics, debug); + Files.createDirectories(file.getParent()); + try (final var out = Files.newOutputStream(file); + final var writer = new OutputStreamWriter(out, UTF_8)) { + final var store = new Properties(); + + store.setProperty("serverId", serverId.toString()); + store.setProperty("enabled", Boolean.toString(enabled)); + store.setProperty("submitErrors", Boolean.toString(errorTracking)); + store.setProperty("submitAdditionalMetrics", Boolean.toString(additionalMetrics)); + store.setProperty("debug", Boolean.toString(debug)); + + store.store(writer, COMMENT); + } } catch (final IOException e) { throw new RuntimeException("Failed to save metrics config", e); } - return new SimpleConfig(serverId, additionalMetrics, debug, enabled, errorTracking, firstRun, externallyManaged); + return new SimpleConfig(serverId, additionalMetrics, debug, enabled, errorTracking, firstRun); } private static Optional readOrEmpty(final Path file) throws RuntimeException { @@ -101,19 +117,23 @@ private static Optional readOrEmpty(final Path file) throws RuntimeE } } - private static void save(final Path file, final boolean externallyManaged, final String comment, final UUID serverId, final boolean enabled, final boolean errorTracking, final boolean additionalMetrics, final boolean debug) throws IOException { - Files.createDirectories(file.getParent()); - try (final var out = Files.newOutputStream(file); - final var writer = new OutputStreamWriter(out, UTF_8)) { - final var properties = new Properties(); + @SuppressWarnings("PatternValidation") + public boolean preSubmissionStart() { + if (Boolean.getBoolean("faststats.first-run")) return false; + + if (firstRun()) { + var separatorLength = 0; + final var split = ONBOARDING_MESSAGE.split("\n"); + for (final var s : split) if (s.length() > separatorLength) separatorLength = s.length(); - properties.setProperty("serverId", serverId.toString()); - if (!externallyManaged) properties.setProperty("enabled", Boolean.toString(enabled)); - properties.setProperty("submitErrors", Boolean.toString(errorTracking)); - properties.setProperty("submitAdditionalMetrics", Boolean.toString(additionalMetrics)); - properties.setProperty("debug", Boolean.toString(debug)); + final var logger = LoggerFactory.factory().getLogger(getClass()); + logger.log(Level.CONFIG, "-".repeat(separatorLength)); + for (final var s : split) logger.log(Level.CONFIG, s); + logger.log(Level.CONFIG, "-".repeat(separatorLength)); - properties.store(writer, comment); + System.setProperty("faststats.first-run", "true"); + return false; } + return true; } } diff --git a/config/src/main/java/module-info.java b/config/src/main/java/module-info.java index a37c649..2b5cca3 100644 --- a/config/src/main/java/module-info.java +++ b/config/src/main/java/module-info.java @@ -5,6 +5,7 @@ exports dev.faststats.config; requires dev.faststats.core; + requires java.logging; requires static org.jetbrains.annotations; requires static org.jspecify; diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index b1501a3..1e770a0 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -92,17 +92,6 @@ protected SimpleMetrics( this.url = url; } - protected String getOnboardingMessage() { - return """ - This plugin uses FastStats to collect anonymous usage statistics. - No personal or identifying information is ever collected. - To opt out, set 'enabled=false' in the metrics configuration file. - Learn more at: https://faststats.dev/info - - Since this is your first start with FastStats, metrics submission will not start - until you restart the server to allow you to opt out if you prefer."""; - } - protected long getInitialDelay() { return TimeUnit.SECONDS.toMillis(Long.getLong("faststats.initial-delay", 30)); } @@ -117,29 +106,7 @@ protected void startSubmitting() { startSubmitting(getInitialDelay(), getPeriod(), TimeUnit.MILLISECONDS); } - @SuppressWarnings("PatternValidation") - protected boolean preSubmissionStart() { - /* - if (Boolean.getBoolean("faststats.first-run")) { - logger.info("Skipping metrics submission due to first-run flag"); - return false; - } - - if (config.firstRun()) { - var separatorLength = 0; - final var split = getOnboardingMessage().split("\n"); - for (final var s : split) if (s.length() > separatorLength) separatorLength = s.length(); - - logger.info("-".repeat(separatorLength)); - for (final var s : split) logger.info(s); - logger.info("-".repeat(separatorLength)); - - System.setProperty("faststats.first-run", "true"); - if (!config.externallyManaged()) return false; - } - */ - return true; // todo: move to config module? - } + protected abstract boolean preSubmissionStart(); private void startSubmitting(final long initialDelay, final long period, final TimeUnit unit) { if (!preSubmissionStart()) return; diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index 2a82ac1..4fd403b 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -15,7 +15,12 @@ @NullMarked public final class MockMetrics extends SimpleMetrics { public MockMetrics(final UUID serverId, @Token final String token, @Nullable final ErrorTracker tracker, final boolean debug) { - super(new SimpleConfig(serverId, true, debug, true, true, false, false), Set.of(), token, tracker, null, URI.create("http://localhost:5000/v1/collect"), debug); + super(new SimpleConfig(serverId, true, debug, true, true, false), Set.of(), token, tracker, null, URI.create("http://localhost:5000/v1/collect"), debug); + } + + @Override + protected boolean preSubmissionStart() { + return ((SimpleConfig) getConfig()).preSubmissionStart(); } @Override diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java b/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java index 93d90c1..8e67086 100644 --- a/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java +++ b/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java @@ -35,6 +35,11 @@ private FabricMetricsImpl(final Factory factory, final ModContainer mod, final P ServerLifecycleEvents.SERVER_STOPPING.register(server -> shutdown()); } + @Override + protected boolean preSubmissionStart() { + return ((SimpleConfig) getConfig()).preSubmissionStart(); + } + @Override protected void appendDefaultData(final JsonObject metrics) { assert server != null : "Server not initialized"; diff --git a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java index 1cd5c95..d2538ea 100644 --- a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java +++ b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java @@ -21,6 +21,11 @@ private HytaleMetricsImpl(final Factory factory, final Path config) throws Illeg startSubmitting(); } + @Override + protected boolean preSubmissionStart() { + return ((SimpleConfig) getConfig()).preSubmissionStart(); + } + @Override protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_version", HytaleServer.get().getServerName()); diff --git a/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java b/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java index 6dc6e41..e137c38 100644 --- a/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java +++ b/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java @@ -21,6 +21,11 @@ private MinestomMetricsImpl(final Factory factory, final Path config) throws Ill startSubmitting(); } + @Override + protected boolean preSubmissionStart() { + return ((SimpleConfig) getConfig()).preSubmissionStart(); + } + @Override protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("minecraft_version", MinecraftServer.VERSION_NAME); diff --git a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java index 9d6037b..51afb33 100644 --- a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java +++ b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java @@ -28,6 +28,11 @@ private NukkitMetricsImpl(final Factory factory, final PluginBase plugin, final startSubmitting(); } + @Override + protected boolean preSubmissionStart() { + return ((SimpleConfig) getConfig()).preSubmissionStart(); + } + @Override protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("minecraft_version", server.getVersion()); diff --git a/sponge/build.gradle.kts b/sponge/build.gradle.kts index ee394e7..06fbed9 100644 --- a/sponge/build.gradle.kts +++ b/sponge/build.gradle.kts @@ -6,6 +6,5 @@ repositories { dependencies { api(project(":core")) - implementation(project(":config")) compileOnly("org.spongepowered:spongeapi:8.3.0-SNAPSHOT") } diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeConfig.java b/sponge/src/main/java/dev/faststats/sponge/SpongeConfig.java new file mode 100644 index 0000000..9d1d5c5 --- /dev/null +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeConfig.java @@ -0,0 +1,141 @@ +package dev.faststats.sponge; + +import dev.faststats.core.Config; +import dev.faststats.core.internal.LoggerFactory; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.spongepowered.api.Sponge; +import org.spongepowered.plugin.PluginContainer; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiPredicate; +import java.util.logging.Level; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@ApiStatus.Internal +public record SpongeConfig( + UUID serverId, + boolean additionalMetrics, + boolean debug, + boolean enabled, + boolean errorTracking, + boolean firstRun +) implements Config { + + private static final String COMMENT = """ + FastStats (https://faststats.dev) collects anonymous usage statistics. + # This helps developers understand how their projects are used in the real world. + # + # No IP addresses, player data, or personal information is collected. + # The server ID below is randomly generated and can be regenerated at any time. + # + # Enabling metrics has no noticeable performance impact. + # Enabling metrics is recommended, you can do so in the Sponge metrics.config, + # by setting the "global-state" property to "TRUE". + # + # If you suspect a developer is collecting personal data or bypassing the Sponge config, + # please report it at: https://faststats.dev/abuse + # + # For more information, visit: https://faststats.dev/info + """; + private static final String ONBOARDING_MESSAGE = """ + This plugin uses FastStats to collect anonymous usage statistics. + No personal or identifying information is ever collected. + It is recommended to enable metrics by setting 'global-state=TRUE' in the sponge metrics config. + Learn more at: https://faststats.dev/info + """; + + @Contract(mutates = "io") + public static SpongeConfig read(final PluginContainer plugin, final Path file) throws RuntimeException { + final var properties = readOrEmpty(file); + final var firstRun = properties.isEmpty(); + final var saveConfig = new AtomicBoolean(firstRun); + + final var serverId = properties.map(object -> object.getProperty("serverId")).map(string -> { + try { + final var trimmed = string.trim(); + final var corrected = trimmed.length() > 36 ? trimmed.substring(0, 36) : trimmed; + if (!corrected.equals(string)) saveConfig.set(true); + return UUID.fromString(corrected); + } catch (final IllegalArgumentException e) { + saveConfig.set(true); + return UUID.randomUUID(); + } + }).orElseGet(() -> { + saveConfig.set(true); + return UUID.randomUUID(); + }); + + final BiPredicate predicate = (key, defaultValue) -> { + return properties.map(object -> object.getProperty(key)).map(Boolean::parseBoolean).orElseGet(() -> { + saveConfig.set(true); + return defaultValue; + }); + }; + + final var errorTracking = predicate.test("submitErrors", true); + final var additionalMetrics = predicate.test("submitAdditionalMetrics", true); + final var debug = predicate.test("debug", false); + + if (saveConfig.get()) try { + Files.createDirectories(file.getParent()); + try (final var out = Files.newOutputStream(file); + final var writer = new OutputStreamWriter(out, UTF_8)) { + final var store = new Properties(); + + store.setProperty("serverId", serverId.toString()); + store.setProperty("submitErrors", Boolean.toString(errorTracking)); + store.setProperty("submitAdditionalMetrics", Boolean.toString(additionalMetrics)); + store.setProperty("debug", Boolean.toString(debug)); + + store.store(writer, COMMENT); + } + } catch (final IOException e) { + throw new RuntimeException("Failed to save metrics config", e); + } + + final var enabled = Sponge.metricsConfigManager().effectiveCollectionState(plugin).asBoolean(); + return new SpongeConfig(serverId, additionalMetrics, debug, enabled, errorTracking, firstRun); + } + + private static Optional readOrEmpty(final Path file) throws RuntimeException { + if (!Files.isRegularFile(file)) return Optional.empty(); + try (final var reader = Files.newBufferedReader(file, UTF_8)) { + final var properties = new Properties(); + properties.load(reader); + return Optional.of(properties); + } catch (final IOException e) { + throw new RuntimeException("Failed to read metrics config", e); + } + } + + @SuppressWarnings("PatternValidation") + public boolean preSubmissionStart() { + if (Boolean.getBoolean("faststats.first-run")) return false; + + if (firstRun()) { + var separatorLength = 0; + final var split = ONBOARDING_MESSAGE.split("\n"); + for (final var s : split) if (s.length() > separatorLength) separatorLength = s.length(); + + final var logger = LoggerFactory.factory().getLogger(getClass()); + logger.log(Level.CONFIG, "-".repeat(separatorLength)); + for (final var s : split) logger.log(Level.CONFIG, s); + logger.log(Level.CONFIG, "-".repeat(separatorLength)); + + System.setProperty("faststats.first-run", "true"); + return false; + } + return true; + } +} + + diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java index 1db17d6..c96d34c 100644 --- a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java @@ -1,7 +1,6 @@ package dev.faststats.sponge; import com.google.gson.JsonObject; -import dev.faststats.config.SimpleConfig; import dev.faststats.core.Metrics; import dev.faststats.core.SimpleMetrics; import org.apache.logging.log4j.Logger; @@ -14,22 +13,6 @@ import java.nio.file.Path; final class SpongeMetricsImpl extends SimpleMetrics implements SpongeMetrics { - public static final String COMMENT = """ - FastStats (https://faststats.dev) collects anonymous usage statistics. - # This helps developers understand how their projects are used in the real world. - # - # No IP addresses, player data, or personal information is collected. - # The server ID below is randomly generated and can be regenerated at any time. - # - # Enabling metrics has no noticeable performance impact. - # Enabling metrics is recommended, you can do so in the Sponge metrics.config, - # by setting the "global-state" property to "TRUE". - # - # If you suspect a developer is collecting personal data or bypassing the Sponge config, - # please report it at: https://faststats.dev/abuse - # - # For more information, visit: https://faststats.dev/info - """; private final PluginContainer plugin; @@ -41,20 +24,14 @@ private SpongeMetricsImpl( final PluginContainer plugin, final Path config ) throws IllegalStateException { - super(factory, SimpleConfig.read(config, COMMENT, true, Sponge.metricsConfigManager() - .effectiveCollectionState(plugin).asBoolean())); + super(factory, SpongeConfig.read(plugin, config)); this.plugin = plugin; startSubmitting(); } @Override - protected String getOnboardingMessage() { - return """ - This plugin uses FastStats to collect anonymous usage statistics. - No personal or identifying information is ever collected. - It is recommended to enable metrics by setting 'global-state=TRUE' in the sponge metrics config. - Learn more at: https://faststats.dev/info - """; + protected boolean preSubmissionStart() { + return ((SpongeConfig) getConfig()).preSubmissionStart(); } @Override diff --git a/sponge/src/main/java/module-info.java b/sponge/src/main/java/module-info.java index 8eb72fc..e709333 100644 --- a/sponge/src/main/java/module-info.java +++ b/sponge/src/main/java/module-info.java @@ -6,8 +6,8 @@ requires com.google.gson; requires com.google.guice; - requires dev.faststats.config; requires dev.faststats.core; + requires java.logging; requires static org.jetbrains.annotations; requires static org.jspecify; diff --git a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java index fadfa55..969ad21 100644 --- a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java +++ b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java @@ -34,6 +34,11 @@ private VelocityMetricsImpl( startSubmitting(); } + @Override + protected boolean preSubmissionStart() { + return ((SimpleConfig) getConfig()).preSubmissionStart(); + } + @Override protected void appendDefaultData(final JsonObject metrics) { final var pluginVersion = plugin.getDescription().getVersion().orElse("unknown"); From dc8614e9100ac17e12b8602d21bdbe4b5c206a19 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 20 Apr 2026 22:33:57 +0200 Subject: [PATCH 40/40] Major metrics schema refactor --- .../main/java/com/example/ExamplePlugin.java | 11 +-- .../dev/faststats/bukkit/BukkitContext.java | 39 +++++++++ .../dev/faststats/bukkit/BukkitMetrics.java | 18 +--- .../faststats/bukkit/BukkitMetricsImpl.java | 25 ++---- bukkit/src/main/java/module-info.java | 2 +- .../main/java/com/example/ExamplePlugin.java | 12 +-- .../dev/faststats/bungee/BungeeContext.java | 25 ++++++ .../dev/faststats/bungee/BungeeMetrics.java | 16 +--- .../faststats/bungee/BungeeMetricsImpl.java | 22 ++--- bungeecord/src/main/java/module-info.java | 2 +- .../dev/faststats/config/SimpleConfig.java | 4 +- config/src/main/java/module-info.java | 2 +- .../example/ErrorTrackerExample.java | 2 +- .../faststats/example/FeatureFlagExample.java | 6 +- .../faststats/example/MetricTypesExample.java | 2 +- .../{core/flags => }/Attributes.java | 2 +- .../java/dev/faststats/{core => }/Config.java | 2 +- .../dev/faststats/{core => }/ErrorHelper.java | 2 +- .../faststats/{core => }/ErrorTracker.java | 42 ++++----- .../java/dev/faststats/FastStatsContext.java | 85 +++++++++++++++++++ .../{core/flags => }/FeatureFlag.java | 2 +- .../{core/flags => }/FeatureFlagService.java | 49 +---------- .../dev/faststats/{core => }/Metrics.java | 47 ++++------ .../dev/faststats/{core => }/MurmurHash3.java | 2 +- .../{core/flags => }/SimpleAttributes.java | 2 +- .../java/dev/faststats/SimpleContext.java | 51 +++++++++++ .../{core => }/SimpleErrorTracker.java | 2 +- .../{core/flags => }/SimpleFeatureFlag.java | 2 +- .../flags => }/SimpleFeatureFlagService.java | 7 +- .../faststats/{core => }/SimpleMetrics.java | 44 +++++----- .../java/dev/faststats/{core => }/Token.java | 4 +- .../{core => }/data/ArrayMetric.java | 2 +- .../dev/faststats/{core => }/data/Metric.java | 22 ++--- .../{core => }/data/SimpleMetric.java | 2 +- .../{core => }/data/SingleValueMetric.java | 2 +- .../faststats/{core => }/data/SourceId.java | 4 +- .../{core => }/internal/Constants.java | 4 +- .../faststats/{core => }/internal/Logger.java | 2 +- .../{core => }/internal/LoggerFactory.java | 2 +- .../{core => }/internal/SimpleLogger.java | 2 +- .../internal/SimpleLoggerFactory.java | 2 +- .../{core => }/internal/package-info.java | 2 +- core/src/main/java/module-info.java | 11 ++- .../java/dev/faststats/AnonymizationTest.java | 1 - .../java/dev/faststats/ErrorTrackerTest.java | 1 - .../test/java/dev/faststats/MockMetrics.java | 3 - .../src/main/java/com/example/ExampleMod.java | 12 +-- .../dev/faststats/fabric/FabricContext.java | 28 ++++++ .../dev/faststats/fabric/FabricMetrics.java | 33 +------ .../faststats/fabric/FabricMetricsImpl.java | 28 +++--- fabric/src/main/java/module-info.java | 2 +- .../main/java/com/example/ExamplePlugin.java | 12 +-- .../dev/faststats/hytale/HytaleContext.java | 22 +++++ .../dev/faststats/hytale/HytaleMetrics.java | 17 +--- .../faststats/hytale/HytaleMetricsImpl.java | 24 +++--- .../faststats/hytale/logger/HytaleLogger.java | 2 +- .../hytale/logger/HytaleLoggerFactory.java | 4 +- hytale/src/main/java/module-info.java | 4 +- .../faststats/minestom/MinestomContext.java | 23 +++++ .../faststats/minestom/MinestomMetrics.java | 16 +--- .../minestom/MinestomMetricsImpl.java | 23 ++--- minestom/src/main/java/module-info.java | 2 +- .../dev/faststats/nukkit/NukkitContext.java | 27 ++++++ .../dev/faststats/nukkit/NukkitMetrics.java | 16 +--- .../faststats/nukkit/NukkitMetricsImpl.java | 21 ++--- nukkit/src/main/java/module-info.java | 2 +- .../main/java/com/example/ExamplePlugin.java | 20 +++-- .../dev/faststats/sponge/SpongeConfig.java | 4 +- .../dev/faststats/sponge/SpongeContext.java | 35 ++++++++ .../dev/faststats/sponge/SpongeMetrics.java | 21 +---- .../faststats/sponge/SpongeMetricsImpl.java | 28 +++--- sponge/src/main/java/module-info.java | 2 +- .../main/java/com/example/ExamplePlugin.java | 22 +++-- .../faststats/velocity/VelocityContext.java | 49 +++++++++++ .../faststats/velocity/VelocityMetrics.java | 6 +- .../velocity/VelocityMetricsImpl.java | 24 ++++-- velocity/src/main/java/module-info.java | 2 +- 77 files changed, 672 insertions(+), 454 deletions(-) create mode 100644 bukkit/src/main/java/dev/faststats/bukkit/BukkitContext.java create mode 100644 bungeecord/src/main/java/dev/faststats/bungee/BungeeContext.java rename core/src/main/java/dev/faststats/{core/flags => }/Attributes.java (99%) rename core/src/main/java/dev/faststats/{core => }/Config.java (98%) rename core/src/main/java/dev/faststats/{core => }/ErrorHelper.java (99%) rename core/src/main/java/dev/faststats/{core => }/ErrorTracker.java (95%) create mode 100644 core/src/main/java/dev/faststats/FastStatsContext.java rename core/src/main/java/dev/faststats/{core/flags => }/FeatureFlag.java (99%) rename core/src/main/java/dev/faststats/{core/flags => }/FeatureFlagService.java (64%) rename core/src/main/java/dev/faststats/{core => }/Metrics.java (71%) rename core/src/main/java/dev/faststats/{core => }/MurmurHash3.java (99%) rename core/src/main/java/dev/faststats/{core/flags => }/SimpleAttributes.java (96%) create mode 100644 core/src/main/java/dev/faststats/SimpleContext.java rename core/src/main/java/dev/faststats/{core => }/SimpleErrorTracker.java (99%) rename core/src/main/java/dev/faststats/{core/flags => }/SimpleFeatureFlag.java (98%) rename core/src/main/java/dev/faststats/{core/flags => }/SimpleFeatureFlagService.java (98%) rename core/src/main/java/dev/faststats/{core => }/SimpleMetrics.java (91%) rename core/src/main/java/dev/faststats/{core => }/Token.java (93%) rename core/src/main/java/dev/faststats/{core => }/data/ArrayMetric.java (96%) rename core/src/main/java/dev/faststats/{core => }/data/Metric.java (95%) rename core/src/main/java/dev/faststats/{core => }/data/SimpleMetric.java (97%) rename core/src/main/java/dev/faststats/{core => }/data/SingleValueMetric.java (95%) rename core/src/main/java/dev/faststats/{core => }/data/SourceId.java (93%) rename core/src/main/java/dev/faststats/{core => }/internal/Constants.java (90%) rename core/src/main/java/dev/faststats/{core => }/internal/Logger.java (95%) rename core/src/main/java/dev/faststats/{core => }/internal/LoggerFactory.java (93%) rename core/src/main/java/dev/faststats/{core => }/internal/SimpleLogger.java (97%) rename core/src/main/java/dev/faststats/{core => }/internal/SimpleLoggerFactory.java (82%) rename core/src/main/java/dev/faststats/{core => }/internal/package-info.java (63%) create mode 100644 fabric/src/main/java/dev/faststats/fabric/FabricContext.java create mode 100644 hytale/src/main/java/dev/faststats/hytale/HytaleContext.java create mode 100644 minestom/src/main/java/dev/faststats/minestom/MinestomContext.java create mode 100644 nukkit/src/main/java/dev/faststats/nukkit/NukkitContext.java create mode 100644 sponge/src/main/java/dev/faststats/sponge/SpongeContext.java create mode 100644 velocity/src/main/java/dev/faststats/velocity/VelocityContext.java diff --git a/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java b/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java index 90884f3..299e0ce 100644 --- a/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -1,16 +1,18 @@ package com.example; +import dev.faststats.ErrorTracker; +import dev.faststats.bukkit.BukkitContext; import dev.faststats.bukkit.BukkitMetrics; -import dev.faststats.core.ErrorTracker; -import dev.faststats.core.data.Metric; +import dev.faststats.data.Metric; import org.bukkit.plugin.java.JavaPlugin; import java.util.concurrent.atomic.AtomicInteger; public final class ExamplePlugin extends JavaPlugin { private final AtomicInteger gameCount = new AtomicInteger(); + private final BukkitContext context = new BukkitContext(this, "YOUR_TOKEN_HERE"); - private final BukkitMetrics metrics = BukkitMetrics.factory() + private final BukkitMetrics metrics = context.metrics() // 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")) @@ -22,8 +24,7 @@ public final class ExamplePlugin extends JavaPlugin { // This is useful for cleaning up cached data .onFlush(() -> gameCount.set(0)) // reset game count on flush - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project - .create(this); + .create(); @Override public void onEnable() { diff --git a/bukkit/src/main/java/dev/faststats/bukkit/BukkitContext.java b/bukkit/src/main/java/dev/faststats/bukkit/BukkitContext.java new file mode 100644 index 0000000..9fa127b --- /dev/null +++ b/bukkit/src/main/java/dev/faststats/bukkit/BukkitContext.java @@ -0,0 +1,39 @@ +package dev.faststats.bukkit; + +import dev.faststats.SimpleContext; +import dev.faststats.Token; +import dev.faststats.config.SimpleConfig; +import org.bukkit.plugin.Plugin; + +import java.nio.file.Path; + +/** + * Bukkit FastStats context. + * + * @since 0.23.0 + */ +public final class BukkitContext extends SimpleContext { + final Plugin plugin; + + public BukkitContext(final Plugin plugin, @Token final String token) { + super(SimpleConfig.read(getConfigPath(plugin)), token); + this.plugin = plugin; + } + + @Override + public BukkitMetrics.Factory metrics() { + return new BukkitMetricsImpl.Factory(this); + } + + private static Path getConfigPath(final Plugin plugin) { + return getPluginsFolder(plugin).resolve("faststats").resolve("config.properties"); + } + + private static Path getPluginsFolder(final Plugin plugin) { + try { + return plugin.getServer().getPluginsFolder().toPath(); + } catch (final NoSuchMethodError e) { + return plugin.getDataFolder().getParentFile().toPath(); + } + } +} diff --git a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetrics.java b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetrics.java index 810cca6..7c88a42 100644 --- a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetrics.java +++ b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetrics.java @@ -1,9 +1,8 @@ package dev.faststats.bukkit; -import dev.faststats.core.Metrics; +import dev.faststats.Metrics; import org.bukkit.plugin.IllegalPluginAccessException; import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.Contract; /** * Bukkit metrics implementation. @@ -11,17 +10,6 @@ * @since 0.1.0 */ public sealed interface BukkitMetrics extends Metrics permits BukkitMetricsImpl { - /** - * Creates a new metrics factory for Bukkit. - * - * @return the metrics factory - * @since 0.1.0 - */ - @Contract(pure = true) - static Factory factory() { - return new BukkitMetricsImpl.Factory(); - } - /** * Registers additional exception handlers on Paper-based implementations. * @@ -32,8 +20,8 @@ static Factory factory() { @Override void ready() throws IllegalPluginAccessException; - interface Factory extends Metrics.Factory { + interface Factory extends Metrics.Factory { @Override - BukkitMetrics create(Plugin object) throws IllegalStateException; + BukkitMetrics create() throws IllegalStateException; } } diff --git a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java index 0168505..b22e484 100644 --- a/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java +++ b/bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java @@ -1,13 +1,12 @@ package dev.faststats.bukkit; import com.google.gson.JsonObject; +import dev.faststats.SimpleMetrics; import dev.faststats.config.SimpleConfig; -import dev.faststats.core.SimpleMetrics; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import java.nio.file.Path; import java.util.Optional; import java.util.function.Supplier; @@ -21,8 +20,8 @@ final class BukkitMetricsImpl extends SimpleMetrics implements BukkitMetrics { @Async.Schedule @Contract(mutates = "io") @SuppressWarnings({"deprecation", "Convert2MethodRef"}) - private BukkitMetricsImpl(final Factory factory, final Plugin plugin, final Path config) throws IllegalStateException { - super(factory, SimpleConfig.read(config)); + private BukkitMetricsImpl(final Factory factory, final Plugin plugin) throws IllegalStateException { + super(factory); this.plugin = plugin; final var server = plugin.getServer(); @@ -102,20 +101,14 @@ private Optional tryOrEmpty(final Supplier supplier) { } } - static final class Factory extends SimpleMetrics.Factory implements BukkitMetrics.Factory { - @Override - public BukkitMetrics create(final Plugin plugin) throws IllegalStateException { - final var dataFolder = getPluginsFolder(plugin).resolve("faststats"); - final var config = dataFolder.resolve("config.properties"); - return new BukkitMetricsImpl(this, plugin, config); + static final class Factory extends SimpleMetrics.Factory implements BukkitMetrics.Factory { + public Factory(final BukkitContext context) { + super(context); } - private static Path getPluginsFolder(final Plugin plugin) { - try { - return plugin.getServer().getPluginsFolder().toPath(); - } catch (final NoSuchMethodError e) { - return plugin.getDataFolder().getParentFile().toPath(); - } + @Override + public BukkitMetrics create() throws IllegalStateException { + return new BukkitMetricsImpl(this, ((BukkitContext) context).plugin); } } } diff --git a/bukkit/src/main/java/module-info.java b/bukkit/src/main/java/module-info.java index d8eced3..f74287e 100644 --- a/bukkit/src/main/java/module-info.java +++ b/bukkit/src/main/java/module-info.java @@ -6,7 +6,7 @@ requires com.google.gson; requires dev.faststats.config; - requires dev.faststats.core; + requires dev.faststats; requires java.logging; requires org.bukkit; diff --git a/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java b/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java index 223a64d..091ad34 100644 --- a/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/bungeecord/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -1,17 +1,18 @@ package com.example; -import dev.faststats.bungee.BungeeMetrics; -import dev.faststats.core.ErrorTracker; -import dev.faststats.core.Metrics; -import dev.faststats.core.data.Metric; +import dev.faststats.ErrorTracker; +import dev.faststats.Metrics; +import dev.faststats.bungee.BungeeContext; +import dev.faststats.data.Metric; import net.md_5.bungee.api.plugin.Plugin; import java.util.concurrent.atomic.AtomicInteger; public class ExamplePlugin extends Plugin { private final AtomicInteger gameCount = new AtomicInteger(); + private final BungeeContext context = new BungeeContext(this, "YOUR_TOKEN_HERE"); - private final Metrics metrics = BungeeMetrics.factory() + private final Metrics metrics = context.metrics() // 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")) @@ -23,7 +24,6 @@ public class ExamplePlugin extends Plugin { // This is useful for cleaning up cached data .onFlush(() -> gameCount.set(0)) // reset game count on flush - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create(this); @Override diff --git a/bungeecord/src/main/java/dev/faststats/bungee/BungeeContext.java b/bungeecord/src/main/java/dev/faststats/bungee/BungeeContext.java new file mode 100644 index 0000000..a5d813a --- /dev/null +++ b/bungeecord/src/main/java/dev/faststats/bungee/BungeeContext.java @@ -0,0 +1,25 @@ +package dev.faststats.bungee; + +import dev.faststats.SimpleContext; +import dev.faststats.Token; +import dev.faststats.config.SimpleConfig; +import net.md_5.bungee.api.plugin.Plugin; + +/** + * BungeeCord FastStats context. + * + * @since 0.23.0 + */ +public final class BungeeContext extends SimpleContext { + final Plugin plugin; + + public BungeeContext(final Plugin plugin, @Token final String token) { + super(SimpleConfig.read(plugin.getProxy().getPluginsFolder().toPath().resolve("faststats").resolve("config.properties")), token); + this.plugin = plugin; + } + + @Override + public BungeeMetrics.Factory metrics() { + return new BungeeMetricsImpl.Factory(this); + } +} diff --git a/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetrics.java b/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetrics.java index 434f655..1c2176d 100644 --- a/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetrics.java +++ b/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetrics.java @@ -1,7 +1,6 @@ package dev.faststats.bungee; -import dev.faststats.core.Metrics; -import net.md_5.bungee.api.plugin.Plugin; +import dev.faststats.Metrics; import org.jetbrains.annotations.Contract; /** @@ -10,17 +9,6 @@ * @since 0.1.0 */ public sealed interface BungeeMetrics extends Metrics permits BungeeMetricsImpl { - /** - * Creates a new metrics factory for BungeeCord. - * - * @return the metrics factory - * @since 0.1.0 - */ - @Contract(pure = true) - static Factory factory() { - return new BungeeMetricsImpl.Factory(); - } - - interface Factory extends Metrics.Factory { + interface Factory extends Metrics.Factory { } } diff --git a/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java b/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java index e71045e..57d0663 100644 --- a/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java +++ b/bungeecord/src/main/java/dev/faststats/bungee/BungeeMetricsImpl.java @@ -1,24 +1,22 @@ package dev.faststats.bungee; import com.google.gson.JsonObject; +import dev.faststats.Metrics; +import dev.faststats.SimpleMetrics; import dev.faststats.config.SimpleConfig; -import dev.faststats.core.Metrics; -import dev.faststats.core.SimpleMetrics; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.plugin.Plugin; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import java.nio.file.Path; - final class BungeeMetricsImpl extends SimpleMetrics implements BungeeMetrics { private final ProxyServer server; private final Plugin plugin; @Async.Schedule @Contract(mutates = "io") - private BungeeMetricsImpl(final Factory factory, final Plugin plugin, final Path config) throws IllegalStateException { - super(factory, SimpleConfig.read(config)); + private BungeeMetricsImpl(final Factory factory, final Plugin plugin) throws IllegalStateException { + super(factory); this.server = plugin.getProxy(); this.plugin = plugin; @@ -40,12 +38,14 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", server.getName()); } - static final class Factory extends SimpleMetrics.Factory implements BungeeMetrics.Factory { + static final class Factory extends SimpleMetrics.Factory implements BungeeMetrics.Factory { + public Factory(final BungeeContext context) { + super(context); + } + @Override - public Metrics create(final Plugin plugin) throws IllegalStateException { - final var dataFolder = plugin.getProxy().getPluginsFolder().toPath().resolve("faststats"); - final var config = dataFolder.resolve("config.properties"); - return new BungeeMetricsImpl(this, plugin, config); + public Metrics create() throws IllegalStateException { + return new BungeeMetricsImpl(this, ((BungeeContext) context).plugin); } } } diff --git a/bungeecord/src/main/java/module-info.java b/bungeecord/src/main/java/module-info.java index 8380b46..1f8b5d5 100644 --- a/bungeecord/src/main/java/module-info.java +++ b/bungeecord/src/main/java/module-info.java @@ -6,7 +6,7 @@ requires com.google.gson; requires dev.faststats.config; - requires dev.faststats.core; + requires dev.faststats; requires java.logging; requires static org.jetbrains.annotations; diff --git a/config/src/main/java/dev/faststats/config/SimpleConfig.java b/config/src/main/java/dev/faststats/config/SimpleConfig.java index e50548c..71e0b69 100644 --- a/config/src/main/java/dev/faststats/config/SimpleConfig.java +++ b/config/src/main/java/dev/faststats/config/SimpleConfig.java @@ -1,7 +1,7 @@ package dev.faststats.config; -import dev.faststats.core.Config; -import dev.faststats.core.internal.LoggerFactory; +import dev.faststats.Config; +import dev.faststats.internal.LoggerFactory; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; diff --git a/config/src/main/java/module-info.java b/config/src/main/java/module-info.java index 2b5cca3..251f400 100644 --- a/config/src/main/java/module-info.java +++ b/config/src/main/java/module-info.java @@ -4,7 +4,7 @@ module dev.faststats.config { exports dev.faststats.config; - requires dev.faststats.core; + requires dev.faststats; requires java.logging; requires static org.jetbrains.annotations; diff --git a/core/example/src/main/java/dev/faststats/example/ErrorTrackerExample.java b/core/example/src/main/java/dev/faststats/example/ErrorTrackerExample.java index aa8d643..b2a3785 100644 --- a/core/example/src/main/java/dev/faststats/example/ErrorTrackerExample.java +++ b/core/example/src/main/java/dev/faststats/example/ErrorTrackerExample.java @@ -1,6 +1,6 @@ package dev.faststats.example; -import dev.faststats.core.ErrorTracker; +import dev.faststats.ErrorTracker; import java.lang.reflect.InvocationTargetException; import java.nio.file.AccessDeniedException; diff --git a/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java b/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java index 1461d99..556ff7d 100644 --- a/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java +++ b/core/example/src/main/java/dev/faststats/example/FeatureFlagExample.java @@ -1,8 +1,8 @@ package dev.faststats.example; -import dev.faststats.core.flags.Attributes; -import dev.faststats.core.flags.FeatureFlag; -import dev.faststats.core.flags.FeatureFlagService; +import dev.faststats.Attributes; +import dev.faststats.FeatureFlag; +import dev.faststats.FeatureFlagService; import java.time.Duration; import java.time.Instant; diff --git a/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java b/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java index 066b5b4..5d7ac3e 100644 --- a/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java +++ b/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java @@ -1,6 +1,6 @@ package dev.faststats.example; -import dev.faststats.core.data.Metric; +import dev.faststats.data.Metric; public final class MetricTypesExample { // Single value metrics diff --git a/core/src/main/java/dev/faststats/core/flags/Attributes.java b/core/src/main/java/dev/faststats/Attributes.java similarity index 99% rename from core/src/main/java/dev/faststats/core/flags/Attributes.java rename to core/src/main/java/dev/faststats/Attributes.java index ad08cb8..83fddca 100644 --- a/core/src/main/java/dev/faststats/core/flags/Attributes.java +++ b/core/src/main/java/dev/faststats/Attributes.java @@ -1,4 +1,4 @@ -package dev.faststats.core.flags; +package dev.faststats; import com.google.gson.JsonPrimitive; import org.jetbrains.annotations.Contract; diff --git a/core/src/main/java/dev/faststats/core/Config.java b/core/src/main/java/dev/faststats/Config.java similarity index 98% rename from core/src/main/java/dev/faststats/core/Config.java rename to core/src/main/java/dev/faststats/Config.java index f1e39f5..0c9cb5d 100644 --- a/core/src/main/java/dev/faststats/core/Config.java +++ b/core/src/main/java/dev/faststats/Config.java @@ -1,4 +1,4 @@ -package dev.faststats.core; +package dev.faststats; import org.jetbrains.annotations.Contract; diff --git a/core/src/main/java/dev/faststats/core/ErrorHelper.java b/core/src/main/java/dev/faststats/ErrorHelper.java similarity index 99% rename from core/src/main/java/dev/faststats/core/ErrorHelper.java rename to core/src/main/java/dev/faststats/ErrorHelper.java index fe427c5..57a8bfc 100644 --- a/core/src/main/java/dev/faststats/core/ErrorHelper.java +++ b/core/src/main/java/dev/faststats/ErrorHelper.java @@ -1,4 +1,4 @@ -package dev.faststats.core; +package dev.faststats; import com.google.gson.JsonArray; import com.google.gson.JsonObject; diff --git a/core/src/main/java/dev/faststats/core/ErrorTracker.java b/core/src/main/java/dev/faststats/ErrorTracker.java similarity index 95% rename from core/src/main/java/dev/faststats/core/ErrorTracker.java rename to core/src/main/java/dev/faststats/ErrorTracker.java index a02b928..0873177 100644 --- a/core/src/main/java/dev/faststats/core/ErrorTracker.java +++ b/core/src/main/java/dev/faststats/ErrorTracker.java @@ -1,4 +1,4 @@ -package dev.faststats.core; +package dev.faststats; import org.intellij.lang.annotations.RegExp; import org.jetbrains.annotations.Contract; @@ -11,7 +11,7 @@ /** * An error tracker. * - * @since 0.10.0 + * @since 0.23.0 */ public sealed interface ErrorTracker permits SimpleErrorTracker { /** @@ -25,7 +25,7 @@ public sealed interface ErrorTracker permits SimpleErrorTracker { * @see #contextUnaware() * @see #trackError(String, boolean) * @see #trackError(Throwable, boolean) - * @since 0.10.0 + * @since 0.23.0 */ @Contract(value = " -> new") static ErrorTracker contextAware() { @@ -45,7 +45,7 @@ static ErrorTracker contextAware() { * @see #contextAware() * @see #trackError(String) * @see #trackError(Throwable) - * @since 0.10.0 + * @since 0.23.0 */ @Contract(value = " -> new", pure = true) static ErrorTracker contextUnaware() { @@ -58,7 +58,7 @@ static ErrorTracker contextUnaware() { * @param message the error message * @see #trackError(Throwable) * @see #trackError(String, boolean) - * @since 0.10.0 + * @since 0.23.0 */ @Contract(mutates = "this") void trackError(String message); @@ -68,7 +68,7 @@ static ErrorTracker contextUnaware() { * * @param error the error * @see #trackError(Throwable, boolean) - * @since 0.10.0 + * @since 0.23.0 */ @Contract(mutates = "this") void trackError(Throwable error); @@ -81,7 +81,7 @@ static ErrorTracker contextUnaware() { * @param message the error message * @param handled whether the error was handled * @see #trackError(Throwable, boolean) - * @since 0.20.0 + * @since 0.23.0 */ @Contract(mutates = "this") void trackError(String message, boolean handled); @@ -93,7 +93,7 @@ static ErrorTracker contextUnaware() { * * @param error the error * @param handled whether the error was handled - * @since 0.20.0 + * @since 0.23.0 */ @Contract(mutates = "this") void trackError(Throwable error, boolean handled); @@ -106,7 +106,7 @@ static ErrorTracker contextUnaware() { * * @param type the error type * @return the error tracker - * @since 0.22.0 + * @since 0.23.0 */ @Contract(value = "_ -> this", mutates = "this") ErrorTracker ignoreError(Class type); @@ -125,7 +125,7 @@ static ErrorTracker contextUnaware() { * * @param pattern the regex pattern to match against error messages * @return the error tracker - * @since 0.21.0 + * @since 0.23.0 */ @Contract(value = "_ -> this", mutates = "this") ErrorTracker ignoreError(Pattern pattern); @@ -138,7 +138,7 @@ static ErrorTracker contextUnaware() { * @param pattern the regex pattern string to match against error messages * @return the error tracker * @see #ignoreError(Pattern) - * @since 0.21.0 + * @since 0.23.0 */ @Contract(value = "_ -> this", mutates = "this") default ErrorTracker ignoreError(@RegExp final String pattern) { @@ -156,7 +156,7 @@ default ErrorTracker ignoreError(@RegExp final String pattern) { * @param type the error type * @param pattern the regex pattern to match against error messages * @return the error tracker - * @since 0.21.0 + * @since 0.23.0 */ @Contract(value = "_, _ -> this", mutates = "this") ErrorTracker ignoreError(Class type, Pattern pattern); @@ -170,7 +170,7 @@ default ErrorTracker ignoreError(@RegExp final String pattern) { * @param pattern the regex pattern string to match against error messages * @return the error tracker * @see #ignoreError(Class, Pattern) - * @since 0.21.0 + * @since 0.23.0 */ @Contract(value = "_, _ -> this", mutates = "this") default ErrorTracker ignoreError(final Class type, @RegExp final String pattern) { @@ -187,7 +187,7 @@ default ErrorTracker ignoreError(final Class type, @RegExp * @param replacement the replacement string * @return the error tracker * @see java.util.regex.Matcher#replaceAll(String) - * @since 0.22.0 + * @since 0.23.0 */ @Contract(value = "_, _ -> this", mutates = "this") ErrorTracker anonymize(Pattern pattern, String replacement); @@ -200,7 +200,7 @@ default ErrorTracker ignoreError(final Class type, @RegExp * @return the error tracker * @see #anonymize(Pattern, String) * @see java.util.regex.Matcher#replaceAll(String) - * @since 0.22.0 + * @since 0.23.0 */ @Contract(value = "_, _ -> this", mutates = "this") default ErrorTracker anonymize(@RegExp final String pattern, final String replacement) { @@ -214,7 +214,7 @@ default ErrorTracker anonymize(@RegExp final String pattern, final String replac * * @param loader the class loader * @throws IllegalStateException if the error context is already attached - * @since 0.10.0 + * @since 0.23.0 */ void attachErrorContext(@Nullable ClassLoader loader) throws IllegalStateException; @@ -227,7 +227,7 @@ default ErrorTracker anonymize(@RegExp final String pattern, final String replac * This should be called during shutdown to prevent {@link BootstrapMethodError} * when the provider's JAR file is closed. * - * @since 0.13.0 + * @since 0.23.0 */ void detachErrorContext(); @@ -235,7 +235,7 @@ default ErrorTracker anonymize(@RegExp final String pattern, final String replac * Returns whether an error context is attached. * * @return whether an error context is attached - * @since 0.13.0 + * @since 0.23.0 */ boolean isContextAttached(); @@ -245,7 +245,7 @@ default ErrorTracker anonymize(@RegExp final String pattern, final String replac * The purpose of this handler is to allow custom error handling like logging. * * @param errorEvent the error event handler - * @since 0.11.0 + * @since 0.23.0 */ @Contract(mutates = "this") void setContextErrorHandler(@Nullable BiConsumer<@Nullable ClassLoader, Throwable> errorEvent); @@ -254,7 +254,7 @@ default ErrorTracker anonymize(@RegExp final String pattern, final String replac * Returns the error event handler which will be called when an error is tracked automatically. * * @return the error event handler - * @since 0.11.0 + * @since 0.23.0 */ @Contract(pure = true) Optional> getContextErrorHandler(); @@ -265,7 +265,7 @@ default ErrorTracker anonymize(@RegExp final String pattern, final String replac * @param loader the class loader * @param error the error * @return whether the error occurred in the same class loader - * @since 0.14.0 + * @since 0.23.0 */ @Contract(pure = true) static boolean isSameLoader(final ClassLoader loader, final Throwable error) { diff --git a/core/src/main/java/dev/faststats/FastStatsContext.java b/core/src/main/java/dev/faststats/FastStatsContext.java new file mode 100644 index 0000000..070759c --- /dev/null +++ b/core/src/main/java/dev/faststats/FastStatsContext.java @@ -0,0 +1,85 @@ +package dev.faststats; + +import org.jetbrains.annotations.Contract; + +import java.time.Duration; + +/** + * Shared FastStats context. + *

+ * Platform-specific contexts should extend this class to provide a shared + * configuration, token, and metrics factory for their environment. + * + * @since 0.23.0 + */ +public interface FastStatsContext { + /** + * Get the metrics configuration shared by services created from this context. + * + * @return the shared configuration + * @since 0.23.0 + */ + @Contract(pure = true) + Config getConfig(); + + /** + * Get the token shared by services created from this context. + * + * @return the shared token + * @since 0.23.0 + */ + @Token + @Contract(pure = true) + String getToken(); + + /** + * Creates a new platform metrics factory bound to this context. + * + * @return a new platform metrics factory + * @since 0.23.0 + */ + @Contract(value = "-> new", pure = true) + Metrics.Factory metrics(); + + /** + * Creates a new feature flag service backed by this context token. + * + * @return the feature flag service + * @since 0.23.0 + */ + @Contract(value = "-> new", pure = true) + FeatureFlagService featureFlags(); + + /** + * Creates a new feature flag service backed by this context token and attributes. + * + * @param attributes the global targeting attributes + * @return the feature flag service + * @since 0.23.0 + */ + @Contract(value = "_ -> new", pure = true) + FeatureFlagService featureFlags(final Attributes attributes); + + /** + * Creates a new feature flag service backed by this context token, and TTL. + * + * @param ttl the cache time-to-live for resolved flag values + * @return the feature flag service + * @throws IllegalArgumentException if the TTL is negative + * @since 0.23.0 + */ + @Contract(value = "_ -> new", pure = true) + FeatureFlagService featureFlags(final Duration ttl); + + /** + * Creates a new feature flag service backed by this context token, attributes, and TTL. + * + * @param attributes the global targeting attributes + * @param ttl the cache time-to-live for resolved flag values + * @return the feature flag service + * @throws IllegalArgumentException if the TTL is negative + * @since 0.23.0 + */ + @Contract(value = "_, _ -> new", pure = true) + FeatureFlagService featureFlags(final Attributes attributes, final Duration ttl); +} diff --git a/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java b/core/src/main/java/dev/faststats/FeatureFlag.java similarity index 99% rename from core/src/main/java/dev/faststats/core/flags/FeatureFlag.java rename to core/src/main/java/dev/faststats/FeatureFlag.java index 77532a1..bc2e703 100644 --- a/core/src/main/java/dev/faststats/core/flags/FeatureFlag.java +++ b/core/src/main/java/dev/faststats/FeatureFlag.java @@ -1,4 +1,4 @@ -package dev.faststats.core.flags; +package dev.faststats; import org.jetbrains.annotations.Contract; diff --git a/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java b/core/src/main/java/dev/faststats/FeatureFlagService.java similarity index 64% rename from core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java rename to core/src/main/java/dev/faststats/FeatureFlagService.java index d576a1b..73cb2fc 100644 --- a/core/src/main/java/dev/faststats/core/flags/FeatureFlagService.java +++ b/core/src/main/java/dev/faststats/FeatureFlagService.java @@ -1,8 +1,6 @@ -package dev.faststats.core.flags; +package dev.faststats; -import dev.faststats.core.Token; import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.Nullable; import java.time.Duration; import java.util.Optional; @@ -15,51 +13,6 @@ * @since 0.23.0 */ public sealed interface FeatureFlagService permits SimpleFeatureFlagService { - /** - * Creates a feature flag service for the given environment token - * and a default cache TTL of five minutes. - * - * @param token the environment token - * @return a new feature flag service - * @see #create(String, Attributes) - * @since 0.23.0 - */ - @Contract(value = "_ -> new", pure = true) - static FeatureFlagService create(@Token final String token) { - return create(token, null); - } - - /** - * Creates a feature flag service for the given environment token - * and global targeting attributes with a default cache TTL of five minutes. - * - * @param token the environment token - * @param attributes the global targeting attributes - * @return a new feature flag service - * @see #create(String, Attributes, Duration) - * @since 0.23.0 - */ - @Contract(value = "_, _ -> new", pure = true) - static FeatureFlagService create(@Token final String token, @Nullable final Attributes attributes) { - return create(token, attributes, Duration.ofMinutes(5)); - } - - /** - * Creates a feature flag service for the given environment token, - * global targeting attributes, and cache TTL. - * - * @param token the environment token - * @param attributes the global targeting attributes - * @param ttl the cache time-to-live for resolved flag values - * @return a new feature flag service - * @throws IllegalArgumentException if the TTL is negative - * @since 0.23.0 - */ - @Contract(value = "_, _, _ -> new", pure = true) - static FeatureFlagService create(@Token final String token, @Nullable final Attributes attributes, final Duration ttl) throws IllegalArgumentException { - return new SimpleFeatureFlagService(token, attributes, ttl); - } - /** * Define a boolean feature flag. * diff --git a/core/src/main/java/dev/faststats/core/Metrics.java b/core/src/main/java/dev/faststats/Metrics.java similarity index 71% rename from core/src/main/java/dev/faststats/core/Metrics.java rename to core/src/main/java/dev/faststats/Metrics.java index d931cc2..b051fcf 100644 --- a/core/src/main/java/dev/faststats/core/Metrics.java +++ b/core/src/main/java/dev/faststats/Metrics.java @@ -1,6 +1,6 @@ -package dev.faststats.core; +package dev.faststats; -import dev.faststats.core.data.Metric; +import dev.faststats.data.Metric; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; @@ -9,14 +9,14 @@ /** * Metrics interface. * - * @since 0.1.0 + * @since 0.23.0 */ public interface Metrics { /** * Get the token used to authenticate with the metrics server and identify the project. * * @return the metrics token - * @since 0.1.0 + * @since 0.23.0 */ @Token @Contract(pure = true) @@ -26,7 +26,7 @@ public interface Metrics { * Get the error tracker for this metrics instance. * * @return the error tracker - * @since 0.10.0 + * @since 0.23.0 */ @Contract(pure = true) Optional getErrorTracker(); @@ -35,7 +35,7 @@ public interface Metrics { * Get the metrics configuration. * * @return the metrics configuration - * @since 0.1.0 + * @since 0.23.0 */ @Contract(pure = true) Config getConfig(); @@ -48,7 +48,7 @@ public interface Metrics { * No-op in most implementations. * * @apiNote Refer to your {@code Metrics} provider's documentation. - * @since 0.14.0 + * @since 0.23.0 */ default void ready() { } @@ -58,7 +58,7 @@ default void ready() { *

* This method should be called when the application is shutting down. * - * @since 0.1.0 + * @since 0.23.0 */ @Contract(mutates = "this") void shutdown(); @@ -66,9 +66,9 @@ default void ready() { /** * A metrics factory. * - * @since 0.1.0 + * @since 0.23.0 */ - interface Factory> { + interface Factory> { /** * Adds a metric to the metrics submission. *

@@ -77,7 +77,7 @@ interface Factory> { * @param metric the metric to add * @return the metrics factory * @throws IllegalArgumentException if the metric is already added - * @since 0.16.0 + * @since 0.23.0 */ @Contract(mutates = "this") F addMetric(Metric metric) throws IllegalArgumentException; @@ -89,7 +89,7 @@ interface Factory> { * * @param flush the flush callback * @return the metrics factory - * @since 0.15.0 + * @since 0.23.0 */ @Contract(mutates = "this") F onFlush(Runnable flush); @@ -101,38 +101,23 @@ interface Factory> { * * @param tracker the error tracker * @return the metrics factory - * @since 0.10.0 + * @since 0.23.0 */ @Contract(mutates = "this") F errorTracker(ErrorTracker tracker); - /** - * Sets the token used to authenticate with the metrics server and identify the project. - *

- * This token can be found in the settings of your project under "Your API Token". - * - * @param token the metrics token - * @return the metrics factory - * @throws IllegalArgumentException if the token does not match the {@link Token#PATTERN} - * @since 0.1.0 - */ - @Contract(mutates = "this") - F token(@Token String token) throws IllegalArgumentException; - /** * Creates a new metrics instance. *

* Metrics submission will start automatically. * - * @param object a required object as defined by the implementation * @return the metrics instance * @throws IllegalStateException if the token is not specified - * @see #token(String) - * @since 0.1.0 + * @since 0.23.0 */ @Async.Schedule - @Contract(value = "_ -> new", mutates = "io") - Metrics create(T object) throws IllegalStateException; + @Contract(value = " -> new", mutates = "io") + Metrics create() throws IllegalStateException; } } diff --git a/core/src/main/java/dev/faststats/core/MurmurHash3.java b/core/src/main/java/dev/faststats/MurmurHash3.java similarity index 99% rename from core/src/main/java/dev/faststats/core/MurmurHash3.java rename to core/src/main/java/dev/faststats/MurmurHash3.java index 157b765..3d4dcca 100644 --- a/core/src/main/java/dev/faststats/core/MurmurHash3.java +++ b/core/src/main/java/dev/faststats/MurmurHash3.java @@ -1,4 +1,4 @@ -package dev.faststats.core; +package dev.faststats; import com.google.gson.JsonObject; import org.jetbrains.annotations.Contract; diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java b/core/src/main/java/dev/faststats/SimpleAttributes.java similarity index 96% rename from core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java rename to core/src/main/java/dev/faststats/SimpleAttributes.java index a137801..8413a4e 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleAttributes.java +++ b/core/src/main/java/dev/faststats/SimpleAttributes.java @@ -1,4 +1,4 @@ -package dev.faststats.core.flags; +package dev.faststats; import com.google.gson.JsonPrimitive; diff --git a/core/src/main/java/dev/faststats/SimpleContext.java b/core/src/main/java/dev/faststats/SimpleContext.java new file mode 100644 index 0000000..9a81f23 --- /dev/null +++ b/core/src/main/java/dev/faststats/SimpleContext.java @@ -0,0 +1,51 @@ +package dev.faststats; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.time.Duration; + +@ApiStatus.Internal +public abstract class SimpleContext implements FastStatsContext { + private final Config config; + private final @Token String token; + + // todo: add docs + protected SimpleContext(final Config config, @Token final String token) throws IllegalArgumentException { + if (!token.matches(Token.PATTERN)) { + throw new IllegalArgumentException("Invalid token '" + token + "', must match '" + Token.PATTERN + "'"); + } + this.config = config; + this.token = token; + } + + @Override + public final Config getConfig() { + return config; + } + + @Override + public final @Token String getToken() { + return token; + } + + @Override + public final FeatureFlagService featureFlags() { + return new SimpleFeatureFlagService(token, null, Duration.ofMinutes(5)); + } + + @Override + public final FeatureFlagService featureFlags(final Attributes attributes) { + return new SimpleFeatureFlagService(token, attributes, Duration.ofMinutes(5)); + } + + @Override + public final FeatureFlagService featureFlags(final Duration ttl) { + return new SimpleFeatureFlagService(token, null, ttl); + } + + @Override + public final FeatureFlagService featureFlags(@Nullable final Attributes attributes, final Duration ttl) { + return new SimpleFeatureFlagService(token, attributes, ttl); + } +} diff --git a/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java b/core/src/main/java/dev/faststats/SimpleErrorTracker.java similarity index 99% rename from core/src/main/java/dev/faststats/core/SimpleErrorTracker.java rename to core/src/main/java/dev/faststats/SimpleErrorTracker.java index 3a72d7d..9e9ca28 100644 --- a/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java +++ b/core/src/main/java/dev/faststats/SimpleErrorTracker.java @@ -1,4 +1,4 @@ -package dev.faststats.core; +package dev.faststats; import com.google.gson.JsonArray; import com.google.gson.JsonObject; diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java b/core/src/main/java/dev/faststats/SimpleFeatureFlag.java similarity index 98% rename from core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java rename to core/src/main/java/dev/faststats/SimpleFeatureFlag.java index b7f4670..1106769 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlag.java +++ b/core/src/main/java/dev/faststats/SimpleFeatureFlag.java @@ -1,4 +1,4 @@ -package dev.faststats.core.flags; +package dev.faststats; import org.jspecify.annotations.Nullable; diff --git a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java b/core/src/main/java/dev/faststats/SimpleFeatureFlagService.java similarity index 98% rename from core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java rename to core/src/main/java/dev/faststats/SimpleFeatureFlagService.java index c439117..c0eb8f5 100644 --- a/core/src/main/java/dev/faststats/core/flags/SimpleFeatureFlagService.java +++ b/core/src/main/java/dev/faststats/SimpleFeatureFlagService.java @@ -1,13 +1,12 @@ -package dev.faststats.core.flags; +package dev.faststats; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; -import dev.faststats.core.Token; -import dev.faststats.core.internal.Logger; -import dev.faststats.core.internal.LoggerFactory; +import dev.faststats.internal.Logger; +import dev.faststats.internal.LoggerFactory; import org.jspecify.annotations.Nullable; import java.net.URI; diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/SimpleMetrics.java similarity index 91% rename from core/src/main/java/dev/faststats/core/SimpleMetrics.java rename to core/src/main/java/dev/faststats/SimpleMetrics.java index 1e770a0..f61fb83 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/SimpleMetrics.java @@ -1,10 +1,11 @@ -package dev.faststats.core; +package dev.faststats; import com.google.gson.JsonObject; -import dev.faststats.core.data.Metric; -import dev.faststats.core.internal.Constants; -import dev.faststats.core.internal.Logger; -import dev.faststats.core.internal.LoggerFactory; +import dev.faststats.data.Metric; +import dev.faststats.internal.Constants; +import dev.faststats.internal.Logger; +import dev.faststats.internal.LoggerFactory; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.MustBeInvokedByOverriders; @@ -32,6 +33,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; +@ApiStatus.Internal public abstract class SimpleMetrics implements Metrics { protected final Logger logger = LoggerFactory.factory().getLogger(getClass()); @@ -50,11 +52,9 @@ public abstract class SimpleMetrics implements Metrics { @Contract(mutates = "io") @SuppressWarnings("PatternValidation") - protected SimpleMetrics(final Factory factory, final Config config) throws IllegalStateException { - if (factory.token == null) throw new IllegalStateException("Token must be specified"); - + protected SimpleMetrics(final Factory factory, final Config config) throws IllegalStateException { this.config = config; - this.token = factory.token; + this.token = factory.context.getToken(); this.metrics = config.additionalMetrics() ? Set.copyOf(factory.metrics) : Set.of(); final var debug = config.debug() || Boolean.getBoolean("faststats.debug"); this.logger.setFilter(level -> debug || level.equals(Level.CONFIG)); @@ -63,6 +63,11 @@ protected SimpleMetrics(final Factory factory, final Config config) throws this.url = getMetricsServerUrl(); } + @Contract(mutates = "io") + protected SimpleMetrics(final Factory factory) throws IllegalStateException { + this(factory, factory.context.getConfig()); + } + private URI getMetricsServerUrl() { final var property = System.getProperty("faststats.metrics-server"); if (property != null) try { @@ -257,7 +262,7 @@ public Optional getErrorTracker() { } @Override - public dev.faststats.core.Config getConfig() { + public dev.faststats.Config getConfig() { return config; } @@ -280,11 +285,15 @@ public void shutdown() { } } - public abstract static class Factory> implements Metrics.Factory { + public abstract static class Factory> implements Metrics.Factory { private final Set> metrics = new HashSet<>(0); + protected final FastStatsContext context; private @Nullable ErrorTracker tracker; private @Nullable Runnable flush; - private @Nullable String token; + + protected Factory(final FastStatsContext context) { + this.context = context; + } @Override @SuppressWarnings("unchecked") @@ -306,16 +315,5 @@ public F errorTracker(final ErrorTracker tracker) { this.tracker = tracker; return (F) this; } - - @Override - @SuppressWarnings("unchecked") - public F token(@Token final String token) throws IllegalArgumentException { - if (!token.matches(Token.PATTERN)) { - throw new IllegalArgumentException("Invalid token '" + token + "', must match '" + Token.PATTERN + "'"); - } - this.token = token; - return (F) this; - } } - } diff --git a/core/src/main/java/dev/faststats/core/Token.java b/core/src/main/java/dev/faststats/Token.java similarity index 93% rename from core/src/main/java/dev/faststats/core/Token.java rename to core/src/main/java/dev/faststats/Token.java index 35ed3d3..6eb09d9 100644 --- a/core/src/main/java/dev/faststats/core/Token.java +++ b/core/src/main/java/dev/faststats/Token.java @@ -1,4 +1,4 @@ -package dev.faststats.core; +package dev.faststats; import org.intellij.lang.annotations.Pattern; import org.jetbrains.annotations.NonNls; @@ -15,7 +15,7 @@ /** * An annotation to mark a token. * - * @since 0.1.0 + * @since 0.23.0 */ @NonNls @Pattern(Token.PATTERN) diff --git a/core/src/main/java/dev/faststats/core/data/ArrayMetric.java b/core/src/main/java/dev/faststats/data/ArrayMetric.java similarity index 96% rename from core/src/main/java/dev/faststats/core/data/ArrayMetric.java rename to core/src/main/java/dev/faststats/data/ArrayMetric.java index bcba91a..376fff9 100644 --- a/core/src/main/java/dev/faststats/core/data/ArrayMetric.java +++ b/core/src/main/java/dev/faststats/data/ArrayMetric.java @@ -1,4 +1,4 @@ -package dev.faststats.core.data; +package dev.faststats.data; import com.google.gson.JsonArray; import com.google.gson.JsonElement; diff --git a/core/src/main/java/dev/faststats/core/data/Metric.java b/core/src/main/java/dev/faststats/data/Metric.java similarity index 95% rename from core/src/main/java/dev/faststats/core/data/Metric.java rename to core/src/main/java/dev/faststats/data/Metric.java index 7fb753c..5b18918 100644 --- a/core/src/main/java/dev/faststats/core/data/Metric.java +++ b/core/src/main/java/dev/faststats/data/Metric.java @@ -1,4 +1,4 @@ -package dev.faststats.core.data; +package dev.faststats.data; import com.google.gson.JsonElement; import org.jetbrains.annotations.Contract; @@ -11,14 +11,14 @@ * A metric. * * @param the metric data type - * @since 0.16.0 + * @since 0.23.0 */ public interface Metric { /** * Get the source id. * * @return the source id - * @since 0.16.0 + * @since 0.23.0 */ @SourceId @Contract(pure = true) @@ -30,7 +30,7 @@ public interface Metric { * @return an optional containing the metric data * @throws Exception if unable to compute the metric data * @implSpec The implementation must be thread-safe and pure (i.e. not modify any shared state). - * @since 0.16.0 + * @since 0.23.0 */ @Contract(pure = true) Optional compute() throws Exception; @@ -43,7 +43,7 @@ public interface Metric { * @implSpec The implementation must call {@link #compute()} to get the metric data * and follow the same thread-safety and pureness requirements. * @see #compute() - * @since 0.16.0 + * @since 0.23.0 */ @Contract(pure = true) Optional getData() throws Exception; @@ -57,7 +57,7 @@ public interface Metric { * @throws IllegalArgumentException if the source id is invalid * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). * @see #compute() - * @since 0.16.0 + * @since 0.23.0 */ @Contract(value = "_, _ -> new", pure = true) static Metric stringArray(@SourceId final String id, final Callable callable) throws IllegalArgumentException { @@ -73,7 +73,7 @@ static Metric stringArray(@SourceId final String id, final Callable booleanArray(@SourceId final String id, final Callable callable) throws IllegalArgumentException { @@ -89,7 +89,7 @@ static Metric booleanArray(@SourceId final String id, final Callable< * @throws IllegalArgumentException if the source id is invalid * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). * @see #compute() - * @since 0.16.0 + * @since 0.23.0 */ @Contract(value = "_, _ -> new", pure = true) static Metric numberArray(@SourceId final String id, final Callable callable) throws IllegalArgumentException { @@ -105,7 +105,7 @@ static Metric numberArray(@SourceId final String id, final Callable bool(@SourceId final String id, final Callable<@Nullable Boolean> callable) throws IllegalArgumentException { @@ -121,7 +121,7 @@ static Metric bool(@SourceId final String id, final Callable<@Nullable * @throws IllegalArgumentException if the source id is invalid * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). * @see #compute() - * @since 0.16.0 + * @since 0.23.0 */ @Contract(value = "_, _ -> new", pure = true) static Metric string(@SourceId final String id, final Callable<@Nullable String> callable) throws IllegalArgumentException { @@ -137,7 +137,7 @@ static Metric string(@SourceId final String id, final Callable<@Nullable * @throws IllegalArgumentException if the source id is invalid * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). * @see #compute() - * @since 0.16.0 + * @since 0.23.0 */ @Contract(value = "_, _ -> new", pure = true) static Metric number(@SourceId final String id, final Callable<@Nullable Number> callable) throws IllegalArgumentException { diff --git a/core/src/main/java/dev/faststats/core/data/SimpleMetric.java b/core/src/main/java/dev/faststats/data/SimpleMetric.java similarity index 97% rename from core/src/main/java/dev/faststats/core/data/SimpleMetric.java rename to core/src/main/java/dev/faststats/data/SimpleMetric.java index 8d635e4..0012354 100644 --- a/core/src/main/java/dev/faststats/core/data/SimpleMetric.java +++ b/core/src/main/java/dev/faststats/data/SimpleMetric.java @@ -1,4 +1,4 @@ -package dev.faststats.core.data; +package dev.faststats.data; import org.jspecify.annotations.Nullable; diff --git a/core/src/main/java/dev/faststats/core/data/SingleValueMetric.java b/core/src/main/java/dev/faststats/data/SingleValueMetric.java similarity index 95% rename from core/src/main/java/dev/faststats/core/data/SingleValueMetric.java rename to core/src/main/java/dev/faststats/data/SingleValueMetric.java index 458679c..8cf335a 100644 --- a/core/src/main/java/dev/faststats/core/data/SingleValueMetric.java +++ b/core/src/main/java/dev/faststats/data/SingleValueMetric.java @@ -1,4 +1,4 @@ -package dev.faststats.core.data; +package dev.faststats.data; import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; diff --git a/core/src/main/java/dev/faststats/core/data/SourceId.java b/core/src/main/java/dev/faststats/data/SourceId.java similarity index 93% rename from core/src/main/java/dev/faststats/core/data/SourceId.java rename to core/src/main/java/dev/faststats/data/SourceId.java index c7295ec..f702b76 100644 --- a/core/src/main/java/dev/faststats/core/data/SourceId.java +++ b/core/src/main/java/dev/faststats/data/SourceId.java @@ -1,4 +1,4 @@ -package dev.faststats.core.data; +package dev.faststats.data; import org.intellij.lang.annotations.Pattern; import org.jetbrains.annotations.NonNls; @@ -15,7 +15,7 @@ /** * An annotation to mark a source id. * - * @since 0.16.0 + * @since 0.23.0 */ @NonNls @Pattern(SourceId.PATTERN) diff --git a/core/src/main/java/dev/faststats/core/internal/Constants.java b/core/src/main/java/dev/faststats/internal/Constants.java similarity index 90% rename from core/src/main/java/dev/faststats/core/internal/Constants.java rename to core/src/main/java/dev/faststats/internal/Constants.java index b379046..065c307 100644 --- a/core/src/main/java/dev/faststats/core/internal/Constants.java +++ b/core/src/main/java/dev/faststats/internal/Constants.java @@ -1,6 +1,6 @@ -package dev.faststats.core.internal; +package dev.faststats.internal; -import dev.faststats.core.SimpleMetrics; +import dev.faststats.SimpleMetrics; import java.io.IOException; import java.util.Properties; diff --git a/core/src/main/java/dev/faststats/core/internal/Logger.java b/core/src/main/java/dev/faststats/internal/Logger.java similarity index 95% rename from core/src/main/java/dev/faststats/core/internal/Logger.java rename to core/src/main/java/dev/faststats/internal/Logger.java index d5fd1d9..3f5f232 100644 --- a/core/src/main/java/dev/faststats/core/internal/Logger.java +++ b/core/src/main/java/dev/faststats/internal/Logger.java @@ -1,4 +1,4 @@ -package dev.faststats.core.internal; +package dev.faststats.internal; import org.intellij.lang.annotations.PrintFormat; import org.jspecify.annotations.Nullable; diff --git a/core/src/main/java/dev/faststats/core/internal/LoggerFactory.java b/core/src/main/java/dev/faststats/internal/LoggerFactory.java similarity index 93% rename from core/src/main/java/dev/faststats/core/internal/LoggerFactory.java rename to core/src/main/java/dev/faststats/internal/LoggerFactory.java index 5a4a1af..567bd5d 100644 --- a/core/src/main/java/dev/faststats/core/internal/LoggerFactory.java +++ b/core/src/main/java/dev/faststats/internal/LoggerFactory.java @@ -1,4 +1,4 @@ -package dev.faststats.core.internal; +package dev.faststats.internal; import java.util.ServiceLoader; diff --git a/core/src/main/java/dev/faststats/core/internal/SimpleLogger.java b/core/src/main/java/dev/faststats/internal/SimpleLogger.java similarity index 97% rename from core/src/main/java/dev/faststats/core/internal/SimpleLogger.java rename to core/src/main/java/dev/faststats/internal/SimpleLogger.java index 37d96ed..d16cc69 100644 --- a/core/src/main/java/dev/faststats/core/internal/SimpleLogger.java +++ b/core/src/main/java/dev/faststats/internal/SimpleLogger.java @@ -1,4 +1,4 @@ -package dev.faststats.core.internal; +package dev.faststats.internal; import org.jspecify.annotations.Nullable; diff --git a/core/src/main/java/dev/faststats/core/internal/SimpleLoggerFactory.java b/core/src/main/java/dev/faststats/internal/SimpleLoggerFactory.java similarity index 82% rename from core/src/main/java/dev/faststats/core/internal/SimpleLoggerFactory.java rename to core/src/main/java/dev/faststats/internal/SimpleLoggerFactory.java index aab3ef8..dcb8b9c 100644 --- a/core/src/main/java/dev/faststats/core/internal/SimpleLoggerFactory.java +++ b/core/src/main/java/dev/faststats/internal/SimpleLoggerFactory.java @@ -1,4 +1,4 @@ -package dev.faststats.core.internal; +package dev.faststats.internal; final class SimpleLoggerFactory implements LoggerFactory { @Override diff --git a/core/src/main/java/dev/faststats/core/internal/package-info.java b/core/src/main/java/dev/faststats/internal/package-info.java similarity index 63% rename from core/src/main/java/dev/faststats/core/internal/package-info.java rename to core/src/main/java/dev/faststats/internal/package-info.java index 93362ed..dfe3b56 100644 --- a/core/src/main/java/dev/faststats/core/internal/package-info.java +++ b/core/src/main/java/dev/faststats/internal/package-info.java @@ -1,4 +1,4 @@ @ApiStatus.Internal -package dev.faststats.core.internal; +package dev.faststats.internal; import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index e665b64..0f76310 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -1,11 +1,10 @@ import org.jspecify.annotations.NullMarked; @NullMarked -module dev.faststats.core { - exports dev.faststats.core.data; - exports dev.faststats.core.flags; - exports dev.faststats.core.internal; - exports dev.faststats.core; +module dev.faststats { + exports dev.faststats.data; + exports dev.faststats.internal; + exports dev.faststats; requires com.google.gson; requires java.logging; @@ -14,5 +13,5 @@ requires static org.jetbrains.annotations; requires static org.jspecify; - uses dev.faststats.core.internal.LoggerFactory; + uses dev.faststats.internal.LoggerFactory; } diff --git a/core/src/test/java/dev/faststats/AnonymizationTest.java b/core/src/test/java/dev/faststats/AnonymizationTest.java index 512a955..aa7be74 100644 --- a/core/src/test/java/dev/faststats/AnonymizationTest.java +++ b/core/src/test/java/dev/faststats/AnonymizationTest.java @@ -1,7 +1,6 @@ package dev.faststats; import com.google.gson.JsonObject; -import dev.faststats.core.ErrorTracker; import org.jspecify.annotations.NullMarked; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/dev/faststats/ErrorTrackerTest.java b/core/src/test/java/dev/faststats/ErrorTrackerTest.java index f500cd4..60983aa 100644 --- a/core/src/test/java/dev/faststats/ErrorTrackerTest.java +++ b/core/src/test/java/dev/faststats/ErrorTrackerTest.java @@ -1,6 +1,5 @@ package dev.faststats; -import dev.faststats.core.ErrorTracker; import org.junit.jupiter.api.Test; import java.net.URL; diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index 4fd403b..00bdc21 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -2,9 +2,6 @@ import com.google.gson.JsonObject; import dev.faststats.config.SimpleConfig; -import dev.faststats.core.ErrorTracker; -import dev.faststats.core.SimpleMetrics; -import dev.faststats.core.Token; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; diff --git a/fabric/example-mod/src/main/java/com/example/ExampleMod.java b/fabric/example-mod/src/main/java/com/example/ExampleMod.java index 779163a..4672e62 100644 --- a/fabric/example-mod/src/main/java/com/example/ExampleMod.java +++ b/fabric/example-mod/src/main/java/com/example/ExampleMod.java @@ -1,20 +1,20 @@ package com.example; -import dev.faststats.core.ErrorTracker; -import dev.faststats.core.Metrics; -import dev.faststats.core.data.Metric; -import dev.faststats.fabric.FabricMetrics; +import dev.faststats.ErrorTracker; +import dev.faststats.Metrics; +import dev.faststats.data.Metric; +import dev.faststats.fabric.FabricContext; import net.fabricmc.api.ModInitializer; public class ExampleMod implements ModInitializer { - private final Metrics metrics = FabricMetrics.factory() + private final FabricContext context = new FabricContext("YOUR_TOKEN_HERE"); + private final Metrics metrics = context.metrics() // Custom metrics require a corresponding data source in your project settings .addMetric(Metric.number("example_metric", () -> 42)) // Error tracking must be enabled in the project settings .errorTracker(ErrorTracker.contextAware()) - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create("example-mod"); // your mod id as defined in fabric.mod.json @Override diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricContext.java b/fabric/src/main/java/dev/faststats/fabric/FabricContext.java new file mode 100644 index 0000000..2fc32be --- /dev/null +++ b/fabric/src/main/java/dev/faststats/fabric/FabricContext.java @@ -0,0 +1,28 @@ +package dev.faststats.fabric; + +import dev.faststats.SimpleContext; +import dev.faststats.Token; +import dev.faststats.config.SimpleConfig; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; + +/** + * Fabric FastStats context. + * + * @since 0.23.0 + */ +public final class FabricContext extends SimpleContext { + final ModContainer mod; + + public FabricContext(final String modId, @Token final String token) { + super(SimpleConfig.read(FabricLoader.getInstance().getConfigDir().resolve("faststats").resolve("config.properties")), token); + this.mod = FabricLoader.getInstance().getModContainer(modId).orElseThrow(() -> { + return new IllegalArgumentException("Mod not found: " + modId); + }); + } + + @Override + public FabricMetrics.Factory metrics() { + return new FabricMetricsImpl.Factory(this); + } +} diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java b/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java index f6ce2df..786c515 100644 --- a/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java +++ b/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java @@ -1,8 +1,6 @@ package dev.faststats.fabric; -import dev.faststats.core.Metrics; -import org.jetbrains.annotations.Async; -import org.jetbrains.annotations.Contract; +import dev.faststats.Metrics; /** * Fabric metrics implementation. @@ -10,33 +8,6 @@ * @since 0.12.0 */ public sealed interface FabricMetrics extends Metrics permits FabricMetricsImpl { - /** - * Creates a new metrics factory for Fabric. - * - * @return the metrics factory - * @since 0.12.0 - */ - @Contract(pure = true) - static Factory factory() { - return new FabricMetricsImpl.Factory(); - } - - interface Factory extends Metrics.Factory { - /** - * Creates a new metrics instance. - *

- * Metrics submission will start automatically. - * - * @param modId the mod id - * @return the metrics instance - * @throws IllegalStateException if the token is not specified - * @throws IllegalArgumentException if the mod is not found - * @see #token(String) - * @since 0.12.0 - */ - @Override - @Async.Schedule - @Contract(value = "_ -> new", mutates = "io") - Metrics create(String modId) throws IllegalStateException, IllegalArgumentException; + interface Factory extends Metrics.Factory { } } diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java b/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java index 8e67086..f6f683f 100644 --- a/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java +++ b/fabric/src/main/java/dev/faststats/fabric/FabricMetricsImpl.java @@ -1,18 +1,16 @@ package dev.faststats.fabric; import com.google.gson.JsonObject; +import dev.faststats.Metrics; +import dev.faststats.SimpleMetrics; import dev.faststats.config.SimpleConfig; -import dev.faststats.core.Metrics; -import dev.faststats.core.SimpleMetrics; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; -import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; import net.minecraft.server.MinecraftServer; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.Nullable; -import java.nio.file.Path; import java.util.Optional; import java.util.function.Supplier; @@ -23,8 +21,8 @@ final class FabricMetricsImpl extends SimpleMetrics implements FabricMetrics { @Async.Schedule @Contract(mutates = "io") - private FabricMetricsImpl(final Factory factory, final ModContainer mod, final Path config) throws IllegalStateException { - super(factory, SimpleConfig.read(config)); + private FabricMetricsImpl(final Factory factory, final ModContainer mod) throws IllegalStateException { + super(factory); this.mod = mod; @@ -58,18 +56,14 @@ private Optional tryOrEmpty(final Supplier supplier) { } } - static final class Factory extends SimpleMetrics.Factory implements FabricMetrics.Factory { - @Override - public Metrics create(final String modId) throws IllegalStateException, IllegalArgumentException { - final var fabric = FabricLoader.getInstance(); - final var mod = fabric.getModContainer(modId).orElseThrow(() -> { - return new IllegalArgumentException("Mod not found: " + modId); - }); - - final var dataFolder = fabric.getConfigDir().resolve("faststats"); - final var config = dataFolder.resolve("config.properties"); + static final class Factory extends SimpleMetrics.Factory implements FabricMetrics.Factory { + public Factory(final FabricContext context) { + super(context); + } - return new FabricMetricsImpl(this, mod, config); + @Override + public Metrics create() throws IllegalStateException, IllegalArgumentException { + return new FabricMetricsImpl(this, ((FabricContext) context).mod); } } } diff --git a/fabric/src/main/java/module-info.java b/fabric/src/main/java/module-info.java index d4d6554..2ca3373 100644 --- a/fabric/src/main/java/module-info.java +++ b/fabric/src/main/java/module-info.java @@ -6,7 +6,7 @@ requires com.google.gson; requires dev.faststats.config; - requires dev.faststats.core; + requires dev.faststats; requires net.fabricmc.loader; requires org.slf4j; diff --git a/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java b/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java index 98a7958..54453c2 100644 --- a/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -2,20 +2,20 @@ import com.hypixel.hytale.server.core.plugin.JavaPlugin; import com.hypixel.hytale.server.core.plugin.JavaPluginInit; -import dev.faststats.core.ErrorTracker; -import dev.faststats.core.Metrics; -import dev.faststats.core.data.Metric; -import dev.faststats.hytale.HytaleMetrics; +import dev.faststats.ErrorTracker; +import dev.faststats.Metrics; +import dev.faststats.data.Metric; +import dev.faststats.hytale.HytaleContext; public class ExamplePlugin extends JavaPlugin { - private final Metrics metrics = HytaleMetrics.factory() + private final HytaleContext context = new HytaleContext(this, "YOUR_TOKEN_HERE"); + private final Metrics metrics = context.metrics() // Custom metrics require a corresponding data source in your project settings .addMetric(Metric.number("example_metric", () -> 42)) // Error tracking must be enabled in the project settings .errorTracker(ErrorTracker.contextAware()) - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create(this); public ExamplePlugin(final JavaPluginInit init) { diff --git a/hytale/src/main/java/dev/faststats/hytale/HytaleContext.java b/hytale/src/main/java/dev/faststats/hytale/HytaleContext.java new file mode 100644 index 0000000..47f3407 --- /dev/null +++ b/hytale/src/main/java/dev/faststats/hytale/HytaleContext.java @@ -0,0 +1,22 @@ +package dev.faststats.hytale; + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import dev.faststats.SimpleContext; +import dev.faststats.Token; +import dev.faststats.config.SimpleConfig; + +/** + * Hytale FastStats context. + * + * @since 0.23.0 + */ +public final class HytaleContext extends SimpleContext { + public HytaleContext(final JavaPlugin plugin, @Token final String token) { + super(SimpleConfig.read(plugin.getDataDirectory().toAbsolutePath().getParent().resolve("faststats").resolve("config.properties")), token); + } + + @Override + public HytaleMetrics.Factory metrics() { + return new HytaleMetricsImpl.Factory(this); + } +} diff --git a/hytale/src/main/java/dev/faststats/hytale/HytaleMetrics.java b/hytale/src/main/java/dev/faststats/hytale/HytaleMetrics.java index 96748f7..e217866 100644 --- a/hytale/src/main/java/dev/faststats/hytale/HytaleMetrics.java +++ b/hytale/src/main/java/dev/faststats/hytale/HytaleMetrics.java @@ -1,8 +1,6 @@ package dev.faststats.hytale; -import com.hypixel.hytale.server.core.plugin.JavaPlugin; -import dev.faststats.core.Metrics; -import org.jetbrains.annotations.Contract; +import dev.faststats.Metrics; /** * Hytale metrics implementation. @@ -10,17 +8,6 @@ * @since 0.9.0 */ public sealed interface HytaleMetrics extends Metrics permits HytaleMetricsImpl { - /** - * Creates a new metrics factory for Hytale. - * - * @return the metrics factory - * @since 0.9.0 - */ - @Contract(pure = true) - static Factory factory() { - return new HytaleMetricsImpl.Factory(); - } - - interface Factory extends Metrics.Factory { + interface Factory extends Metrics.Factory { } } diff --git a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java index d2538ea..3f0973e 100644 --- a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java +++ b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java @@ -2,21 +2,19 @@ import com.google.gson.JsonObject; import com.hypixel.hytale.server.core.HytaleServer; -import com.hypixel.hytale.server.core.plugin.JavaPlugin; import com.hypixel.hytale.server.core.universe.Universe; +import dev.faststats.Config; +import dev.faststats.Metrics; +import dev.faststats.SimpleMetrics; import dev.faststats.config.SimpleConfig; -import dev.faststats.core.Metrics; -import dev.faststats.core.SimpleMetrics; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import java.nio.file.Path; - final class HytaleMetricsImpl extends SimpleMetrics implements HytaleMetrics { @Async.Schedule @Contract(mutates = "io") - private HytaleMetricsImpl(final Factory factory, final Path config) throws IllegalStateException { - super(factory, SimpleConfig.read(config)); + private HytaleMetricsImpl(final Factory factory) throws IllegalStateException { + super(factory); startSubmitting(); } @@ -33,12 +31,14 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", "Hytale"); } - static final class Factory extends SimpleMetrics.Factory implements HytaleMetrics.Factory { + static final class Factory extends SimpleMetrics.Factory implements HytaleMetrics.Factory { + public Factory(final HytaleContext context) { + super(context); + } + @Override - public Metrics create(final JavaPlugin plugin) throws IllegalStateException { - final var mods = plugin.getDataDirectory().toAbsolutePath().getParent(); - final var config = mods.resolve("faststats").resolve("config.properties"); - return new HytaleMetricsImpl(this, config); + public Metrics create() throws IllegalStateException { + return new HytaleMetricsImpl(this); } } } diff --git a/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLogger.java b/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLogger.java index 3b9ae5c..7f276d1 100644 --- a/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLogger.java +++ b/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLogger.java @@ -6,7 +6,7 @@ import java.util.function.Predicate; import java.util.logging.Level; -final class HytaleLogger implements dev.faststats.core.internal.Logger { +final class HytaleLogger implements dev.faststats.internal.Logger { private final com.hypixel.hytale.logger.HytaleLogger logger; private volatile @Nullable Predicate filter; diff --git a/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLoggerFactory.java b/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLoggerFactory.java index 81d2cce..2a7c420 100644 --- a/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLoggerFactory.java +++ b/hytale/src/main/java/dev/faststats/hytale/logger/HytaleLoggerFactory.java @@ -1,8 +1,8 @@ package dev.faststats.hytale.logger; -public final class HytaleLoggerFactory implements dev.faststats.core.internal.LoggerFactory { +public final class HytaleLoggerFactory implements dev.faststats.internal.LoggerFactory { @Override - public dev.faststats.core.internal.Logger getLogger(final String name) { + public dev.faststats.internal.Logger getLogger(final String name) { return new HytaleLogger(name); } } diff --git a/hytale/src/main/java/module-info.java b/hytale/src/main/java/module-info.java index 5fa387f..a937216 100644 --- a/hytale/src/main/java/module-info.java +++ b/hytale/src/main/java/module-info.java @@ -6,11 +6,11 @@ requires com.google.gson; requires dev.faststats.config; - requires dev.faststats.core; + requires dev.faststats; requires java.logging; requires static org.jetbrains.annotations; requires static org.jspecify; - provides dev.faststats.core.internal.LoggerFactory with dev.faststats.hytale.logger.HytaleLoggerFactory; + provides dev.faststats.internal.LoggerFactory with dev.faststats.hytale.logger.HytaleLoggerFactory; } diff --git a/minestom/src/main/java/dev/faststats/minestom/MinestomContext.java b/minestom/src/main/java/dev/faststats/minestom/MinestomContext.java new file mode 100644 index 0000000..2433f73 --- /dev/null +++ b/minestom/src/main/java/dev/faststats/minestom/MinestomContext.java @@ -0,0 +1,23 @@ +package dev.faststats.minestom; + +import dev.faststats.SimpleContext; +import dev.faststats.Token; +import dev.faststats.config.SimpleConfig; + +import java.nio.file.Path; + +/** + * Minestom FastStats context. + * + * @since 0.23.0 + */ +public final class MinestomContext extends SimpleContext { + public MinestomContext(@Token final String token) { + super(SimpleConfig.read(Path.of("faststats", "config.properties")), token); + } + + @Override + public MinestomMetrics.Factory metrics() { + return new MinestomMetricsImpl.Factory(this); + } +} diff --git a/minestom/src/main/java/dev/faststats/minestom/MinestomMetrics.java b/minestom/src/main/java/dev/faststats/minestom/MinestomMetrics.java index f4df6b9..aa48ccf 100644 --- a/minestom/src/main/java/dev/faststats/minestom/MinestomMetrics.java +++ b/minestom/src/main/java/dev/faststats/minestom/MinestomMetrics.java @@ -1,9 +1,8 @@ package dev.faststats.minestom; -import dev.faststats.core.Metrics; +import dev.faststats.Metrics; import net.minestom.server.Auth; import net.minestom.server.MinecraftServer; -import org.jetbrains.annotations.Contract; /** * Minestom metrics implementation. @@ -11,17 +10,6 @@ * @since 0.1.0 */ public sealed interface MinestomMetrics extends Metrics permits MinestomMetricsImpl { - /** - * Creates a new metrics factory forMinestom. - * - * @return the metrics factory - * @since 0.1.0 - */ - @Contract(pure = true) - static Factory factory() { - return new MinestomMetricsImpl.Factory(); - } - /** * Registers additional exception handlers. * @@ -31,6 +19,6 @@ static Factory factory() { @Override void ready(); - interface Factory extends Metrics.Factory { + interface Factory extends Metrics.Factory { } } diff --git a/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java b/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java index e137c38..1577c9d 100644 --- a/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java +++ b/minestom/src/main/java/dev/faststats/minestom/MinestomMetricsImpl.java @@ -1,22 +1,20 @@ package dev.faststats.minestom; import com.google.gson.JsonObject; +import dev.faststats.ErrorTracker; +import dev.faststats.Metrics; +import dev.faststats.SimpleMetrics; import dev.faststats.config.SimpleConfig; -import dev.faststats.core.ErrorTracker; -import dev.faststats.core.Metrics; -import dev.faststats.core.SimpleMetrics; import net.minestom.server.Auth; import net.minestom.server.MinecraftServer; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import java.nio.file.Path; - final class MinestomMetricsImpl extends SimpleMetrics implements MinestomMetrics { @Async.Schedule @Contract(mutates = "io") - private MinestomMetricsImpl(final Factory factory, final Path config) throws IllegalStateException { - super(factory, SimpleConfig.read(config)); + private MinestomMetricsImpl(final Factory factory) throws IllegalStateException { + super(factory); startSubmitting(); } @@ -48,11 +46,14 @@ private void registerExceptionHandler(final ErrorTracker errorTracker) { }); } - static final class Factory extends SimpleMetrics.Factory implements MinestomMetrics.Factory { + static final class Factory extends SimpleMetrics.Factory implements MinestomMetrics.Factory { + Factory(final MinestomContext context) { + super(context); + } + @Override - public Metrics create(final MinecraftServer server) throws IllegalStateException { - final var config = Path.of("faststats", "config.properties"); - return new MinestomMetricsImpl(this, config); + public Metrics create() throws IllegalStateException { + return new MinestomMetricsImpl(this); } } } diff --git a/minestom/src/main/java/module-info.java b/minestom/src/main/java/module-info.java index ff84749..e0c4b0c 100644 --- a/minestom/src/main/java/module-info.java +++ b/minestom/src/main/java/module-info.java @@ -6,7 +6,7 @@ requires com.google.gson; requires dev.faststats.config; - requires dev.faststats.core; + requires dev.faststats; requires net.minestom.server; requires org.slf4j; diff --git a/nukkit/src/main/java/dev/faststats/nukkit/NukkitContext.java b/nukkit/src/main/java/dev/faststats/nukkit/NukkitContext.java new file mode 100644 index 0000000..f45e96f --- /dev/null +++ b/nukkit/src/main/java/dev/faststats/nukkit/NukkitContext.java @@ -0,0 +1,27 @@ +package dev.faststats.nukkit; + +import cn.nukkit.plugin.PluginBase; +import dev.faststats.SimpleContext; +import dev.faststats.Token; +import dev.faststats.config.SimpleConfig; + +import java.nio.file.Path; + +/** + * Nukkit FastStats context. + * + * @since 0.23.0 + */ +public final class NukkitContext extends SimpleContext { + final PluginBase plugin; + + public NukkitContext(final PluginBase plugin, @Token final String token) { + super(SimpleConfig.read(Path.of(plugin.getServer().getPluginPath(), "faststats", "config.properties")), token); + this.plugin = plugin; + } + + @Override + public NukkitMetrics.Factory metrics() { + return new NukkitMetricsImpl.Factory(this); + } +} diff --git a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetrics.java b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetrics.java index 2420e2a..a8cd305 100644 --- a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetrics.java +++ b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetrics.java @@ -1,7 +1,6 @@ package dev.faststats.nukkit; -import cn.nukkit.plugin.PluginBase; -import dev.faststats.core.Metrics; +import dev.faststats.Metrics; import org.jetbrains.annotations.Contract; /** @@ -10,17 +9,6 @@ * @since 0.8.0 */ public sealed interface NukkitMetrics extends Metrics permits NukkitMetricsImpl { - /** - * Creates a new metrics factory for Nukkit. - * - * @return the metrics factory - * @since 0.8.0 - */ - @Contract(pure = true) - static Factory factory() { - return new NukkitMetricsImpl.Factory(); - } - - interface Factory extends Metrics.Factory { + interface Factory extends Metrics.Factory { } } diff --git a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java index 51afb33..d91ff38 100644 --- a/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java +++ b/nukkit/src/main/java/dev/faststats/nukkit/NukkitMetricsImpl.java @@ -3,13 +3,12 @@ import cn.nukkit.Server; import cn.nukkit.plugin.PluginBase; import com.google.gson.JsonObject; +import dev.faststats.Metrics; +import dev.faststats.SimpleMetrics; import dev.faststats.config.SimpleConfig; -import dev.faststats.core.Metrics; -import dev.faststats.core.SimpleMetrics; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; -import java.nio.file.Path; import java.util.Optional; import java.util.function.Supplier; @@ -19,8 +18,8 @@ final class NukkitMetricsImpl extends SimpleMetrics implements NukkitMetrics { @Async.Schedule @Contract(mutates = "io") - private NukkitMetricsImpl(final Factory factory, final PluginBase plugin, final Path config) throws IllegalStateException { - super(factory, SimpleConfig.read(config)); + private NukkitMetricsImpl(final Factory factory, final PluginBase plugin) throws IllegalStateException { + super(factory); this.server = plugin.getServer(); this.plugin = plugin; @@ -50,12 +49,14 @@ private Optional tryOrEmpty(final Supplier supplier) { } } - static final class Factory extends SimpleMetrics.Factory implements NukkitMetrics.Factory { + static final class Factory extends SimpleMetrics.Factory implements NukkitMetrics.Factory { + Factory(final NukkitContext context) { + super(context); + } + @Override - public Metrics create(final PluginBase plugin) throws IllegalStateException { - final var dataFolder = Path.of(plugin.getServer().getPluginPath(), "faststats"); - final var config = dataFolder.resolve("config.properties"); - return new NukkitMetricsImpl(this, plugin, config); + public Metrics create() throws IllegalStateException { + return new NukkitMetricsImpl(this, ((NukkitContext) context).plugin); } } } diff --git a/nukkit/src/main/java/module-info.java b/nukkit/src/main/java/module-info.java index 1b104b5..c8722c4 100644 --- a/nukkit/src/main/java/module-info.java +++ b/nukkit/src/main/java/module-info.java @@ -6,7 +6,7 @@ requires com.google.gson; requires dev.faststats.config; - requires dev.faststats.core; + requires dev.faststats; requires static org.jetbrains.annotations; requires static org.jspecify; diff --git a/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java b/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java index bb6caab..35d6a6e 100644 --- a/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -1,35 +1,41 @@ package com.example; import com.google.inject.Inject; -import dev.faststats.core.ErrorTracker; -import dev.faststats.core.Metrics; -import dev.faststats.core.data.Metric; -import dev.faststats.sponge.SpongeMetrics; +import dev.faststats.ErrorTracker; +import dev.faststats.Metrics; +import dev.faststats.data.Metric; +import dev.faststats.sponge.SpongeContext; +import org.apache.logging.log4j.Logger; import org.jspecify.annotations.Nullable; import org.spongepowered.api.Server; +import org.spongepowered.api.config.ConfigDir; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.lifecycle.StartedEngineEvent; import org.spongepowered.api.event.lifecycle.StoppingEngineEvent; import org.spongepowered.plugin.PluginContainer; import org.spongepowered.plugin.builtin.jvm.Plugin; +import java.nio.file.Path; + @Plugin("example") public class ExamplePlugin { private @Inject PluginContainer pluginContainer; - private @Inject SpongeMetrics.Factory factory; + private @Inject Logger logger; + private @ConfigDir(sharedRoot = true) + @Inject Path dataDirectory; private @Nullable Metrics metrics = null; @Listener public void onServerStart(final StartedEngineEvent event) { - this.metrics = factory + final var context = new SpongeContext(pluginContainer, logger, dataDirectory, "YOUR_TOKEN_HERE"); + this.metrics = context.metrics() // Custom metrics require a corresponding data source in your project settings .addMetric(Metric.number("example_metric", () -> 42)) // Error tracking must be enabled in the project settings .errorTracker(ErrorTracker.contextAware()) - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create(pluginContainer); } diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeConfig.java b/sponge/src/main/java/dev/faststats/sponge/SpongeConfig.java index 9d1d5c5..6dd3a95 100644 --- a/sponge/src/main/java/dev/faststats/sponge/SpongeConfig.java +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeConfig.java @@ -1,7 +1,7 @@ package dev.faststats.sponge; -import dev.faststats.core.Config; -import dev.faststats.core.internal.LoggerFactory; +import dev.faststats.Config; +import dev.faststats.internal.LoggerFactory; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.spongepowered.api.Sponge; diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeContext.java b/sponge/src/main/java/dev/faststats/sponge/SpongeContext.java new file mode 100644 index 0000000..b177408 --- /dev/null +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeContext.java @@ -0,0 +1,35 @@ +package dev.faststats.sponge; + +import dev.faststats.SimpleContext; +import dev.faststats.Token; +import org.apache.logging.log4j.Logger; +import org.spongepowered.api.config.ConfigDir; +import org.spongepowered.plugin.PluginContainer; + +import java.nio.file.Path; + +/** + * Sponge FastStats context. + * + * @since 0.23.0 + */ +public final class SpongeContext extends SimpleContext { + final PluginContainer plugin; + final Logger logger; + + public SpongeContext( + final PluginContainer plugin, + final Logger logger, + @ConfigDir(sharedRoot = true) final Path dataDirectory, + @Token final String token // fixme: cannot have a token here + ) { + super(SpongeConfig.read(plugin, dataDirectory.resolve("faststats").resolve("config.properties")), token); + this.plugin = plugin; + this.logger = logger; + } + + @Override + public SpongeMetrics.Factory metrics() { + return new SpongeMetrics.Factory(this); + } +} diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeMetrics.java b/sponge/src/main/java/dev/faststats/sponge/SpongeMetrics.java index 380df38..8dcc125 100644 --- a/sponge/src/main/java/dev/faststats/sponge/SpongeMetrics.java +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeMetrics.java @@ -1,11 +1,7 @@ package dev.faststats.sponge; -import com.google.inject.Inject; -import dev.faststats.core.Metrics; -import org.apache.logging.log4j.Logger; -import org.spongepowered.api.config.ConfigDir; - -import java.nio.file.Path; +import dev.faststats.FastStatsContext; +import dev.faststats.Metrics; /** * Sponge metrics implementation. @@ -14,17 +10,8 @@ */ public sealed interface SpongeMetrics extends Metrics permits SpongeMetricsImpl { final class Factory extends SpongeMetricsImpl.Factory { - /** - * Creates a new metrics factory for Sponge. - * - * @param logger the logger - * @param dataDirectory the data directory - * @apiNote This instance is automatically injected into your plugin. - * @since 0.12.0 - */ - @Inject - private Factory(final Logger logger, @ConfigDir(sharedRoot = true) final Path dataDirectory) { - super(logger, dataDirectory); + public Factory(final FastStatsContext context) { + super(context); } } } diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java index c96d34c..d67289c 100644 --- a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java @@ -1,8 +1,9 @@ package dev.faststats.sponge; import com.google.gson.JsonObject; -import dev.faststats.core.Metrics; -import dev.faststats.core.SimpleMetrics; +import dev.faststats.FastStatsContext; +import dev.faststats.Metrics; +import dev.faststats.SimpleMetrics; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; @@ -10,8 +11,6 @@ import org.spongepowered.api.Sponge; import org.spongepowered.plugin.PluginContainer; -import java.nio.file.Path; - final class SpongeMetricsImpl extends SimpleMetrics implements SpongeMetrics { private final PluginContainer plugin; @@ -21,10 +20,9 @@ final class SpongeMetricsImpl extends SimpleMetrics implements SpongeMetrics { private SpongeMetricsImpl( final Factory factory, final Logger logger, - final PluginContainer plugin, - final Path config + final PluginContainer plugin ) throws IllegalStateException { - super(factory, SpongeConfig.read(plugin, config)); + super(factory); this.plugin = plugin; startSubmitting(); } @@ -43,19 +41,15 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", Sponge.platform().container(Platform.Component.IMPLEMENTATION).metadata().id()); } - static class Factory extends SimpleMetrics.Factory { - protected final Logger logger; - protected final Path dataDirectory; - - public Factory(final Logger logger, final Path dataDirectory) { - this.logger = logger; - this.dataDirectory = dataDirectory; + static class Factory extends SimpleMetrics.Factory { + public Factory(final FastStatsContext context) { + super(context); } @Override - public Metrics create(final PluginContainer plugin) throws IllegalStateException, IllegalArgumentException { - final var faststats = dataDirectory.resolve("faststats"); - return new SpongeMetricsImpl(this, logger, plugin, faststats.resolve("config.properties")); + public Metrics create() throws IllegalStateException, IllegalArgumentException { + final var context = (SpongeContext) this.context; + return new SpongeMetricsImpl(this, context.logger, context.plugin); } } } diff --git a/sponge/src/main/java/module-info.java b/sponge/src/main/java/module-info.java index e709333..61c8a82 100644 --- a/sponge/src/main/java/module-info.java +++ b/sponge/src/main/java/module-info.java @@ -6,7 +6,7 @@ requires com.google.gson; requires com.google.guice; - requires dev.faststats.core; + requires dev.faststats; requires java.logging; requires static org.jetbrains.annotations; diff --git a/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java b/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java index 29d136d..50b074e 100644 --- a/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/velocity/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -5,33 +5,37 @@ import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.Plugin; -import dev.faststats.core.ErrorTracker; -import dev.faststats.core.Metrics; -import dev.faststats.core.data.Metric; -import dev.faststats.velocity.VelocityMetrics; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ProxyServer; +import dev.faststats.ErrorTracker; +import dev.faststats.Metrics; +import dev.faststats.data.Metric; +import dev.faststats.velocity.VelocityContext; import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +import java.nio.file.Path; @Plugin(id = "example", name = "Example Plugin", version = "1.0.0", url = "https://example.com", authors = {"Your Name"}) public class ExamplePlugin { - private final VelocityMetrics.Factory metricsFactory; + private final VelocityContext context; private @Nullable Metrics metrics = null; @Inject - public ExamplePlugin(final VelocityMetrics.Factory factory) { - this.metricsFactory = factory; + public ExamplePlugin(final ProxyServer server, final Logger logger, @DataDirectory final Path dataDirectory) { + this.context = new VelocityContext(server, logger, dataDirectory, "YOUR_TOKEN_HERE"); } @Subscribe public void onProxyInitialize(final ProxyInitializeEvent event) { - this.metrics = metricsFactory + this.metrics = context.metrics() // Custom metrics require a corresponding data source in your project settings .addMetric(Metric.number("example_metric", () -> 42)) // Error tracking must be enabled in the project settings .errorTracker(ErrorTracker.contextAware()) - .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project .create(this); } diff --git a/velocity/src/main/java/dev/faststats/velocity/VelocityContext.java b/velocity/src/main/java/dev/faststats/velocity/VelocityContext.java new file mode 100644 index 0000000..eab1f88 --- /dev/null +++ b/velocity/src/main/java/dev/faststats/velocity/VelocityContext.java @@ -0,0 +1,49 @@ +package dev.faststats.velocity; + +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ProxyServer; +import dev.faststats.Config; +import dev.faststats.FastStatsContext; +import dev.faststats.Token; +import dev.faststats.config.SimpleConfig; +import org.slf4j.Logger; + +import java.nio.file.Path; + +/** + * Velocity FastStats context. + * + * @since 0.23.0 + */ +public final class VelocityContext extends FastStatsContext { + private final ProxyServer server; + private final Logger logger; + private final Path dataDirectory; + + public VelocityContext( + final Config config, + final ProxyServer server, + final Logger logger, + final Path dataDirectory, + @Token final String token + ) { + super(config, token); + this.server = server; + this.logger = logger; + this.dataDirectory = dataDirectory; + } + + public VelocityContext( + final ProxyServer server, + final Logger logger, + @DataDirectory final Path dataDirectory, + @Token final String token + ) { + this(SimpleConfig.read(dataDirectory.resolveSibling("faststats").resolve("config.properties")), server, logger, dataDirectory, token); + } + + @Override + public VelocityMetrics.Factory metrics() { + return new VelocityMetrics.Factory(this, server, logger, dataDirectory); + } +} diff --git a/velocity/src/main/java/dev/faststats/velocity/VelocityMetrics.java b/velocity/src/main/java/dev/faststats/velocity/VelocityMetrics.java index d552d33..1aa36b5 100644 --- a/velocity/src/main/java/dev/faststats/velocity/VelocityMetrics.java +++ b/velocity/src/main/java/dev/faststats/velocity/VelocityMetrics.java @@ -3,7 +3,7 @@ import com.google.inject.Inject; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; -import dev.faststats.core.Metrics; +import dev.faststats.Metrics; import org.slf4j.Logger; import java.nio.file.Path; @@ -15,6 +15,10 @@ */ public sealed interface VelocityMetrics extends Metrics permits VelocityMetricsImpl { final class Factory extends VelocityMetricsImpl.Factory { + public Factory(final VelocityContext context, final ProxyServer server, final Logger logger, @DataDirectory final Path dataDirectory) { + super(context, server, logger, dataDirectory); + } + /** * Creates a new metrics factory for Velocity. * diff --git a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java index 969ad21..3e3a1a2 100644 --- a/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java +++ b/velocity/src/main/java/dev/faststats/velocity/VelocityMetricsImpl.java @@ -4,9 +4,11 @@ import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; +import dev.faststats.Config; +import dev.faststats.FastStatsContext; +import dev.faststats.Metrics; +import dev.faststats.SimpleMetrics; import dev.faststats.config.SimpleConfig; -import dev.faststats.core.Metrics; -import dev.faststats.core.SimpleMetrics; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; import org.slf4j.Logger; @@ -23,10 +25,10 @@ private VelocityMetricsImpl( final Factory factory, final Logger logger, final ProxyServer server, - final Path config, + final Config config, final PluginContainer plugin ) throws IllegalStateException { - super(factory, SimpleConfig.read(config)); + super(factory, config); this.server = server; this.plugin = plugin; @@ -49,12 +51,17 @@ protected void appendDefaultData(final JsonObject metrics) { metrics.addProperty("server_type", server.getVersion().getName()); } - static class Factory extends SimpleMetrics.Factory { + static class Factory extends SimpleMetrics.Factory { protected final Logger logger; protected final Path dataDirectory; protected final ProxyServer server; public Factory(final ProxyServer server, final Logger logger, @DataDirectory final Path dataDirectory) { + this(null, server, logger, dataDirectory); + } + + public Factory(final FastStatsContext context, final ProxyServer server, final Logger logger, @DataDirectory final Path dataDirectory) { + super(context); this.logger = logger; this.dataDirectory = dataDirectory; this.server = server; @@ -70,13 +77,16 @@ public Factory(final ProxyServer server, final Logger logger, @DataDirectory fin * @throws IllegalStateException if the token is not specified * @throws IllegalArgumentException if the given object is not a valid plugin * @see #token(String) - * @since 0.1.0 + * @since 0.23.0 */ @Override public Metrics create(final Object plugin) throws IllegalStateException, IllegalArgumentException { final var faststats = dataDirectory.resolveSibling("faststats"); final var container = server.getPluginManager().ensurePluginContainer(plugin); - return new VelocityMetricsImpl(this, logger, server, faststats.resolve("config.properties"), container); + final var config = hasContext() + ? getConfigOrThrow() + : SimpleConfig.read(faststats.resolve("config.properties")); + return new VelocityMetricsImpl(this, logger, server, config, container); } } } diff --git a/velocity/src/main/java/module-info.java b/velocity/src/main/java/module-info.java index 77b01de..0d80b4f 100644 --- a/velocity/src/main/java/module-info.java +++ b/velocity/src/main/java/module-info.java @@ -8,7 +8,7 @@ requires com.google.guice; requires com.velocitypowered.api; requires dev.faststats.config; - requires dev.faststats.core; + requires dev.faststats; requires org.slf4j; requires static org.jetbrains.annotations;