Skip to content

Commit

Permalink
Merge pull request #231 from sladkoff/revert-230-revert-229-feature/h…
Browse files Browse the repository at this point in the history
…ealth-checks

feat: health checks
  • Loading branch information
sladkoff authored Feb 29, 2024
2 parents 334f5bf + 95ba5a6 commit aaeff43
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 15 deletions.
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
}
}
```
28 changes: 28 additions & 0 deletions src/main/java/de/sldk/mc/HealthController.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 4 additions & 1 deletion src/main/java/de/sldk/mc/MetricsController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 7 additions & 3 deletions src/main/java/de/sldk/mc/MetricsServer.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);

Expand Down
33 changes: 33 additions & 0 deletions src/main/java/de/sldk/mc/health/ConcurrentHealthChecks.java
Original file line number Diff line number Diff line change
@@ -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<HealthCheck> checks;

private ConcurrentHealthChecks(final Set<HealthCheck> 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);
}
}
64 changes: 64 additions & 0 deletions src/main/java/de/sldk/mc/health/HealthCheck.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
21 changes: 21 additions & 0 deletions src/main/java/de/sldk/mc/health/HealthChecks.java
Original file line number Diff line number Diff line change
@@ -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);
}
20 changes: 14 additions & 6 deletions src/main/kotlin/de/sldk/mc/PrometheusExporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
19 changes: 16 additions & 3 deletions src/test/java/de/sldk/mc/exporter/PrometheusExporterTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {

Expand All @@ -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();
}

Expand Down Expand Up @@ -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);
}

}

0 comments on commit aaeff43

Please sign in to comment.