diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index f8f4412..b9150af 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -36,15 +36,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; public abstract class SimpleMetrics implements Metrics { - protected 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."""; - private final HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .version(HttpClient.Version.HTTP_1_1) @@ -99,6 +90,17 @@ protected SimpleMetrics(Config config, Set> charts, @Token String token 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)); } @@ -121,12 +123,16 @@ private void startSubmitting(long initialDelay, long period, TimeUnit unit) { if (config.firstRun) { - printInfo("-".repeat(80)); - for (var s : ONBOARDING_MESSAGE.split("\n")) printInfo(s); - printInfo("-".repeat(80)); + var separatorLength = 0; + var split = getOnboardingMessage().split("\n"); + for (var s : split) if (s.length() > separatorLength) separatorLength = s.length(); + + printInfo("-".repeat(separatorLength)); + for (var s : split) printInfo(s); + printInfo("-".repeat(separatorLength)); System.setProperty("faststats.first-run", "true"); - return; + if (!config.externallyManaged()) return; } var enabled = Boolean.parseBoolean(System.getProperty("faststats.enabled", "true")); @@ -350,7 +356,8 @@ public record Config( boolean debug, boolean enabled, boolean errorTracking, - boolean firstRun + boolean firstRun, + boolean externallyManaged ) implements Metrics.Config { public static final String DEFAULT_COMMENT = """ @@ -413,7 +420,7 @@ public static Config read(Path file, String comment, boolean externallyManaged, throw new RuntimeException("Failed to save metrics config", e); } - return new Config(serverId, additionalMetrics, debug, enabled, errorTracking, firstRun); + return new Config(serverId, additionalMetrics, debug, enabled, errorTracking, firstRun, externallyManaged); } private static Optional readOrEmpty(Path file) throws RuntimeException { diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index a1dabf1..9235012 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -16,7 +16,7 @@ @NullMarked public class MockMetrics extends SimpleMetrics { public MockMetrics(UUID serverId, @Token String token, @Nullable ErrorTracker tracker, boolean debug) { - super(new Config(serverId, true, debug, true, true, false), Set.of(), token, tracker, URI.create("http://localhost:5000/v1/collect"), debug); + super(new Config(serverId, true, debug, true, true, false, false), Set.of(), token, tracker, URI.create("http://localhost:5000/v1/collect"), debug); } @Override diff --git a/settings.gradle.kts b/settings.gradle.kts index fdff68c..f0cb6bd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,4 +12,5 @@ include("hytale:example-plugin") include("minestom") include("nukkit") include("sponge") +include("sponge:example-plugin") include("velocity") \ No newline at end of file diff --git a/sponge/example-plugin/build.gradle.kts b/sponge/example-plugin/build.gradle.kts new file mode 100644 index 0000000..a7322a5 --- /dev/null +++ b/sponge/example-plugin/build.gradle.kts @@ -0,0 +1,13 @@ +repositories { + maven("https://repo.spongepowered.org/repository/maven-public/") +} + +dependencies { + compileOnly("org.spongepowered:spongeapi:8.0.0") + implementation(project(":sponge")) +} + +tasks.shadowJar { + // optionally relocate faststats + relocate("dev.faststats", "com.example.utils.faststats") +} diff --git a/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java b/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java new file mode 100644 index 0000000..1d1922a --- /dev/null +++ b/sponge/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -0,0 +1,68 @@ +package com.example; + +import com.google.inject.Inject; +import dev.faststats.core.ErrorTracker; +import dev.faststats.core.Metrics; +import dev.faststats.core.chart.Chart; +import dev.faststats.sponge.SpongeMetrics; +import org.jspecify.annotations.Nullable; +import org.spongepowered.api.Server; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.lifecycle.StartedEngineEvent; +import org.spongepowered.api.event.lifecycle.StoppingEngineEvent; +import org.spongepowered.plugin.PluginContainer; +import org.spongepowered.plugin.builtin.jvm.Plugin; + + +@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; + + private @Nullable Metrics metrics = null; + + @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 charts + // For this to work you have to create a corresponding data source in your project settings first + .addChart(Chart.number("example_chart", () -> 42)) + .addChart(Chart.string("example_string", () -> "Hello, World!")) + .addChart(Chart.bool("example_boolean", () -> true)) + .addChart(Chart.stringArray("example_string_array", () -> new String[]{"Option 1", "Option 2"})) + .addChart(Chart.numberArray("example_number_array", () -> new Number[]{1, 2, 3})) + .addChart(Chart.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 + + .token("bafe240e8d1a4b919e5083928539799d") // required -> token can be found in the settings of your project + .create(pluginContainer); + ERROR_TRACKER.trackError(new RuntimeException("Something went wrong!", new Exception("Cause"))); + } + + @Listener + public void onServerStop(final StoppingEngineEvent event) { + if (metrics != null) metrics.shutdown(); + } + + public void doSomethingWrong() { + try { + // Do something that might throw an error + throw new RuntimeException("Something went wrong!"); + } catch (Exception e) { + CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e); + } + } +} diff --git a/sponge/example-plugin/src/main/resources/META-INF/sponge_plugins.json b/sponge/example-plugin/src/main/resources/META-INF/sponge_plugins.json new file mode 100644 index 0000000..d0c7e92 --- /dev/null +++ b/sponge/example-plugin/src/main/resources/META-INF/sponge_plugins.json @@ -0,0 +1,23 @@ +{ + "loader": { + "name": "java_plain", + "version": "1.0" + }, + "license": "MIT", + "global": { + "version": "1.0.0", + "contributors": [ + { + "name": "Your Name", + "description": "The creator of this plugin" + } + ] + }, + "plugins": [ + { + "id": "example", + "name": "Example Plugin", + "entrypoint": "com.example.ExamplePlugin" + } + ] +} \ No newline at end of file diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java index 6144ad5..ee3cec5 100644 --- a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java @@ -9,7 +9,6 @@ import org.jspecify.annotations.Nullable; import org.spongepowered.api.Platform; import org.spongepowered.api.Sponge; -import org.spongepowered.api.config.ConfigDir; import org.spongepowered.plugin.PluginContainer; import java.nio.file.Path; @@ -23,7 +22,8 @@ final class SpongeMetricsImpl extends SimpleMetrics implements SpongeMetrics { # 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 config. + # 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, # please report it at: https://faststats.dev/abuse @@ -51,6 +51,16 @@ private SpongeMetricsImpl( 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 + """; + } + @Override protected void appendDefaultData(JsonObject charts) { charts.addProperty("online_mode", Sponge.server().isOnlineModeEnabled()); @@ -79,14 +89,14 @@ static class Factory extends SimpleMetrics.Factory { protected final Logger logger; protected final Path dataDirectory; - public Factory(Logger logger, @ConfigDir(sharedRoot = true) Path dataDirectory) { + public Factory(Logger logger, Path dataDirectory) { this.logger = logger; this.dataDirectory = dataDirectory; } @Override public Metrics create(PluginContainer plugin) throws IllegalStateException, IllegalArgumentException { - var faststats = dataDirectory.resolveSibling("faststats"); + var faststats = dataDirectory.resolve("faststats"); return new SpongeMetricsImpl(this, logger, plugin, faststats.resolve("config.properties")); } }