Featured image for Java topics.

The first article in this series presented a legacy Java application that we want to upgrade to a microservices architecture. In the second article, we upgraded the Java EE environment to Jakarta EE. Now, we will add a powerful collection of microservices functionality, writing very little code of our own. Specifically, we will:

Read the whole series:

About Eclipse MicroProfile

MicroProfile, a project maintained by the Eclipse Foundation, provides highly sophisticated tools that Java programmers can insert into their applications with minimal effort. In this article, we will implement and test the following components:

Installation and configuration

This article uses the same GitHub repository as the previous articles in the series. We are working in Red Hat OpenShift.

Start your upgrade by switching to the git tag that contains the source code used to implement the MicroProfile adoption:

$ git checkout tags/Microprofile_version

Now, delete the previous version of the application to start with a clean environment:

$ oc delete all --selector app=weather-app-eap-cloud-ready 
$ oc delete is weather-app-eap-cloud-ready
$ oc delete bc weather-app-eap-cloud-ready

Next, we'll install Jaeger, Prometheus, and Grafana in an OpenShift environment. We'll also create the cloud-ready weather application.

Install Jaeger

Import the Jaeger image from the catalog:

$ oc import-image distributed-tracing/jaeger-all-in-one-rhel8 --from=registry.redhat.io/distributed-tracing/jaeger-all-in-one-rhel8 --confirm

Create the Jaeger application:

$ oc new-app -i jaeger-all-in-one-rhel8

Expose the route in order to make the Jaeger application available outside of OpenShift:

$ oc expose svc jaeger-all-in-one-rhel8 --port=16686

Install the weather application

Now, we'll create the cloud-ready weather application. First, import the image for JBoss EAP XP 2.0 for OpenJDK 11:

$ oc import-image jboss-eap-7/eap-xp2-openjdk11-openshift-rhel8 --from=registry.redhat.io/jboss-eap-7/eap-xp2-openjdk11-openshift-rhel8 --confirm

Create the build for the cloud-ready weather app that will be deployed on JBoss EAP XP 2.0:

$ oc new-build eap-xp2-openjdk11-openshift-rhel8 --binary=true --name=weather-app-eap-cloud-ready

Change to the directory for the weather-app-eap-cloud-ready project. Start the build of the application on OpenShift:

$ oc start-build weather-app-eap-cloud-ready --from-dir=. --wait

Create the weather application for JBoss EAP XP 2.0 and configure it:

$ oc create -f k8s/weather-app-eap-cloud-ready.yaml

Install Prometheus

Next, we create the Prometheus environment that will collect the values provided by the MicroProfile Metrics specification, which we'll integrate shortly. Start by importing the Prometheus image from the catalog:

$ oc import-image openshift4/ose-prometheus --from=registry.redhat.io/openshift4/ose-prometheus --confirm

Create the config map with the Prometheus configurations:

$ oc create configmap prometheus --from-file=k8s/prometheus.yml

Create the Prometheus application:

$ oc create -f k8s/ose-prometheus.yaml

Install Grafana

Finally, we create the Grafana environment used to collect the values provided by the MicroProfile Metrics specification. Import the Grafana image from the catalog:

$ oc import-image openshift4/ose-grafana --from=registry.redhat.io/openshift4/ose-grafana --confirm

Create the config map with the Grafana configurations:

$ oc create configmap grafana --from-file=k8s/datasource-prometheus.yaml --from-file=k8s/grafana-dashboard.yaml --from-file=k8s/jboss_eap_grafana_dashboard.json

Create the Grafana application:

$ oc create -f k8s/ose-grafana.yaml

Configure JBoss EAP XP

This section shows how easy it is to implement the MicroProfile specifications using JBoss EAP XP and OpenShift. We'll use the JBoss EAP XP container image at registry.redhat.io/jboss-eap-7/eap-xp2-openjdk11-openshift-rhel8.

To move from JBoss EAP to JBoss EAP XP, you need to update the Maven pom.xml file as follows:

...
<properties>
    ...
    <version.microprofile.bom>2.0.0.GA</version.microprofile.bom>
    ...
</properties>
...
<dependencyManagement>
    ...
    <!-- importing the microprofile BOM adds MicroProfile specs -->
    <dependency>
        <groupId>org.jboss.bom</groupId>
           <artifactId>jboss-eap-xp-microprofile</artifactId>
           <version>${version.microprofile.bom}</version>
           <type>pom</type>
           <scope>import</scope>
     </dependency>
     ...
</dependencyManagement>
...

You can use the following command to get the route to the application, then use it to test the application:

$ oc get route weather-app-eap-cloud-ready --template='{{ .spec.host }}'

Copy the output of this command into your browser. You should be able to connect to the weather application and check the weather in the selected cities.

Activate the MicroProfile services

