diff --git a/genai-observability/README.adoc b/genai-observability/README.adoc
index 777f7342..f216e92b 100644
--- a/genai-observability/README.adoc
+++ b/genai-observability/README.adoc
@@ -4,7 +4,7 @@
This example demonstrates *GenAI observability* in Apache Camel 4.23+ with Spring Boot:
OpenTelemetry `gen_ai.*` span attributes and Micrometer metrics for `langchain4j-chat`
-LLM calls, visualized with Prometheus and VictoriaTraces.
+LLM calls, visualized with Prometheus, VictoriaTraces and Perses.
It pairs with the community blog post *GenAI Observability with Spring Boot and the Camel Observability Stack*
(`docs/blog-drafts/genai-observability-02-spring-boot-obs-stack.adoc` in the
@@ -14,12 +14,29 @@ The example uses `camel-ai-observability-starter` for Spring Boot configuration
GenAI observability toggle (`camel.aiobservability.enabled`), together with `camel-ai-observability`
for span and metric emission when a tracing or metrics backend is present.
+Two routes call two different (small) Ollama models, so every GenAI metric and span carries a
+distinct `gen_ai.request.model` and dashboards show per-model series side by side.
+
+The `camel-observability-services-starter` dependency configures an opinionated observability
+setup: all Actuator endpoints move to a dedicated management port `9876` under the `/observe`
+base path, with the Prometheus endpoint mapped to `/observe/metrics`. Both the standard
+Spring Boot / Camel metrics and the `gen_ai.*` metrics are exposed there, since they share
+the same Micrometer registry.
+
+Traces use the Spring Boot idiomatic setup: Micrometer Tracing with the OpenTelemetry bridge
+(`spring-boot-micrometer-tracing-opentelemetry`, `micrometer-tracing-bridge-otel` and
+`opentelemetry-exporter-otlp`). Spring Boot auto-configures the OpenTelemetry SDK, the OTLP
+span exporter (`management.opentelemetry.tracing.export.otlp.endpoint`) and a tracing handler
+on the Actuator `ObservationRegistry`. Camel route spans (via `camel-opentelemetry2`) and the
+`gen_ai.*` client spans (recorded as Micrometer Observations) end up in the same trace.
+
=== Prerequisites
* Java 17+
* Maven 3.9+
-* Docker (for observability stack)
-* https://ollama.com/[Ollama] with `llama3.2` pulled
+* https://camel.apache.org/manual/camel-jbang.html[Camel CLI (JBang)] 4.22+
+* Docker (used by the Camel CLI to start the observability stack)
+* https://ollama.com/[Ollama] with `llama3.2:1b` and `qwen3:0.6b` pulled
== Build
@@ -34,17 +51,21 @@ Terminal 1 — Ollama:
[source,shell]
----
-ollama pull llama3.2
+ollama pull llama3.2:1b
+ollama pull qwen3:0.6b
ollama serve
----
-Terminal 2 — observability stack (Prometheus + VictoriaTraces + Perses):
+Terminal 2 — observability stack (Prometheus + VictoriaTraces + VictoriaLogs + Perses):
[source,shell]
----
-docker compose up -d
+camel infra run observability
----
+The stack starts on fixed ports and the bundled Prometheus is already configured to scrape
+the application metrics at `host.docker.internal:9876/observe/metrics`.
+
Terminal 3 — Spring Boot application:
[source,shell]
@@ -52,16 +73,83 @@ Terminal 3 — Spring Boot application:
mvn spring-boot:run
----
+To use different models than the defaults:
+
+[source,shell]
+----
+mvn spring-boot:run -Dspring-boot.run.arguments="--langchain4j.ollama.chat-model-1.model-name=llama3.2 --langchain4j.ollama.chat-model-2.model-name=qwen3:1.7b"
+----
+
== Verify GenAI observability
+Metrics are exposed on the management port `9876` (not the application port `8080`):
+
[source,shell]
----
-curl -s http://localhost:8080/actuator/prometheus | grep gen_ai
+curl -s http://localhost:9876/observe/metrics | grep gen_ai
----
* Prometheus: `http://localhost:9090`
-* VictoriaTraces: `http://localhost:9428/select/vmui`
-* Perses: `http://localhost:8088`
+* VictoriaTraces: `http://localhost:10428/select/vmui`
+* Perses: `http://localhost:3000`
+
+== Perses dashboards
+
+The observability stack provisions Perses with a general Camel dashboard (uptime, exchanges,
+routes, JVM):
+
+* Camel overview: `http://localhost:3000/projects/camel/dashboards/overview`
+
+A GenAI dashboard (LLM calls, per-model latency, error ratio, token usage) can be created
+through the Perses REST API using the dashboard definition in `perses-genai-dashboard.json`:
+
+[source,shell]
+----
+curl -X POST http://localhost:3000/api/v1/projects \
+ -H 'Content-Type: application/json' \
+ -d '{"kind":"Project","metadata":{"name":"camel_genai"},"spec":{}}'
+
+curl -X POST http://localhost:3000/api/v1/projects/camel_genai/dashboards \
+ -H 'Content-Type: application/json' \
+ --data @perses-genai-dashboard.json
+----
+
+* GenAI overview: `http://localhost:3000/projects/camel_genai/dashboards/overview`
+
+The dashboard itself contains a "How this dashboard was created" section with the same
+instructions. Note that Perses state lives in the container: after restarting the
+observability stack, re-run the two commands above to recreate the GenAI dashboard.
+
+=== Reading the GenAI dashboard
+
+image::docs/genai-dashboard-calls.png[GenAI Summary and LLM Calls sections]
+
+* *GenAI Summary* — running totals across all models: LLM calls, errors, in-flight calls,
+ and input/output token counters. With calls that take longer than the timer period,
+ "In-flight Calls" sits permanently at 1: the route serializes requests, and a new one
+ starts as soon as the previous completes.
+* *Call rate* — calls per second, one series per `gen_ai.request.model`. A fast model
+ settles at the timer frequency; a slow model's rate is capped by its own latency.
+* *Error ratio* — failed calls (tagged `error!="none"`) over total, per model. Flat 0%
+ lines mean every call succeeded.
+* *Mean / Max LLM latency* — per-model duration of the LLM call itself (the
+ `gen_ai.client.operation` timer, not the whole route).
+
+image::docs/genai-dashboard-tokens.png[Token Usage section]
+
+* *Token throughput* and *Avg tokens per call* — from the `gen_ai.client.token.usage`
+ counter, split by model and `gen_ai.token.type` (input/output).
+
+The screenshots were taken with `llama3.2` and `qwen3.5:0.8b` (via the model-name
+overrides shown above); the default models behave similarly, since the `qwen3` family
+also reasons before answering. The two models make the point of GenAI observability
+visible on identical prompts:
+`llama3.2` answers in about a second with a few dozen output tokens, while
+`qwen3.5:0.8b` - a *thinking* model - emits roughly 2K output tokens per call (mostly
+reasoning tokens before the one-sentence answer), which drives both its ~30-45s latency
+and its dominant share of token throughput. Same workload, an order of magnitude more
+token spend - exactly the kind of cost/latency trade-off these metrics are meant to
+surface.
== Help and contributions
diff --git a/genai-observability/docker-compose.yml b/genai-observability/docker-compose.yml
deleted file mode 100644
index 71034773..00000000
--- a/genai-observability/docker-compose.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-# Observability stack for Camel GenAI observability blog (Blog 2)
-# Images aligned with camel-test-infra-observability
-
-services:
- prometheus:
- image: quay.io/prometheus/prometheus:v3.13.2
- ports:
- - "9090:9090"
- volumes:
- - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
-
- victoriatraces:
- image: mirror.gcr.io/victoriametrics/victoria-traces:v0.10.0
- ports:
- - "9428:9428"
-
- perses:
- image: mirror.gcr.io/persesdev/perses:v0.54.0
- ports:
- - "8088:8080"
diff --git a/genai-observability/docs/genai-dashboard-calls.png b/genai-observability/docs/genai-dashboard-calls.png
new file mode 100644
index 00000000..98569acb
Binary files /dev/null and b/genai-observability/docs/genai-dashboard-calls.png differ
diff --git a/genai-observability/docs/genai-dashboard-tokens.png b/genai-observability/docs/genai-dashboard-tokens.png
new file mode 100644
index 00000000..89bc1cf6
Binary files /dev/null and b/genai-observability/docs/genai-dashboard-tokens.png differ
diff --git a/genai-observability/perses-genai-dashboard.json b/genai-observability/perses-genai-dashboard.json
new file mode 100644
index 00000000..79b1441f
--- /dev/null
+++ b/genai-observability/perses-genai-dashboard.json
@@ -0,0 +1,566 @@
+{
+ "kind": "Dashboard",
+ "metadata": {
+ "name": "overview"
+ },
+ "spec": {
+ "display": {
+ "name": "GenAI Overview"
+ },
+ "duration": "30m",
+ "panels": {
+ "llmCalls": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "LLM Calls"
+ },
+ "plugin": {
+ "kind": "StatChart",
+ "spec": {
+ "calculation": "last-number",
+ "format": {
+ "unit": "decimal",
+ "shortValues": true
+ },
+ "sparkline": {}
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "sum(gen_ai_client_operation_seconds_count)"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "llmErrors": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "LLM Errors"
+ },
+ "plugin": {
+ "kind": "StatChart",
+ "spec": {
+ "calculation": "last-number",
+ "format": {
+ "unit": "decimal",
+ "shortValues": true
+ },
+ "sparkline": {}
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "sum(gen_ai_client_operation_seconds_count{error!=\"none\"}) or vector(0)"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "inflightCalls": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "In-flight Calls"
+ },
+ "plugin": {
+ "kind": "StatChart",
+ "spec": {
+ "calculation": "last-number",
+ "format": {
+ "unit": "decimal",
+ "shortValues": true
+ },
+ "sparkline": {}
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "sum(gen_ai_client_operation_active_seconds_count)"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "inputTokens": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "Input Tokens"
+ },
+ "plugin": {
+ "kind": "StatChart",
+ "spec": {
+ "calculation": "last-number",
+ "format": {
+ "unit": "decimal",
+ "shortValues": true
+ },
+ "sparkline": {}
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "sum(gen_ai_client_token_usage_total{gen_ai_token_type=\"input\"})"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "outputTokens": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "Output Tokens"
+ },
+ "plugin": {
+ "kind": "StatChart",
+ "spec": {
+ "calculation": "last-number",
+ "format": {
+ "unit": "decimal",
+ "shortValues": true
+ },
+ "sparkline": {}
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "sum(gen_ai_client_token_usage_total{gen_ai_token_type=\"output\"})"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "callRate": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "Call rate (calls/s)"
+ },
+ "plugin": {
+ "kind": "TimeSeriesChart",
+ "spec": {
+ "legend": {
+ "position": "bottom"
+ },
+ "yAxis": {
+ "format": {
+ "unit": "decimal",
+ "decimalPlaces": 2
+ }
+ }
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "sum by (gen_ai_system, gen_ai_request_model) (rate(gen_ai_client_operation_seconds_count[1m]))",
+ "seriesNameFormat": "{{gen_ai_system}} {{gen_ai_request_model}}"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "errorRatio": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "Error ratio"
+ },
+ "plugin": {
+ "kind": "TimeSeriesChart",
+ "spec": {
+ "legend": {
+ "position": "bottom"
+ },
+ "yAxis": {
+ "format": {
+ "unit": "percent-decimal",
+ "decimalPlaces": 2
+ }
+ }
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "sum by (gen_ai_request_model) ((rate(gen_ai_client_operation_seconds_count{error!=\"none\"}[5m]) or rate(gen_ai_client_operation_seconds_count[5m]) * 0)) / on(gen_ai_request_model) group_left sum by (gen_ai_request_model) (rate(gen_ai_client_operation_seconds_count[5m]) > 0)",
+ "seriesNameFormat": "{{gen_ai_request_model}}"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "latencyMean": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "Mean LLM latency"
+ },
+ "plugin": {
+ "kind": "TimeSeriesChart",
+ "spec": {
+ "legend": {
+ "position": "bottom"
+ },
+ "yAxis": {
+ "format": {
+ "unit": "seconds",
+ "decimalPlaces": 2
+ }
+ }
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "sum by (gen_ai_system, gen_ai_request_model) (rate(gen_ai_client_operation_seconds_sum[5m])) / sum by (gen_ai_system, gen_ai_request_model) (rate(gen_ai_client_operation_seconds_count[5m]))",
+ "seriesNameFormat": "{{gen_ai_system}} {{gen_ai_request_model}}"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "latencyMax": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "Max LLM latency"
+ },
+ "plugin": {
+ "kind": "TimeSeriesChart",
+ "spec": {
+ "legend": {
+ "position": "bottom"
+ },
+ "yAxis": {
+ "format": {
+ "unit": "seconds",
+ "decimalPlaces": 2
+ }
+ }
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "gen_ai_client_operation_seconds_max",
+ "seriesNameFormat": "{{gen_ai_system}} {{gen_ai_request_model}}"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "tokenThroughput": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "Token throughput (tokens/s)"
+ },
+ "plugin": {
+ "kind": "TimeSeriesChart",
+ "spec": {
+ "legend": {
+ "position": "bottom"
+ },
+ "yAxis": {
+ "format": {
+ "unit": "decimal",
+ "decimalPlaces": 2
+ }
+ }
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "sum by (gen_ai_request_model, gen_ai_token_type) (rate(gen_ai_client_token_usage_total[1m]))",
+ "seriesNameFormat": "{{gen_ai_request_model}} {{gen_ai_token_type}}"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "tokensPerCall": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "Avg tokens per call"
+ },
+ "plugin": {
+ "kind": "TimeSeriesChart",
+ "spec": {
+ "legend": {
+ "position": "bottom"
+ },
+ "yAxis": {
+ "format": {
+ "unit": "decimal",
+ "decimalPlaces": 2
+ }
+ }
+ }
+ },
+ "queries": [
+ {
+ "kind": "TimeSeriesQuery",
+ "spec": {
+ "plugin": {
+ "kind": "PrometheusTimeSeriesQuery",
+ "spec": {
+ "query": "sum by (gen_ai_request_model, gen_ai_token_type) (rate(gen_ai_client_token_usage_total[5m])) / on(gen_ai_request_model) group_left sum by (gen_ai_request_model) (rate(gen_ai_client_operation_seconds_count[5m]))",
+ "seriesNameFormat": "{{gen_ai_request_model}} {{gen_ai_token_type}}"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "howTo": {
+ "kind": "Panel",
+ "spec": {
+ "display": {
+ "name": "How this dashboard was created"
+ },
+ "plugin": {
+ "kind": "Markdown",
+ "spec": {
+ "text": "## How this dashboard was created\n\nMetrics come from **camel-ai-observability**: the Spring Boot app exposes `gen_ai.*` meters on\n`http://localhost:9876/observe/metrics` and the stack's Prometheus scrapes that endpoint.\n\nThe dashboard is plain JSON managed through the Perses REST API. To recreate it (e.g. after\nrestarting `camel infra run observability`), from the `genai-observability` example directory:\n\n```\ncurl -X POST http://localhost:3000/api/v1/projects \\\n -H 'Content-Type: application/json' \\\n -d '{\"kind\":\"Project\",\"metadata\":{\"name\":\"camel_genai\"},\"spec\":{}}'\n\ncurl -X POST http://localhost:3000/api/v1/projects/camel_genai/dashboards \\\n -H 'Content-Type: application/json' \\\n --data @perses-genai-dashboard.json\n```\n\nTo export the current state of this dashboard (after UI edits):\n\n```\ncurl http://localhost:3000/api/v1/projects/camel_genai/dashboards/overview\n```\n\nKey metrics used: `gen_ai_client_operation_seconds` (timer, tags `gen_ai_system`,\n`gen_ai_request_model`, `error`), `gen_ai_client_token_usage_total` (counter, tag\n`gen_ai_token_type`), `gen_ai_client_operation_active_seconds` (in-flight calls).\n"
+ }
+ }
+ }
+ }
+ },
+ "layouts": [
+ {
+ "kind": "Grid",
+ "spec": {
+ "display": {
+ "title": "GenAI Summary",
+ "collapse": {
+ "open": true
+ }
+ },
+ "items": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 4,
+ "height": 4,
+ "content": {
+ "$ref": "#/spec/panels/llmCalls"
+ }
+ },
+ {
+ "x": 4,
+ "y": 0,
+ "width": 4,
+ "height": 4,
+ "content": {
+ "$ref": "#/spec/panels/llmErrors"
+ }
+ },
+ {
+ "x": 8,
+ "y": 0,
+ "width": 4,
+ "height": 4,
+ "content": {
+ "$ref": "#/spec/panels/inflightCalls"
+ }
+ },
+ {
+ "x": 12,
+ "y": 0,
+ "width": 4,
+ "height": 4,
+ "content": {
+ "$ref": "#/spec/panels/inputTokens"
+ }
+ },
+ {
+ "x": 16,
+ "y": 0,
+ "width": 4,
+ "height": 4,
+ "content": {
+ "$ref": "#/spec/panels/outputTokens"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "kind": "Grid",
+ "spec": {
+ "display": {
+ "title": "LLM Calls",
+ "collapse": {
+ "open": true
+ }
+ },
+ "items": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 12,
+ "height": 8,
+ "content": {
+ "$ref": "#/spec/panels/callRate"
+ }
+ },
+ {
+ "x": 12,
+ "y": 0,
+ "width": 12,
+ "height": 8,
+ "content": {
+ "$ref": "#/spec/panels/errorRatio"
+ }
+ },
+ {
+ "x": 0,
+ "y": 8,
+ "width": 12,
+ "height": 8,
+ "content": {
+ "$ref": "#/spec/panels/latencyMean"
+ }
+ },
+ {
+ "x": 12,
+ "y": 8,
+ "width": 12,
+ "height": 8,
+ "content": {
+ "$ref": "#/spec/panels/latencyMax"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "kind": "Grid",
+ "spec": {
+ "display": {
+ "title": "Token Usage",
+ "collapse": {
+ "open": true
+ }
+ },
+ "items": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 12,
+ "height": 8,
+ "content": {
+ "$ref": "#/spec/panels/tokenThroughput"
+ }
+ },
+ {
+ "x": 12,
+ "y": 0,
+ "width": 12,
+ "height": 8,
+ "content": {
+ "$ref": "#/spec/panels/tokensPerCall"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "kind": "Grid",
+ "spec": {
+ "display": {
+ "title": "About",
+ "collapse": {
+ "open": false
+ }
+ },
+ "items": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 24,
+ "height": 12,
+ "content": {
+ "$ref": "#/spec/panels/howTo"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/genai-observability/pom.xml b/genai-observability/pom.xml
index 1462f6b0..e1abf9d6 100644
--- a/genai-observability/pom.xml
+++ b/genai-observability/pom.xml
@@ -34,7 +34,7 @@
AI
- 1.19.0-beta29
+ 1.19.0
UTF-8
UTF-8
@@ -79,6 +79,10 @@
org.apache.camel.springboot
camel-langchain4j-chat-starter
+
+ org.apache.camel.springboot
+ camel-yaml-dsl-starter
+
org.apache.camel
camel-ai-observability
@@ -86,8 +90,23 @@
dev.langchain4j
- langchain4j-ollama-spring-boot-starter
- ${langchain4j-beta-version}
+ langchain4j-ollama
+ ${langchain4j-version}
+
+
+
+ org.springframework.boot
+ spring-boot-micrometer-tracing-opentelemetry
+
+
+ io.micrometer
+ micrometer-tracing-bridge-otel
+
+
+ io.opentelemetry
+ opentelemetry-exporter-otlp
org.springframework.boot
diff --git a/genai-observability/prometheus.yml b/genai-observability/prometheus.yml
deleted file mode 100644
index a0a9125a..00000000
--- a/genai-observability/prometheus.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-#
-# 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
-#
-
-global:
- scrape_interval: 15s
-
-scrape_configs:
- - job_name: camel-spring-boot
- metrics_path: /actuator/prometheus
- static_configs:
- - targets:
- - host.docker.internal:8080
diff --git a/genai-observability/src/main/java/org/apache/camel/example/genai/ChatModelConfiguration.java b/genai-observability/src/main/java/org/apache/camel/example/genai/ChatModelConfiguration.java
new file mode 100644
index 00000000..9ede0b8a
--- /dev/null
+++ b/genai-observability/src/main/java/org/apache/camel/example/genai/ChatModelConfiguration.java
@@ -0,0 +1,61 @@
+/*
+ * 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.apache.camel.example.genai;
+
+import java.time.Duration;
+
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.ollama.OllamaChatModel;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Two chat models against the same Ollama instance, so the GenAI dashboards show per-model series
+ * (gen_ai.request.model is a tag/attribute on every metric and span).
+ */
+@Configuration
+public class ChatModelConfiguration {
+
+ @Value("${langchain4j.ollama.chat-model.base-url}")
+ private String baseUrl;
+
+ @Value("${langchain4j.ollama.chat-model.temperature}")
+ private Double temperature;
+
+ @Value("${langchain4j.ollama.chat-model.timeout}")
+ private Duration timeout;
+
+ @Bean
+ ChatModel chatModel1(@Value("${langchain4j.ollama.chat-model-1.model-name}") String modelName) {
+ return buildModel(modelName);
+ }
+
+ @Bean
+ ChatModel chatModel2(@Value("${langchain4j.ollama.chat-model-2.model-name}") String modelName) {
+ return buildModel(modelName);
+ }
+
+ private ChatModel buildModel(String modelName) {
+ return OllamaChatModel.builder()
+ .baseUrl(baseUrl)
+ .modelName(modelName)
+ .temperature(temperature)
+ .timeout(timeout)
+ .build();
+ }
+}
diff --git a/genai-observability/src/main/resources/application.properties b/genai-observability/src/main/resources/application.properties
index 7571a31b..d4e9ef86 100644
--- a/genai-observability/src/main/resources/application.properties
+++ b/genai-observability/src/main/resources/application.properties
@@ -19,11 +19,13 @@
spring.application.name=genai-observability
server.port=8080
-# LangChain4j Ollama (auto-configures chatLanguageModel bean)
+# LangChain4j Ollama (used by ChatModelConfiguration to build the chatModel1/chatModel2 beans).
+# Two small models so the GenAI dashboards show per-model series.
langchain4j.ollama.chat-model.base-url=http://localhost:11434
-langchain4j.ollama.chat-model.model-name=llama3.2
langchain4j.ollama.chat-model.temperature=0.2
langchain4j.ollama.chat-model.timeout=PT120S
+langchain4j.ollama.chat-model-1.model-name=llama3.2:1b
+langchain4j.ollama.chat-model-2.model-name=qwen3:0.6b
# Camel YAML routes
camel.main.routes-include-pattern=camel/*
@@ -37,7 +39,6 @@ management.endpoints.web.exposure.include=health,prometheus,info
management.endpoint.prometheus.access=read_only
management.prometheus.metrics.export.enabled=true
-# OTLP export to VictoriaTraces (docker compose stack)
-camel.opentelemetry2.export-target=jaeger
-otel.exporter.otlp.endpoint=http://localhost:9428/insert/opentelemetry/v1/traces
-otel.exporter.otlp.protocol=http/protobuf
+# OTLP/HTTP trace export to VictoriaTraces (camel infra run observability)
+management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:10428/insert/opentelemetry/v1/traces
+management.tracing.sampling.probability=1.0
diff --git a/genai-observability/src/main/resources/camel/genai-route.camel.yaml b/genai-observability/src/main/resources/camel/genai-route.camel.yaml
index af7a4a8a..affe70ff 100644
--- a/genai-observability/src/main/resources/camel/genai-route.camel.yaml
+++ b/genai-observability/src/main/resources/camel/genai-route.camel.yaml
@@ -6,19 +6,33 @@
#
- route:
- id: genai-chat
+ id: genai-chat-1
from:
- uri: timer:genai
+ uri: timer:genai1
parameters:
period: "15000"
steps:
- setBody:
constant: "In one sentence, what is Apache Camel integration?"
- to:
- uri: langchain4j-chat:demo
+ uri: langchain4j-chat:model1
parameters:
- chatModel: "#chatLanguageModel"
+ chatModel: "#chatModel1"
- log:
- message: "LLM reply: ${body}"
+ message: "[${header.CamelLangChain4jChatResponseModel}] LLM reply: ${body}"
+
+- route:
+ id: genai-chat-2
+ from:
+ uri: timer:genai2
+ parameters:
+ period: "20000"
+ steps:
+ - setBody:
+ constant: "In one sentence, what is an Enterprise Integration Pattern?"
+ - to:
+ uri: langchain4j-chat:model2
+ parameters:
+ chatModel: "#chatModel2"
- log:
- message: "Models: req=${header.CamelLangChain4jChatRequestModel} resp=${header.CamelLangChain4jChatResponseModel}"
+ message: "[${header.CamelLangChain4jChatResponseModel}] LLM reply: ${body}"