Compare commits

...

1 Commits

Author SHA1 Message Date
Sebastian Hartte 53f7046f93 Metrics WIP 2020-09-10 18:55:22 +02:00
16 changed files with 421 additions and 5 deletions
@@ -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<LoadingCache<CableBusRenderState, List<BakedQuad>>> cacheForMetrics = new WeakReference<>(
null);
static {
Metrics.cache("cable_bus_model_cache", () -> cacheForMetrics.get());
}
private final LoadingCache<CableBusRenderState, List<BakedQuad>> 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<CableBusRenderState, List<BakedQuad>>) (key, value) -> value.size())//
.build(new CacheLoader<CableBusRenderState, List<BakedQuad>>() {
@@ -84,6 +95,7 @@ public class CableBusBakedModel implements IBakedModel {
return model;
}
});
cacheForMetrics = new WeakReference<>(cableModelCache);
}
@Override
+11
View File
@@ -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<Integer> formationPlaneEntityLimit;
public final ConfigValue<Integer> craftingCalculationTimePerTick;
public final ConfigValue<Integer> prometheusMetricsServer;
// Spatial IO/Dimension
public final ConfigValue<Double> 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();
+17
View File
@@ -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)
@@ -157,4 +157,8 @@ final class StorageData extends WorldSavedData implements IWorldGridStorageData
return tag;
}
public int size() {
return storage.size();
}
}
@@ -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.
*
* <p>
* 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
*
* <p>
* drawback of the singleton build style
*/
public static void onServerStarting(MinecraftServer server) {
@@ -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);
@@ -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<Cache<?, ?>> 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<Cache<?, ?>> 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());
}
}
}
+31
View File
@@ -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<Number> valueSupplier;
public Gauge(String id, Supplier<Number> valueSupplier) {
super(id);
this.valueSupplier = valueSupplier;
}
public Supplier<Number> getValueSupplier() {
return valueSupplier;
}
public Number getValue() {
return valueSupplier.get();
}
@Override
public void accept(MetricVisitor visitor) {
visitor.visitGauge(getId(), getValue());
}
}
+34
View File
@@ -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);
}
@@ -0,0 +1,7 @@
package appeng.metrics;
public interface MetricVisitor {
void visitGauge(String id, Number value);
}
+51
View File
@@ -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<Metric> 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<Number> 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<Cache<?, ?>> cacheSupplier) {
metrics.add(new CacheMetrics(name, cacheSupplier));
}
public static synchronized void visit(MetricVisitor visitor) {
for (Metric metric : metrics) {
metric.accept(visitor);
}
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -0,0 +1,54 @@
package appeng.metrics.reporter;
import appeng.metrics.MetricVisitor;
/**
* Reports the metrics in a format compatible with Prometheus.
* <p>
* See <a href=
* "https://prometheus.io/docs/instrumenting/exposition_formats/">Prometheus
* Docs</a>
*/
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('_');
}
}
}
}
+2 -1
View File
@@ -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;
@@ -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<CommandSource> 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);
}
}