diff --git a/examples/metrics/http-status-count-se/README.md b/examples/metrics/http-status-count-se/README.md new file mode 100644 index 00000000000..6eff1943ef3 --- /dev/null +++ b/examples/metrics/http-status-count-se/README.md @@ -0,0 +1,102 @@ +# http-status-count-se + +This Helidon SE project illustrates a service which updates a family of counters based on the HTTP status returned in each response. + +The main source in this example is identical to that in the Helidon SE QuickStart application except in these ways: +* The `HttpStatusMetricService` class creates and updates the status metrics. +* The `Main` class has a two small enhancements: + * The `createRouting` method instantiates `HttpStatusMetricService` and sets up routing for it. + * The `startServer` method has an additional variant to simplify a new unit test. + +## Incorporating status metrics into your own application +Use this example for inspiration in writing your own service or just use the `HttpStatusMetricService` directly in your own application. + +1. Copy and paste the `HttpStatusMetricService` class into your application, adjusting the package declaration as needed. +2. Register routing for an instance of `HttpStatusMetricService`, as shown here: + ```java + Routing.Builder builder = Routing.builder() + ... + .register(HttpStatusMetricService.create() + ... + ``` + +## Build and run + + +With JDK17+ +```bash +mvn package +java -jar target/http-status-count-se.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, +... +``` + +## Try health + +```bash +curl -s -X GET http://localhost:8080/health +``` +```listing +{"outcome":"UP",... + +``` diff --git a/examples/metrics/http-status-count-se/pom.xml b/examples/metrics/http-status-count-se/pom.xml new file mode 100644 index 00000000000..571ec5d6634 --- /dev/null +++ b/examples/metrics/http-status-count-se/pom.xml @@ -0,0 +1,103 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 3.0.0-SNAPSHOT + ../../../applications/se/pom.xml + + io.helidon.examples + http-status-count-se + 1.0-SNAPSHOT + + Helidon Examples Metrics HTTP Status Counters + + + io.helidon.examples.se.httpstatuscount.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.config + helidon-config-yaml + + + io.helidon.metrics + helidon-metrics + + + io.helidon.health + helidon-health + + + io.helidon.health + helidon-health-checks + + + io.helidon.media + helidon-media-jsonp + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webclient + helidon-webclient + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.helidon.build-tools + helidon-maven-plugin + + + third-party-license-report + + + + + + diff --git a/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/GreetService.java b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/GreetService.java new file mode 100644 index 00000000000..9e94b7ea7ec --- /dev/null +++ b/examples/metrics/http-status-count-se/src/main/java/io/helidon/examples/se/httpstatuscount/GreetService.java @@ -0,0 +1,150 @@ +/* + * 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.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.http.Http; +import io.helidon.config.Config; +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.JsonException; +import jakarta.json.JsonObject; + + +/** + * A simple service 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 + */ + +public class GreetService implements Service { + + /** + * The config value for the key {@code greeting}. + */ + private final AtomicReference greeting = new AtomicReference<>(); + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + private static final Logger LOGGER = Logger.getLogger(GreetService.class.getName()); + + GreetService(Config config) { + greeting.set(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) + .get("/{name}", this::getMessageHandler) + .put("/greeting", this::updateGreetingHandler); + } + + /** + * Return a worldly greeting message. + * @param request the server request + * @param response the server response + */ + private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) { + sendResponse(response, "World"); + } + + /** + * Return a greeting message using the name that was provided. + * @param request the server request + * @param response the server response + */ + private void getMessageHandler(ServerRequest request, ServerResponse response) { + String name = request.path().param("name"); + sendResponse(response, name); + } + + private void sendResponse(ServerResponse response, String name) { + String msg = String.format("%s %s!", greeting.get(), name); + + JsonObject returnObject = JSON.createObjectBuilder() + .add("message", msg) + .build(); + response.send(returnObject); + } + + private static 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