package com.hypixel.hytale.metrics; import com.hypixel.hytale.codec.Codec; import com.hypixel.hytale.codec.ExtraInfo; import com.hypixel.hytale.codec.schema.SchemaContext; import com.hypixel.hytale.codec.schema.config.Schema; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.logger.backend.HytaleFileHandler; import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; import java.io.BufferedWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.locks.StampedLock; import java.util.function.Function; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.bson.BsonDocument; import org.bson.BsonValue; import org.bson.codecs.BsonDocumentCodec; import org.bson.codecs.EncoderContext; import org.bson.json.JsonMode; import org.bson.json.JsonWriter; import org.bson.json.JsonWriterSettings; public class MetricsRegistry implements Codec { private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); public static final JsonWriterSettings JSON_SETTINGS = JsonWriterSettings.builder() .outputMode(JsonMode.STRICT) .indent(false) .newLineCharacters("\n") .int64Converter((value, writer) -> writer.writeNumber(Long.toString(value))) .build(); private static final EncoderContext ENCODER_CONTEXT = EncoderContext.builder().build(); private static final BsonDocumentCodec BSON_DOCUMENT_CODEC = new BsonDocumentCodec(); @Nullable private final Function appendFunc; private final StampedLock lock = new StampedLock(); private final Map> map = new Object2ObjectLinkedOpenHashMap(); public MetricsRegistry() { this.appendFunc = null; } public MetricsRegistry(Function appendFunc) { this.appendFunc = appendFunc; } public MetricsRegistry register(String id, MetricsRegistry metricsRegistry) { long stamp = this.lock.writeLock(); try { if (this.map.putIfAbsent(id, new MetricsRegistry.Metric<>(null, metricsRegistry)) != null) { throw new IllegalArgumentException("Metric already registered: " + id); } } finally { this.lock.unlockWrite(stamp); } return this; } public MetricsRegistry register(String id, Function func, Codec codec) { long stamp = this.lock.writeLock(); try { if (this.map.putIfAbsent(id, new MetricsRegistry.Metric<>(func, codec)) != null) { throw new IllegalArgumentException("Metric already registered: " + id); } } finally { this.lock.unlockWrite(stamp); } return this; } public MetricsRegistry register(String id, @Nonnull Function func) { return this.register(id, func.andThen(r -> r == null ? null : r.toMetricResults()), MetricResults.CODEC); } @Deprecated public MetricsRegistry register(String id, Function func, Function> codecFunc) { long stamp = this.lock.writeLock(); try { if (this.map.putIfAbsent(id, new MetricsRegistry.Metric<>(func, codecFunc)) != null) { throw new IllegalArgumentException("Metric already registered: " + id); } } finally { this.lock.unlockWrite(stamp); } return this; } @Override public T decode(BsonValue bsonValue, ExtraInfo extraInfo) { throw new UnsupportedOperationException("Not implemented"); } @Override public BsonValue encode(T t, ExtraInfo extraInfo) { BsonDocument document = new BsonDocument(); long stamp = this.lock.readLock(); try { for (Entry> entry : this.map.entrySet()) { String key = entry.getKey(); BsonValue value = entry.getValue().encode(t, extraInfo); if (value != null) { document.put(key, value); } } } finally { this.lock.unlockRead(stamp); } if (this.appendFunc != null) { MetricProvider metricProvider = this.appendFunc.apply(t); if (metricProvider != null) { MetricResults metricResults = metricProvider.toMetricResults(); if (metricResults != null) { document.putAll(metricResults.getBson()); } } } return document; } @Nonnull @Override public Schema toSchema(@Nonnull SchemaContext context) { throw new UnsupportedOperationException("Not implemented"); } @Nonnull public MetricResults toMetricResults(T t) { return new MetricResults(this.dumpToBson(t).asDocument()); } public BsonValue dumpToBson(T t) { ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get(); BsonDocument bson = this.encode(t, extraInfo).asDocument(); extraInfo.getValidationResults().logOrThrowValidatorExceptions(LOGGER); return bson; } @Nonnull public Path dumpToJson(T t) throws IOException { Path path = createDumpPath(".dump.json"); this.dumpToJson(path, t); return path; } public void dumpToJson(@Nonnull Path path, T t) throws IOException { BsonValue bson = this.dumpToBson(t); try (BufferedWriter writer = Files.newBufferedWriter(path)) { BSON_DOCUMENT_CODEC.encode(new JsonWriter(writer, JSON_SETTINGS), bson.asDocument(), ENCODER_CONTEXT); } } @Nonnull public static Path createDumpPath(@Nullable String ext) throws IOException { return createDumpPath((String)null, ext); } @Nonnull public static Path createDumpPath(@Nonnull Path dir, @Nullable String ext) { return createDatePath(dir, null, ext); } @Nonnull public static Path createDumpPath(@Nullable String prefix, @Nullable String ext) throws IOException { Path path = Paths.get("dumps"); if (!Files.exists(path)) { Files.createDirectories(path); } return createDatePath(path, prefix, ext); } @Nonnull public static Path createDatePath(@Nonnull Path dir, @Nullable String prefix, @Nullable String suffix) { String name = HytaleFileHandler.LOG_FILE_DATE_FORMAT.format(LocalDateTime.now()); if (prefix != null) { name = prefix + name; } Path file = suffix != null ? dir.resolve(name + suffix) : dir.resolve(name); int i = 0; while (Files.exists(file)) { if (suffix != null) { file = dir.resolve(name + "_" + i++ + suffix); } else { file = dir.resolve(name + "_" + i++); } } return file; } private static class Metric { @Nullable private final Function func; @CheckForNull private final Codec codec; @CheckForNull private final Function> codecFunc; public Metric(@Nullable Function func, @Nullable Codec codec) { this.func = func; this.codec = codec; this.codecFunc = null; } public Metric(@Nullable Function func, @Nullable Function> codecFunc) { this.func = func; this.codec = null; this.codecFunc = codecFunc; } @Nullable public BsonValue encode(T t, ExtraInfo extraInfo) { if (this.func == null) { assert this.codec != null; return this.codec.encode(null, extraInfo); } else { R value = this.func.apply(t); return value == null ? null : this.getCodec(value).encode(value, extraInfo); } } @Nonnull public Codec getCodec(R value) { if (this.codec != null) { return this.codec; } else { assert this.codecFunc != null; return this.codecFunc.apply(value); } } } }