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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/main/java/dev/faststats/core/Metrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ interface Factory<T> {
*
* @since 0.1.0
*/
interface Config {
sealed interface Config permits SimpleMetrics.Config {
/**
* The server id.
*
Expand Down
125 changes: 52 additions & 73 deletions core/src/main/java/dev/faststats/core/SimpleMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ public abstract class SimpleMetrics implements Metrics {

@Contract(mutates = "io")
@SuppressWarnings("PatternValidation")
protected SimpleMetrics(Factory<?> factory, Path config) throws IllegalStateException {
protected SimpleMetrics(Factory<?> factory, Config config) throws IllegalStateException {
if (factory.token == null) throw new IllegalStateException("Token must be specified");

this.config = new Config(config);
this.charts = this.config.additionalMetrics ? Set.copyOf(factory.charts) : Set.of();
this.debug = factory.debug || Boolean.getBoolean("faststats.debug") || this.config.debug();
this.config = config;
this.charts = config.additionalMetrics ? Set.copyOf(factory.charts) : Set.of();
this.debug = factory.debug || Boolean.getBoolean("faststats.debug") || config.debug();
this.token = factory.token;
this.tracker = this.config.errorTracking ? factory.tracker : null;
this.tracker = config.errorTracking ? factory.tracker : null;
this.url = factory.url;

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
Expand All @@ -80,6 +80,11 @@ protected SimpleMetrics(Factory<?> factory, Path config) throws IllegalStateExce
}, "metrics-shutdown-thread " + getClass().getName()));
}

@Contract(mutates = "io")
protected SimpleMetrics(Factory<?> factory, Path config) throws IllegalStateException {
this(factory, Config.read(config));
}

@VisibleForTesting
protected SimpleMetrics(Config config, Set<Chart<?>> charts, @Token String token, @Nullable ErrorTracker tracker, URI url, boolean debug) {
if (!token.matches(Token.PATTERN)) {
Expand Down Expand Up @@ -339,21 +344,43 @@ public Metrics.Factory<T> url(URI url) {
}
}

protected static final class Config implements Metrics.Config {
private final UUID serverId;
private final boolean additionalMetrics;
private final boolean debug;
private final boolean enabled;
private final boolean errorTracking;
private final boolean firstRun;
public record Config(
UUID serverId,
boolean additionalMetrics,
boolean debug,
boolean enabled,
boolean errorTracking,
boolean firstRun
) implements Metrics.Config {

public static final String DEFAULT_COMMENT = """
FastStats (https://faststats.dev) collects anonymous usage statistics for plugin 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 disable them by setting 'enabled=false'.
#
# If you suspect a plugin 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")
protected Config(Path file) {
public static Config read(Path file) throws RuntimeException {
return read(file, DEFAULT_COMMENT, false, false);
}

@Contract(mutates = "io")
public static Config read(Path file, String comment, boolean externallyManaged, boolean externallyEnabled) throws RuntimeException {
var properties = readOrEmpty(file);
this.firstRun = properties.isEmpty();
var saveConfig = new AtomicBoolean(this.firstRun);
var firstRun = properties.isEmpty();
var saveConfig = new AtomicBoolean(firstRun);

this.serverId = properties.map(object -> object.getProperty("serverId")).map(string -> {
var serverId = properties.map(object -> object.getProperty("serverId")).map(string -> {
try {
var trimmed = string.trim();
var corrected = trimmed.length() > 36 ? trimmed.substring(0, 36) : trimmed;
Expand All @@ -375,54 +402,21 @@ protected Config(Path file) {
});
};

this.enabled = predicate.test("enabled", true);
this.errorTracking = predicate.test("submitErrors", true);
this.additionalMetrics = predicate.test("submitAdditionalMetrics", true);
this.debug = predicate.test("debug", false);
var enabled = externallyManaged ? externallyEnabled : predicate.test("enabled", true);
var errorTracking = predicate.test("submitErrors", true);
var additionalMetrics = predicate.test("submitAdditionalMetrics", true);
var debug = predicate.test("debug", false);

if (saveConfig.get()) try {
save(file, serverId, enabled, errorTracking, additionalMetrics, debug);
save(file, externallyManaged, comment, serverId, enabled, errorTracking, additionalMetrics, debug);
} catch (IOException e) {
throw new RuntimeException("Failed to save metrics config", e);
}
}

@VisibleForTesting
public Config(UUID serverId, boolean enabled, boolean errorTracking, boolean additionalMetrics, boolean debug) {
this.serverId = serverId;
this.enabled = enabled;
this.debug = debug;
this.errorTracking = errorTracking;
this.additionalMetrics = additionalMetrics;
this.firstRun = false;
}

@Override
public UUID serverId() {
return serverId;
}

@Override
public boolean enabled() {
return enabled;
}

@Override
public boolean errorTracking() {
return errorTracking;
}

@Override
public boolean additionalMetrics() {
return additionalMetrics;
}

@Override
public boolean debug() {
return debug;
return new Config(serverId, additionalMetrics, debug, enabled, errorTracking, firstRun);
}

private static Optional<Properties> readOrEmpty(Path file) {
private static Optional<Properties> readOrEmpty(Path file) throws RuntimeException {
if (!Files.isRegularFile(file)) return Optional.empty();
try (var reader = Files.newBufferedReader(file, UTF_8)) {
var properties = new Properties();
Expand All @@ -433,33 +427,18 @@ private static Optional<Properties> readOrEmpty(Path file) {
}
}

private static void save(Path file, UUID serverId, boolean enabled, boolean errorTracking, boolean additionalMetrics, boolean debug) throws IOException {
private static void save(Path file, boolean externallyManaged, String comment, UUID serverId, boolean enabled, boolean errorTracking, boolean additionalMetrics, boolean debug) throws IOException {
Files.createDirectories(file.getParent());
try (var out = Files.newOutputStream(file);
var writer = new OutputStreamWriter(out, UTF_8)) {
var properties = new Properties();

properties.setProperty("serverId", serverId.toString());
properties.setProperty("enabled", Boolean.toString(enabled));
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));

var comment = """
FastStats (https://faststats.dev) collects anonymous usage statistics for plugin 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 disable them by setting 'enabled=false'.
#
# If you suspect a plugin 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
""";
properties.store(writer, comment);
}
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/test/java/dev/faststats/MockMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
@NullMarked
public class MockMetrics extends SimpleMetrics {
public MockMetrics(UUID serverId, @Token String token, @Nullable ErrorTracker tracker, boolean debug) {
super(new SimpleMetrics.Config(serverId, true, true, true, debug), Set.of(), token, tracker, URI.create("http://localhost:5000/v1/collect"), debug);
super(new Config(serverId, true, debug, true, true, false), Set.of(), token, tracker, URI.create("http://localhost:5000/v1/collect"), debug);
}

@Override
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=0.11.1
version=0.12.0
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ include("hytale")
include("hytale:example-plugin")
include("minestom")
include("nukkit")
include("sponge")
include("velocity")
10 changes: 10 additions & 0 deletions sponge/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
val moduleName by extra("dev.faststats.sponge")

repositories {
maven("https://repo.spongepowered.org/repository/maven-public/")
}

dependencies {
api(project(":core"))
compileOnly("org.spongepowered:spongeapi:8.0.0")
}
30 changes: 30 additions & 0 deletions sponge/src/main/java/dev/faststats/sponge/SpongeMetrics.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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;

/**
* Sponge metrics implementation.
*
* @since 0.12.0
*/
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(Logger logger, @ConfigDir(sharedRoot = true) Path dataDirectory) {
super(logger, dataDirectory);
}
}
}
93 changes: 93 additions & 0 deletions sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package dev.faststats.sponge;

import com.google.gson.JsonObject;
import dev.faststats.core.Metrics;
import dev.faststats.core.SimpleMetrics;
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.api.config.ConfigDir;
import org.spongepowered.plugin.PluginContainer;

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 for plugin 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.
# Enabling metrics is recommended, you can do so in the Sponge config.
#
# If you suspect a plugin 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 Logger logger;
private final PluginContainer plugin;

@Async.Schedule
@Contract(mutates = "io")
private SpongeMetricsImpl(
SimpleMetrics.Factory<?> factory,
Logger logger,
PluginContainer plugin,
Path config
) throws IllegalStateException {
super(factory, SimpleMetrics.Config.read(config, COMMENT, true, Sponge.metricsConfigManager()
.effectiveCollectionState(plugin).asBoolean()));

this.logger = logger;
this.plugin = plugin;

startSubmitting();
}

@Override
protected void appendDefaultData(JsonObject charts) {
charts.addProperty("online_mode", Sponge.server().isOnlineModeEnabled());
charts.addProperty("player_count", Sponge.server().onlinePlayers().size());
charts.addProperty("plugin_version", plugin.metadata().version().toString());
charts.addProperty("minecraft_version", Sponge.platform().minecraftVersion().name());
charts.addProperty("server_type", Sponge.platform().container(Platform.Component.IMPLEMENTATION).metadata().id());
}

@Override
protected void printError(String message, @Nullable Throwable throwable) {
logger.error(message, throwable);
}

@Override
protected void printInfo(String message) {
logger.info(message);
}

@Override
protected void printWarning(String message) {
logger.warn(message);
}

static class Factory extends SimpleMetrics.Factory<PluginContainer> {
protected final Logger logger;
protected final Path dataDirectory;

public Factory(Logger logger, @ConfigDir(sharedRoot = true) Path dataDirectory) {
this.logger = logger;
this.dataDirectory = dataDirectory;
}

@Override
public Metrics create(PluginContainer plugin) throws IllegalStateException, IllegalArgumentException {
var faststats = dataDirectory.resolveSibling("faststats");
return new SpongeMetricsImpl(this, logger, plugin, faststats.resolve("config.properties"));
}
}
}
13 changes: 13 additions & 0 deletions sponge/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import org.jspecify.annotations.NullMarked;

@NullMarked
module dev.faststats.sponge {
exports dev.faststats.sponge;

requires com.google.gson;
requires com.google.guice;
requires dev.faststats.core;

requires static org.jetbrains.annotations;
requires static org.jspecify;
}
Loading