As stated at the beginning of this article, I have chosen five tools from the MicroProfile project for our application. We'll install and use each of them in the following sections.

MicroProfile Health

The MicroProfile Health specification defines a runtime mechanism for validating the availability and status of a MicroProfile implementation. To implement MicroProfile Health, set this dependency in the Maven pom.xml file:

<!-- Import the MicroProfile Health API, we use provided scope as the API is included in JBoss EAP XP -->
<dependency>
    <groupId>org.eclipse.microprofile.health</groupId>
    <artifactId>microprofile-health-api</artifactId>
    <scope>provided</scope>
</dependency>

Readiness checks

Next, we'll create two classes to implement health checks for the application. Start with a readiness check (com.redhat.example.weather.DatabaseHealthCheck), which verifies that the application is ready to service the customers’ requests:

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
import org.eclipse.microprofile.health.Readiness;

@Readiness
@ApplicationScoped
public class DatabaseHealthCheck implements HealthCheck {

    @Override
    public HealthCheckResponse call() {

        HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("Database connection health check");
        String hostName = (System.getenv("WEATHER_POSTGRESQL_SERVICE_HOST") != null) ? System.getenv("WEATHER_POSTGRESQL_SERVICE_HOST") : "localhost";
        Integer port = (System.getenv("WEATHER_POSTGRESQL_SERVICE_PORT") != null) ? Integer.parseInt(System.getenv("WEATHER_POSTGRESQL_SERVICE_PORT")) : 5432;

        try {
            pingServer(hostName, port);
            responseBuilder.up();
        } catch (IOException e) {

            responseBuilder.down()
                    .withData("error", e.getMessage());
        }

        return responseBuilder.build();
    }

    private void pingServer(String dbhost, int port) throws IOException {
            Socket socket = new Socket(dbhost, port);
            socket.close();
    }

}

Liveness checks

The second class (com.redhat.example.weather.DatabaseConnectionHealthCheck) implements a liveness check, which verifies whether the application is still able to serve the requests:

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
import org.eclipse.microprofile.health.Liveness;

@Liveness
@ApplicationScoped
public class DatabaseConnectionHealthCheck implements HealthCheck {

    @Resource
    private DataSource datasource;

    @Override
    public HealthCheckResponse call() {

        HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("Database sql connection health check");
        try (var connection = datasource.getConnection()) {
            boolean isValid = connection.isValid(5);

            DatabaseMetaData metaData = connection.getMetaData();

            responseBuilder = responseBuilder
                        .withData("databaseProductName", metaData.getDatabaseProductName())
                        .withData("databaseProductVersion", metaData.getDatabaseProductVersion())
                        .withData("driverName", metaData.getDriverName())
                        .withData("driverVersion", metaData.getDriverVersion())
                        .withData("isValid", isValid);

            return responseBuilder.state(isValid).build();


        } catch(SQLException  e) {
            responseBuilder = responseBuilder
                   .withData("exceptionMessage", e.getMessage());
            return responseBuilder.down().build();
        }
    }
    
}

Note that we've used the @Liveness and @Readiness annotations on the class to enable the health checks.

Viewing the application's health status

To view the health status, click on the health route in the OpenShift web console or get the value using the following command:

$ oc get route health --template='{{ .spec.host }}'

Append the path health to the command output. You can also verify the status of your application using the following paths after your health route:

  • /health/live: To check whether the application is up and running.
  • /health/ready: To check whether the application is ready to serve requests.

You should obtain results like those in Figure 1 (click to enlarge).

The health check shows that the database is up and running.
Figure 1: Results of a health check.

Automatic restarts

With health checks in place, you can instruct OpenShift and Kubernetes to automatically observe your application and restart the pod when a failure is reported by the checks implemented through the MicroProfile Health specification. I set up automatic restarts using the DeploymentConfig file (k8s/weather-app-eap-cloud-ready.yaml under the src folder), where I defined the liveness and readiness probes and set the application path that implements the checks:

apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
...
spec:
  replicas: 1
  ...
    spec:
      containers:
        - resources: {}
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 9990
              scheme: HTTP
            initialDelaySeconds: 25
            timeoutSeconds: 1
            periodSeconds: 10
            successThreshold: 1
            failureThreshold: 3
          name: weather-app-eap-cloud-ready
          livenessProbe:
            httpGet:
              path: /health/live
              port: 9990
              scheme: HTTP
            initialDelaySeconds: 25
            timeoutSeconds: 1
            periodSeconds: 10
            successThreshold: 1
            failureThreshold: 3

MicroProfile Metrics

MicroProfile Metrics allows applications to log metrics during execution, which you can use to monitor essential system parameters. You can implement this service without touching your code. Just add the following dependencies to your Maven pom.xml file:

