Helidon SE Health Check Guide

This guide describes how to create a sample Helidon SE project that can be used to run some basic examples using both built-in and custom health checks.

What You Need

For this 15 minute tutorial, you will need the following:

A Helidon SE ApplicationYou can use your own application or use the Helidon SE Quickstart to create a sample application.
Java SE 17 (Open JDK 17)Helidon requires Java 17+.
Maven 3.6.1+Helidon requires Maven 3.6.1+.
Docker 18.09+You need Docker if you want to build and deploy Docker containers.
Kubectl 1.16.5+If you want to deploy to Kubernetes, you need kubectl and a Kubernetes cluster (you can install one on your desktop.
Verify Prerequisites
java -version
mvn --version
docker --version
kubectl version
Copied
Setting JAVA_HOME
# On Mac
export JAVA_HOME=`/usr/libexec/java_home -v 17`

# On Linux
# Use the appropriate path to your JDK
export JAVA_HOME=/usr/lib/jvm/jdk-17
Copied

Create a Sample SE Project

Generate the project sources using the Helidon SE Maven archetype. The result is a simple project that can be used for the examples in this guide.

Run the Maven archetype:
mvn -U archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-se \
    -DarchetypeVersion=3.2.16 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-se \
    -Dpackage=io.helidon.examples.quickstart.se
Copied

Using the Built-In Health Checks

Helidon has a set of built-in health checks that can be optionally enabled to report various health check statuses that are commonly used:

  • deadlock detection

  • available disk space

  • available heap memory

The following example will demonstrate how to use the built-in health checks. These examples are all executed from the root directory of your project (helidon-quickstart-se).

Notice that the built-in health check dependency is already in the project’s pom.xml file:
<dependency>
    <groupId>io.helidon.health</groupId>
    <artifactId>helidon-health-checks</artifactId>
</dependency>
Copied
Have a look at Main.java, and the createRouting method:
private static Routing createRouting(Config config) {

    HealthSupport health = HealthSupport.builder()
      .addLiveness(HealthChecks.healthChecks())  
      .build();

    return Routing.builder()
      .register(health)  
      .build();
}
Copied
  • Add built-in health checks (requires the helidon-health-checks dependency).
  • Register the created health support with web server routing (adds the /health endpoint).
Build the application, skipping unit tests, then run it:
mvn package -DskipTests=true
java -jar target/helidon-quickstart-se.jar
Copied
Verify the health endpoint in a new terminal window:
curl http://localhost:8080/health
Copied
JSON response:
{
  "status": "UP",
  "checks": [
    {
      "name": "deadlock",
      "status": "UP"
    },
    {
      "name": "diskSpace",
      "status": "UP",
      "data": {
        "free": "319.58 GB",
        "freeBytes": 343144304640,
        "percentFree": "68.63%",
        "total": "465.63 GB",
        "totalBytes": 499963174912
      }
    },
    {
      "name": "heapMemory",
      "status": "UP",
      "data": {
        "free": "196.84 MB",
        "freeBytes": 206404016,
        "max": "3.56 GB",
        "maxBytes": 3817865216,
        "percentFree": "98.66%",
        "total": "245.50 MB",
        "totalBytes": 257425408
      }
    }
  ]
}
Copied

Custom Liveness Health Checks

You can create application specific custom health checks and integrate them with Helidon using the HealthSupport class, which is a WebServer service that contains a collection of registered HealthCheck instances. When queried, it invokes the registered health check and returns a response with a status code representing the overall state of the application.

Notice the custom health checks dependency is already in the project’s pom.xml file:
<dependency>
    <groupId>io.helidon.health</groupId>
    <artifactId>helidon-health</artifactId>
</dependency>
Copied
Replace the HealthSupport builder in the Main.createRouting method:
HealthSupport health = HealthSupport.builder()
  .addLiveness(() -> HealthCheckResponse.named("LivenessCheck")
      .up()
      .withData("time", System.currentTimeMillis())
      .build()) 
  .build();
Copied
  • Add a custom liveness health check. This example returns UP and current time.
Build and run the application, then verify the custom health endpoint:
curl http://localhost:8080/health
Copied
JSON response:
{
    "status": "UP",
    "checks": [
        {
            "name": "LivenessCheck",
            "status": "UP",
            "data": {
                "time": 1546958376613
            }
        }
    ]
}
Copied

Custom Readiness Health Checks

You can add readiness checks to indicate that the application is ready to be used. In this example, the server will wait five seconds before it becomes ready.

Add a readyTime variable to the Main class, then set it five seconds after the application starts:
import java.util.concurrent.atomic.AtomicLong; 

public final class Main {

  private static AtomicLong readyTime = new AtomicLong(0); 

    static WebServer startServer() throws IOException {

      server.start();

        // Server threads are not daemon. No need to block. Just react.
      try {
        Thread.sleep(5000); 
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }

      readyTime.set(System.currentTimeMillis()); 
      return server;
Copied
  • Import AtomicLong.
  • Declare the readyTime variable.
  • Sleep five seconds.
  • Set the readyTime to the time when the server became ready.
Add a readiness check to the HealhSupport builder in the Main.createRouting method:
HealthSupport health = HealthSupport.builder()
  .addLiveness(() -> HealthCheckResponse.named("LivenessCheck")
      .up()
      .withData("time", System.currentTimeMillis())
      .build())
  .addReadiness(() -> HealthCheckResponse.named("ReadinessCheck")
      .status(readyTime.get() != 0 )
      .withData( "time", readyTime.get())
      .build()) 
  .build();
Copied
  • Add the readiness check.
Build and run the application. Issue the curl command with -v within five seconds and you see the application is not ready:
curl -v  http://localhost:8080/health/ready
Copied
HTTP response:
...
< HTTP/1.1 503 Service Unavailable 
...
{
  "status": "DOWN",
  "checks": [
    {
      "name": "ReadinessCheck",
      "status": "DOWN",
      "data": {
        "time,": 0
      }
    }
  ]
}
Copied
  • The HTTP status is 503 since the application is not ready.
After five seconds you will see the application is ready:
curl -v http://localhost:8080/health/ready
Copied
JSON response:
...
< HTTP/1.1 200 OK 
...
{
  "status": "UP",
  "checks": [
    {
      "name": "ReadinessCheck",
      "status": "UP",
      "data": {
        "time,": 1566243562097
      }
    }
  ]
}
Copied
  • The HTTP status is 200 indicating that the application is ready.

Custom Startup Health Checks

You can create custom startup health checks to indicate when the application has fully started and, therefore, when the readiness and liveness checks are meaningful.

This example reuses the readyTime field added above for the custom readiness check and adds a startup check that waits three additional seconds past the "ready" time before declaring the application started.

Add a startup check to the HealhSupport builder in the Main.createRouting method:
HealthSupport health = HealthSupport.builder()
  .addLiveness(() -> HealthCheckResponse.named("LivenessCheck")
      .up()
      .withData("time", System.currentTimeMillis())
      .build())
  .addReadiness(() -> HealthCheckResponse.named("ReadinessCheck")
      .status(readyTime.get() != 0 )
      .withData("time", readyTime.get())
      .build())
  .addStartup(() -> HealthCheckResponse.named("StartupCheck") 
      .status(readyTime.get() != 0
              && Duration.ofMillis(System.currentTimeMillis() - readyTime.get()).getSeconds() >= 3)
      .withData("time", readyTime.get())
      .build())
  .build();
Copied
  • Add the startup check.
Build and run the application. Issue the curl command with -v within eight seconds and you see the application is not reported as started:
curl -v  http://localhost:8080/health/started
Copied
HTTP response:
...
< HTTP/1.1 503 Service Unavailable 
...
{
  "status": "DOWN",
  "checks": [
    {
      "name": "StartupCheck",
      "status": "DOWN",
      "data": {
        "time": 1566243562097
      }
    }
  ]
}
Copied
  • The HTTP status is 503 since the application is not started.
After eight seconds you will see the application is started:
curl -v http://localhost:8080/health/started
Copied
JSON response:
...
< HTTP/1.1 200 OK 
...
{
  "status": "UP",
  "checks": [
    {
      "name": "StartupCheck",
      "status": "UP",
      "data": {
        "time": 1566243562097
      }
    }
  ]
}
Copied
  • The HTTP status is 200 indicating that the application is started.

When using the health check URLs, you can get the following health check data

Get all of liveness, readiness, and startup data from a single query:
curl http://localhost:8080/health
Copied
JSON response:
{
  "status": "UP",
  "checks": [
    {
      "name": "LivenessCheck",
      "status": "UP",
      "data": {
        "time": 1566244094548
      }
    },
    {
      "name": "ReadinessCheck",
      "status": "UP",
      "data": {
        "time,": 1566244093012
      }
    },
    {
      "name": "StartupCheck",
      "status": "UP",
      "data": {
        "time": 1566244093012
      }
    }
  ]
}
Copied

Combine Built-In and Custom Health Checks

You can combine built-in and custom health checks using the same HealthSupport builder.

Register a custom health check in the Main.createRouting method:
HealthSupport health = HealthSupport.builder()
    .addLiveness(HealthChecks.healthChecks())  
    .addLiveness(() -> HealthCheckResponse.named("LivenessCheck")
      .up()
      .withData("time", System.currentTimeMillis())
      .build())
    .addReadiness(() -> HealthCheckResponse.named("ReadinessCheck")
      .status(readyTime.get() != 0)
      .withData("time", readyTime.get())
      .build())
    .addStartup(() -> HealthCheckResponse.named("StartupCheck")
      .status(readyTime.get() != 0
              && Duration.ofMillis(System.currentTimeMillis() - readyTime.get()).getSeconds() >= 3)
      .withData("time", readyTime.get())
      .build())
    .build();
Copied
  • Add the built-in health checks back to HealthSupport builder.
Build and run the application, then verify the health endpoint. You will see both the built-in and custom health check data:
curl http://localhost:8080/health
Copied
JSON response:
{
  "status": "UP",
  "checks": [
    {
      "name": "LivenessCheck",
      "status": "UP",
      "data": {
        "time": 1566245527673
      }
    },
    {
      "name": "ReadinessCheck",
      "status": "UP",
      "data": {
        "time,": 1566245527620
      },
    {
      "name": "StartupCheck",
      "status": "UP",
      "data": {
        "time,": 1566245527620
      }
    },
    {
      "name": "deadlock",
      "status": "UP"
    },
    {
      "name": "diskSpace",
      "status": "UP",
      "data": {
        "free": "326.17 GB",
        "freeBytes": 350224424960,
        "percentFree": "70.05%",
        "total": "465.63 GB",
        "totalBytes": 499963174912
      }
    },
    {
      "name": "heapMemory",
      "status": "UP",
      "data": {
        "free": "247.76 MB",
        "freeBytes": 259791680,
        "max": "4.00 GB",
        "maxBytes": 4294967296,
        "percentFree": "99.80%",
        "total": "256.00 MB",
        "totalBytes": 268435456
      }
    }
  ]
}
Copied

Custom Health Check URL Path

You can use a custom URL path for heath checks by setting the WebContext. In the following example, only the liveness URL is changed, but you can do the same for the readiness, startup, and default health checks.

Register a custom URL path with the custom health check in the Main.createRouting method:
HealthSupport health = HealthSupport.builder()
    .webContext("/probe/live")
    .addLiveness(() -> HealthCheckResponse.named("livenessProbe")
      .up()
      .withData("time", System.currentTimeMillis())
      .build())
    .build();
Copied
  • Change the liveness URL path using a WebContext.
Build and run the application, then verify that the liveness endpoint is using the /probe/live:
curl http://localhost:8080/probe/live
Copied
JSON response:
{
  "status": "UP",
  "checks": [
    {
      "name": "livenessProbe",
      "status": "UP",
      "data": {
        "time": 1546958376613
      }
    }
  ]
}
Copied

Using Liveness, Readiness, and Startup Health Checks with Kubernetes

The following example shows how to integrate the Helidon health API in an application that implements health endpoints for the Kubernetes liveness, readiness, and startup probes.

Change the HealthSupport builder in the Main.createRouting method to use the built-in liveness checks and custom liveness, readiness, and startup checks:
HealthSupport health = HealthSupport.builder()
    .addLiveness(HealthChecks.healthChecks()) 
    .addLiveness(() -> HealthCheckResponse.named("LivenessCheck")  
      .up()
      .withData("time", System.currentTimeMillis())
      .build())
    .addReadiness(() -> HealthCheckResponse.named("ReadinessCheck")  
      .status(readyTime.get() != 0 )
      .withData("time", readyTime.get())
      .build())
    .addStartup(() -> HealthCheckResponse.named("StartupCheck")  
      .status(readyTime.get() != 0
              && Duration.ofMillis(System.currentTimeMillis() - readyTime.get()).getSeconds() >= 3)
      .withData("time", readyTime.get())
      .build())
    .build();
Copied
  • Add built-in health checks.
  • Add a custom liveness check.
  • Add a custom readiness check.
  • Add a custom startup check.
Build and run the application, then verify the liveness, readiness, and started endpoints:
curl http://localhost:8080/health/live
curl http://localhost:8080/health/ready
curl http://localhost:8080/health/started
Copied
Stop the application and build the docker image:
docker build -t helidon-quickstart-se .
Copied
Create the Kubernetes YAML specification, named health.yaml, with the following content:
kind: Service
apiVersion: v1
metadata:
  name: helidon-health 
  labels:
    app: helidon-health
spec:
  type: NodePort
  selector:
    app: helidon-health
  ports:
    - port: 8080
      targetPort: 8080
      name: http
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: helidon-health 
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helidon-health
  template:
    metadata:
      labels:
        app: helidon-health
        version: v1
    spec:
      containers:
        - name: helidon-health
          image: helidon-quickstart-se
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
          livenessProbe:
            httpGet:
              path: /health/live 
              port: 8080
            initialDelaySeconds: 5 
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health/ready 
              port: 8080
            initialDelaySeconds: 5 
            periodSeconds: 2
            timeoutSeconds: 3
          startupProbe:
            httpGet:
              path: /health/started 
              port: 8080
            initialDelaySeconds: 8 
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
---
Copied
  • A service of type NodePort that serves the default routes on port 8080.
  • A deployment with one replica of a pod.
  • The HTTP endpoint for the liveness probe.
  • The liveness probe configuration.
  • The HTTP endpoint for the readiness probe.
  • The readiness probe configuration.
  • The HTTP endpoint for the startup probe.
  • The startup probe configuration.
Create and deploy the application into Kubernetes:
kubectl apply -f ./health.yaml
Copied
Get the service information:
kubectl get service/helidon-health
Copied
NAME             TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
helidon-health   NodePort   10.107.226.62   <none>        8080:30116/TCP   4s 
Copied
  • A service of type NodePort that serves the default routes on port 30116.
Verify the health endpoints using port '30116', your port may be different:
curl http://localhost:30116/health
Copied
Delete the application, cleaning up Kubernetes resources:
kubectl delete -f ./health.yaml
Copied

Summary

This guide demonstrated how to use health checks in a Helidon SE application as follows:

  • Access the default health check

  • Create and use custom readiness, liveness, and startup checks

  • Customize the health check root path

  • Integrate Helidon health check with Kubernetes

Refer to the following reference for additional information: