Skip to content

Commit

Permalink
Add Micrometer features to Observability example
Browse files Browse the repository at this point in the history
  • Loading branch information
llowinge committed Jul 3, 2023
1 parent 526a843 commit 923672b
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 34 deletions.
74 changes: 72 additions & 2 deletions observability/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions observability/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
19 changes: 19 additions & 0 deletions observability/src/main/java/org/acme/observability/Routes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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!"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public CustomLivenessCheck() {
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
}
}
22 changes: 10 additions & 12 deletions observability/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@

@QuarkusIntegrationTest
public class ObservabilityIT extends ObservabilityTest {

// Is run in prod mode
@Override
protected String getManagementPrefix() {
return "http://localhost:9000";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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"),
Expand Down

0 comments on commit 923672b

Please sign in to comment.