This is an automated email from the ASF dual-hosted git repository. jamesnetherton pushed a commit to branch camel-quarkus-main in repository https://gitbox.apache.org/repos/asf/camel-quarkus-examples.git
commit fc9a8c459d2243ca77c635f8ddb59b5c935f77f6 Author: Lukas Lowinger <llowi...@redhat.com> AuthorDate: Thu Jun 29 13:31:27 2023 +0200 Add Micrometer features to Observability example --- observability/README.adoc | 74 +++++++++++++++++++++- observability/pom.xml | 4 ++ .../main/java/org/acme/observability/Routes.java | 19 ++++++ .../java/org/acme/observability/TimerRoute.java | 1 + .../health/camel/CustomLivenessCheck.java | 4 +- .../TimerCounter.java} | 16 +++-- .../src/main/resources/application.properties | 22 +++---- .../org/acme/observability/ObservabilityIT.java | 6 ++ .../org/acme/observability/ObservabilityTest.java | 33 +++++----- 9 files changed, 138 insertions(+), 41 deletions(-) diff --git a/observability/README.adoc b/observability/README.adoc index 48bbb77..c403256 100644 --- a/observability/README.adoc +++ b/observability/README.adoc @@ -19,18 +19,88 @@ workspace. Any modifications in your project will automatically take effect in t TIP: Please refer to the Development mode section of https://camel.apache.org/camel-quarkus/latest/first-steps.html#_development_mode[Camel Quarkus User guide] for more details. +=== How to enable metrics +To enable observability features in Camel Quarkus, we need to add some additional dependencies to the project's pom.xml file. +The most important one (see link:pom.xml#L97-L100[pom.xml]): + +[source, xml] +---- +<dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-micrometer</artifactId> +</dependency> +---- + +After adding this dependency, you can benefit from both https://camel.apache.org/components/next/micrometer-component.html[Camel Micrometer] and https://quarkus.io/guides/micrometer[Quarkus Micrometer] worlds. +We are able to use multiple ways to achieve create meters for our custom metrics. + +First of them is using Camel micrometer component (see link:src/main/java/org/acme/observability/Routes.java[Routes.java]): + +[source, java] +---- +.to("micrometer:counter:org.acme.observability.greeting-provider?tags=type=events,purpose=example") +---- + +which will count each call to `platform-http:/greeting-provider` endpoint. + +Second approach is to benefit from auto-injected `MeterRegistry` (see link:src/main/java/org/acme/observability/Routes.java#L28[injection]) and use it directly (see link:src/main/java/org/acme/observability/Routes.java#L36[registry call]): + +[source, java] +---- +registry.counter("org.acme.observability.greeting", "type", "events", "purpose", "example").increment(); +---- + +which will count each call to `from("platform-http:/greeting")` endpoint. + +Finally last approach is to use Micrometer annotations (see https://quarkus.io/guides/micrometer#does-micrometer-support-annotations[which] are supported by Quarkus) by defining bean link:src/main/java/org/acme/observability/micrometer/TimerCounter.java[TimerCounter.java] as follows: + +[source, java] +---- +@ApplicationScoped +@Named("timerCounter") +public class TimerCounter { + + @Counted(value = "org.acme.observability.timer-counter", extraTags = { "purpose", "example" }) + public void count() { + } +} +---- + +and invoking it from Camel via (see link:src/main/java/org/acme/observability/TimerRoute.java[TimerRoute.java]): + +[source, java] +---- +.bean("timerCounter", "count") +---- +It will count each time the timer is fired. + +How to explore our custom metrics will be shown in the next chapter. === Metrics endpoint -Metrics are exposed on an HTTP endpoint at `/q/metrics`. You can also browse application specific metrics from the `/q/metrics/application` endpoint. +Metrics are exposed on an HTTP endpoint at `/q/metrics` on port `9000`. + +NOTE: Note we are using different port (9000) for the management endpoint then our application (8080) is listening on. +This is caused by using link:src/main/resources/application.properties#L22[`quarkus.management.enabled = true`] (see https://quarkus.io/guides/management-interface-reference for more information). To view all Camel metrics do: [source,shell] ---- -$ curl localhost:8080/q/metrics/application +$ curl localhost:9000/q/metrics +---- + +To view only our previously created metrics, use: + +[source,shell] +---- +$ curl -s localhost:9000/q/metrics | grep -i 'purpose="example"' ---- +and you should see 3 lines of different metrics (with the same value, as they are all triggered by the timer). + +NOTE: Maybe you've noticed the Prometheus output format. If you would rather use JSON format, please follow https://quarkus.io/guides/micrometer#management-interface. + === Health endpoint Camel provides some out of the box liveness and readiness checks. To see this working, interrogate the `/q/health/live` and `/q/health/ready` endpoints: diff --git a/observability/pom.xml b/observability/pom.xml index baeba7d..d52f6f2 100644 --- a/observability/pom.xml +++ b/observability/pom.xml @@ -82,6 +82,10 @@ <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-platform-http</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-bean</artifactId> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-http</artifactId> diff --git a/observability/src/main/java/org/acme/observability/Routes.java b/observability/src/main/java/org/acme/observability/Routes.java index 27bf12d..86730d7 100644 --- a/observability/src/main/java/org/acme/observability/Routes.java +++ b/observability/src/main/java/org/acme/observability/Routes.java @@ -16,19 +16,38 @@ */ package org.acme.observability; +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.Exchange; import org.apache.camel.builder.RouteBuilder; +@ApplicationScoped public class Routes extends RouteBuilder { + // Quarkus will inject this automatically for us + private final MeterRegistry registry; + + public Routes(MeterRegistry registry) { + this.registry = registry; + } + + private void countGreeting(Exchange exchange) { + // This is our custom metric: just counting how many times the method is called + registry.counter("org.acme.observability.greeting", "type", "events", "purpose", "example").increment(); + } + @Override public void configure() throws Exception { from("platform-http:/greeting") .removeHeaders("*") + .process(this::countGreeting) .to("http://localhost:{{greeting-provider-app.service.port}}/greeting-provider"); from("platform-http:/greeting-provider") // Random delay to simulate latency + .to("micrometer:counter:org.acme.observability.greeting-provider?tags=type=events,purpose=example") .delay(simple("${random(1000, 5000)}")) .setBody(constant("Hello From Camel Quarkus!")); } + } diff --git a/observability/src/main/java/org/acme/observability/TimerRoute.java b/observability/src/main/java/org/acme/observability/TimerRoute.java index 0b8fd0f..2d237bc 100644 --- a/observability/src/main/java/org/acme/observability/TimerRoute.java +++ b/observability/src/main/java/org/acme/observability/TimerRoute.java @@ -23,6 +23,7 @@ public class TimerRoute extends RouteBuilder { @Override public void configure() throws Exception { from("timer:greeting?period=10000") + .bean("timerCounter", "count") .to("http://{{greeting-app.service.host}}:{{greeting-app.service.port}}/greeting"); } } diff --git a/observability/src/main/java/org/acme/observability/health/camel/CustomLivenessCheck.java b/observability/src/main/java/org/acme/observability/health/camel/CustomLivenessCheck.java index 17e5ca9..6294ccd 100644 --- a/observability/src/main/java/org/acme/observability/health/camel/CustomLivenessCheck.java +++ b/observability/src/main/java/org/acme/observability/health/camel/CustomLivenessCheck.java @@ -39,8 +39,8 @@ public class CustomLivenessCheck extends AbstractHealthCheck { protected void doCall(HealthCheckResultBuilder builder, Map<String, Object> options) { int hits = hitCount.incrementAndGet(); - // Flag the check as DOWN on every 5th invocation, else it is UP - if (hits % 5 == 0) { + // Flag the check as DOWN on every 5th invocation (but not on Kubernetes), else it is UP + if (hits % 5 == 0 && System.getenv("KUBERNETES_NAMESPACE") == null) { builder.down(); } else { builder.up(); diff --git a/observability/src/main/java/org/acme/observability/TimerRoute.java b/observability/src/main/java/org/acme/observability/micrometer/TimerCounter.java similarity index 68% copy from observability/src/main/java/org/acme/observability/TimerRoute.java copy to observability/src/main/java/org/acme/observability/micrometer/TimerCounter.java index 0b8fd0f..54ce6ad 100644 --- a/observability/src/main/java/org/acme/observability/TimerRoute.java +++ b/observability/src/main/java/org/acme/observability/micrometer/TimerCounter.java @@ -14,15 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.acme.observability; +package org.acme.observability.micrometer; -import org.apache.camel.builder.RouteBuilder; +import io.micrometer.core.annotation.Counted; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Named; -public class TimerRoute extends RouteBuilder { +@ApplicationScoped +@Named("timerCounter") +public class TimerCounter { - @Override - public void configure() throws Exception { - from("timer:greeting?period=10000") - .to("http://{{greeting-app.service.host}}:{{greeting-app.service.port}}/greeting"); + @Counted(value = "org.acme.observability.timer-counter", extraTags = { "purpose", "example" }) + public void count() { } } diff --git a/observability/src/main/resources/application.properties b/observability/src/main/resources/application.properties index a0956de..1007fa2 100644 --- a/observability/src/main/resources/application.properties +++ b/observability/src/main/resources/application.properties @@ -19,25 +19,23 @@ # Quarkus # quarkus.banner.enabled = false +quarkus.management.enabled = true # Identifier for the origin of spans created by the application -quarkus.application.name=camel-quarkus-observability +quarkus.application.name = camel-quarkus-observability # For OTLP -quarkus.otel.exporter.otlp.traces.endpoint=http://${TELEMETRY_COLLECTOR_COLLECTOR_SERVICE_HOST:localhost}:4317 +quarkus.otel.exporter.otlp.traces.endpoint = http://${TELEMETRY_COLLECTOR_COLLECTOR_SERVICE_HOST:localhost}:4317 # For Jaeger -# quarkus.otel.exporter.jaeger.traces.endpoint=http://${MY_JAEGER_COLLECTOR_SERVICE_HOST:localhost}:14250 - -# Allow metrics to be exported as JSON. Not strictly required and is disabled by default -quarkus.micrometer.export.json.enabled = true +# quarkus.otel.exporter.jaeger.traces.endpoint = http://${MY_JAEGER_COLLECTOR_SERVICE_HOST:localhost}:14250 # # Camel # camel.context.name = camel-quarkus-observability -greeting-app.service.host=${CAMEL_QUARKUS_OBSERVABILITY_SERVICE_HOST:localhost} -greeting-app.service.port=${CAMEL_QUARKUS_OBSERVABILITY_SERVICE_PORT_HTTP:${quarkus.http.port}} -%test.greeting-app.service.port=${quarkus.http.test-port} -greeting-provider-app.service.host=localhost -greeting-provider-app.service.port=${quarkus.http.port} -%test.greeting-provider-app.service.port=${quarkus.http.test-port} +greeting-app.service.host = ${CAMEL_QUARKUS_OBSERVABILITY_SERVICE_HOST:localhost} +greeting-app.service.port = ${CAMEL_QUARKUS_OBSERVABILITY_SERVICE_PORT_HTTP:${quarkus.http.port}} +%test.greeting-app.service.port = ${quarkus.http.test-port} +greeting-provider-app.service.host = localhost +greeting-provider-app.service.port = ${quarkus.http.port} +%test.greeting-provider-app.service.port = ${quarkus.http.test-port} diff --git a/observability/src/test/java/org/acme/observability/ObservabilityIT.java b/observability/src/test/java/org/acme/observability/ObservabilityIT.java index ea6c2b4..e69623e 100644 --- a/observability/src/test/java/org/acme/observability/ObservabilityIT.java +++ b/observability/src/test/java/org/acme/observability/ObservabilityIT.java @@ -20,4 +20,10 @@ import io.quarkus.test.junit.QuarkusIntegrationTest; @QuarkusIntegrationTest public class ObservabilityIT extends ObservabilityTest { + + // Is run in prod mode + @Override + protected String getManagementPrefix() { + return "http://localhost:9000"; + } } diff --git a/observability/src/test/java/org/acme/observability/ObservabilityTest.java b/observability/src/test/java/org/acme/observability/ObservabilityTest.java index d207f0c..8db44ea 100644 --- a/observability/src/test/java/org/acme/observability/ObservabilityTest.java +++ b/observability/src/test/java/org/acme/observability/ObservabilityTest.java @@ -16,20 +16,24 @@ */ package org.acme.observability; +import java.util.Arrays; + import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import io.restassured.path.json.JsonPath; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; -import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; @QuarkusTest public class ObservabilityTest { + // Management interface is listening on 9001 + protected String getManagementPrefix() { + return "http://localhost:9001"; + } + @Test public void greeting() { RestAssured.get("/greeting") @@ -40,35 +44,28 @@ public class ObservabilityTest { @Test public void metrics() { // Verify Camel metrics are available - JsonPath path = given() - .when().accept(ContentType.JSON) - .get("/q/metrics") + String prometheusMetrics = RestAssured + .get(getManagementPrefix() + "/q/metrics") .then() .statusCode(200) .extract() - .body() - .jsonPath(); - - long camelMetricCount = path.getMap("$.") - .keySet() - .stream() - .filter(key -> key.toString().toLowerCase().startsWith("camel")) - .count(); + .body().asString(); - assertTrue(camelMetricCount > 0); + assertEquals(3, + Arrays.stream(prometheusMetrics.split("\n")).filter(line -> line.contains("purpose=\"example\"")).count()); } @Test public void health() { // Verify liveness - RestAssured.get("/q/health/live") + RestAssured.get(getManagementPrefix() + "/q/health/live") .then() .statusCode(200) .body("status", is("UP"), "checks.findAll { it.name == 'custom-liveness-check' }.status", Matchers.contains("UP")); // Verify readiness - RestAssured.get("/q/health/ready") + RestAssured.get(getManagementPrefix() + "/q/health/ready") .then() .statusCode(200) .body("status", is("UP"),