diff --git a/README.md b/README.md index bd8b13e..b20e463 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,22 @@ After startup, the Prometheus metrics endpoint should be available at ``localhos The metrics port can be customized in the plugin's config.yml (a default config will be created after the first use). +## Feature Overview + +### Prometheus Exporter + +The plugin exports a variety of metrics about your Minecraft server to Prometheus. These metrics can be used to monitor the health and performance of your server. + +The metrics are exposed at ``localhost:9940/metrics`` by default. See the rest of the README for more information on how to configure Prometheus to scrape these metrics. + +### Custom Health Checks + +The plugin can be configured to perform custom health checks on your server. These checks can be used to monitor the health of your server and alert you if something goes wrong. + +The aggregated health checks are exposed at ``localhost:9940/health`` by default. + +See [Health Checks](#health-checks) for more information on how to build your own health checks in your plugins. + ## Installation & Configuration ### Plugin config @@ -167,7 +183,10 @@ This doesn't support all statistics in the list because they are provided by the ## Plugin Integration -By integrating your own plugin with the Minecraft Prometheus Exporter, you can **monitor your plugin**: Collect metrics about your plugin's performance or usage. +By integrating your own plugin with the Minecraft Prometheus Exporter, you can: + +1. **Monitor your plugin's performance**: Collect metrics about your plugin's performance and resource usage. +2. **Provide custom health checks**: Monitor the health of your plugin and alert you if something goes wrong. ### Collect metrics about your own plugin @@ -209,3 +228,43 @@ public class MyPluginCommand extends PluginCommand { } ``` + + +### Provide a health check from your own plugin + +You can easily collect metrics about your own plugin. + +#### Add compile-time dependency to your plugin + +1. Get the latest `minecraft-prometheus-exporter-3.0.0.jar` from the [releases](https://github.com/sladkoff/minecraft-prometheus-exporter/releases) page. +2. Add the jar to your project's classpath. + +#### Create a health check + +Create your custom health check by extending the `HealthCheck` class. + +```java +public class CustomHealthCheck implements HealthCheck { + @Override + public boolean isHealthy() { + return true; // Your custom health check logic + } +} +``` + +#### Register the health check + +Register your health check in your plugin's `onEnable` method or similar. + +This will add your health check to the list of health checks that are aggregated and exposed by the Minecraft Prometheus Exporter + +```java +public class MyPlugin extends JavaPlugin { + + @Override + public void onEnable() { + // Register your health check + getServer().servicesManager.load(HealthChecks.class).add(new CustomHealthCheck()); + } +} +``` diff --git a/src/main/java/de/sldk/mc/HealthController.java b/src/main/java/de/sldk/mc/HealthController.java new file mode 100644 index 0000000..a9718ce --- /dev/null +++ b/src/main/java/de/sldk/mc/HealthController.java @@ -0,0 +1,28 @@ +package de.sldk.mc; + +import de.sldk.mc.health.HealthChecks; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +public class HealthController extends Handler.Abstract { + + private final HealthChecks checks; + + private HealthController(final HealthChecks checks) { + this.checks = checks; + } + + public static Handler create(final HealthChecks checks) { + return new HealthController(checks); + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + response.setStatus(checks.isHealthy() ? HttpStatus.OK_200 : HttpStatus.SERVICE_UNAVAILABLE_503); + callback.succeeded(); + return true; + } +} diff --git a/src/main/java/de/sldk/mc/MetricsController.java b/src/main/java/de/sldk/mc/MetricsController.java index 2e7f998..f2621b9 100644 --- a/src/main/java/de/sldk/mc/MetricsController.java +++ b/src/main/java/de/sldk/mc/MetricsController.java @@ -20,10 +20,13 @@ public class MetricsController extends Handler.Abstract { private final MetricRegistry metricRegistry = MetricRegistry.getInstance(); private final PrometheusExporter exporter; - public MetricsController(PrometheusExporter exporter) { + private MetricsController(PrometheusExporter exporter) { this.exporter = exporter; } + public static Handler create(final PrometheusExporter exporter) { + return new MetricsController(exporter); + } @Override public boolean handle(Request request, Response response, Callback callback) { diff --git a/src/main/java/de/sldk/mc/MetricsServer.java b/src/main/java/de/sldk/mc/MetricsServer.java index b40f553..c2ab4de 100644 --- a/src/main/java/de/sldk/mc/MetricsServer.java +++ b/src/main/java/de/sldk/mc/MetricsServer.java @@ -1,6 +1,7 @@ package de.sldk.mc; import org.eclipse.jetty.http.pathmap.PathSpec; +import de.sldk.mc.health.HealthChecks; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.PathMappingsHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; @@ -12,20 +13,23 @@ public class MetricsServer { private final String host; private final int port; private final PrometheusExporter prometheusExporter; + private final HealthChecks healthChecks; private Server server; - public MetricsServer(String host, int port, PrometheusExporter prometheusExporter) { + public MetricsServer(String host, int port, PrometheusExporter prometheusExporter, HealthChecks healthChecks) { this.host = host; this.port = port; this.prometheusExporter = prometheusExporter; - } + this.healthChecks = healthChecks; + } public void start() throws Exception { GzipHandler gzipHandler = new GzipHandler(); var pathMappings = new PathMappingsHandler(); - pathMappings.addMapping(PathSpec.from("/metrics"), new MetricsController(prometheusExporter)); + pathMappings.addMapping(PathSpec.from("/metrics"), MetricsController.create(prometheusExporter)); + pathMappings.addMapping(PathSpec.from("/health"), HealthController.create(healthChecks)); gzipHandler.setHandler(pathMappings); diff --git a/src/main/java/de/sldk/mc/health/ConcurrentHealthChecks.java b/src/main/java/de/sldk/mc/health/ConcurrentHealthChecks.java new file mode 100644 index 0000000..4767683 --- /dev/null +++ b/src/main/java/de/sldk/mc/health/ConcurrentHealthChecks.java @@ -0,0 +1,33 @@ +package de.sldk.mc.health; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class ConcurrentHealthChecks implements HealthChecks { + + private final Set checks; + + private ConcurrentHealthChecks(final Set checks) { + this.checks = checks; + } + + public static HealthChecks create() { + return new ConcurrentHealthChecks(ConcurrentHashMap.newKeySet()); + } + + @Override + public boolean isHealthy() { + for (final HealthCheck check : checks) if (!check.isHealthy()) return false; + return true; + } + + @Override + public void add(final HealthCheck check) { + checks.add(check); + } + + @Override + public void remove(final HealthCheck check) { + checks.remove(check); + } +} diff --git a/src/main/java/de/sldk/mc/health/HealthCheck.java b/src/main/java/de/sldk/mc/health/HealthCheck.java new file mode 100644 index 0000000..c9648f6 --- /dev/null +++ b/src/main/java/de/sldk/mc/health/HealthCheck.java @@ -0,0 +1,64 @@ +package de.sldk.mc.health; + +/** + * Health check. + */ +public interface HealthCheck { + + /** + * Checks if the current state is healthy. + * + * @return {@code true} if the state is healthy and {@code false} otherwise + */ + boolean isHealthy(); + + /** + * Creates a compound health check from the provided ones reporting healthy status if all the checks report it. + * + * @param checks merged health checks + * @return compound health check + */ + static HealthCheck allOf(final HealthCheck... checks) { + return new AllOf(checks); + } + + /** + * Creates a compound health check from the provided ones reporting healthy status if any check reports it. + * + * @param checks merged health checks + * @return compound health check + */ + static HealthCheck anyOf(final HealthCheck... checks) { + return new AnyOf(checks); + } + + final class AllOf implements HealthCheck { + private final HealthCheck[] checks; + + private AllOf(final HealthCheck[] checks) { + this.checks = checks; + } + + @Override + public boolean isHealthy() { + for (final HealthCheck check : checks) if (!check.isHealthy()) return false; + + return true; + } + } + + final class AnyOf implements HealthCheck { + private final HealthCheck[] checks; + + private AnyOf(final HealthCheck[] checks) { + this.checks = checks; + } + + @Override + public boolean isHealthy() { + for (final HealthCheck check : checks) if (check.isHealthy()) return true; + + return false; + } + } +} diff --git a/src/main/java/de/sldk/mc/health/HealthChecks.java b/src/main/java/de/sldk/mc/health/HealthChecks.java new file mode 100644 index 0000000..39c8af1 --- /dev/null +++ b/src/main/java/de/sldk/mc/health/HealthChecks.java @@ -0,0 +1,21 @@ +package de.sldk.mc.health; + +/** + * Dynamic compound health checks. + */ +public interface HealthChecks extends HealthCheck { + + /** + * Adds the provided health check to this one. + * + * @param check added health check + */ + void add(HealthCheck check); + + /** + * Removes the provided health check from this one. + * + * @param check removed health check + */ + void remove(HealthCheck check); +} diff --git a/src/main/kotlin/de/sldk/mc/PrometheusExporter.kt b/src/main/kotlin/de/sldk/mc/PrometheusExporter.kt index 9847d76..b1cd0d4 100644 --- a/src/main/kotlin/de/sldk/mc/PrometheusExporter.kt +++ b/src/main/kotlin/de/sldk/mc/PrometheusExporter.kt @@ -3,28 +3,36 @@ package de.sldk.mc import de.sldk.mc.config.PrometheusExporterConfig +import de.sldk.mc.health.ConcurrentHealthChecks +import de.sldk.mc.health.HealthChecks +import org.bukkit.plugin.ServicePriority import org.bukkit.plugin.java.JavaPlugin import java.util.logging.Level + class PrometheusExporter : JavaPlugin() { private val config: PrometheusExporterConfig = PrometheusExporterConfig(this) - private var server: MetricsServer? = null + private var metricsServer: MetricsServer? = null @Override override fun onEnable() { config.loadDefaultsAndSave() config.enableConfiguredMetrics() - startMetricsServer() + + val healthChecks = ConcurrentHealthChecks.create() + server.servicesManager.register(HealthChecks::class.java, healthChecks, this, ServicePriority.Normal) + + startMetricsServer(healthChecks) } - private fun startMetricsServer() { + private fun startMetricsServer(healthChecks: HealthChecks) { val host = config[PrometheusExporterConfig.HOST] val port = config[PrometheusExporterConfig.PORT] - server = MetricsServer(host, port, this) + metricsServer = MetricsServer(host, port, this, healthChecks) try { - server?.start() + metricsServer?.start() getLogger().info("Started Prometheus metrics endpoint at: $host:$port") } catch (e: Exception) { getLogger().severe("Could not start embedded Jetty server: " + e.message) @@ -35,7 +43,7 @@ class PrometheusExporter : JavaPlugin() { @Override override fun onDisable() { try { - server?.stop() + metricsServer?.stop() } catch (e: Exception) { getLogger().log(Level.WARNING, "Failed to stop metrics server gracefully: " + e.message) getLogger().log(Level.FINE, "Failed to stop metrics server gracefully", e) diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index d002667..d8cf14f 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: PrometheusExporter -version: 3.0.0 +version: 3.0.1-SNAPSHOT author: sldk main: de.sldk.mc.PrometheusExporter website: https://github.com/sladkoff/minecraft-prometheus-exporter diff --git a/src/test/java/de/sldk/mc/exporter/PrometheusExporterTest.java b/src/test/java/de/sldk/mc/exporter/PrometheusExporterTest.java index a06e72e..0f9eec7 100644 --- a/src/test/java/de/sldk/mc/exporter/PrometheusExporterTest.java +++ b/src/test/java/de/sldk/mc/exporter/PrometheusExporterTest.java @@ -1,10 +1,9 @@ package de.sldk.mc.exporter; -import static org.assertj.core.api.Assertions.assertThat; - import de.sldk.mc.MetricsServer; import de.sldk.mc.PrometheusExporter; +import de.sldk.mc.health.ConcurrentHealthChecks; import io.prometheus.client.CollectorRegistry; import io.prometheus.client.Counter; import io.prometheus.client.exporter.common.TextFormat; @@ -21,6 +20,8 @@ import java.io.IOException; import java.net.ServerSocket; +import static org.assertj.core.api.Assertions.assertThat; + @ExtendWith(MockitoExtension.class) public class PrometheusExporterTest { @@ -34,7 +35,9 @@ public class PrometheusExporterTest { void setup() throws Exception { CollectorRegistry.defaultRegistry.clear(); metricsServerPort = getRandomFreePort(); - metricsServer = new MetricsServer("localhost", metricsServerPort, exporterMock); + metricsServer = new MetricsServer( + "localhost", metricsServerPort, exporterMock, ConcurrentHealthChecks.create() + ); metricsServer.start(); } @@ -83,4 +86,14 @@ void metrics_server_should_return_404_on_unknown_paths() { .statusCode(HttpStatus.NOT_FOUND_404); } + @Test + void metrics_server_should_return_200_on_health_check() { + String requestPath = URIUtil.newURI("http", "localhost", metricsServerPort, "/health", null); + + RestAssured.when() + .get(requestPath) + .then() + .statusCode(HttpStatus.OK_200); + } + }