T processErrors(Throwable ex, ServerRequest request, ServerResponse response) {
+
+ if (ex.getCause() instanceof JsonException) {
+ LOGGER.log(Level.FINE, "Invalid JSON", ex);
+ JsonObject jsonErrorObject = JSON.createObjectBuilder()
+ .add("error", "Invalid JSON")
+ .build();
+ response.status(Http.Status.BAD_REQUEST_400).send(jsonErrorObject);
+ } else {
+ LOGGER.log(Level.FINE, "Internal error", ex);
+ JsonObject jsonErrorObject = JSON.createObjectBuilder()
+ .add("error", "Internal error")
+ .build();
+ response.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(jsonErrorObject);
+ }
+
+ return null;
+ }
+
+ private void updateGreetingFromJson(JsonObject jo, ServerResponse response) {
+ if (!jo.containsKey("greeting")) {
+ JsonObject jsonErrorObject = JSON.createObjectBuilder()
+ .add("error", "No greeting provided")
+ .build();
+ response.status(Http.Status.BAD_REQUEST_400)
+ .send(jsonErrorObject);
+ return;
+ }
+
+ greeting.set(jo.getString("greeting"));
+ response.status(Http.Status.NO_CONTENT_204).send();
+ }
+
+ /**
+ * Set the greeting to use in future messages.
+ * @param request the server request
+ * @param response the server response
+ */
+ private void updateGreetingHandler(ServerRequest request,
+ ServerResponse response) {
+ request.content().as(JsonObject.class)
+ .thenAccept(jo -> updateGreetingFromJson(jo, response))
+ .exceptionally(ex -> processErrors(ex, request, response));
+ }
+}
diff --git a/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/HttpStatusMetricService.java b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/HttpStatusMetricService.java
new file mode 100644
index 00000000000..364a08927d1
--- /dev/null
+++ b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/HttpStatusMetricService.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.se.httpstatuscount;
+
+import io.helidon.metrics.api.RegistryFactory;
+import io.helidon.webserver.Routing;
+import io.helidon.webserver.ServerRequest;
+import io.helidon.webserver.ServerResponse;
+import io.helidon.webserver.Service;
+
+import org.eclipse.microprofile.metrics.Counter;
+import org.eclipse.microprofile.metrics.Metadata;
+import org.eclipse.microprofile.metrics.MetricRegistry;
+import org.eclipse.microprofile.metrics.MetricType;
+import org.eclipse.microprofile.metrics.MetricUnits;
+import org.eclipse.microprofile.metrics.Tag;
+
+/**
+ * Helidon SE service to update a family of counters based on the HTTP status of each response. Add an instance of this service
+ * to the application's routing.
+ *
+ * The service uses one {@link org.eclipse.microprofile.metrics.Counter} for each HTTP status family (1xx, 2xx, etc.).
+ * All counters share the same name--{@value STATUS_COUNTER_NAME}--and each has the tag {@value STATUS_TAG_NAME} with
+ * value {@code 1xx}, {@code 2xx}, etc.
+ *
+ */
+public class HttpStatusMetricService implements Service {
+
+ static final String STATUS_COUNTER_NAME = "httpStatus";
+
+ static final String STATUS_TAG_NAME = "range";
+
+ private final Counter[] responseCounters = new Counter[6];
+
+ static HttpStatusMetricService create() {
+ return new HttpStatusMetricService();
+ }
+
+ private HttpStatusMetricService() {
+ MetricRegistry appRegistry = RegistryFactory.getInstance().getRegistry(MetricRegistry.Type.APPLICATION);
+ Metadata metadata = Metadata.builder()
+ .withName(STATUS_COUNTER_NAME)
+ .withDisplayName("HTTP response values")
+ .withDescription("Counts the number of HTTP responses in each status category (1xx, 2xx, etc.)")
+ .withType(MetricType.COUNTER)
+ .withUnit(MetricUnits.NONE)
+ .build();
+ // Declare the counters and keep references to them.
+ for (int i = 1; i < responseCounters.length; i++) {
+ responseCounters[i] = appRegistry.counter(metadata, new Tag(STATUS_TAG_NAME, i + "xx"));
+ }
+ }
+
+ @Override
+ public void update(Routing.Rules rules) {
+ rules.any(this::updateRange);
+ }
+
+ // Edited to adopt Ciaran's fix later in the thread.
+ private void updateRange(ServerRequest request, ServerResponse response) {
+ response.whenSent()
+ .thenAccept(this::logMetric);
+ request.next();
+ }
+
+ private void logMetric(ServerResponse response) {
+ int range = response.status().code() / 100;
+ if (range > 0 && range < responseCounters.length) {
+ responseCounters[range].inc();
+ }
+ }
+}
diff --git a/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/Main.java b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/Main.java
new file mode 100644
index 00000000000..2137d1a2bae
--- /dev/null
+++ b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/Main.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.se.httpstatuscount;
+
+import io.helidon.common.LogConfig;
+import io.helidon.common.reactive.Single;
+import io.helidon.config.Config;
+import io.helidon.health.HealthSupport;
+import io.helidon.health.checks.HealthChecks;
+import io.helidon.media.jsonp.JsonpSupport;
+import io.helidon.metrics.MetricsSupport;
+import io.helidon.webserver.Routing;
+import io.helidon.webserver.WebServer;
+
+/**
+ * The application main class.
+ */
+public final class Main {
+
+ /**
+ * Cannot be instantiated.
+ */
+ private Main() {
+ }
+
+ /**
+ * Application main entry point.
+ * @param args command line arguments.
+ */
+ public static void main(final String[] args) {
+ startServer();
+ }
+
+ /**
+ * Start the server.
+ * @return the created {@link WebServer} instance
+ */
+ static Single startServer() {
+ return startServer(createRouting(Config.create()));
+ }
+
+ static Single startServer(Routing.Builder routingBuilder) {
+
+ // load logging configuration
+ LogConfig.configureRuntime();
+
+ // By default this will pick up application.yaml from the classpath
+ Config config = Config.create();
+
+ WebServer server = WebServer.builder(routingBuilder)
+ .config(config.get("server"))
+ .addMediaSupport(JsonpSupport.create())
+ .build();
+
+ Single webserver = server.start();
+
+ // Try to start the server. If successful, print some info and arrange to
+ // print a message at shutdown. If unsuccessful, print the exception.
+ webserver.thenAccept(ws -> {
+ System.out.println("WEB server is up! http://localhost:" + ws.port() + "/greet");
+ ws.whenShutdown().thenRun(() -> System.out.println("WEB server is DOWN. Good bye!"));
+ })
+ .exceptionallyAccept(t -> {
+ System.err.println("Startup failed: " + t.getMessage());
+ t.printStackTrace(System.err);
+ });
+
+ return webserver;
+ }
+
+ /**
+ * Creates new {@link Routing}.
+ *
+ * @return routing configured with JSON support, a health check, and a service
+ * @param config configuration of this server
+ */
+ static Routing.Builder createRouting(Config config) {
+ SimpleGreetService simpleGreetService = new SimpleGreetService(config);
+ GreetService greetService = new GreetService(config);
+
+ HealthSupport health = HealthSupport.builder()
+ .addLiveness(HealthChecks.healthChecks()) // Adds a convenient set of checks
+ .build();
+
+ Routing.Builder builder = Routing.builder()
+ .register(MetricsSupport.create()) // Metrics at "/metrics"
+ .register(health) // Health at "/health"
+ .register(HttpStatusMetricService.create()) // no endpoint, just metrics updates
+ .register("/simple-greet", simpleGreetService)
+ .register("/greet", greetService);
+
+
+ return builder;
+ }
+}
diff --git a/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/SimpleGreetService.java b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/SimpleGreetService.java
new file mode 100644
index 00000000000..2d9508ab7e3
--- /dev/null
+++ b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/SimpleGreetService.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.se.httpstatuscount;
+
+import java.util.Collections;
+import java.util.logging.Logger;
+
+import io.helidon.config.Config;
+import io.helidon.metrics.api.RegistryFactory;
+import io.helidon.webserver.Routing;
+import io.helidon.webserver.ServerRequest;
+import io.helidon.webserver.ServerResponse;
+import io.helidon.webserver.Service;
+
+import jakarta.json.Json;
+import jakarta.json.JsonBuilderFactory;
+import jakarta.json.JsonObject;
+import org.eclipse.microprofile.metrics.Counter;
+import org.eclipse.microprofile.metrics.MetricRegistry;
+
+/**
+ * A simple service to greet you. Examples:
+ *
+ * Get default greeting message:
+ * curl -X GET http://localhost:8080/simple-greet
+ *
+ * The message is returned as a JSON object
+ */
+public class SimpleGreetService implements Service {
+
+ private static final Logger LOGGER = Logger.getLogger(SimpleGreetService.class.getName());
+ private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
+
+ private final MetricRegistry registry = RegistryFactory.getInstance()
+ .getRegistry(MetricRegistry.Type.APPLICATION);
+ private final Counter accessCtr = registry.counter("accessctr");
+
+ private final String greeting;
+
+ SimpleGreetService(Config config) {
+ greeting = config.get("app.greeting").asString().orElse("Ciao");
+ }
+
+
+ /**
+ * A service registers itself by updating the routing rules.
+ *
+ * @param rules the routing rules.
+ */
+ @Override
+ public void update(Routing.Rules rules) {
+ rules.get("/", this::getDefaultMessageHandler);
+ rules.get("/greet-count", this::countAccess, this::getDefaultMessageHandler);
+ }
+
+ /**
+ * Return a worldly greeting message.
+ *
+ * @param request the server request
+ * @param response the server response
+ */
+ private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) {
+ String msg = String.format("%s %s!", greeting, "World");
+ LOGGER.info("Greeting message is " + msg);
+ JsonObject returnObject = JSON.createObjectBuilder()
+ .add("message", msg)
+ .build();
+ response.send(returnObject);
+ }
+
+
+ private void countAccess(ServerRequest request, ServerResponse response) {
+ accessCtr.inc();
+ request.next();
+ }
+}
diff --git a/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/package-info.java b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/package-info.java
new file mode 100644
index 00000000000..d5b622ea310
--- /dev/null
+++ b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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.
+ */
+/**
+ * HTTP status count example.
+ */
+package io.helidon.examples.se.httpstatuscount;
diff --git a/examples/metrics/http-status-count-se/src/main/resources/application.yaml b/examples/metrics/http-status-count-se/src/main/resources/application.yaml
new file mode 100644
index 00000000000..e1e6249d8d4
--- /dev/null
+++ b/examples/metrics/http-status-count-se/src/main/resources/application.yaml
@@ -0,0 +1,24 @@
+#
+# Copyright (c) 2022 Oracle and/or its affiliates.
+#
+# Licensed 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.
+#
+
+server:
+ port: 8080
+ host: 0.0.0.0
+
+app:
+ greeting: "Hello"
+
+
diff --git a/examples/metrics/http-status-count-se/src/main/resources/logging.properties b/examples/metrics/http-status-count-se/src/main/resources/logging.properties
new file mode 100644
index 00000000000..d73eb5b6607
--- /dev/null
+++ b/examples/metrics/http-status-count-se/src/main/resources/logging.properties
@@ -0,0 +1,34 @@
+#
+# Copyright (c) 2022 Oracle and/or its affiliates.
+#
+# Licensed 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.
+#
+
+# Example Logging Configuration File
+# For more information see $JAVA_HOME/jre/lib/logging.properties
+
+# Send messages to the console
+handlers=io.helidon.common.HelidonConsoleHandler
+
+# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread
+java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n
+
+# Global logging level. Can be overridden by specific loggers
+.level=INFO
+
+# Component specific log levels
+#io.helidon.webserver.level=INFO
+#io.helidon.config.level=INFO
+#io.helidon.security.level=INFO
+#io.helidon.common.level=INFO
+#io.netty.level=INFO
diff --git a/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/MainTest.java b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/MainTest.java
new file mode 100644
index 00000000000..a8dfdf64d0f
--- /dev/null
+++ b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/MainTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.se.httpstatuscount;
+
+import java.util.concurrent.TimeUnit;
+import java.util.Collections;
+import jakarta.json.Json;
+import jakarta.json.JsonBuilderFactory;
+import jakarta.json.JsonObject;
+
+import io.helidon.media.jsonp.JsonpSupport;
+import io.helidon.webclient.WebClient;
+import io.helidon.webclient.WebClientResponse;
+import io.helidon.webserver.WebServer;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class MainTest {
+
+ private static final JsonBuilderFactory JSON_BUILDER = Json.createBuilderFactory(Collections.emptyMap());
+ private static final JsonObject TEST_JSON_OBJECT = JSON_BUILDER.createObjectBuilder()
+ .add("greeting", "Hola")
+ .build();
+
+ private static WebServer webServer;
+ private static WebClient webClient;
+
+ @BeforeAll
+ public static void startTheServer() {
+ webServer = Main.startServer().await();
+
+ webClient = WebClient.builder()
+ .baseUri("http://localhost:" + webServer.port())
+ .addMediaSupport(JsonpSupport.create())
+ .build();
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception {
+ if (webServer != null) {
+ webServer.shutdown()
+ .toCompletableFuture()
+ .get(10, TimeUnit.SECONDS);
+ }
+ }
+
+
+ @Test
+ public void testMicroprofileMetrics() {
+ String get = webClient.get()
+ .path("/simple-greet/greet-count")
+ .request(String.class)
+ .await();
+
+ assertThat(get, containsString("Hello World!"));
+
+ String openMetricsOutput = webClient.get()
+ .path("/metrics")
+ .request(String.class)
+ .await();
+
+ assertThat("Metrics output", openMetricsOutput, containsString("application_accessctr_total"));
+ }
+
+ @Test
+ public void testMetrics() throws Exception {
+ WebClientResponse response = webClient.get()
+ .path("/metrics")
+ .request()
+ .await();
+ assertThat(response.status().code(), is(200));
+ }
+
+ @Test
+ public void testHealth() throws Exception {
+ WebClientResponse response = webClient.get()
+ .path("health")
+ .request()
+ .await();
+ assertThat(response.status().code(), is(200));
+ }
+
+ @Test
+ public void testSimpleGreet() throws Exception {
+ JsonObject jsonObject = webClient.get()
+ .path("/simple-greet")
+ .request(JsonObject.class)
+ .await();
+ assertThat(jsonObject.getString("message"), is("Hello World!"));
+ }
+ @Test
+ public void testGreetings() {
+ JsonObject jsonObject;
+ WebClientResponse response;
+
+ jsonObject = webClient.get()
+ .path("/greet/Joe")
+ .request(JsonObject.class)
+ .await();
+ assertThat(jsonObject.getString("message"), is("Hello Joe!"));
+
+ response = webClient.put()
+ .path("/greet/greeting")
+ .submit(TEST_JSON_OBJECT)
+ .await();
+ assertThat(response.status().code(), is(204));
+
+ jsonObject = webClient.get()
+ .path("/greet/Joe")
+ .request(JsonObject.class)
+ .await();
+ assertThat(jsonObject.getString("message"), is("Hola Joe!"));
+ }
+}
diff --git a/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusService.java b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusService.java
new file mode 100644
index 00000000000..68816a3c0b6
--- /dev/null
+++ b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusService.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.se.httpstatuscount;
+
+import io.helidon.common.http.Http;
+import io.helidon.webserver.Routing;
+import io.helidon.webserver.ServerRequest;
+import io.helidon.webserver.ServerResponse;
+import io.helidon.webserver.Service;
+
+/**
+ * Test-only service that allows the client to specify what HTTP status the service should return in its response.
+ * This allows the client to know which status family counter should be updated.
+ */
+public class StatusService implements Service {
+
+ @Override
+ public void update(Routing.Rules rules) {
+ rules.get("/{status}", this::respondWithRequestedStatus);
+ }
+
+ private void respondWithRequestedStatus(ServerRequest request, ServerResponse response) {
+ String statusText = request.path().param("status");
+ int status;
+ String msg;
+ try {
+ status = Integer.parseInt(statusText);
+ msg = "Successful conversion";
+ } catch (NumberFormatException ex) {
+ status = Http.Status.INTERNAL_SERVER_ERROR_500.code();
+ msg = "Unsuccessful conversion";
+ }
+ response.status(status)
+ .send(msg);
+ }
+}
diff --git a/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusTest.java b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusTest.java
new file mode 100644
index 00000000000..5eee561e696
--- /dev/null
+++ b/examples/metrics/http-status-count-se/src/test/java/io/helidon/examples/se/httpstatuscount/StatusTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.se.httpstatuscount;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import io.helidon.common.http.Http;
+import io.helidon.common.http.MediaType;
+import io.helidon.config.Config;
+import io.helidon.media.jsonp.JsonpSupport;
+import io.helidon.metrics.api.RegistryFactory;
+import io.helidon.webclient.WebClient;
+import io.helidon.webclient.WebClientResponse;
+import io.helidon.webserver.Routing;
+import io.helidon.webserver.WebServer;
+
+import org.eclipse.microprofile.metrics.Counter;
+import org.eclipse.microprofile.metrics.MetricID;
+import org.eclipse.microprofile.metrics.MetricRegistry;
+import org.eclipse.microprofile.metrics.Tag;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class StatusTest {
+
+ private static WebServer webServer;
+ private static WebClient webClient;
+
+ private final Counter[] STATUS_COUNTERS = new Counter[6];
+
+ @BeforeAll
+ static void init() {
+ Routing.Builder routingBuilder = Main.createRouting(Config.create());
+ routingBuilder.register("/status", new StatusService());
+
+ webServer = Main.startServer(routingBuilder).await();
+
+ webClient = WebClient.builder()
+ .baseUri("http://localhost:" + webServer.port())
+ .addMediaSupport(JsonpSupport.create())
+ .build();
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception {
+ if (webServer != null) {
+ webServer.shutdown()
+ .toCompletableFuture()
+ .get(10, TimeUnit.SECONDS);
+ }
+ }
+
+ @BeforeEach
+ void findStatusMetrics() {
+ MetricRegistry metricRegistry = RegistryFactory.getInstance().getRegistry(MetricRegistry.Type.APPLICATION);
+ for (int i = 1; i < STATUS_COUNTERS.length; i++) {
+ STATUS_COUNTERS[i] = metricRegistry.counter(new MetricID(HttpStatusMetricService.STATUS_COUNTER_NAME,
+ new Tag(HttpStatusMetricService.STATUS_TAG_NAME, i + "xx")));
+ }
+ }
+
+ @Test
+ void checkStatusMetrics() throws ExecutionException, InterruptedException {
+ checkAfterStatus(171);
+ checkAfterStatus(200);
+ checkAfterStatus(201);
+ checkAfterStatus(204);
+ checkAfterStatus(301);
+ checkAfterStatus(401);
+ checkAfterStatus(404);
+ }
+
+ @Test
+ void checkStatusAfterGreet() throws ExecutionException, InterruptedException {
+ long[] before = new long[6];
+ for (int i = 1; i < 6; i++) {
+ before[i] = STATUS_COUNTERS[i].getCount();
+ }
+ WebClientResponse response = webClient.get()
+ .path("/greet")
+ .accept(MediaType.APPLICATION_JSON)
+ .request()
+ .get();
+ assertThat("Status of /greet", response.status().code(), is(Http.Status.OK_200.code()));
+ checkCounters(response.status().code(), before);
+ }
+
+ void checkAfterStatus(int status) throws ExecutionException, InterruptedException {
+ long[] before = new long[6];
+ for (int i = 1; i < 6; i++) {
+ before[i] = STATUS_COUNTERS[i].getCount();
+ }
+ WebClientResponse response = webClient.get()
+ .path("/status/" + status)
+ .accept(MediaType.APPLICATION_JSON)
+ .request()
+ .get();
+ assertThat("Response status", response.status().code(), is(status));
+ checkCounters(status, before);
+ }
+
+ private void checkCounters(int status, long[] before) {
+ int family = status / 100;
+ for (int i = 1; i < 6; i++) {
+ long expectedDiff = i == family ? 1 : 0;
+ assertThat("Diff in counter " + family + "xx", STATUS_COUNTERS[i].getCount() - before[i], is(expectedDiff));
+ }
+ }
+}
diff --git a/examples/metrics/http-status-count-se/src/test/resources/application.yaml b/examples/metrics/http-status-count-se/src/test/resources/application.yaml
new file mode 100644
index 00000000000..9684e5692c3
--- /dev/null
+++ b/examples/metrics/http-status-count-se/src/test/resources/application.yaml
@@ -0,0 +1,26 @@
+#
+# Copyright (c) 2022 Oracle and/or its affiliates.
+#
+# Licensed 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.
+#
+
+server:
+ port: 8080
+ host: 0.0.0.0
+
+app:
+ greeting: "Hello"
+
+security:
+ enabled: false
+
diff --git a/examples/metrics/pom.xml b/examples/metrics/pom.xml
index 87241d25482..e79d38d55c8 100644
--- a/examples/metrics/pom.xml
+++ b/examples/metrics/pom.xml
@@ -35,6 +35,7 @@
exemplar
kpi
filtering
+ http-status-count-se
diff --git a/examples/microprofile/http-status-count-mp/README.md b/examples/microprofile/http-status-count-mp/README.md
new file mode 100644
index 00000000000..0e574905de9
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/README.md
@@ -0,0 +1,78 @@
+# http-status-count-mp
+
+This Helidon MP project illustrates a filter which updates a family of counters based on the HTTP status returned in each response.
+
+The addition of the single filter class `HttpStatusMetricFilter` is the only difference from the Helidon MP QuickStart project.
+
+## Incorporating status metrics into your own application
+Use this example for inspiration in writing your own filter or just use the filter directly in your own application by copying and pasting the `HttpStatusMetricFilter` class into your application, adjusting the package declaration as needed. Helidon MP discovers and uses your filter automatically.
+
+## Build and run
+
+
+With JDK17+
+```bash
+mvn package
+java -jar target/http-status-count-mp.jar
+```
+
+## Exercise the application
+```bash
+curl -X GET http://localhost:8080/simple-greet
+```
+```listing
+{"message":"Hello World!"}
+```
+
+```bash
+curl -X GET http://localhost:8080/greet
+```
+```listing
+{"message":"Hello World!"}
+```
+```bash
+curl -X GET http://localhost:8080/greet/Joe
+```
+```listing
+{"message":"Hello Joe!"}
+```
+```bash
+curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting
+
+curl -X GET http://localhost:8080/greet/Jose
+```
+```listing
+{"message":"Hola Jose!"}
+```
+
+## Try metrics
+```bash
+# Prometheus Format
+curl -s -X GET http://localhost:8080/metrics/application
+```
+
+```listing
+...
+# TYPE application_httpStatus_total counter
+# HELP application_httpStatus_total Counts the number of HTTP responses in each status category (1xx, 2xx, etc.)
+application_httpStatus_total{range="1xx"} 0
+application_httpStatus_total{range="2xx"} 5
+application_httpStatus_total{range="3xx"} 0
+application_httpStatus_total{range="4xx"} 0
+application_httpStatus_total{range="5xx"} 0
+...
+```
+# JSON Format
+
+```bash
+curl -H "Accept: application/json" -X GET http://localhost:8080/metrics
+```
+```json
+{
+...
+ "httpStatus;range=1xx": 0,
+ "httpStatus;range=2xx": 5,
+ "httpStatus;range=3xx": 0,
+ "httpStatus;range=4xx": 0,
+ "httpStatus;range=5xx": 0,
+...
diff --git a/examples/microprofile/http-status-count-mp/app.yaml b/examples/microprofile/http-status-count-mp/app.yaml
new file mode 100644
index 00000000000..7f243250ec8
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/app.yaml
@@ -0,0 +1,32 @@
+#
+# Copyright (c) 2018, 2021 Oracle and/or its affiliates. All rights reserved.
+#
+# Licensed 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.
+#
+
+kind: Service
+apiVersion: v1
+metadata:
+ name: http-status-count-mp
+ labels:
+ app: http-status-count-mp
+spec:
+ type: NodePort
+ selector:
+ app: http-status-count-mp
+ ports:
+ - port: 8080
+ targetPort: 8080
+ name: http
+---
+
diff --git a/examples/microprofile/http-status-count-mp/pom.xml b/examples/microprofile/http-status-count-mp/pom.xml
new file mode 100644
index 00000000000..9b417d51b13
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/pom.xml
@@ -0,0 +1,136 @@
+
+
+
+
+ 4.0.0
+
+ io.helidon.applications
+ helidon-mp
+ 3.0.0-RC2
+
+
+ io.helidon.examples
+ http-status-count-mp
+ 1.0-SNAPSHOT
+
+ Helidon Examples Metrics HTTP Status Counters
+
+
+ 3.0.0-RC2
+
+
+
+
+ io.helidon.microprofile.bundles
+ helidon-microprofile
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ io.helidon.media
+ helidon-media-jackson
+
+
+ org.eclipse.microprofile.metrics
+ microprofile-metrics-api
+
+
+ io.helidon.microprofile.metrics
+ helidon-microprofile-metrics
+
+
+ io.helidon.microprofile.health
+ helidon-microprofile-health
+
+
+ org.jboss
+ jandex
+ runtime
+
+
+ jakarta.activation
+ jakarta.activation-api
+ runtime
+
+
+ io.helidon.microprofile.bundles
+ helidon-microprofile-core
+
+
+ org.glassfish.jersey.media
+ jersey-media-json-binding
+ runtime
+
+
+ io.helidon.webclient
+ helidon-webclient
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ io.helidon.microprofile.tests
+ helidon-microprofile-tests-junit5
+ test
+
+
+ org.hamcrest
+ hamcrest-all
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ copy-libs
+
+
+
+
+ io.helidon.build-tools
+ helidon-maven-plugin
+
+
+ third-party-license-report
+
+
+
+
+ org.jboss.jandex
+ jandex-maven-plugin
+
+
+ make-index
+
+
+
+
+
+
diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetResource.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetResource.java
new file mode 100644
index 00000000000..750b54aeecf
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetResource.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.mp.httpstatuscount;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
+import org.eclipse.microprofile.openapi.annotations.media.Content;
+import org.eclipse.microprofile.openapi.annotations.media.Schema;
+import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
+import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
+
+/**
+ * A simple JAX-RS resource to greet you. Examples:
+ *
+ * Get default greeting message:
+ * curl -X GET http://localhost:8080/greet
+ *
+ * Get greeting message for Joe:
+ * curl -X GET http://localhost:8080/greet/Joe
+ *
+ * Change greeting
+ * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting
+ *
+ * The message is returned as a JSON object.
+ */
+@Path("/greet")
+@RequestScoped
+public class GreetResource {
+
+ /**
+ * The greeting message provider.
+ */
+ private final GreetingProvider greetingProvider;
+
+ /**
+ * Using constructor injection to get a configuration property.
+ * By default this gets the value from META-INF/microprofile-config
+ *
+ * @param greetingConfig the configured greeting message
+ */
+ @Inject
+ public GreetResource(GreetingProvider greetingConfig) {
+ this.greetingProvider = greetingConfig;
+ }
+
+ /**
+ * Return a worldly greeting message.
+ *
+ * @return {@link Message}
+ */
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public Message getDefaultMessage() {
+ return createResponse("World");
+ }
+
+ /**
+ * Return a greeting message using the name that was provided.
+ *
+ * @param name the name to greet
+ * @return {@link Message}
+ */
+ @Path("/{name}")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public Message getMessage(@PathParam("name") String name) {
+ return createResponse(name);
+ }
+
+ /**
+ * Set the greeting to use in future messages.
+ *
+ * @param message the new greeting
+ * @return {@link Response}
+ */
+ @Path("/greeting")
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @RequestBody(name = "greeting",
+ required = true,
+ content = @Content(mediaType = "application/json",
+ schema = @Schema(type = SchemaType.OBJECT, requiredProperties = { "greeting" })))
+ @APIResponses({
+ @APIResponse(name = "normal", responseCode = "204", description = "Greeting updated"),
+ @APIResponse(name = "missing 'greeting'", responseCode = "400",
+ description = "JSON did not contain setting for 'greeting'")})
+ public Response updateGreeting(Message message) {
+
+ if (message.getGreeting() == null || message.getGreeting().isEmpty()) {
+ Message error = new Message();
+ error.setMessage("No greeting provided");
+ return Response.status(Response.Status.BAD_REQUEST).entity(error).build();
+ }
+
+ greetingProvider.setMessage(message.getGreeting());
+ return Response.status(Response.Status.NO_CONTENT).build();
+ }
+
+ private Message createResponse(String who) {
+ String msg = String.format("%s %s!", greetingProvider.getMessage(), who);
+
+ return new Message(msg);
+ }
+}
diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetingProvider.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetingProvider.java
new file mode 100644
index 00000000000..92594448af4
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/GreetingProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.mp.httpstatuscount;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+/**
+ * Provider for greeting message.
+ */
+@ApplicationScoped
+public class GreetingProvider {
+ private final AtomicReference message = new AtomicReference<>();
+
+ /**
+ * Create a new greeting provider, reading the message from configuration.
+ *
+ * @param message greeting to use
+ */
+ @Inject
+ public GreetingProvider(@ConfigProperty(name = "app.greeting") String message) {
+ this.message.set(message);
+ }
+
+ String getMessage() {
+ return message.get();
+ }
+
+ void setMessage(String message) {
+ this.message.set(message);
+ }
+}
diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/HttpStatusMetricFilter.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/HttpStatusMetricFilter.java
new file mode 100644
index 00000000000..7f7ab5b12e0
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/HttpStatusMetricFilter.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.mp.httpstatuscount;
+
+import java.io.IOException;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.ConstrainedTo;
+import jakarta.ws.rs.RuntimeType;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ContainerResponseFilter;
+import jakarta.ws.rs.ext.Provider;
+import org.eclipse.microprofile.metrics.Counter;
+import org.eclipse.microprofile.metrics.Metadata;
+import org.eclipse.microprofile.metrics.MetricRegistry;
+import org.eclipse.microprofile.metrics.MetricType;
+import org.eclipse.microprofile.metrics.MetricUnits;
+import org.eclipse.microprofile.metrics.Tag;
+
+/**
+ * REST service filter to update a family of counters based on the HTTP status of each response.
+ *
+ * The filter uses one {@link org.eclipse.microprofile.metrics.Counter} for each HTTP status family (1xx, 2xx, etc.).
+ * All counters share the same name--{@value STATUS_COUNTER_NAME}--and each has the tag {@value STATUS_TAG_NAME} with
+ * value {@code 1xx}, {@code 2xx}, etc.
+ *
+ */
+@ConstrainedTo(RuntimeType.SERVER)
+@Provider
+public class HttpStatusMetricFilter implements ContainerResponseFilter {
+
+ static final String STATUS_COUNTER_NAME = "httpStatus";
+ static final String STATUS_TAG_NAME = "range";
+
+ @Inject
+ private MetricRegistry metricRegistry;
+
+ private final Counter[] responseCounters = new Counter[6];
+
+ @PostConstruct
+ private void init() {
+ Metadata metadata = Metadata.builder()
+ .withName(STATUS_COUNTER_NAME)
+ .withDisplayName("HTTP response values")
+ .withDescription("Counts the number of HTTP responses in each status category (1xx, 2xx, etc.)")
+ .withType(MetricType.COUNTER)
+ .withUnit(MetricUnits.NONE)
+ .build();
+ // Declare the counters and keep references to them.
+ for (int i = 1; i < responseCounters.length; i++) {
+ responseCounters[i] = metricRegistry.counter(metadata, new Tag(STATUS_TAG_NAME, i + "xx"));
+ }
+ }
+
+ @Override
+ public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext)
+ throws IOException {
+ updateCountForStatus(containerResponseContext.getStatus());
+ }
+
+ private void updateCountForStatus(int statusCode) {
+ int range = statusCode / 100;
+ if (range > 0 && range < responseCounters.length) {
+ responseCounters[range].inc();
+ }
+ }
+}
diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/Message.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/Message.java
new file mode 100644
index 00000000000..c595a737df4
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/Message.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.mp.httpstatuscount;
+
+/**
+ * Greeting message.
+ */
+public class Message {
+
+ private String message;
+
+ private String greeting;
+
+ /**
+ * Creates a new instance.
+ */
+ public Message() {
+ }
+
+ /**
+ * Creates a new instance with a preset message.
+ *
+ * @param message initial message
+ */
+ public Message(String message) {
+ this.message = message;
+ }
+
+ /**
+ * Sets the message content.
+ *
+ * @param message the new message
+ */
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ /**
+ *
+ * @return the current message content
+ */
+ public String getMessage() {
+ return this.message;
+ }
+
+ /**
+ * Sets the greeting.
+ *
+ * @param greeting new greeting
+ */
+ public void setGreeting(String greeting) {
+ this.greeting = greeting;
+ }
+
+ /**
+ *
+ * @return the greeting
+ */
+ public String getGreeting() {
+ return this.greeting;
+ }
+}
diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/SimpleGreetResource.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/SimpleGreetResource.java
new file mode 100644
index 00000000000..9e8d299f9b2
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/SimpleGreetResource.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.mp.httpstatuscount;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.metrics.MetricUnits;
+import org.eclipse.microprofile.metrics.annotation.Counted;
+import org.eclipse.microprofile.metrics.annotation.Timed;
+
+/**
+ * A simple JAX-RS resource to greet you. Examples:
+ *
+ * Get default greeting message:
+ * curl -X GET http://localhost:8080/simple-greet
+ *
+ * The message is returned as a JSON object.
+ */
+@Path("/simple-greet")
+public class SimpleGreetResource {
+
+ private static final String PERSONALIZED_GETS_COUNTER_NAME = "personalizedGets";
+ private static final String PERSONALIZED_GETS_COUNTER_DESCRIPTION = "Counts personalized GET operations";
+ private static final String GETS_TIMER_NAME = "allGets";
+ private static final String GETS_TIMER_DESCRIPTION = "Tracks all GET operations";
+ private final String message;
+
+ /**
+ * Creates a new instance using the configured default greeting.
+ * @param message initial greeting message
+ */
+ @Inject
+ public SimpleGreetResource(@ConfigProperty(name = "app.greeting") String message) {
+ this.message = message;
+ }
+
+ /**
+ * Return a worldly greeting message.
+ *
+ * @return {@link Message}
+ */
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public Message getDefaultMessage() {
+ String msg = String.format("%s %s!", message, "World");
+ Message message = new Message();
+ message.setMessage(msg);
+ return message;
+ }
+
+ /**
+ * Returns a personalized greeting.
+ *
+ * @param name name with which to personalize the greeting
+ * @return personalized greeting message
+ */
+ @Path("/{name}")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Counted(name = PERSONALIZED_GETS_COUNTER_NAME,
+ absolute = true,
+ description = PERSONALIZED_GETS_COUNTER_DESCRIPTION)
+ @Timed(name = GETS_TIMER_NAME,
+ description = GETS_TIMER_DESCRIPTION,
+ unit = MetricUnits.SECONDS,
+ absolute = true)
+ public String getMessage(@PathParam("name") String name) {
+ return String.format("Hello %s", name);
+ }
+
+}
diff --git a/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/package-info.java b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/package-info.java
new file mode 100644
index 00000000000..8230c98afb1
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/java/io/helidon/examples/mp/httpstatuscount/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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.
+ *
+ */
+/**
+ * HTTP status count example.
+ */
+package io.helidon.examples.mp.httpstatuscount;
diff --git a/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/beans.xml b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/beans.xml
new file mode 100644
index 00000000000..5d94aab5a26
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/beans.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/microprofile-config.properties
new file mode 100644
index 00000000000..e3b22ac3a9b
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/microprofile-config.properties
@@ -0,0 +1,11 @@
+# Microprofile server properties
+server.port=8080
+server.host=0.0.0.0
+
+# Change the following to true to enable the optional MicroProfile Metrics REST.request metrics
+metrics.rest-request.enabled=false
+
+# Application properties. This is the default greeting
+app.greeting=Hello
+
+
diff --git a/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/native-image/reflect-config.json b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/native-image/reflect-config.json
new file mode 100644
index 00000000000..fe51488c706
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/resources/META-INF/native-image/reflect-config.json
@@ -0,0 +1 @@
+[]
diff --git a/examples/microprofile/http-status-count-mp/src/main/resources/application.yaml b/examples/microprofile/http-status-count-mp/src/main/resources/application.yaml
new file mode 100644
index 00000000000..3a5ab924b72
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/resources/application.yaml
@@ -0,0 +1,16 @@
+#
+# Copyright (c) 2022 Oracle and/or its affiliates.
+#
+# Licensed 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.
+#
+server.port: 8080
diff --git a/examples/microprofile/http-status-count-mp/src/main/resources/logging.properties b/examples/microprofile/http-status-count-mp/src/main/resources/logging.properties
new file mode 100644
index 00000000000..337435218ef
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/main/resources/logging.properties
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2022 Oracle and/or its affiliates.
+#
+# Licensed 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.
+#
+# Example Logging Configuration File
+# For more information see $JAVA_HOME/jre/lib/logging.properties
+
+# Send messages to the console
+handlers=io.helidon.common.HelidonConsoleHandler
+
+# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread
+java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n
+
+# Global logging level. Can be overridden by specific loggers
+.level=INFO
+
+# Quiet Weld
+org.jboss.level=WARNING
+
+# Component specific log levels
+#io.helidon.webserver.level=INFO
+#io.helidon.config.level=INFO
+#io.helidon.security.level=INFO
+#io.helidon.common.level=INFO
+#io.netty.level=INFO
diff --git a/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/MainTest.java b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/MainTest.java
new file mode 100644
index 00000000000..5e29e4e2849
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/MainTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.mp.httpstatuscount;
+
+import org.eclipse.microprofile.metrics.Counter;
+import org.eclipse.microprofile.metrics.MetricRegistry;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+
+import io.helidon.media.jackson.JacksonSupport;
+import io.helidon.microprofile.tests.junit5.HelidonTest;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.MethodOrderer.MethodName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@HelidonTest
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class MainTest {
+
+ @Inject
+ private MetricRegistry registry;
+
+ @Inject
+ private WebTarget target;
+
+
+ @Test
+ public void testMicroprofileMetrics() {
+ String message = target.path("simple-greet/Joe")
+ .request()
+ .get(String.class);
+
+ assertThat(message, is("Hello Joe"));
+ Counter counter = registry.counter("personalizedGets");
+ double before = counter.getCount();
+
+ message = target.path("simple-greet/Eric")
+ .request()
+ .get(String.class);
+
+ assertThat(message, is("Hello Eric"));
+ double after = counter.getCount();
+ assertEquals(1d, after - before, "Difference in personalized greeting counter between successive calls");
+ }
+
+ @Test
+ public void testMetrics() throws Exception {
+ Response response = target
+ .path("metrics")
+ .request()
+ .get();
+ assertThat(response.getStatus(), is(200));
+ }
+
+ @Test
+ public void testHealth() throws Exception {
+ Response response = target
+ .path("health")
+ .request()
+ .get();
+ assertThat(response.getStatus(), is(200));
+ }
+
+ @Test
+ public void testGreet() throws Exception {
+ Message message = target
+ .path("simple-greet")
+ .request()
+ .get(Message.class);
+ assertThat(message.getMessage(), is("Hello World!"));
+ }
+
+ @Test
+ public void testGreetings() throws Exception {
+ Message jsonMessage = target
+ .path("greet/Joe")
+ .request()
+ .get(Message.class);
+ assertThat(jsonMessage.getMessage(), is("Hello Joe!"));
+
+ try (Response r = target
+ .path("greet/greeting")
+ .request()
+ .put(Entity.entity("{\"greeting\" : \"Hola\"}", MediaType.APPLICATION_JSON))) {
+ assertThat(r.getStatus(), is(204));
+ }
+
+ jsonMessage = target
+ .path("greet/Jose")
+ .request()
+ .get(Message.class);
+ assertThat(jsonMessage.getMessage(), is("Hola Jose!"));
+ }
+
+}
diff --git a/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusResource.java b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusResource.java
new file mode 100644
index 00000000000..f900d6f3ce3
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusResource.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.mp.httpstatuscount;
+
+import io.helidon.common.http.Http;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+/**
+ * Test-only resource that allows the client to specify what HTTP status the service should return in its response.
+ * This allows the client to know which status family counter should be updated.
+ */
+@RequestScoped
+@Path("/status")
+public class StatusResource {
+
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path("/{status}")
+ public Response reportStatus(@PathParam("status") String statusText) {
+ int status;
+ String msg;
+ try {
+ status = Integer.parseInt(statusText);
+ msg = "Successful conversion";
+ } catch (NumberFormatException ex) {
+ status = Http.Status.INTERNAL_SERVER_ERROR_500.code();
+ msg = "Unsuccessful conversion";
+ }
+ return Response.status(status).entity(msg).build();
+ }
+}
diff --git a/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusTest.java b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusTest.java
new file mode 100644
index 00000000000..a124a1d3d1c
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/test/java/io/helidon/examples/mp/httpstatuscount/StatusTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.examples.mp.httpstatuscount;
+
+import io.helidon.common.http.Http;
+import io.helidon.microprofile.tests.junit5.AddBean;
+import io.helidon.microprofile.tests.junit5.HelidonTest;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.eclipse.microprofile.metrics.Counter;
+import org.eclipse.microprofile.metrics.MetricID;
+import org.eclipse.microprofile.metrics.MetricRegistry;
+import org.eclipse.microprofile.metrics.Tag;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@HelidonTest
+@AddBean(StatusResource.class)
+public class StatusTest {
+
+ @Inject
+ private WebTarget webTarget;
+
+ @Inject
+ private MetricRegistry metricRegistry;
+
+ private final Counter[] STATUS_COUNTERS = new Counter[6];
+
+ @BeforeEach
+ void findStatusMetrics() {
+ for (int i = 1; i < STATUS_COUNTERS.length; i++) {
+ STATUS_COUNTERS[i] = metricRegistry.counter(new MetricID(HttpStatusMetricFilter.STATUS_COUNTER_NAME,
+ new Tag(HttpStatusMetricFilter.STATUS_TAG_NAME, i + "xx")));
+ }
+ }
+
+ @Test
+ void checkStatusMetrics() {
+ checkAfterStatus(171);
+ checkAfterStatus(200);
+ checkAfterStatus(201);
+ checkAfterStatus(204);
+ checkAfterStatus(301);
+ checkAfterStatus(401);
+ checkAfterStatus(404);
+ }
+
+ @Test
+ void checkStatusAfterGreet() {
+ long[] before = new long[6];
+ for (int i = 1; i < 6; i++) {
+ before[i] = STATUS_COUNTERS[i].getCount();
+ }
+ Response response = webTarget.path("/greet")
+ .request(MediaType.APPLICATION_JSON)
+ .get();
+ assertThat("Status of /greet", response.getStatus(), is(Http.Status.OK_200.code()));
+ checkCounters(response.getStatus(), before);
+ }
+
+ void checkAfterStatus(int status) {
+ String path = "/status/" + status;
+ long[] before = new long[6];
+ for (int i = 1; i < 6; i++) {
+ before[i] = STATUS_COUNTERS[i].getCount();
+ }
+ Response response = webTarget.path(path)
+ .request(MediaType.TEXT_PLAIN_TYPE)
+ .get();
+ assertThat("Response status", response.getStatus(), is(status));
+ checkCounters(status, before);
+ }
+
+ private void checkCounters(int status, long[] before) {
+ int family = status / 100;
+ for (int i = 1; i < 6; i++) {
+ long expectedDiff = i == family ? 1 : 0;
+ assertThat("Diff in counter " + family + "xx", STATUS_COUNTERS[i].getCount() - before[i], is(expectedDiff));
+ }
+ }
+
+}
diff --git a/examples/microprofile/http-status-count-mp/src/test/resources/application.yaml b/examples/microprofile/http-status-count-mp/src/test/resources/application.yaml
new file mode 100644
index 00000000000..94f34514486
--- /dev/null
+++ b/examples/microprofile/http-status-count-mp/src/test/resources/application.yaml
@@ -0,0 +1,2 @@
+security:
+ enabled: false
\ No newline at end of file
diff --git a/examples/microprofile/pom.xml b/examples/microprofile/pom.xml
index 45c3a228a6c..e332ce84f81 100644
--- a/examples/microprofile/pom.xml
+++ b/examples/microprofile/pom.xml
@@ -47,5 +47,6 @@
tls
multiport
bean-validation
+ http-status-count-mp