diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/AbstractConfigurateDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/AbstractConfigurateDao.java index 4b84bc37..b25f70e7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/AbstractConfigurateDao.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/AbstractConfigurateDao.java @@ -89,17 +89,16 @@ public abstract class AbstractConfigurateDao extends AbstractDao { // the uuid cache instance private final FileUuidCache uuidCache = new FileUuidCache(); // the action logger instance - private final FileActionLogger actionLogger = new FileActionLogger(); + private final FileActionLogger actionLogger; // the file used to store uuid data private Path uuidDataFile; - // the file used to store logged actions - private Path actionLogFile; protected AbstractConfigurateDao(LuckPermsPlugin plugin, ConfigurateLoader loader, String name, String dataDirectoryName) { super(plugin, name); this.loader = loader; this.dataDirectoryName = dataDirectoryName; + this.actionLogger = new FileActionLogger(plugin); } /** @@ -135,15 +134,15 @@ public abstract class AbstractConfigurateDao extends AbstractDao { Files.createDirectories(this.dataDirectory); this.uuidDataFile = MoreFiles.createFileIfNotExists(this.dataDirectory.resolve("uuidcache.txt")); - this.actionLogFile = MoreFiles.createFileIfNotExists(this.dataDirectory.resolve("actions.log")); - this.uuidCache.load(this.uuidDataFile); - this.actionLogger.init(this.actionLogFile); + + this.actionLogger.init(this.dataDirectory.resolve("actions.json")); } @Override public void shutdown() { this.uuidCache.save(this.uuidDataFile); + this.actionLogger.flush(); } @Override @@ -152,10 +151,8 @@ public abstract class AbstractConfigurateDao extends AbstractDao { } @Override - public Log getLog() { - // File based daos don't support viewing log data from in-game. - // You can just read the file in a text editor. - return Log.empty(); + public Log getLog() throws IOException { + return this.actionLogger.getLog(); } protected ConfigurationNode processBulkUpdate(BulkUpdate bulkUpdate, ConfigurationNode node) { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileActionLogger.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileActionLogger.java index 7c878057..a818b34f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileActionLogger.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/file/FileActionLogger.java @@ -25,48 +25,152 @@ package me.lucko.luckperms.common.storage.dao.file; -import me.lucko.luckperms.api.LogEntry; -import me.lucko.luckperms.common.command.CommandManager; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import me.lucko.luckperms.api.LogEntry; +import me.lucko.luckperms.common.actionlog.ExtendedLogEntry; +import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.buffers.BufferedRequest; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.utils.gson.JObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Date; -import java.util.logging.FileHandler; -import java.util.logging.Formatter; -import java.util.logging.Level; -import java.util.logging.LogRecord; -import java.util.logging.Logger; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.locks.ReentrantLock; public class FileActionLogger { - private static final String LOG_FORMAT = "%s(%s): [%s] %s(%s) --> %s"; - private final Logger actionLogger = Logger.getLogger("luckperms_actions"); - public void init(Path file) { - try { - FileHandler fh = new FileHandler(file.toString(), 0, 1, true); - fh.setFormatter(new Formatter() { - @Override - public String format(LogRecord record) { - return new Date(record.getMillis()).toString() + ": " + record.getMessage() + "\n"; - } - }); - this.actionLogger.addHandler(fh); - this.actionLogger.setUseParentHandlers(false); - this.actionLogger.setLevel(Level.ALL); - this.actionLogger.setFilter(record -> true); - } catch (Exception e) { - e.printStackTrace(); - } + /** + * The path to save logger content to + */ + private Path contentFile; + + /** + * Lock to ensure the file isn't written to by multiple threads + */ + private final ReentrantLock writeLock = new ReentrantLock(); + + /** + * The queue of entries pending save to the file + */ + private final Queue entryQueue = new ConcurrentLinkedQueue<>(); + + private final SaveBuffer saveBuffer; + + public FileActionLogger(LuckPermsPlugin plugin) { + this.saveBuffer = new SaveBuffer(plugin); + } + + public void init(Path contentFile) { + this.contentFile = contentFile; } public void logAction(LogEntry entry) { - this.actionLogger.info(String.format(LOG_FORMAT, - (entry.getActor().equals(CommandManager.CONSOLE_UUID) ? "" : entry.getActor() + " "), - entry.getActorName(), - Character.toString(entry.getType().getCode()), - entry.getActed().map(e -> e.toString() + " ").orElse(""), - entry.getActedName(), - entry.getAction()) - ); + this.entryQueue.add(entry); + this.saveBuffer.request(); + } + + public void flush() { + this.writeLock.lock(); + try { + // don't perform the i/o process if there's nothing to be written + if (this.entryQueue.peek() == null) { + return; + } + + try { + // read existing array data into memory + JsonArray array; + + if (Files.exists(this.contentFile)) { + try (JsonReader reader = new JsonReader(Files.newBufferedReader(this.contentFile, StandardCharsets.UTF_8))) { + array = Streams.parse(reader).getAsJsonArray(); + } catch (IOException e) { + e.printStackTrace(); + array = new JsonArray(); + } + } else { + array = new JsonArray(); + } + + // poll the queue for new entries + for (LogEntry e; (e = this.entryQueue.poll()) != null; ) { + JObject object = new JObject() + .add("timestamp", e.getTimestamp()) + .add("actor", e.getActor().toString()) + .add("actorName", e.getActorName()) + .add("type", Character.toString(e.getType().getCode())) + .add("actedName", e.getActedName()) + .add("action", e.getAction()); + + if (e.getActed().isPresent()) { + object.add("acted", e.getActed().get().toString()); + } + + array.add(object.toJson()); + } + + // write the full content back to the file + try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(this.contentFile, StandardCharsets.UTF_8))) { + writer.setIndent(" "); + Streams.write(array, writer); + } + } catch (IOException e) { + e.printStackTrace(); + } + } finally { + this.writeLock.unlock(); + } + } + + public Log getLog() throws IOException { + Log.Builder log = Log.builder(); + try (JsonReader reader = new JsonReader(Files.newBufferedReader(this.contentFile, StandardCharsets.UTF_8))) { + JsonArray array = Streams.parse(reader).getAsJsonArray(); + for (JsonElement element : array) { + JsonObject object = element.getAsJsonObject(); + + UUID actedUuid = null; + if (object.has("acted")) { + actedUuid = UUID.fromString(object.get("acted").getAsString()); + } + + ExtendedLogEntry e = ExtendedLogEntry.build() + .timestamp(object.get("timestamp").getAsLong()) + .actor(UUID.fromString(object.get("actor").getAsString())) + .actorName(object.get("actorName").getAsString()) + .type(LogEntry.Type.valueOf(object.get("type").getAsCharacter())) + .acted(actedUuid) + .actedName(object.get("actedName").getAsString()) + .action(object.get("action").getAsString()) + .build(); + + log.add(e); + } + } + return log.build(); + } + + private final class SaveBuffer extends BufferedRequest { + public SaveBuffer(LuckPermsPlugin plugin) { + super(2000L, 500L, plugin.getBootstrap().getScheduler().async()); + } + + @Override + protected Void perform() { + FileActionLogger.this.flush(); + return null; + } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/dao/mongodb/MongoDao.java b/common/src/main/java/me/lucko/luckperms/common/storage/dao/mongodb/MongoDao.java index 36d5afd5..49de4fa5 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/dao/mongodb/MongoDao.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/dao/mongodb/MongoDao.java @@ -183,7 +183,7 @@ public class MongoDao extends AbstractDao { .timestamp(d.getLong("timestamp")) .actor(d.get("actor", UUID.class)) .actorName(d.getString("actorName")) - .type(LogEntry.Type.valueOf(d.getString("type").toCharArray()[0])) + .type(LogEntry.Type.valueOf(d.getString("type").charAt(0))) .acted(actedUuid) .actedName(d.getString("actedName")) .action(d.getString("action"))