From b1953632c58551ad1450c034ee4a14e7f5eb01f7 Mon Sep 17 00:00:00 2001 From: Luck Date: Fri, 5 Aug 2022 20:33:33 +0100 Subject: [PATCH] Add healthcheck for standalone app --- .../standalone/app/LuckPermsApplication.java | 24 ++++- .../app/integration/HealthReporter.java | 77 +++++++++++++++ .../DockerCommandSocket.java | 4 +- .../app/utils/HeartbeatHttpServer.java | 96 +++++++++++++++++++ .../TerminalInterface.java | 3 +- standalone/docker/Dockerfile | 3 + .../standalone/LPStandalonePlugin.java | 19 ++-- .../standalone/StandaloneHealthReporter.java | 70 ++++++++++++++ .../StandaloneContextManager.java | 2 +- .../StandaloneDummyConnectionListener.java | 2 +- .../{dummy => stub}/StandaloneEventBus.java | 2 +- 11 files changed, 288 insertions(+), 14 deletions(-) create mode 100644 standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/HealthReporter.java rename standalone/app/src/main/java/me/lucko/luckperms/standalone/app/{integration => utils}/DockerCommandSocket.java (96%) create mode 100644 standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/HeartbeatHttpServer.java rename standalone/app/src/main/java/me/lucko/luckperms/standalone/app/{integration => utils}/TerminalInterface.java (96%) create mode 100644 standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneHealthReporter.java rename standalone/src/main/java/me/lucko/luckperms/standalone/{dummy => stub}/StandaloneContextManager.java (98%) rename standalone/src/main/java/me/lucko/luckperms/standalone/{dummy => stub}/StandaloneDummyConnectionListener.java (97%) rename standalone/src/main/java/me/lucko/luckperms/standalone/{dummy => stub}/StandaloneEventBus.java (97%) diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java index a3da8e1e5..2c6359947 100644 --- a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java @@ -26,9 +26,11 @@ package me.lucko.luckperms.standalone.app; import me.lucko.luckperms.standalone.app.integration.CommandExecutor; -import me.lucko.luckperms.standalone.app.integration.DockerCommandSocket; +import me.lucko.luckperms.standalone.app.integration.HealthReporter; import me.lucko.luckperms.standalone.app.integration.ShutdownCallback; -import me.lucko.luckperms.standalone.app.integration.TerminalInterface; +import me.lucko.luckperms.standalone.app.utils.DockerCommandSocket; +import me.lucko.luckperms.standalone.app.utils.HeartbeatHttpServer; +import me.lucko.luckperms.standalone.app.utils.TerminalInterface; import net.luckperms.api.LuckPerms; @@ -55,12 +57,16 @@ public class LuckPermsApplication implements AutoCloseable { private LuckPerms luckPermsApi; /** A command executor interface to run LuckPerms commands */ private CommandExecutor commandExecutor; + /** An interface that can poll the health of the application */ + private HealthReporter healthReporter; /** If the application is running */ private final AtomicBoolean running = new AtomicBoolean(true); /** The docker command socket */ private DockerCommandSocket dockerCommandSocket; + /** The heartbeat http server */ + private HeartbeatHttpServer heartbeatHttpServer; public LuckPermsApplication(ShutdownCallback shutdownCallback) { this.shutdownCallback = shutdownCallback; @@ -75,6 +81,7 @@ public class LuckPermsApplication implements AutoCloseable { List arguments = Arrays.asList(args); if (arguments.contains("--docker")) { this.dockerCommandSocket = DockerCommandSocket.createAndStart(3000, terminal); + this.heartbeatHttpServer = HeartbeatHttpServer.createAndStart(3001, this.healthReporter); } terminal.start(); // blocking @@ -95,6 +102,14 @@ public class LuckPermsApplication implements AutoCloseable { LOGGER.warn(e); } } + + if (this.heartbeatHttpServer != null) { + try { + this.heartbeatHttpServer.close(); + } catch (Exception e) { + LOGGER.warn(e); + } + } } public AtomicBoolean runningState() { @@ -111,6 +126,11 @@ public class LuckPermsApplication implements AutoCloseable { this.commandExecutor = commandExecutor; } + // called before start() + public void setHealthReporter(HealthReporter healthReporter) { + this.healthReporter = healthReporter; + } + public String getVersion() { return "@version@"; } diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/HealthReporter.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/HealthReporter.java new file mode 100644 index 000000000..b6ddeb7dd --- /dev/null +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/HealthReporter.java @@ -0,0 +1,77 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.standalone.app.integration; + +import com.google.gson.Gson; + +import java.util.Map; + +/** + * An interface able to provide information about the application/plugin health. + */ +public interface HealthReporter { + + /** + * Polls the current health status. + * + * @return the health status + */ + Health poll(); + + final class Health { + private static final Gson GSON = new Gson(); + + private final boolean up; + private final Map details; + + Health(boolean up, Map details) { + this.up = up; + this.details = details; + } + + public boolean isUp() { + return this.up; + } + + public Map details() { + return this.details; + } + + @Override + public String toString() { + return GSON.toJson(this); + } + + public static Health up(Map details) { + return new Health(true, details); + } + + public static Health down(Map details) { + return new Health(false, details); + } + } + +} diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/DockerCommandSocket.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/DockerCommandSocket.java similarity index 96% rename from standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/DockerCommandSocket.java rename to standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/DockerCommandSocket.java index f331d9aa1..d1e025c1d 100644 --- a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/DockerCommandSocket.java +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/DockerCommandSocket.java @@ -23,7 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.standalone.app.integration; +package me.lucko.luckperms.standalone.app.utils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -56,7 +56,7 @@ public class DockerCommandSocket extends ServerSocket implements Runnable { thread.setDaemon(true); thread.start(); - LOGGER.info("Created Docker command socket on port 3000"); + LOGGER.info("Created Docker command socket on port " + port); } catch (Exception e) { LOGGER.error("Error starting docker command socket", e); } diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/HeartbeatHttpServer.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/HeartbeatHttpServer.java new file mode 100644 index 000000000..5a9067631 --- /dev/null +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/HeartbeatHttpServer.java @@ -0,0 +1,96 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.standalone.app.utils; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import me.lucko.luckperms.standalone.app.integration.HealthReporter; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Provides a tiny http server indicating the current status of the app + */ +public class HeartbeatHttpServer implements HttpHandler, AutoCloseable { + private static final Logger LOGGER = LogManager.getLogger(HeartbeatHttpServer.class); + + private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("heartbeat-http-server-%d") + .build() + ); + + public static HeartbeatHttpServer createAndStart(int port, HealthReporter healthReporter) { + HeartbeatHttpServer socket = null; + + try { + socket = new HeartbeatHttpServer(healthReporter, port); + LOGGER.info("Created Heartbeat HTTP server on port " + port); + } catch (Exception e) { + LOGGER.error("Error starting Heartbeat HTTP server", e); + } + + return socket; + } + + private final HealthReporter healthReporter; + private final HttpServer server; + + public HeartbeatHttpServer(HealthReporter healthReporter, int port) throws IOException { + this.healthReporter = healthReporter; + this.server = HttpServer.create(new InetSocketAddress(port), 50); + this.server.createContext("/health", this); + this.server.setExecutor(EXECUTOR); + this.server.start(); + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + HealthReporter.Health health = this.healthReporter.poll(); + byte[] response = health.toString().getBytes(StandardCharsets.UTF_8); + + exchange.sendResponseHeaders(health.isUp() ? 200 : 503, response.length); + try (OutputStream responseBody = exchange.getResponseBody()) { + responseBody.write(response); + } + } + + @Override + public void close() { + this.server.stop(0); + } +} diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/TerminalInterface.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/TerminalInterface.java similarity index 96% rename from standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/TerminalInterface.java rename to standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/TerminalInterface.java index f55a94c59..1981326cb 100644 --- a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/TerminalInterface.java +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/TerminalInterface.java @@ -23,9 +23,10 @@ * SOFTWARE. */ -package me.lucko.luckperms.standalone.app.integration; +package me.lucko.luckperms.standalone.app.utils; import me.lucko.luckperms.standalone.app.LuckPermsApplication; +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; import net.minecrell.terminalconsole.SimpleTerminalConsole; diff --git a/standalone/docker/Dockerfile b/standalone/docker/Dockerfile index 24df1f186..b3c7132be 100644 --- a/standalone/docker/Dockerfile +++ b/standalone/docker/Dockerfile @@ -20,3 +20,6 @@ RUN mkdir data VOLUME ["/opt/luckperms/data"] CMD ["java", "-jar", "luckperms-standalone.jar", "--docker"] + +HEALTHCHECK --interval=30s --timeout=15s --start-period=20s \ + CMD wget http://localhost:3001/health -q -O - | grep -c '"up":true' || exit 1 diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java index 0ad44e102..bc714410a 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java @@ -38,11 +38,11 @@ import me.lucko.luckperms.common.model.manager.user.StandardUserManager; import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; import me.lucko.luckperms.standalone.app.LuckPermsApplication; -import me.lucko.luckperms.standalone.dummy.StandaloneContextManager; -import me.lucko.luckperms.standalone.dummy.StandaloneDummyConnectionListener; -import me.lucko.luckperms.standalone.dummy.StandaloneEventBus; +import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; +import me.lucko.luckperms.standalone.stub.StandaloneContextManager; +import me.lucko.luckperms.standalone.stub.StandaloneDummyConnectionListener; +import me.lucko.luckperms.standalone.stub.StandaloneEventBus; import net.luckperms.api.LuckPerms; import net.luckperms.api.query.QueryOptions; @@ -57,6 +57,8 @@ import java.util.stream.Stream; public class LPStandalonePlugin extends AbstractLuckPermsPlugin { private final LPStandaloneBootstrap bootstrap; + private boolean running = false; + private StandaloneSenderFactory senderFactory; private StandaloneDummyConnectionListener connectionListener; private StandaloneCommandManager commandManager; @@ -78,6 +80,10 @@ public class LPStandalonePlugin extends AbstractLuckPermsPlugin { return this.bootstrap.getLoader(); } + public boolean isRunning() { + return this.running; + } + @Override protected void setupSenderFactory() { this.senderFactory = new StandaloneSenderFactory(this); @@ -144,16 +150,17 @@ public class LPStandalonePlugin extends AbstractLuckPermsPlugin { @Override protected void registerApiOnPlatform(LuckPerms api) { this.bootstrap.getLoader().setApi(api); + this.bootstrap.getLoader().setHealthReporter(new StandaloneHealthReporter(this)); } @Override protected void performFinalSetup() { - + this.running = true; } @Override protected void removePlatformHooks() { - + this.running = false; } @Override diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneHealthReporter.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneHealthReporter.java new file mode 100644 index 000000000..4eadad16b --- /dev/null +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneHealthReporter.java @@ -0,0 +1,70 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.standalone; + +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.standalone.app.integration.HealthReporter; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; + +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +public class StandaloneHealthReporter implements HealthReporter { + private final LPStandalonePlugin plugin; + + public StandaloneHealthReporter(LPStandalonePlugin plugin) { + this.plugin = plugin; + } + + @Override + public Health poll() { + if (!this.plugin.isRunning()) { + return Health.down(Collections.emptyMap()); + } + + Map meta = this.plugin.getStorage().getMeta().entrySet().stream() + .collect(Collectors.toMap( + e -> render(e.getKey()).toLowerCase(Locale.ROOT), + e -> render(e.getValue()) + )); + + if ("false".equals(meta.get("connected"))) { + return Health.down(Collections.singletonMap("reason", "storage disconnected")); + } + + return Health.up(meta); + } + + private static String render(Component component) { + return PlainTextComponentSerializer.plainText().serialize( + TranslationManager.render(component) + ); + } +} diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/dummy/StandaloneContextManager.java b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneContextManager.java similarity index 98% rename from standalone/src/main/java/me/lucko/luckperms/standalone/dummy/StandaloneContextManager.java rename to standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneContextManager.java index 98d3276e6..0c25f2dd9 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/dummy/StandaloneContextManager.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneContextManager.java @@ -23,7 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.standalone.dummy; +package me.lucko.luckperms.standalone.stub; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.context.manager.ContextManager; diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/dummy/StandaloneDummyConnectionListener.java b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneDummyConnectionListener.java similarity index 97% rename from standalone/src/main/java/me/lucko/luckperms/standalone/dummy/StandaloneDummyConnectionListener.java rename to standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneDummyConnectionListener.java index 66829aa43..5ea39d281 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/dummy/StandaloneDummyConnectionListener.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneDummyConnectionListener.java @@ -23,7 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.standalone.dummy; +package me.lucko.luckperms.standalone.stub; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/dummy/StandaloneEventBus.java b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneEventBus.java similarity index 97% rename from standalone/src/main/java/me/lucko/luckperms/standalone/dummy/StandaloneEventBus.java rename to standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneEventBus.java index 3cd339133..506cf3015 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/dummy/StandaloneEventBus.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneEventBus.java @@ -23,7 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.standalone.dummy; +package me.lucko.luckperms.standalone.stub; import me.lucko.luckperms.common.api.LuckPermsApiProvider; import me.lucko.luckperms.common.event.AbstractEventBus;