Skip to content

Commit aaeff43

Browse files
authored
Merge pull request #231 from sladkoff/revert-230-revert-229-feature/health-checks
feat: health checks
2 parents 334f5bf + 95ba5a6 commit aaeff43

File tree

10 files changed

+248
-15
lines changed

10 files changed

+248
-15
lines changed

README.md

+60-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ After startup, the Prometheus metrics endpoint should be available at ``localhos
2222

2323
The metrics port can be customized in the plugin's config.yml (a default config will be created after the first use).
2424

25+
## Feature Overview
26+
27+
### Prometheus Exporter
28+
29+
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.
30+
31+
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.
32+
33+
### Custom Health Checks
34+
35+
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.
36+
37+
The aggregated health checks are exposed at ``localhost:9940/health`` by default.
38+
39+
See [Health Checks](#health-checks) for more information on how to build your own health checks in your plugins.
40+
2541
## Installation & Configuration
2642

2743
### Plugin config
@@ -167,7 +183,10 @@ This doesn't support all statistics in the list because they are provided by the
167183
168184
## Plugin Integration
169185
170-
By integrating your own plugin with the Minecraft Prometheus Exporter, you can **monitor your plugin**: Collect metrics about your plugin's performance or usage.
186+
By integrating your own plugin with the Minecraft Prometheus Exporter, you can:
187+
188+
1. **Monitor your plugin's performance**: Collect metrics about your plugin's performance and resource usage.
189+
2. **Provide custom health checks**: Monitor the health of your plugin and alert you if something goes wrong.
171190
172191
### Collect metrics about your own plugin
173192
@@ -209,3 +228,43 @@ public class MyPluginCommand extends PluginCommand {
209228

210229
}
211230
```
231+
232+
233+
### Provide a health check from your own plugin
234+
235+
You can easily collect metrics about your own plugin.
236+
237+
#### Add compile-time dependency to your plugin
238+
239+
1. Get the latest `minecraft-prometheus-exporter-3.0.0.jar` from the [releases](https://github.com/sladkoff/minecraft-prometheus-exporter/releases) page.
240+
2. Add the jar to your project's classpath.
241+
242+
#### Create a health check
243+
244+
Create your custom health check by extending the `HealthCheck` class.
245+
246+
```java
247+
public class CustomHealthCheck implements HealthCheck {
248+
@Override
249+
public boolean isHealthy() {
250+
return true; // Your custom health check logic
251+
}
252+
}
253+
```
254+
255+
#### Register the health check
256+
257+
Register your health check in your plugin's `onEnable` method or similar.
258+
259+
This will add your health check to the list of health checks that are aggregated and exposed by the Minecraft Prometheus Exporter
260+
261+
```java
262+
public class MyPlugin extends JavaPlugin {
263+
264+
@Override
265+
public void onEnable() {
266+
// Register your health check
267+
getServer().servicesManager.load(HealthChecks.class).add(new CustomHealthCheck());
268+
}
269+
}
270+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package de.sldk.mc;
2+
3+
import de.sldk.mc.health.HealthChecks;
4+
import org.eclipse.jetty.http.HttpStatus;
5+
import org.eclipse.jetty.server.Handler;
6+
import org.eclipse.jetty.server.Request;
7+
import org.eclipse.jetty.server.Response;
8+
import org.eclipse.jetty.util.Callback;
9+
10+
public class HealthController extends Handler.Abstract {
11+
12+
private final HealthChecks checks;
13+
14+
private HealthController(final HealthChecks checks) {
15+
this.checks = checks;
16+
}
17+
18+
public static Handler create(final HealthChecks checks) {
19+
return new HealthController(checks);
20+
}
21+
22+
@Override
23+
public boolean handle(Request request, Response response, Callback callback) throws Exception {
24+
response.setStatus(checks.isHealthy() ? HttpStatus.OK_200 : HttpStatus.SERVICE_UNAVAILABLE_503);
25+
callback.succeeded();
26+
return true;
27+
}
28+
}

src/main/java/de/sldk/mc/MetricsController.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ public class MetricsController extends Handler.Abstract {
2020
private final MetricRegistry metricRegistry = MetricRegistry.getInstance();
2121
private final PrometheusExporter exporter;
2222

23-
public MetricsController(PrometheusExporter exporter) {
23+
private MetricsController(PrometheusExporter exporter) {
2424
this.exporter = exporter;
2525
}
2626

27+
public static Handler create(final PrometheusExporter exporter) {
28+
return new MetricsController(exporter);
29+
}
2730

2831
@Override
2932
public boolean handle(Request request, Response response, Callback callback) {

src/main/java/de/sldk/mc/MetricsServer.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package de.sldk.mc;
22

33
import org.eclipse.jetty.http.pathmap.PathSpec;
4+
import de.sldk.mc.health.HealthChecks;
45
import org.eclipse.jetty.server.Server;
56
import org.eclipse.jetty.server.handler.PathMappingsHandler;
67
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
@@ -12,20 +13,23 @@ public class MetricsServer {
1213
private final String host;
1314
private final int port;
1415
private final PrometheusExporter prometheusExporter;
16+
private final HealthChecks healthChecks;
1517

1618
private Server server;
1719

18-
public MetricsServer(String host, int port, PrometheusExporter prometheusExporter) {
20+
public MetricsServer(String host, int port, PrometheusExporter prometheusExporter, HealthChecks healthChecks) {
1921
this.host = host;
2022
this.port = port;
2123
this.prometheusExporter = prometheusExporter;
22-
}
24+
this.healthChecks = healthChecks;
25+
}
2326

2427
public void start() throws Exception {
2528
GzipHandler gzipHandler = new GzipHandler();
2629

2730
var pathMappings = new PathMappingsHandler();
28-
pathMappings.addMapping(PathSpec.from("/metrics"), new MetricsController(prometheusExporter));
31+
pathMappings.addMapping(PathSpec.from("/metrics"), MetricsController.create(prometheusExporter));
32+
pathMappings.addMapping(PathSpec.from("/health"), HealthController.create(healthChecks));
2933

3034
gzipHandler.setHandler(pathMappings);
3135

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package de.sldk.mc.health;
2+
3+
import java.util.Set;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
6+
public final class ConcurrentHealthChecks implements HealthChecks {
7+
8+
private final Set<HealthCheck> checks;
9+
10+
private ConcurrentHealthChecks(final Set<HealthCheck> checks) {
11+
this.checks = checks;
12+
}
13+
14+
public static HealthChecks create() {
15+
return new ConcurrentHealthChecks(ConcurrentHashMap.newKeySet());
16+
}
17+
18+
@Override
19+
public boolean isHealthy() {
20+
for (final HealthCheck check : checks) if (!check.isHealthy()) return false;
21+
return true;
22+
}
23+
24+
@Override
25+
public void add(final HealthCheck check) {
26+
checks.add(check);
27+
}
28+
29+
@Override
30+
public void remove(final HealthCheck check) {
31+
checks.remove(check);
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package de.sldk.mc.health;
2+
3+
/**
4+
* Health check.
5+
*/
6+
public interface HealthCheck {
7+
8+
/**
9+
* Checks if the current state is healthy.
10+
*
11+
* @return {@code true} if the state is healthy and {@code false} otherwise
12+
*/
13+
boolean isHealthy();
14+
15+
/**
16+
* Creates a compound health check from the provided ones reporting healthy status if all the checks report it.
17+
*
18+
* @param checks merged health checks
19+
* @return compound health check
20+
*/
21+
static HealthCheck allOf(final HealthCheck... checks) {
22+
return new AllOf(checks);
23+
}
24+
25+
/**
26+
* Creates a compound health check from the provided ones reporting healthy status if any check reports it.
27+
*
28+
* @param checks merged health checks
29+
* @return compound health check
30+
*/
31+
static HealthCheck anyOf(final HealthCheck... checks) {
32+
return new AnyOf(checks);
33+
}
34+
35+
final class AllOf implements HealthCheck {
36+
private final HealthCheck[] checks;
37+
38+
private AllOf(final HealthCheck[] checks) {
39+
this.checks = checks;
40+
}
41+
42+
@Override
43+
public boolean isHealthy() {
44+
for (final HealthCheck check : checks) if (!check.isHealthy()) return false;
45+
46+
return true;
47+
}
48+
}
49+
50+
final class AnyOf implements HealthCheck {
51+
private final HealthCheck[] checks;
52+
53+
private AnyOf(final HealthCheck[] checks) {
54+
this.checks = checks;
55+
}
56+
57+
@Override
58+
public boolean isHealthy() {
59+
for (final HealthCheck check : checks) if (check.isHealthy()) return true;
60+
61+
return false;
62+
}
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package de.sldk.mc.health;
2+
3+
/**
4+
* Dynamic compound health checks.
5+
*/
6+
public interface HealthChecks extends HealthCheck {
7+
8+
/**
9+
* Adds the provided health check to this one.
10+
*
11+
* @param check added health check
12+
*/
13+
void add(HealthCheck check);
14+
15+
/**
16+
* Removes the provided health check from this one.
17+
*
18+
* @param check removed health check
19+
*/
20+
void remove(HealthCheck check);
21+
}

src/main/kotlin/de/sldk/mc/PrometheusExporter.kt

+14-6
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,36 @@
33
package de.sldk.mc
44

55
import de.sldk.mc.config.PrometheusExporterConfig
6+
import de.sldk.mc.health.ConcurrentHealthChecks
7+
import de.sldk.mc.health.HealthChecks
8+
import org.bukkit.plugin.ServicePriority
69
import org.bukkit.plugin.java.JavaPlugin
710
import java.util.logging.Level
811

12+
913
class PrometheusExporter : JavaPlugin() {
1014
private val config: PrometheusExporterConfig = PrometheusExporterConfig(this)
11-
private var server: MetricsServer? = null
15+
private var metricsServer: MetricsServer? = null
1216

1317
@Override
1418
override fun onEnable() {
1519
config.loadDefaultsAndSave()
1620
config.enableConfiguredMetrics()
17-
startMetricsServer()
21+
22+
val healthChecks = ConcurrentHealthChecks.create()
23+
server.servicesManager.register(HealthChecks::class.java, healthChecks, this, ServicePriority.Normal)
24+
25+
startMetricsServer(healthChecks)
1826
}
1927

20-
private fun startMetricsServer() {
28+
private fun startMetricsServer(healthChecks: HealthChecks) {
2129
val host = config[PrometheusExporterConfig.HOST]
2230
val port = config[PrometheusExporterConfig.PORT]
2331

24-
server = MetricsServer(host, port, this)
32+
metricsServer = MetricsServer(host, port, this, healthChecks)
2533

2634
try {
27-
server?.start()
35+
metricsServer?.start()
2836
getLogger().info("Started Prometheus metrics endpoint at: $host:$port")
2937
} catch (e: Exception) {
3038
getLogger().severe("Could not start embedded Jetty server: " + e.message)
@@ -35,7 +43,7 @@ class PrometheusExporter : JavaPlugin() {
3543
@Override
3644
override fun onDisable() {
3745
try {
38-
server?.stop()
46+
metricsServer?.stop()
3947
} catch (e: Exception) {
4048
getLogger().log(Level.WARNING, "Failed to stop metrics server gracefully: " + e.message)
4149
getLogger().log(Level.FINE, "Failed to stop metrics server gracefully", e)

src/main/resources/plugin.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: PrometheusExporter
2-
version: 3.0.0
2+
version: 3.0.1-SNAPSHOT
33
author: sldk
44
main: de.sldk.mc.PrometheusExporter
55
website: https://github.com/sladkoff/minecraft-prometheus-exporter

src/test/java/de/sldk/mc/exporter/PrometheusExporterTest.java

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package de.sldk.mc.exporter;
22

33

4-
import static org.assertj.core.api.Assertions.assertThat;
5-
64
import de.sldk.mc.MetricsServer;
75
import de.sldk.mc.PrometheusExporter;
6+
import de.sldk.mc.health.ConcurrentHealthChecks;
87
import io.prometheus.client.CollectorRegistry;
98
import io.prometheus.client.Counter;
109
import io.prometheus.client.exporter.common.TextFormat;
@@ -21,6 +20,8 @@
2120
import java.io.IOException;
2221
import java.net.ServerSocket;
2322

23+
import static org.assertj.core.api.Assertions.assertThat;
24+
2425
@ExtendWith(MockitoExtension.class)
2526
public class PrometheusExporterTest {
2627

@@ -34,7 +35,9 @@ public class PrometheusExporterTest {
3435
void setup() throws Exception {
3536
CollectorRegistry.defaultRegistry.clear();
3637
metricsServerPort = getRandomFreePort();
37-
metricsServer = new MetricsServer("localhost", metricsServerPort, exporterMock);
38+
metricsServer = new MetricsServer(
39+
"localhost", metricsServerPort, exporterMock, ConcurrentHealthChecks.create()
40+
);
3841
metricsServer.start();
3942
}
4043

@@ -83,4 +86,14 @@ void metrics_server_should_return_404_on_unknown_paths() {
8386
.statusCode(HttpStatus.NOT_FOUND_404);
8487
}
8588

89+
@Test
90+
void metrics_server_should_return_200_on_health_check() {
91+
String requestPath = URIUtil.newURI("http", "localhost", metricsServerPort, "/health", null);
92+
93+
RestAssured.when()
94+
.get(requestPath)
95+
.then()
96+
.statusCode(HttpStatus.OK_200);
97+
}
98+
8699
}

0 commit comments

Comments
 (0)