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:

Prerequisite product versions for Helidon 4.4.1
Java SE 21 (Open JDK 21)Helidon requires Java 21+ (25+ recommended).
Maven 3.8+Helidon requires Maven 3.8+.
Docker 18.09+If you want to build and run 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 21`

# On Linux
# Use the appropriate path to your JDK
export JAVA_HOME=/usr/lib/jvm/jdk-21
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=4.4.1 \
    -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:

  • deadlock detection

  • available disk space

  • available heap memory

The following example shows 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 pom.xml file in the generated project already contains dependencies for Helidon’s health component and for the built-in health checks.

Generated dependencies related to health
<dependencies>
    <dependency>
        <groupId>io.helidon.webserver.observe</groupId>
        <artifactId>helidon-webserver-observe-health</artifactId>
    </dependency>
    <dependency>
        <groupId>io.helidon.health</groupId>
        <artifactId>helidon-health-checks</artifactId>
    </dependency>
</dependencies>
Copied

Handling health checks is part of Helidon’s observability support. By default, when you add the dependency for the built-in health checks, Helidon automatically registers the built-in checks.

Build and run the project
mvn clean package
java -jar target/helidon-quickstart-se.jar
Copied

In another window, access the application’s health endpoint.

Access the health endpoint
curl -v http://localhost:8080/observe/health
Copied

The verbose curl output reports the HTTP status:

< HTTP/1.1 204 No Content
Copied

The successful status means all health checks reported UP.

To see the details about each health check, add the following features configuration fragment in the server section of the application.yaml. Make sure the features key is at the same level as port and host that are already in the file.

Configuration fragment to include details in the health output (nested under server)
server:
  port: 8080
  host: 0.0.0.0
  features: 
    observe:
      observers:
        health:
          details: true
Copied
  • Added features config section.

Press ^C to stop the running server, rebuild it, and rerun it.

Stop, rebuild, and rerun the server
^C
mvn clean package
java -jar target/helidon-quickstart-se.jar
Copied

In the other window access the health endpoint again.

Access the health endpoint
curl -v http://localhost:8080/observe/health
Copied

This time the curl output shows not only the HTTP status—​as 200 instead of 204 because the response now contains data—​but also the detailed output for all health checks.

Health check details
{
  "status": "UP", 
  "checks": [ 
    {
      "name": "diskSpace",
      "status": "UP",
      "data": {
        "total": "465.63 GB",
        "percentFree": "14.10%",
        "totalBytes": 499963174912,
        "free": "65.67 GB",
        "freeBytes": 70513274880
      }
    },
    {
      "name": "heapMemory",
      "status": "UP",
      "data": {
        "total": "516.00 MB",
        "percentFree": "99.82%",
        "max": "8.00 GB",
        "totalBytes": 541065216,
        "maxBytes": 8589934592,
        "free": "500.87 MB",
        "freeBytes": 525201320
      }
    },
    {
      "name": "deadlock",
      "status": "UP"
    }
  ]
}
Copied
  • Overall application health status
  • List of individual health checks.

Adding Custom Health Checks

You can add your own custom health checks. These typically assess the conditions in and around your application and report whether the service should be considered started, live, and/or ready.

The following trivial but illustrative example adds a custom start-up check that reports DOWN until the server has been running for eight seconds and reports UP thereafter. Note the two main steps in the example code:

  1. Create an explicit instance of ObserveFeature which contains a custom HealthObserver with the custom check.
  2. Add that ObserveFeature instance to the WebServerConfig.Builder as a feature.
Updated Main#main, augmenting the creation of WebServer instance with a custom health check
void snippet1(Config config) {
    AtomicLong serverStartTime = new AtomicLong();  

    HealthObserver healthObserver = HealthObserver.builder() 
            .details(true) 
            .addCheck(() -> HealthCheckResponse.builder() 
                              .status(System.currentTimeMillis() - serverStartTime.get() >= 8000)
                              .detail("time", System.currentTimeMillis())
                              .build(),
                      HealthCheckType.STARTUP,
                      "warmedUp")
            .build();

    ObserveFeature observe = ObserveFeature.builder()
            .config(config.get("server.features.observe")) 
            .addObserver(healthObserver) 
            .build();

    WebServer server = WebServer.builder()
            .config(config.get("server"))
            .addFeature(observe)            
            .routing(Main::routing)
            .build()
            .start();

    serverStartTime.set(System.currentTimeMillis()); 
Copied
  • Declare a variable for holding the server start-up time. (This is set later in the code.)
  • Begin preparing the custom HealthObserver according to this app’s specific needs.
  • Turn on detailed output in HTTP responses to the health endpoint.
  • Add a custom start-up health check:
  • Find and apply configuration for observability observers other than health (because we are about to create our own custom HealthObserver).
  • Add the HealthObserver to the ObserveFeature.
  • Add the ObserveFeature instance as a feature to the webserver.
  • Record when the server has actually started.

Note that the health check type and name are fixed, whereas the health check recomputes the value of the response every time Helidon queries it.

For the next step, be ready to access the health endpoint very quickly after you restart the server!

Stop, rebuild, and rerun the application
^C
mvn package
java -jar target/helidon-quickstart-se.jar
Copied
Access the health endpoint quickly
curl -v http://localhost:8080/observe/health
Copied

If you access the health endpoint before the server has been up for eight seconds, curl reports the response status as 503 Service Unavailable and displays output similar to the following:

Health response shortly after server restart (partial)
{
  "status": "DOWN",
  "checks": [
    {
      "name": "warmedUp",
      "status": "DOWN",
      "data": {
        "time": 1702068978353
      }
    }
  ]
}
Copied

The built-in health checks (not shown in the example output) all report UP but the new custom start-up health check reports DOWN because the server has been up only a short time.

Access the health endpoint again, after the server has been up at least eight seconds.

Access the health endpoint again after 8 seconds
curl -v http://localhost:8080/observe/health
Copied

This time, curl reports 200 OK for the response status and displays different output for the custom health check.

Health response after the server has been running a while (partial)
{
  "status": "UP",
  "checks": [
    {
      "name": "warmedUp",
      "status": "UP",
      "data": {
        "time": 1702069379717
      }
    }
  ]
}
Copied

The example code includes the built-in health checks in Helidon’s overall health assessment of the application. To exclude them invoke the HealthObserver.Builder useSystemServices method (for example, just after invoking details on the builder).

Disable all built-in health checks
HealthObserver.builder()
        .useSystemServices(false)
        .build();
Copied

Alternatively, you could instead remove the dependency on the helidon-health-checks component from the pom.xml file.

Accessing Specific Health Check Types

You can choose which category of health check to retrieve when you access the health endpoint by adding the health check type as an additional part of the resource path:

Get only start-up health checks
curl http://localhost:8080/observe/started
Copied
JSON response:
{
  "status": "UP",
  "checks": [
    {
      "name": "warmedUp",
      "status": "UP",
      "data": {
        "time": 1702069835172
      }
    }
  ]
}
Copied

Applying Configuration to a Custom Health Observer: Customizing the URL path

Earlier examples showed how to add custom health checks by building a custom HealthObserver in which the code set up the behavior of the health subsystem explicitly. Recall that the example code invoked the HealthObserver.Builder details method to turn on detailed output.

Once it creates a custom health observer, your code has full responsibility for determining the observer’s behavior; Helidon does not automatically apply configuration to a custom observer. But your code can easily do so.

The next example customizes the URL path for the health endpoint, first explicitly in the code and then via configuration.

Customizing the endpoint path in the code

Customize the URL path for health checks by invoking the endpoint method on the HealthObserver.Builder.

Set a custom endpoint path
HealthObserver healthObserver = HealthObserver.builder()
        .endpoint("/myhealth") 
        .build();
Copied
  • Changes the health endpoint path to /myhealth.
Build and run the application, then verify that the health check endpoint responds at /myhealth:
curl http://localhost:8080/myhealth
Copied

Earlier you added health config to the application.yaml config file to turn on detailed output. If you want to run an experiment, change that details setting in the config file to false and stop, rebuild, and rerun the application. Now access the health endpoint (at /myhealth, remember). The output remains detailed because your code—​which has full responsibility for determining the custom health observer’s behavior—​does not apply configuration to the custom observer’s builder.

Adding configuration to a custom observer

In addition to preparing the health observer builder with hard-coded settings, your code can also apply configuration for health. This allows someone who deploys your application to control the behavior of the health subsystem using configuration without requiring source code changes to your application.

The generated Main class in the application already creates a Config object for the top-level config node. Using the following code to create the observe feature also applies any health-related configuration settings to the custom health observer. Notice the added line just before the HealthObserver.Build build() invocation near the end of the example code.

Apply health configuration to your custom health observer
HealthObserver healthObserver = HealthObserver.builder()
        .config(config.get("server.features.observe.observers.health")) 
        .build();
Copied
  • Find and apply any health-related settings from configuration at the server.features.observe.observers.health config key.

Your code decides what config key to use for retrieving the configuration. Recall earlier, before adding custom health checks, you added a config section for health—​to set details to true--at server.features.observe.observers.health. Helidon used that configuration to set up the health observer it created automatically. To be consistent for anyone preparing the configuration file, it’s a good idea for your application code—​as it prepares a custom HealthObserver--to look in the same place Helidon does for health config.

Order is important. Here, the code first sets details to true explicitly and later applies configuration. If your end user sets details in the server.features.observe.observers.health config to false, that setting overrides the hard-coded true setting in the code because of where in the code you apply the configuration. Try changing the details value to false in the config file and then stop, rebuild, and rerun the application. Access the health endpoint and notice that the output is no longer detailed.

In general, most applications should apply settings from config after assigning any settings in the code so users have the final say, but there might be exceptions in your particular case.

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.

Add a readyTime variable to the Main class:
private static AtomicLong readyTime = new AtomicLong(0);
Copied
Change the HealthObserver builder in the Main#main method to use new built-in liveness checks and custom liveness, readiness, and startup checks:
ObserveFeature observe = ObserveFeature.builder()
        .config(config.get("server.features.observe"))
        .addObserver(HealthObserver.builder()
                             .useSystemServices(true) 
                             .addCheck(() -> HealthCheckResponse.builder()
                                     .status(readyTime.get() != 0)
                                     .detail("time", readyTime.get())
                                     .build(), HealthCheckType.READINESS) 
                             .addCheck(() -> HealthCheckResponse.builder()
                                     .status(readyTime.get() != 0
                                             && Duration.ofMillis(System.currentTimeMillis()
                                                                  - readyTime.get())
                                                        .getSeconds() >= 3)
                                     .detail("time", readyTime.get())
                                     .build(), HealthCheckType.STARTUP) 
                             .addCheck(() -> HealthCheckResponse.builder()
                                     .status(HealthCheckResponse.Status.UP)
                                     .detail("time", System.currentTimeMillis())
                                     .build(), HealthCheckType.LIVENESS) 
                             .build())
        .build();
Copied
  • Add built-in health checks.
  • Add a custom readiness check.
  • Add a custom start-up check.
  • Add a custom liveness 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 demonstrates how to use health checks in a Helidon SE application as follows:

  • Access the default health checks

  • 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: