From 923672b8e9b753b9a0a4d1b1785c9867c3a3e950 Mon Sep 17 00:00:00 2001 From: Lukas Lowinger Date: Thu, 29 Jun 2023 13:31:27 +0200 Subject: [PATCH] Add Micrometer features to Observability example --- observability/README.adoc | 74 ++++++++++++++++++- observability/pom.xml | 4 + .../java/org/acme/observability/Routes.java | 19 +++++ .../org/acme/observability/TimerRoute.java | 1 + .../health/camel/CustomLivenessCheck.java | 4 +- .../micrometer/TimerCounter.java | 30 ++++++++ .../src/main/resources/application.properties | 22 +++--- .../acme/observability/ObservabilityIT.java | 6 ++ .../acme/observability/ObservabilityTest.java | 33 ++++----- 9 files changed, 159 insertions(+), 34 deletions(-) create mode 100644 observability/src/main/java/org/acme/observability/micrometer/TimerCounter.java diff --git a/observability/README.adoc b/observability/README.adoc index 48bbb77a..c4032560 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] +---- + + org.apache.camel.quarkus + camel-quarkus-micrometer + +---- + +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 baeba7d9..d52f6f2e 100644 --- a/observability/pom.xml +++ b/observability/pom.xml @@ -82,6 +82,10 @@ org.apache.camel.quarkus camel-quarkus-platform-http + + org.apache.camel.quarkus + camel-quarkus-bean + org.apache.camel.quarkus camel-quarkus-http diff --git a/observability/src/main/java/org/acme/observability/Routes.java b/observability/src/main/java/org/acme/observability/Routes.java index 27bf12d9..86730d70 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 0b8fd0fc..2d237bc6 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 17e5ca9b..6294ccda 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 CustomLivenessCheck() { protected void doCall(HealthCheckResultBuilder builder, Map 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/micrometer/TimerCounter.java b/observability/src/main/java/org/acme/observability/micrometer/TimerCounter.java new file mode 100644 index 00000000..54ce6ad8 --- /dev/null +++ b/observability/src/main/java/org/acme/observability/micrometer/TimerCounter.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.acme.observability.micrometer; + +import io.micrometer.core.annotation.Counted; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Named; + +@ApplicationScoped +@Named("timerCounter") +public class TimerCounter { + + @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 a0956de4..1007fa2f 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 ea6c2b44..e69623e0 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 @@ @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 d207f0cb..8db44eab 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 void greeting() { @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"),