<!-- Import the MicroProfile Metrics API, we use provided scope as the API is included in JBoss EAP XP -->
<dependency>
    <groupId>org.eclipse.microprofile.metrics</groupId>
    <artifactId>microprofile-metrics-api</artifactId>
    <scope>provided</scope>
</dependency>

To add more information that helps to test the metrics capabilities, I updated my com.redhat.example.weather.WeatherService class as follows:

...
import org.eclipse.microprofile.metrics.MetricUnits;
import org.eclipse.microprofile.metrics.annotation.Counted;
import org.eclipse.microprofile.metrics.annotation.Metered;
import org.eclipse.microprofile.metrics.annotation.Timed;

@RequestScoped
@Path("weather")
public class WeatherService {
    ...
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Counted(name = "requestCount", description = "How many weather forecast have been performed.")
    @Timed(name = "checksTimer", absolute = true, description = "A measure of how long it takes to perform the getList method.", unit = MetricUnits.MILLISECONDS)
    @Metered(name = "getList", absolute = true)
    public Country getList() {
        return em.find(Country.class,selectedCountry.getCode());
    }
}

Accessing metrics

To get access to the metrics being generated, click on the metric route in the OpenShift web console or get the value using the following command:

$ oc get route metric --template='{{ .spec.host }}'

Then, append the path metrics. The MicroProfile Metrics specification defines three different scopes of metrics that you can check by appending the following paths after your metric route:

  • /metrics/base: Metrics that all MicroProfile vendors must provide.
  • /metrics/vendor: Vendor-specific metrics.
  • /metrics/application: Application-specific metrics.

The metrics look something like what's shown in Figure 2.

The raw metrics collected by MicroProfile Metrics.
Figure 2: Raw metrics collected by MicroProfile Metrics.

Visualizing metrics with Prometheus and Grafana

To provide a more comprehensible display of the metrics exposed by your application in OpenShift, you can integrate them into Prometheus and Grafana.

Prometheus is an open source, community-driven project that collects metrics from targets by scraping the metrics HTTP endpoints. We installed and configured it in the previous steps using the prometheus.yml and ose-prometheus.yaml files under the k8s directory. Click on the ose-prometheus route in the OpenShift web console or get the value using the following command:

$ oc get route ose-prometheus --template='{{ .spec.host }}'

Visiting the URL opens the Prometheus web console. Enter application_getList_total there and click on the Execute button. This will show you the number of invocations of the getList method used by the application. You should see something like Figure 3.

Prometheus provides structure to the raw metrics.
Figure 3: Display of a metric in Prometheus.

Grafana is an open source tool that composes dashboards with statistics from Prometheus to aid in the observability of applications, systems, and networks. I installed and configured Grafana in the previous steps using the datasource-prometheus.yaml, grafana-dashboard.yaml, jboss_eap_grafana_dashboard.json, and ose-grafana.yaml files. These files create the Grafana application, connect it with Prometheus, and pre-load a dashboard with some metrics. To view the dashboard, click on the ose-grafana route in the OpenShift web console or get the value using the following command:

$ oc get route ose-grafana --template='{{ .spec.host }}'

Visting the URL opens the Grafana web console. Enter admin in the username and password fields and skip the page offering to set the new password. Then click on Dashboards—>Manage in the left-side menu. This displays a pre-loaded Red Hat JBoss 7.3 Cloud Ready dashboard that shows the following metrics:

  • Application getList method execution time
  • JBoss Cloud Ready total requests
  • JVM Memory heap
  • Active data source connections

The display looks like Figure 4.

Grafana shows metrics collected by MicroProfile Metrics and filtered through Prometheus.
Figure 4: Dashboard shown by Grafana.

MicroProfile OpenTracing

Distributed tracing allows you to trace the flow of a request across service boundaries. This is particularly important in a microservices environment, where a request typically flows through multiple services. Here again, standards make it easy to enable the service, and I didn’t need to touch my code to obtain it. To make my application traceable, I only added the following dependency to the Maven pom.xml file:

<!-- Import the MicroProfile OpenTracing API, we use provided scope as the API is included in the server -->
<dependency>
    <groupId>org.eclipse.microprofile.opentracing</groupId>
    <artifactId>microprofile-opentracing-api</artifactId>
    <scope>provided</scope>
</dependency>

Visualizing distributed tracing with Jaeger

To add detailed information about the request, I integrated Jaeger into OpenShift. Jaeger is an open source, end-to-end distributed tracing tool that helps monitor and troubleshoot transactions in complex distributed systems. We installed and configured the tool in the previous steps using the Jaeger image from the Red Hat Container Catalog (registry.redhat.io/distributed-tracing/jaeger-all-in-one-rhel8). To use the service, click on the jaeger-all-in-one-rhel8 route in the OpenShift web console or get the value using the following command:

