From 53f7046f9344f1ba63600881403d6b23270f12b8 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sat, 5 Sep 2020 17:42:59 +0200 Subject: [PATCH] Metrics WIP --- .../render/cablebus/CableBusBakedModel.java | 12 +++ src/main/java/appeng/core/AEConfig.java | 11 +++ src/main/java/appeng/core/AppEng.java | 17 ++++ .../appeng/core/worlddata/StorageData.java | 4 + .../java/appeng/core/worlddata/WorldData.java | 15 +++- src/main/java/appeng/hooks/TickHandler.java | 6 ++ .../java/appeng/metrics/CacheMetrics.java | 54 +++++++++++++ src/main/java/appeng/metrics/Gauge.java | 31 ++++++++ src/main/java/appeng/metrics/Metric.java | 34 ++++++++ .../java/appeng/metrics/MetricVisitor.java | 7 ++ src/main/java/appeng/metrics/Metrics.java | 51 ++++++++++++ .../metrics/endpoint/PrometheusEndpoint.java | 78 +++++++++++++++++++ .../metrics/reporter/PrintStreamReporter.java | 24 ++++++ .../metrics/reporter/PrometheusReporter.java | 54 +++++++++++++ src/main/java/appeng/server/Commands.java | 3 +- .../server/subcommands/MetricsCommand.java | 25 ++++++ 16 files changed, 421 insertions(+), 5 deletions(-) create mode 100644 src/main/java/appeng/metrics/CacheMetrics.java create mode 100644 src/main/java/appeng/metrics/Gauge.java create mode 100644 src/main/java/appeng/metrics/Metric.java create mode 100644 src/main/java/appeng/metrics/MetricVisitor.java create mode 100644 src/main/java/appeng/metrics/Metrics.java create mode 100644 src/main/java/appeng/metrics/endpoint/PrometheusEndpoint.java create mode 100644 src/main/java/appeng/metrics/reporter/PrintStreamReporter.java create mode 100644 src/main/java/appeng/metrics/reporter/PrometheusReporter.java create mode 100644 src/main/java/appeng/server/subcommands/MetricsCommand.java diff --git a/src/main/java/appeng/client/render/cablebus/CableBusBakedModel.java b/src/main/java/appeng/client/render/cablebus/CableBusBakedModel.java index 06285aef2..c527f0c43 100644 --- a/src/main/java/appeng/client/render/cablebus/CableBusBakedModel.java +++ b/src/main/java/appeng/client/render/cablebus/CableBusBakedModel.java @@ -18,6 +18,7 @@ package appeng.client.render.cablebus; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; @@ -51,12 +52,21 @@ import net.minecraftforge.client.model.data.IModelData; import appeng.api.parts.IPartModel; import appeng.api.util.AECableType; import appeng.api.util.AEColor; +import appeng.metrics.Metrics; public class CableBusBakedModel implements IBakedModel { // The number of quads overall that will be cached private static final int CACHE_QUAD_COUNT = 5000; + // Keep a weak-ref to the last created cache for reporting metrics + private static WeakReference>> cacheForMetrics = new WeakReference<>( + null); + + static { + Metrics.cache("cable_bus_model_cache", () -> cacheForMetrics.get()); + } + private final LoadingCache> cableModelCache; private final CableBuilder cableBuilder; @@ -74,6 +84,7 @@ public class CableBusBakedModel implements IBakedModel { this.partModels = partModels; this.particleTexture = particleTexture; this.cableModelCache = CacheBuilder.newBuilder()// + .recordStats()// .maximumWeight(CACHE_QUAD_COUNT)// .weigher((Weigher>) (key, value) -> value.size())// .build(new CacheLoader>() { @@ -84,6 +95,7 @@ public class CableBusBakedModel implements IBakedModel { return model; } }); + cacheForMetrics = new WeakReference<>(cableModelCache); } @Override diff --git a/src/main/java/appeng/core/AEConfig.java b/src/main/java/appeng/core/AEConfig.java index db65ae3f9..7b79b3d88 100644 --- a/src/main/java/appeng/core/AEConfig.java +++ b/src/main/java/appeng/core/AEConfig.java @@ -404,6 +404,10 @@ public final class AEConfig { return COMMON.improvedFluidMultiplier.get().floatValue(); } + public int getPrometheusMetricsServerPort() { + return COMMON.prometheusMetricsServer.get(); + } + // Setters keep visibility as low as possible. private static class ClientConfig { @@ -474,6 +478,7 @@ public final class AEConfig { public final BooleanValue removeCrashingItemsOnLoad; public final ConfigValue formationPlaneEntityLimit; public final ConfigValue craftingCalculationTimePerTick; + public final ConfigValue prometheusMetricsServer; // Spatial IO/Dimension public final ConfigValue spatialPowerExponent; @@ -558,6 +563,12 @@ public final class AEConfig { .define("removeCrashingItemsOnLoad", false); builder.pop(); + builder.push("metrics"); + prometheusMetricsServer = builder.comment( + "Enables the Prometheus Metrics exporting endpoint on 127.0.0.1 on this port number. 0 to disable.") + .define("prometheusMetricsServer", 0); + builder.pop(); + builder.push("automation"); formationPlaneEntityLimit = builder.comment("TODO").define("formationPlaneEntityLimit", 128); builder.pop(); diff --git a/src/main/java/appeng/core/AppEng.java b/src/main/java/appeng/core/AppEng.java index 0316326ad..f8c740ca1 100644 --- a/src/main/java/appeng/core/AppEng.java +++ b/src/main/java/appeng/core/AppEng.java @@ -66,6 +66,7 @@ import appeng.entity.TinyTNTPrimedEntity; import appeng.entity.TinyTNTPrimedRenderer; import appeng.hooks.TickHandler; import appeng.integration.Integrations; +import appeng.metrics.endpoint.PrometheusEndpoint; import appeng.parts.PartPlacement; import appeng.server.ServerHelper; @@ -90,6 +91,8 @@ public final class AppEng { } INSTANCE = this; + startMetricsServer(); + ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, AEConfig.CLIENT_SPEC); ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, AEConfig.COMMON_SPEC); @@ -148,6 +151,20 @@ public final class AppEng { registerNetworkHandler(); AddonLoader.loadAddons(Api.INSTANCE); + + startMetricsServer(); + } + + private void startMetricsServer() { + int prometheusPort = AEConfig.instance().getPrometheusMetricsServerPort(); + if (prometheusPort != 0) { + try { + AELog.info("Starting Prometheus Metrics Server on Port %s", prometheusPort); + new PrometheusEndpoint("localhost", prometheusPort); + } catch (Exception e) { + AELog.warn("Failed to start Prometheus Metrics-Server: %s", e); + } + } } @OnlyIn(Dist.CLIENT) diff --git a/src/main/java/appeng/core/worlddata/StorageData.java b/src/main/java/appeng/core/worlddata/StorageData.java index cfe8b2de0..5a3b5da2d 100644 --- a/src/main/java/appeng/core/worlddata/StorageData.java +++ b/src/main/java/appeng/core/worlddata/StorageData.java @@ -157,4 +157,8 @@ final class StorageData extends WorldSavedData implements IWorldGridStorageData return tag; } + public int size() { + return storage.size(); + } + } diff --git a/src/main/java/appeng/core/worlddata/WorldData.java b/src/main/java/appeng/core/worlddata/WorldData.java index 398d91b14..a1f0407fc 100644 --- a/src/main/java/appeng/core/worlddata/WorldData.java +++ b/src/main/java/appeng/core/worlddata/WorldData.java @@ -28,12 +28,13 @@ import com.google.common.base.Preconditions; import net.minecraft.server.MinecraftServer; import net.minecraft.world.server.ServerWorld; +import appeng.metrics.Metrics; import appeng.services.CompassService; import appeng.services.compass.CompassThreadFactory; /** * Singleton access to anything related to world-based data. - * + *

* Data will change depending which world is loaded. Will probably not affect * SMP at all since only one world is loaded, but SSP more, cause they play on * different worlds. @@ -53,8 +54,15 @@ public final class WorldData implements IWorldData { @Nullable private static MinecraftServer server; + static { + Metrics.gauge("grid_storage_count", () -> { + WorldData worldData = (WorldData) instance; + return worldData != null ? worldData.storageData.size() : 0; + }); + } + private final IWorldPlayerData playerData; - private final IWorldGridStorageData storageData; + private final StorageData storageData; private final IWorldCompassData compassData; private WorldData(@Nonnull final ServerWorld overworld) { @@ -80,7 +88,6 @@ public final class WorldData implements IWorldData { /** * @return ae2 data related to a specific world - * * @deprecated do not use singletons which are dependent on specific world state */ @Deprecated @@ -102,7 +109,7 @@ public final class WorldData implements IWorldData { /** * Requires to start up from external from here - * + *

* drawback of the singleton build style */ public static void onServerStarting(MinecraftServer server) { diff --git a/src/main/java/appeng/hooks/TickHandler.java b/src/main/java/appeng/hooks/TickHandler.java index 55f490499..75e1b1690 100644 --- a/src/main/java/appeng/hooks/TickHandler.java +++ b/src/main/java/appeng/hooks/TickHandler.java @@ -57,6 +57,7 @@ import appeng.core.AppEng; import appeng.core.sync.packets.PaintedEntityPacket; import appeng.crafting.CraftingJob; import appeng.me.Grid; +import appeng.metrics.Metrics; import appeng.tile.AEBaseTileEntity; import appeng.util.IWorldCallable; import appeng.util.Platform; @@ -77,6 +78,11 @@ public class TickHandler { return INSTANCE; } + static { + Metrics.gauge("ticking_network_count", () -> INSTANCE.server.networks.size()); + Metrics.gauge("crafting_job_count", INSTANCE.craftingJobs::size); + } + public static void setup(IEventBus eventBus) { eventBus.addListener(INSTANCE::onServerTick); eventBus.addListener(INSTANCE::onWorldTick); diff --git a/src/main/java/appeng/metrics/CacheMetrics.java b/src/main/java/appeng/metrics/CacheMetrics.java new file mode 100644 index 000000000..949942e4a --- /dev/null +++ b/src/main/java/appeng/metrics/CacheMetrics.java @@ -0,0 +1,54 @@ +package appeng.metrics; + +import java.util.function.Supplier; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheStats; + +/** + * Cache Metrics simplify reporting of metrics for a Guava {@link Cache}. + */ +public class CacheMetrics extends Metric { + + private final Supplier> cacheSupplier; + + // Pre-allocate these to avoid repeated concatenation during reporting + private final String sizeId; + private final String hitCountId; + private final String missCountId; + private final String loadSuccessCountId; + private final String loadExceptionCountId; + private final String totalLoadTimeId; + private final String evictionCountId; + + public CacheMetrics(String id, Supplier> cacheSupplier) { + super(id); + this.cacheSupplier = cacheSupplier; + + this.sizeId = id + ".size"; + this.hitCountId = id + ".hit-count"; + this.missCountId = id + ".miss-count"; + this.loadSuccessCountId = id + ".load-success-count"; + this.loadExceptionCountId = id + ".load-exception-count"; + this.totalLoadTimeId = id + ".total-load-time"; + this.evictionCountId = id + ".eviction-count"; + } + + @Override + public void accept(MetricVisitor visitor) { + Cache cache = this.cacheSupplier.get(); + + if (cache != null) { + visitor.visitGauge(sizeId, cache.size()); // Thread-safe + + CacheStats stats = cache.stats(); // Thread-safe + visitor.visitGauge(hitCountId, stats.hitCount()); + visitor.visitGauge(missCountId, stats.missCount()); + visitor.visitGauge(loadSuccessCountId, stats.loadSuccessCount()); + visitor.visitGauge(loadExceptionCountId, stats.loadExceptionCount()); + visitor.visitGauge(totalLoadTimeId, stats.totalLoadTime()); + visitor.visitGauge(evictionCountId, stats.evictionCount()); + } + } + +} diff --git a/src/main/java/appeng/metrics/Gauge.java b/src/main/java/appeng/metrics/Gauge.java new file mode 100644 index 000000000..41f954c6d --- /dev/null +++ b/src/main/java/appeng/metrics/Gauge.java @@ -0,0 +1,31 @@ +package appeng.metrics; + +import java.util.function.Supplier; + +/** + * A Gauge is a Metric that will simply read the current value of something when + * the metrics are being reported. + */ +class Gauge extends Metric { + + private final Supplier valueSupplier; + + public Gauge(String id, Supplier valueSupplier) { + super(id); + this.valueSupplier = valueSupplier; + } + + public Supplier getValueSupplier() { + return valueSupplier; + } + + public Number getValue() { + return valueSupplier.get(); + } + + @Override + public void accept(MetricVisitor visitor) { + visitor.visitGauge(getId(), getValue()); + } + +} diff --git a/src/main/java/appeng/metrics/Metric.java b/src/main/java/appeng/metrics/Metric.java new file mode 100644 index 000000000..2d7a704c9 --- /dev/null +++ b/src/main/java/appeng/metrics/Metric.java @@ -0,0 +1,34 @@ +package appeng.metrics; + +import java.util.Objects; + +abstract class Metric { + + private final String id; + + public Metric(String id) { + this.id = Objects.requireNonNull(id); + } + + public String getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Metric metric = (Metric) o; + return id.equals(metric.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public abstract void accept(MetricVisitor visitor); + +} diff --git a/src/main/java/appeng/metrics/MetricVisitor.java b/src/main/java/appeng/metrics/MetricVisitor.java new file mode 100644 index 000000000..8b4651499 --- /dev/null +++ b/src/main/java/appeng/metrics/MetricVisitor.java @@ -0,0 +1,7 @@ +package appeng.metrics; + +public interface MetricVisitor { + + void visitGauge(String id, Number value); + +} diff --git a/src/main/java/appeng/metrics/Metrics.java b/src/main/java/appeng/metrics/Metrics.java new file mode 100644 index 000000000..7efe338ba --- /dev/null +++ b/src/main/java/appeng/metrics/Metrics.java @@ -0,0 +1,51 @@ +package appeng.metrics; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.LongSupplier; +import java.util.function.Supplier; + +import com.google.common.base.Suppliers; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheStats; + +/** + * Registry to register Metrics. + */ +public final class Metrics { + + private static final List metrics = new ArrayList<>(); + + private Metrics() { + } + + /** + * Registers a Gauge, which is a simple metric with no state that will be + * queried for its current value whenever metrics are being reported. + */ + public static synchronized void gauge(String id, Supplier valueSupplier) { + metrics.add(new Gauge(id, valueSupplier)); + } + + /** + * Register several standard metrics for a Guava {@link Cache}. + */ + public static synchronized void cache(String name, Cache cache) { + metrics.add(new CacheMetrics(name, () -> cache)); + } + + /** + * Register several standard metrics for a Guava {@link Cache} based on a + * supplier to support an underlying changing cache. + */ + public static synchronized void cache(String name, Supplier> cacheSupplier) { + metrics.add(new CacheMetrics(name, cacheSupplier)); + } + + public static synchronized void visit(MetricVisitor visitor) { + for (Metric metric : metrics) { + metric.accept(visitor); + } + } + +} diff --git a/src/main/java/appeng/metrics/endpoint/PrometheusEndpoint.java b/src/main/java/appeng/metrics/endpoint/PrometheusEndpoint.java new file mode 100644 index 000000000..39b7057d6 --- /dev/null +++ b/src/main/java/appeng/metrics/endpoint/PrometheusEndpoint.java @@ -0,0 +1,78 @@ +package appeng.metrics.endpoint; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; + +import appeng.metrics.Metrics; +import appeng.metrics.reporter.PrometheusReporter; + +/** + * Starts an embedded HTTP-Server that will expose the Metrics in a format + * compatible with Prometheus. + */ +public class PrometheusEndpoint implements AutoCloseable { + + private final HttpServer server; + + private final PrometheusReporter reporter = new PrometheusReporter(); + + public PrometheusEndpoint(String hostname, int port) { + try { + this.server = HttpServer.create(); + } catch (IOException e) { + throw new RuntimeException("Failed to create metrics HTTP-server", e); + } + + InetSocketAddress address; + if (hostname.isEmpty()) { + address = new InetSocketAddress((InetAddress) null, port); + } else { + address = new InetSocketAddress(hostname, port); + } + + // Default endpoint is /metrics in Prometheus + this.server.createContext("/metrics", this::handler); + + try { + this.server.bind(address, 0); + } catch (IOException e) { + throw new RuntimeException("Failed to bind metrics HTTP server to " + address, e); + } + + this.server.start(); + + } + + @Override + public void close() { + this.server.stop(0); + } + + private synchronized void handler(HttpExchange exchange) throws IOException { + // Update metrics + reporter.reset(); + Metrics.visit(reporter); + + CharSequence report = getReport(); + + exchange.getResponseHeaders().add("Content-Type", "text/plain; version=0.0.4"); + exchange.sendResponseHeaders(200, report.length()); + + try (Writer writer = new OutputStreamWriter(exchange.getResponseBody(), StandardCharsets.US_ASCII)) { + writer.append(report); + } + exchange.close(); + } + + private CharSequence getReport() { + return reporter.getReport(); + } + +} diff --git a/src/main/java/appeng/metrics/reporter/PrintStreamReporter.java b/src/main/java/appeng/metrics/reporter/PrintStreamReporter.java new file mode 100644 index 000000000..550011e21 --- /dev/null +++ b/src/main/java/appeng/metrics/reporter/PrintStreamReporter.java @@ -0,0 +1,24 @@ +package appeng.metrics.reporter; + +import java.io.PrintStream; + +import appeng.metrics.MetricVisitor; + +/** + * Reports Metrics as simple key-value pairs to a {@link PrintStream} (i.e. + * {@link System#out}). + */ +public class PrintStreamReporter implements MetricVisitor { + + private final PrintStream out; + + public PrintStreamReporter(PrintStream out) { + this.out = out; + } + + @Override + public void visitGauge(String id, Number value) { + out.println(id + '=' + value); + } + +} diff --git a/src/main/java/appeng/metrics/reporter/PrometheusReporter.java b/src/main/java/appeng/metrics/reporter/PrometheusReporter.java new file mode 100644 index 000000000..d75527176 --- /dev/null +++ b/src/main/java/appeng/metrics/reporter/PrometheusReporter.java @@ -0,0 +1,54 @@ +package appeng.metrics.reporter; + +import appeng.metrics.MetricVisitor; + +/** + * Reports the metrics in a format compatible with Prometheus. + *

+ * See Prometheus + * Docs + */ +public class PrometheusReporter implements MetricVisitor { + + private final StringBuilder response = new StringBuilder(); + + @Override + public void visitGauge(String id, Number value) { + printType(id, "gauge"); + appendId(id); + response.append(' ').append(value.toString()).append('\n'); + } + + private void printType(String id, String type) { + response.append("# TYPE "); + appendId(id); + response.append(' ').append(type).append('\n'); + } + + public void reset() { + response.setLength(0); + } + + public int length() { + return response.length(); + } + + public CharSequence getReport() { + return response; + } + + private void appendId(String id) { + response.append("appeng_"); + // Sanitize to [a-zA-Z0-9:_] + for (int i = 0; i < id.length(); i++) { + char ch = id.charAt(i); + if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9' || ch == ':' || ch == '_') { + response.append(ch); + } else { + response.append('_'); + } + } + } + +} diff --git a/src/main/java/appeng/server/Commands.java b/src/main/java/appeng/server/Commands.java index a2bd40714..5d1ae1daa 100644 --- a/src/main/java/appeng/server/Commands.java +++ b/src/main/java/appeng/server/Commands.java @@ -19,6 +19,7 @@ package appeng.server; import appeng.server.subcommands.ChunkLogger; +import appeng.server.subcommands.MetricsCommand; import appeng.server.subcommands.SpatialStorageCommand; import appeng.server.subcommands.Supporters; import appeng.server.subcommands.TestMeteoritesCommand; @@ -27,7 +28,7 @@ import appeng.server.subcommands.TestOreGenCommand; public enum Commands { Chunklogger(4, new ChunkLogger(), false), Supporters(0, new Supporters(), false), TestOreGen(4, new TestOreGenCommand(), true), TestMeteorites(4, new TestMeteoritesCommand(), true), - Spatial(4, new SpatialStorageCommand(), false); + Spatial(4, new SpatialStorageCommand(), false), Metrics(4, new MetricsCommand(), false); public final int level; public final ISubCommand command; diff --git a/src/main/java/appeng/server/subcommands/MetricsCommand.java b/src/main/java/appeng/server/subcommands/MetricsCommand.java new file mode 100644 index 000000000..92481079f --- /dev/null +++ b/src/main/java/appeng/server/subcommands/MetricsCommand.java @@ -0,0 +1,25 @@ +package appeng.server.subcommands; + +import com.mojang.brigadier.context.CommandContext; + +import net.minecraft.command.CommandSource; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.text.StringTextComponent; + +import appeng.metrics.Metrics; +import appeng.metrics.reporter.PrintStreamReporter; +import appeng.server.ISubCommand; + +/** + * A simple way of printing out AE2's metrics on the server console. + */ +public class MetricsCommand implements ISubCommand { + @Override + public void call(MinecraftServer srv, CommandContext ctx, CommandSource sender) { + System.out.println("-------- AE2 Metrics:"); + Metrics.visit(new PrintStreamReporter(System.out)); + System.out.println("-------- END"); + + sender.sendFeedback(new StringTextComponent("Metrics reported to server console..."), false); + } +}