A Guide to Camel Observability Services

Getting Serious About Observability in Apache Camel Applications

Overview Before getting into code, it helps to know what you're actually wiring together. Camel's observability story breaks into three areas: Health checks : Is the application up? Are the routes running? Are any components unhealthy? Metrics : Route throughput, exchange failures, processing time. Exposed via Micrometer, which means Prometheus can scrape it. Tracing : Distributed traces with OpenTelemetry so you can follow a message across service boundaries. Each of these is a separate concern, and Camel treats them that way. Which I appreciate. You don't have to buy the whole package if you only need one piece. Setting Up the Project I'll focus on the Spring Boot path here because that's what we use, but I'll call out the Camel Main differences where they matter. For a Spring Boot project on Camel 4.x, your core dependencies look like this: <dependency> <groupId>org.apache.camel.springboot</groupId> <artifactId>camel-spring-boot-starter</artifactId> <version>4.5.0</version> </dependency> <dependency> <groupId>org.apache.camel.springboot</groupId> <artifactId>camel-observability-services-starter</artifactId> <version>4.5.0</version> </dependency> <dependency> <groupId>org.apache.camel.springboot</groupId> <artifactId>camel-micrometer-starter</artifactId> <version>4.5.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> The camel-observability-services-starter is the key one. It pulls in the health check infrastructure, wires up a default /observe endpoint, and sets some sensible defaults. When I first dropped it into our project I expected a bunch of extra configuration to follow. But it just... worked. At least the health check part did. Health Checks Camel's health check support is built around the HealthCheck and HealthCheckRegistry abstractions. Out of the box, with the observability starter on the classpath, you get route health checks automatically. Each route gets its own check that reports whether it's started, stopped, or in a failed state. Hit the Actuator endpoint to see it: GET /actuator/health The response includes a camelHealth section, something like: { "status": "UP", "components": { "camelHealth": { "status": "UP", "details": { "context": "UP", "route:order-ingestion": "UP", "route:order-processing": "UP" } } } } You can also write your own checks. We had a situation where our route depended on an external SFTP server, and the route itself would start fine but then immediately fail when it tried to connect. So I wrote a custom check: @Component public class SftpConnectivityCheck extends AbstractHealthCheck { private final SftpProperties props; public SftpConnectivityCheck(SftpProperties props) { super("custom", "sftp-connectivity"); this.props = props; } @Override protected void doCall(HealthCheckResultBuilder builder, Map<String, Object> options) { try (Socket socket = new Socket()) { socket.connect( new InetSocketAddress(props.getHost(), props.getPort()), 2000 ); builder.up(); } catch (IOException e) { builder.down() .message("Cannot reach SFTP host: " + e.getMessage()) .error(e); } } } Not fancy. Just a TCP connect with a 2-second timeout. But it saved us from a situation where Kubernetes thought the pod was perfectly healthy while the route was silently dying on every single poll cycle. One thing to watch: by default, health checks feed into the liveness probe behavior, not just readiness. Depending on how aggressive your Kubernetes liveness config is, a flapping external dependency can trigger unnecessary pod restarts. You can control this with camel.health.liveness-probe-enabled and camel.health.readiness-probe-enabled in application.properties . I'd set those explicitly rather than relying on whatever the default happens to be. Metrics with Micrometer This is where things get genuinely useful. With camel-micrometer-starter on the classpath and a Prometheus registry in place, Camel starts publishing metrics for every route automatically. No extra code. The default metrics you get include: camel.exchanges.total : total exchanges processed per route camel.exchanges.failed : failed exchanges camel.exchanges.inflight : currently in-flight exchanges camel.route.policy.milliseconds : processing time distribution (this one is great for spotting slow routes) Scrape /actuator/prometheus and they're all there. The tag set includes the route ID, so you can slice by route in Grafana without any extra setup. Assuming your route IDs are meaningful. More on that later. You can add custom metrics directly inside a route too. On the order-processing service we needed to track how many orders were going to the priority queue versus the standard queue: @Component public class OrderRouter extends RouteBuilder { @Override public void configure() { from("direct:incoming-orders") .choice() .when(simple("${header.orderValue} > 1000")) .to("micrometer:counter:orders.priority?increment=1") .to("direct:priority-queue") .otherwise() .to("micrometer:counter:orders.standard?increment=1") .to("direct:standard-queue"); } } The micrometer:counter URI is a neat trick. You drop metric recording right into the route DSL without writing any surrounding Java. Tags are supported too, if you need them: .to("micrometer:counter:orders.routed?increment=1&tags=channel=${header.channel}") Okay, that's not quite right as I wrote it. The tag value being a Simple expression like ${header.channel} requires URL encoding or using a MicrometerEndpoint directly instead of the string URI. I got burned by this during our Q3 load-testing sprint (we spent about 45 minutes wondering why our tags were literally showing up as the string ${header.channel} in Prometheus). Use the endpoint builder approach when your tag values come from message headers. Tracing with OpenTelemetry This one took me the longest to get right. Not because it's hard, but because there are a few moving parts and the documentation sort of assumes you already know OpenTelemetry well. Add the tracing dependency: <dependency> <groupId>org.apache.camel.springboot</groupId> <artifactId>camel-opentelemetry-starter</artifactId> <version>4.5.0</version> </dependency> Then configure an OTLP exporter in application.properties : camel.opentelemetry.enabled=true otel.service.name=order-processing-service otel.exporter.otlp.endpoint=http://tempo:4317 otel.traces.exporter=otlp That's most of it, actually. Once enabled, Camel creates a span for each exchange as it moves through a route. If the incoming request already carries a trace context (a traceparent header from another service, say), Camel attaches to that trace automatically. Context propagates through HTTP and messaging components without you having to do anything. Where it gets interesting is custom span attributes. Say you want to tag spans with the order ID for easier filtering in Tempo or Jaeger: from("direct:process-order") .process(exchange -> { Span currentSpan = Span.current(); String orderId = exchange.getIn().getHeader("orderId", String.class); if (orderId != null) { currentSpan.setAttribute("order.id", orderId); } }) .to("direct:validate-order"); Simple enough. But I'd wrap that in a reusable Processor rather than scattering Span.current() calls around. Easier to test, and you don't end up with OpenTelemetry API calls tangled up with business logic...