$ oc get route jaeger-all-in-one-rhel8 --template='{{ .spec.host }}'

Visting the URL opens the Jaeger web console. Select weather-app-eap-cloud-ready from the Operation drop-down list box, then GET:com.redhat.example.weather.WeatherService.getList. Click the Find Traces button to visualize the results of the trace. You should see something like Figure 5.

Jaeger console enables to visualize the performance of the application
Figure 5: Traces produced by OpenTracing and Jaeger.

MicroProfile OpenAPI

The MicroProfile OpenAPI specification provides a set of Java interfaces and programming models that allow Java developers to natively produce OpenAPI v3 documents from Java API for RESTful Web Services (JAX-RS) applications. To implement that specification and obtain documentation for my APIs, I didn’t touch my code. I only added the following dependency to the Maven pom.xml file:

<!-- Import the MicroProfile OpenAPI, we use provided scope as the API is included in the server -->
<dependency>
    <groupId>org.eclipse.microprofile.openapi</groupId>
    <artifactId>microprofile-openapi-api</artifactId>
    <scope>provided</scope>
</dependency>

Viewing the OpenAPI structure of your APIs

To use the service, click on the weather-app-eap-cloud-ready route in the OpenShift web console or get the value using the following command:

$ oc get route weather-app-eap-cloud-ready --template='{{ .spec.host }}'

Then, append the path openapi to download the YAML file that contains the OpenAPI structure of your APIs. You should see something like Figure 6. If you want to visualize the data in JSON format, append the path openapi?format=JSON to the weather-app-eap-cloud-ready route.

MicroProfile OpenAPI lets you view documentation of your APIs through a web interface.
Figure 6: Documentation of an API.

Visualize and test API data

I extended the specification with a user interface that can help you visualize and test the API's data. As usual, you need only to add the following dependency to the Maven pom.xml file:

<dependency>
    <groupId>org.microprofile-ext.openapi-ext</groupId>
    <artifactId>openapi-ui</artifactId>
    <version>1.1.4</version>
    <scope>runtime</scope>
</dependency>

The dependency is not directly related to MicroProfile specifications, so it’s not provided by the Maven bill of materials (BOM) and JBoss EAP XP. To test the service, append the path api/openapi-ui to the weather-app-eap-cloud-ready route. The resulting URL (shown in Figure 7) displays the Swagger UI. You can use it to test my APIs, which you will probably agree could use improvement.

The Swagger UI lets you navigate through the schema of your APIs.
Figure 7: An API viewed through the Swagger UI.

MicroProfile Config

MicroProfile Config defines a simple and flexible system for application configuration. To demonstrate how easy it is to use, I configured the OpenAPI UI described in the previous section using the values provided by the Config specification. As usual, I didn’t touch my source code, but only added the following dependency to the Maven pom.xml file:

<!-- Import the MicroProfile Config API, we use provided scope as the API is included in the server -->
<dependency>
    <groupId>org.eclipse.microprofile.config</groupId>
    <artifactId>microprofile-config-api</artifactId>
    <scope>provided</scope>
</dependency>

Then, under the src/main/resources/META-INF directory, I created a microprofile-config.properties file that contains the values I didn’t want to put statically into my source code:

openapi.ui.copyrightBy=Mauro Vocale
openapi.ui.title=Weather Application OpenAPI specifications
openapi.ui.swaggerHeaderVisibility=visible
openapi.ui.exploreFormVisibility=visible

You can see the first value, for example, in the footer section of the OpenAPI UI.

Conclusion to Part 3

In this article, we've enriched our "traditional" Java application with a set of features that prepare it for being used in distributed scenarios such as OpenShift, on-premises, or in a cloud environment. We only needed to make minimal updates to the source code. The final result was obtained through the declaration of the dependencies that are provided, at runtime, from JBoss EAP XP and OpenShift.

The final container image (JBoss EAP XP plus the application) is similar to the one that we built with Jakarta EE and JBoss EAP 7.3 in the previous article. You can get summary information about the application from the OpenShift web console, as shown in Figure 8.

Information about the container image for the application.
Figure 8: Weather app in the OpenShift web console.

Figure 9 shows that our memory utilization is higher than that of the traditional application, as I mentioned in the conclusion to Part 2.

Image size of the Jakarta EE and MicroProfile application.
Figure 9: Summary and size information after upgrading to use MicroProfile.

Figure 10 shows increased memory use, as well. We'll fix this issue in the next and final article in this series, where we will optimize the image's runtime environment.

The original version of our application uses 811.2 MB of memory whereareas the upgraded version uses 1,007 MB
Figure 10: Comparing memory footprints between the original and upgraded application.

Next up: Optimize the runtime environment.

Last updated: January 12, 2024