From 570d6505729297bc61c670128091dd4d69040baf Mon Sep 17 00:00:00 2001 From: josehenriqueventura Date: Thu, 20 Dec 2018 20:02:36 +0000 Subject: [PATCH] TOMEE-2335 - MicroProfile Health Example for custom HealthCheck --- examples/mp-custom-healthcheck/README.adoc | 146 ++++++++++++++++++ examples/mp-custom-healthcheck/pom.xml | 94 +++++++++++ .../java/org/superbiz/WeatherApiStatus.java | 52 +++++++ .../java/org/superbiz/WeatherEndpoint.java | 45 ++++++ .../java/org/superbiz/WeatherException.java | 27 ++++ .../java/org/superbiz/WeatherGateway.java | 63 ++++++++ .../superbiz/WeatherServiceHealthCheck.java | 48 ++++++ .../src/main/resources/beans.xml | 7 + .../org/superbiz/test/WeatherServiceTest.java | 124 +++++++++++++++ .../src/test/resources/arquillian.xml | 29 ++++ examples/pom.xml | 1 + 11 files changed, 636 insertions(+) create mode 100644 examples/mp-custom-healthcheck/README.adoc create mode 100644 examples/mp-custom-healthcheck/pom.xml create mode 100644 examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherApiStatus.java create mode 100644 examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherEndpoint.java create mode 100644 examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherException.java create mode 100644 examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherGateway.java create mode 100644 examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherServiceHealthCheck.java create mode 100644 examples/mp-custom-healthcheck/src/main/resources/beans.xml create mode 100644 examples/mp-custom-healthcheck/src/test/java/org/superbiz/test/WeatherServiceTest.java create mode 100644 examples/mp-custom-healthcheck/src/test/resources/arquillian.xml diff --git a/examples/mp-custom-healthcheck/README.adoc b/examples/mp-custom-healthcheck/README.adoc new file mode 100644 index 00000000000..f3e2e013451 --- /dev/null +++ b/examples/mp-custom-healthcheck/README.adoc @@ -0,0 +1,146 @@ +# MicroProfile Custom Health Check +This is an example of how to use MicroProfile Custom Health Check in TomEE. + +#### Health Feature +Health checks are used to probe the state of services and resources that an application might depend on or even to expose its +state, e.g. in a cluster environment, where an unhealthy node needs to be discarded (terminated, shutdown) and eventually +replaced by another healthy instance. + +By default, https://github.com/eclipse/microprofile-health[microprofile-health-api] provides a basic output of a node by + simply hitting the endpoint http://host:port/health. + +```json +{"checks":[],"outcome":"UP","status":"UP"} +``` + +To provide a customized output, Let’s say we have an application that uses a Weather API, and if the service becomes +unavailable, we should report the service as DOWN. + +To begin with a customized output, is needed to implement the interface https://github.com/eclipse/microprofile-health/blob/master/api/src/main/java/org/eclipse/microprofile/health/HealthCheck.java[HealthCheck], +make the class a managed bean by annotating it with `@ApplicationScoped` plus with `@Health` annotation to active the custom check. +See more here https://github.com/apache/geronimo-health/blob/master/geronimo-health/src/main/java/org/apache/geronimo/microprofile/impl/health/cdi/GeronimoHealthExtension.java[GeronimoHealthExtension.java] + +```java +@Health +@ApplicationScoped +public class WeatherServiceHealthCheck implements HealthCheck { + + @Inject WeatherGateway weatherGateway; + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("OpenWeatherMap"); + try { + WeatherApiStatus status = weatherGateway.getApiStatus(); + return responseBuilder.withData("weatherServiceApiUrl", status.getUrl()) + .withData("weatherServiceApiVersion", status.getVersion()) + .withData("weatherServiceMessage", status.getMessage()) + .up().build(); + } catch (WeatherException e) { + return responseBuilder.withData("weatherServiceErrorMessage", e.getMessage()).down().build(); + } + } +} +``` +In the example above, the health probe name is https://openweathermap.org/appid[OpenWeatherMap] (_illustrative only_) which provides a +subscription plan to access its services and if the limit of calls is exceeded the API becomes unavailable until it's renewed. + +### Examples + +##### Running the application + +``` + mvn clean install tomee:run +``` + +#### Example 1 + +When hitting /health endpoint, OpenWeatherMap tell us that our remaining calls are running out and we should take +an action before it gets unavailable. + +``` +curl http://localhost:8080/mp-custom-healthcheck/health +``` + +```json +{ + "checks":[ + { + "data":{ + "weatherServiceApiVersion":"2.5", + "weatherServiceMessage":"Your account will become unavailable soon due to limitation of your + subscription type. Remaining API calls are 1", + "weatherServiceApiUrl":"http://api.openweathermap.org/data/2.5/" + }, + "name":"OpenWeatherMap", + "state":"UP" + } + ], + "outcome":"UP", + "status":"UP" +} +``` + +#### Example 2 + +Weather API still working fine. + +``` +curl http://localhost:8080/mp-custom-healthcheck/weather/day/status +``` + +```text +Hi, today is a sunny day! +``` + +#### Example 3 + +When hitting one more time /health endpoint, OpenWeatherMap tell us that our account is temporary blocked and this +service is being reported as DOWN. + +``` +curl http://localhost:8080/mp-custom-healthcheck/health +``` + +```json +{ + "checks":[ + { + "data":{ + "weatherServiceErrorMessage":"Your account is temporary blocked due to exceeding of + requests limitation of your subscription type. Please choose the proper subscription + http://openweathermap.org/price" + }, + "name":"weatherservice", + "state":"DOWN" + } + ], + "outcome":"DOWN", + "status":"DOWN" +} +``` + +#### Example 4 + +Weather API has stopped. + +``` +curl http://localhost:8080/mp-custom-healthcheck/weather/day/status +``` + +```text +Weather Service is unavailable at moment, retry later. +``` + +##### Running the tests + +You can also try it out using the link:src/test/java/org/superbiz/rest/WeatherServiceTest.java[WeatherServiceTest.java] available in the project. + + mvn clean test + +``` +[INFO] Results: +[INFO] +[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: +``` + diff --git a/examples/mp-custom-healthcheck/pom.xml b/examples/mp-custom-healthcheck/pom.xml new file mode 100644 index 00000000000..dfc4d077218 --- /dev/null +++ b/examples/mp-custom-healthcheck/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + org.superbiz + mp-custom-healthcheck + 8.0.0-SNAPSHOT + war + OpenEJB :: Examples :: Custom HealthCheck + + + 1.4.0.Final + 1.0 + 3.7.0 + 3.1.0 + ${project.version} + 8.0 + 4.12 + 1.8 + 1.8 + + + + + org.apache.tomee + javaee-api + ${javaee-api.version} + provided + + + + org.eclipse.microprofile.health + microprofile-health-api + ${microprofile-health-api.version} + provided + + + + junit + junit + ${junit.version} + test + + + org.apache.tomee + openejb-cxf-rs + ${tomee.version} + test + + + org.jboss.arquillian.junit + arquillian-junit-container + ${arquillian-junit-container.version} + test + + + org.apache.tomee + arquillian-tomee-remote + ${tomee.version} + test + + + org.apache.tomee + apache-tomee + ${tomee.version} + zip + microprofile + test + + + + + + + org.apache.tomee.maven + tomee-maven-plugin + ${project.version} + + microprofile + ${artifactId} + + + + org.apache.maven.plugins + maven-war-plugin + ${maven-war-plugin.version} + + false + + + + + diff --git a/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherApiStatus.java b/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherApiStatus.java new file mode 100644 index 00000000000..6ae5c193fea --- /dev/null +++ b/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherApiStatus.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.superbiz; + +/** + * WeatherApiStatus is used to provide details received of a + * call which tell the status of the current usage of the API. + */ +public class WeatherApiStatus { + + private String url; + private String version; + private String message; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} diff --git a/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherEndpoint.java b/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherEndpoint.java new file mode 100644 index 00000000000..b3ee0718efe --- /dev/null +++ b/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherEndpoint.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.superbiz; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +/** + * Weather Endpoint which provides: + * GET /weather/day/status + */ +@Path("/weather") +@RequestScoped +public class WeatherEndpoint { + + @Inject + private WeatherGateway weatherService; + + @GET + @Path("/day/status") + public Response dayStatus() { + try { + return Response.ok().entity(weatherService.statusOfDay()).build(); + } catch (WeatherException e) { + return Response.ok().entity(e.getMessage()).build(); + } + } +} diff --git a/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherException.java b/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherException.java new file mode 100644 index 00000000000..b08e66a2c8f --- /dev/null +++ b/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherException.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.superbiz; + +/** + * Used to represent Exceptions with regards to Weather Service. + */ +public class WeatherException extends Exception { + + public WeatherException(String message) { + super(message); + } +} diff --git a/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherGateway.java b/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherGateway.java new file mode 100644 index 00000000000..56e0339cddb --- /dev/null +++ b/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherGateway.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.superbiz; + +import java.util.concurrent.atomic.AtomicInteger; +import javax.enterprise.context.RequestScoped; + +@RequestScoped +public class WeatherGateway { + + private static final AtomicInteger API_COUNTER_CALLS = new AtomicInteger(); + + /** + * This method simulates only one successful call to an + * external service, after the first attempt, exceptions will be thrown. + * @return A Weather message + * @throws WeatherException After the first attempt. + */ + public String statusOfDay() throws WeatherException { + if(API_COUNTER_CALLS.addAndGet(1 ) <= 1) { + return "Hi, today is a sunny day!"; + } + throw new WeatherException("Weather Service is unavailable"); + } + + /** + * This method simulates successful calls to an external service while {@see statusOfDay} + * is not executed for the first time. Once {@see statusOfDay} is executed, + * the variable {@see API_COUNTER_CALLS} will be incremented and this method start behaving + * as if the external service unavailable. + * + * incrementing {@see API_COUNTER_CALLS} + * @return Status of API + * @throws WeatherException After the first execution of the {@see statusOfDay} method. + */ + public WeatherApiStatus getApiStatus() throws WeatherException { + if(API_COUNTER_CALLS.get() == 0) { + WeatherApiStatus weatherApiStatus = new WeatherApiStatus(); + weatherApiStatus.setUrl("http://api.openweathermap.org/data/2.5/"); + weatherApiStatus.setVersion("2.5"); + weatherApiStatus.setMessage("Your account will become unavailable soon due to limitation of " + + "your subscription type. Remaining API calls are 1"); + return weatherApiStatus; + }else{ + throw new WeatherException("Your account is temporary blocked due to exceeding of requests limitation of " + + "your subscription type. Please choose the proper subscription http://openweathermap.org/price"); + } + } +} diff --git a/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherServiceHealthCheck.java b/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherServiceHealthCheck.java new file mode 100644 index 00000000000..c48e91ba1a6 --- /dev/null +++ b/examples/mp-custom-healthcheck/src/main/java/org/superbiz/WeatherServiceHealthCheck.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.superbiz; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import org.eclipse.microprofile.health.Health; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; + +/** + * Custom Health Check for OpenWeatherMap API Service. + */ +@Health +@ApplicationScoped +public class WeatherServiceHealthCheck implements HealthCheck { + + @Inject WeatherGateway weatherGateway; + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("OpenWeatherMap"); + try { + WeatherApiStatus status = weatherGateway.getApiStatus(); + return responseBuilder.withData("weatherServiceApiUrl", status.getUrl()) + .withData("weatherServiceApiVersion", status.getVersion()) + .withData("weatherServiceMessage", status.getMessage()) + .up().build(); + } catch (WeatherException e) { + return responseBuilder.withData("weatherServiceErrorMessage", e.getMessage()).down().build(); + } + } +} diff --git a/examples/mp-custom-healthcheck/src/main/resources/beans.xml b/examples/mp-custom-healthcheck/src/main/resources/beans.xml new file mode 100644 index 00000000000..d942d7ab8df --- /dev/null +++ b/examples/mp-custom-healthcheck/src/main/resources/beans.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/examples/mp-custom-healthcheck/src/test/java/org/superbiz/test/WeatherServiceTest.java b/examples/mp-custom-healthcheck/src/test/java/org/superbiz/test/WeatherServiceTest.java new file mode 100644 index 00000000000..ff20905f070 --- /dev/null +++ b/examples/mp-custom-healthcheck/src/test/java/org/superbiz/test/WeatherServiceTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.superbiz.test; + +import java.io.StringReader; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.junit.InSequence; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.URL; +import org.superbiz.WeatherEndpoint; + +import static org.junit.Assert.assertEquals; + +@RunWith(Arquillian.class) +public class WeatherServiceTest { + + @ArquillianResource + private URL base; + + private Client client; + + @Deployment(testable = false) + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class, "test.war") + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml") + .addPackage(WeatherEndpoint.class.getPackage()); + } + + @Before + public void before() { + this.client = ClientBuilder.newClient(); + } + + @Test + @InSequence(1) + public void testHealthCheckUpService() { + WebTarget webTarget = this.client.target(this.base.toExternalForm()); + String json = webTarget.path("/health").request(MediaType.APPLICATION_JSON).get().readEntity(String.class); + + JsonArray checks = this.readJson(json).getJsonArray("checks"); + JsonObject data = checks.getJsonObject(0).getJsonObject("data"); + + assertEquals("http://api.openweathermap.org/data/2.5/", data.getString("weatherServiceApiUrl")); + assertEquals("2.5", data.getString("weatherServiceApiVersion")); + assertEquals("Your account will become unavailable soon due to limitation of " + + "your subscription type. Remaining API calls are 1", data.getString("weatherServiceMessage")); + + assertEquals("OpenWeatherMap", checks.getJsonObject(0).getString("name")); + assertEquals("UP", checks.getJsonObject(0).getString("state")); + } + + @Test + @InSequence(2) + public void testStatusOfDay() { + WebTarget webTarget = this.client.target(this.base.toExternalForm()); + Response response = webTarget.path("/weather/day/status").request().get(); + assertEquals("Hi, today is a sunny day!", response.readEntity(String.class)); + } + + @Test + @InSequence(3) + public void testHealthCheckDownService() { + WebTarget webTarget = this.client.target(this.base.toExternalForm()); + String json = webTarget.path("/health").request(MediaType.APPLICATION_JSON).get().readEntity(String.class); + + JsonArray checks = this.readJson(json).getJsonArray("checks"); + JsonObject data = checks.getJsonObject(0).getJsonObject("data"); + + assertEquals("Your account is temporary blocked due to exceeding of requests limitation of " + + "your subscription type. Please choose the proper subscription http://openweathermap.org/price", + data.getString("weatherServiceErrorMessage")); + + assertEquals("OpenWeatherMap", checks.getJsonObject(0).getString("name")); + assertEquals("DOWN", checks.getJsonObject(0).getString("state")); + } + + @Test + @InSequence(4) + public void testStatusOfDayErrorMessage() { + WebTarget webTarget = this.client.target(this.base.toExternalForm()); + Response response = webTarget.path("/weather/day/status").request().get(); + assertEquals("Weather Service is unavailable", response.readEntity(String.class)); + } + + private JsonObject readJson(String json){ + return Json.createReader(new StringReader(json)).readObject(); + } + + @After + public void after() { + this.client.close(); + } +} diff --git a/examples/mp-custom-healthcheck/src/test/resources/arquillian.xml b/examples/mp-custom-healthcheck/src/test/resources/arquillian.xml new file mode 100644 index 00000000000..7639e7be76a --- /dev/null +++ b/examples/mp-custom-healthcheck/src/test/resources/arquillian.xml @@ -0,0 +1,29 @@ + + + + + + + -1 + -1 + microprofile + target/apache-tomee-remote + target/arquillian-test-working-dir + + + \ No newline at end of file diff --git a/examples/pom.xml b/examples/pom.xml index 0007646384b..35e83f7183a 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -179,6 +179,7 @@ BROKEN, see TOMEE-2140 websocket-tls-basic-auth concurrency-utils mvc-cxf + mp-custom-healthcheck