This is an automated email from the ASF dual-hosted git repository. luigidemasi pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit 2087d760fe89f2dba6a96488b61e3558037ae4cc Author: Luigi De Masi <[email protected]> AuthorDate: Fri Nov 13 13:07:21 2026 +0100 CAMEL-25648: Extract the Semantic Micrometer integration into its own module Move the exporter and its tests into camel-semantic-micrometer, with normal dependencies on camel-semantic and camel-micrometer. Reuse the shared Camel context tag and keep semantic evaluation independent of Micrometer. Move the instrumentation guide to the bridge documentation, register the module in the build and catalog, and retain a base-module regression for evaluation with no Micrometer on its classpath. Co-authored-by: OpenAI Codex <[email protected]> Signed-off-by: Luigi De Masi <[email protected]> --- bom/camel-bom/pom.xml | 5 + catalog/camel-allcomponents/pom.xml | 5 + .../org/apache/camel/catalog/docs.properties | 1 + .../camel/catalog/docs/semantic-language.adoc | 159 +----------------- .../camel/catalog/docs/semantic-micrometer.adoc | 177 +++++++++++++++++++++ .../org/apache/camel/catalog/others.properties | 1 + .../camel/catalog/others/semantic-micrometer.json | 15 ++ .../pom.xml | 59 ++----- .../services/org/apache/camel/other.properties | 7 + .../generated/resources/semantic-micrometer.json | 15 ++ .../src/main/docs/semantic-micrometer.adoc | 177 +++++++++++++++++++++ .../micrometer/MicrometerSemanticMetrics.java | 3 +- .../micrometer/MicrometerSemanticMetricsTest.java | 49 ------ components/camel-ai/camel-semantic/pom.xml | 10 -- .../src/main/docs/semantic-language.adoc | 159 +----------------- .../semantic/SemanticWithoutMicrometerTest.java | 60 +++++++ components/camel-ai/pom.xml | 1 + design/semantic-audit.adoc | 2 + .../others/examples/json/semantic-micrometer.json | 1 + docs/components/modules/others/nav.adoc | 1 + .../modules/others/pages/semantic-micrometer.adoc | 1 + parent/pom.xml | 5 + .../apache/camel/maven/packaging/MojoHelper.java | 3 +- 23 files changed, 505 insertions(+), 411 deletions(-) diff --git a/bom/camel-bom/pom.xml b/bom/camel-bom/pom.xml index 8536c1164318..4ecaadc9926e 100644 --- a/bom/camel-bom/pom.xml +++ b/bom/camel-bom/pom.xml @@ -2232,6 +2232,11 @@ <artifactId>camel-semantic</artifactId> <version>4.24.0-SNAPSHOT</version> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-semantic-micrometer</artifactId> + <version>4.24.0-SNAPSHOT</version> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-semantic-openai</artifactId> diff --git a/catalog/camel-allcomponents/pom.xml b/catalog/camel-allcomponents/pom.xml index fca54f0d854e..a0d095efde54 100644 --- a/catalog/camel-allcomponents/pom.xml +++ b/catalog/camel-allcomponents/pom.xml @@ -2012,6 +2012,11 @@ <artifactId>camel-semantic</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-semantic-micrometer</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-semantic-openai</artifactId> diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties index 2c98e3f33f4b..97888b9589b9 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties @@ -576,6 +576,7 @@ script-eip seda-component selective-consumer semantic-language +semantic-micrometer semantic-openai semantic-typesafe-ai semantic-wolf-defender diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc index 445e1ccc4955..5175ac896296 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc @@ -1507,7 +1507,7 @@ camel semantic monitoring my-app --source=departmentMetrics The Semantic *Monitoring* sibling, `tui_get_monitoring`, `semantic-monitoring` console and CLI read this same current-value cache. The TUI offers expert/definition grouping, filters and an inspector for counts, durations, distributions, meaning, observation times and collection health. It has no historical-period, reference or -comparison controls. Pausing affects the display only. The optional Micrometer listener below exports +comparison controls. Pausing affects the display only. The optional Micrometer integration exports measurements independently of this viewer. Runtime-specific automatic registration and scrape endpoint integration are separate from the shared listener. @@ -1520,155 +1520,8 @@ Facades are removed at context shutdown. Monitoring also works with JMX disabled === Optional Micrometer instrumentation -`MicrometerSemanticMetrics` consumes typed assessment completion events directly. Add `io.micrometer:micrometer-core` -to your application, or use the registry dependency supplied by your runtime. Micrometer is an optional dependency -of `camel-semantic`; applications that do not register this listener do not need it. Prometheus is not a runtime -dependency of the listener. - -Register one listener before starting the context, passing the application's existing `MeterRegistry`: - -[source,java] ----- -import org.apache.camel.semantic.micrometer.MicrometerSemanticMetrics; - -// registry is the application's existing Micrometer MeterRegistry. -// Optional, increasing score bucket bounds may include negative values. -context.getManagementStrategy().addEventNotifier( - new MicrometerSemanticMetrics(registry, -1, 0, 0.5, 1, 2, 5)); ----- - -The listener does not create or close a registry or install an HTTP server. The application/runtime owns -registry configuration, HTTP exposure, authentication and the registry lifecycle. Give contexts sharing a registry -distinct names. Registering more than one listener in a context fails at startup to prevent double counting. -Stop removes meters created by the listener; restarting starts new counters. Existing application meters remain. - -Each completed assessment member is counted once, including individual batch members. Starts, request outcomes -and locally aggregated `typed_results` snapshots are not counted again. Provider attempts/retries are not inferred -from semantic completions. Audit capture, input capture, JMX, the local monitoring cache and UI polling are not -required. Collection failures are isolated from the assessment and reported by the collection health meters. -The first failure since collector start also logs a warning containing only the exception class, without its -message, stack trace or event payload; subsequent failures only increment the error count. - -[cols="2,3",options="header"] -|=== -|Micrometer name |Meaning -|`camel.semantic.evaluations` |Counter by execution `status`: `success`, `failed`, or `not_executed`. -|`camel.semantic.duration` |Timer recording each available execution duration, including failures. Skipped or -missing durations are not zero-duration observations. Percentile histograms use the original durations. -|`camel.semantic.result.availability` |Successful assessments by `availability`: `available` or `unavailable`. -|`camel.semantic.results` |Counter by returned `answer`. Boolean values are final answers, without another -probability threshold. Choice counts the selected answer, not probability entries. Classification counts each -selected label once per assessment, so membership percentages need not sum to 100%. -|`camel.semantic.classification.empty` |Successful classification results with no selected labels. -|`camel.semantic.score.observations` |Count of available finite numeric score observations. -|`camel.semantic.score.sum`, `.minimum`, `.maximum` |Gauges preserving signed, fractional scores since collector -start. The sum can decrease; do not treat it as a monotonic counter. -|`camel.semantic.score.distribution` |Cumulative counters with inclusive `le` bounds supplied to the constructor and a -`+Inf` bucket. Bounds describe numeric values, not severity or policy decisions. -|`camel.semantic.collection.loss` |Counter by fixed `reason`: `population_limit`, `answer_limit`, `invalid_event`, -or `unsupported_measurement`. Loss is excluded from the corresponding result distribution. -|`camel.semantic.collection.errors` |Number of metric collection failures since collector start. -|=== - -Assessment meters have `camelContext`, `expert`, `definition`, `operation`, `origin` and `result.type` tags. -Missing identity is the empty string; direct calls remain distinguishable by origin and the absence of a definition. -The listener retains at most 100 distinct combinations and 64 answer labels of at most 256 characters per combination. -Score histograms accept at most 64 finite, strictly increasing bounds; an empty array keeps count, sum, minimum, -maximum and the overflow bucket. Configure identical bounds on instances you aggregate together. -Each population can have both successful and failed duration timers. Default percentile histograms can add about -70 bucket series per timer: the 100-population limit alone can therefore allow roughly 14,000 duration bucket -series. Configure the registry's meter filter before registering the listener to choose fewer boundaries, for example: - -[source,java] ----- -import java.time.Duration; - -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.config.MeterFilter; -import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; - -registry.config().meterFilter(new MeterFilter() { - @Override - public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) { - if (id.getName().equals("camel.semantic.duration")) { - return DistributionStatisticConfig.builder().percentilesHistogram(false) - .serviceLevelObjectives(Duration.ofMillis(10).toNanos(), Duration.ofMillis(100).toNanos(), - Duration.ofSeconds(1).toNanos(), Duration.ofSeconds(10).toNanos()) - .build().merge(config); - } - return config; - } -}); ----- - -This example exports five duration buckets per timer in Prometheus, including `+Inf`, while retaining every -observation in the count and sum. Choose boundaries for your latency goals: fewer buckets reduce percentile -resolution. The `camelContext` tag matches Camel's other Micrometer metrics. - -Prompts, input, interpretation, exception messages and per-execution identifiers are never metric labels. -Choose stable expert/definition names and result labels. Bounds limit retained series, but collection loss still -means that a distribution may cover only part of the evaluated population. Collection error counts do not prove -that every event was delivered: the shared AI notification contract remains synchronous and best effort. - -==== Expert-published scalar gauges - -The same listener accepts `SemanticMonitoringEvent` snapshots with `kind=gauge`, a single finite numeric -`values.value`, and an explicit unit of `1`, `ratio`, `bytes`, `seconds`, `milliseconds`, `nanoseconds`, `count`, -`requests` or `assessments`. If supplied, `availability` must be `available`. It exports -`camel.semantic.expert.value` with `source`, `measurement` (the snapshot ID), `unit` and the supplied -`expert`, `definition`, `operation`, `provider`, `taskId`, `taskVersion`, `revision` dimensions. Missing dimensions -are empty strings. Other dimension keys are rejected, including correlation and message identifiers. - -Values replace earlier values for that identity; repeated publication never increments a counter. At most 100 -such identities are retained, independently of assessment populations. The producer owns meaning and update cadence. -The last value remains until the next publication or collector shutdown; scraping does not refresh it. A publication -with an unavailable or invalid value marks an existing gauge as `NaN`, rather than continuing to expose its old value. -Only published snapshots are consumed: the listener does not call `SemanticMetricSource.snapshots()` or perform I/O -while an assessment completes. Unsupported kinds, units, shapes or dimensions increment `unsupported_measurement`; -aggregated counts are not guessed to be deltas, and snapshot means are never converted to latency observations. - -==== Prometheus queries and lifecycle - -Expose the supplied registry through the runtime's normal metrics endpoint. Prometheus scrapes that endpoint and -owns time-series retention; it does not depend on the Camel TUI being connected. Select a scrape interval that fits -your operational needs (for example 15 seconds) and use a rate window containing several scrapes. -The shared listener does not provide automatic Main, Spring Boot or Quarkus registration, a Prometheus query client, -historical TUI views or dashboards. Runtime-specific examples and Quarkus native validation are follow-up work. - -For a Prometheus registry, dots become underscores and counters gain `_total`. These example backend queries aggregate -across instances while retaining the selected expert or definition grouping: - -[source,promql] ----- -# Executed assessment rate per expert; skipped members are excluded. -sum by (expert) (rate(camel_semantic_evaluations_total{status=~"success|failed"}[5m])) - -# Failed execution rate per expert and definition. -sum by (expert, definition) (rate(camel_semantic_evaluations_total{status="failed"}[5m])) - -# Returned-answer distribution per expert and definition. -sum by (expert, definition, result_type, answer) (rate(camel_semantic_results_total[5m])) - -# 95th percentile from actual duration buckets, aggregated across instances. -histogram_quantile(0.95, - sum by (expert, le) (rate(camel_semantic_duration_seconds_bucket[5m]))) - -# Available score distribution using application-declared bounds. -sum by (expert, definition, le) (rate(camel_semantic_score_distribution_total[5m])) - -# Counter resets observed on individual application series. -resets(camel_semantic_evaluations_total[1h]) - -# Collection exclusions over the observation window. -sum by (reason) (increase(camel_semantic_collection_loss_total[5m])) ----- - -Apply `rate` or `increase` before summing across instances so resets are handled per series. A restart resets these -counters; Prometheus history remains in Prometheus. The first scrape after startup cannot reconstruct evaluations -that happened before a counter's first observed sample. Backend labels such as `job` and `instance` distinguish -application instances; they are supplied by the scrape configuration, not unique evaluation IDs. - -Exemplars depend on the runtime's tracing context, registry and exposition format. This listener neither fabricates -trace context nor promises an exemplar for every assessment, particularly for detached or asynchronous completions. -Historical investigations may query independently retained audit records for a matching `[since, until)` interval, -definition/operation and typed result. Audit retention and metric observations can cover different populations. +Add xref:others:semantic-micrometer.adoc[Semantic Micrometer] (`camel-semantic-micrometer`) to export +assessment counts, durations, typed result distributions and supported expert measurements to the +application's Micrometer registry. The integration brings `camel-semantic` and `camel-micrometer` +transitively and documents registration, metric names, cardinality limits, lifecycle and PromQL queries. +`camel-semantic` itself has no Micrometer dependency. diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-micrometer.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-micrometer.adoc new file mode 100644 index 000000000000..c3d175dab600 --- /dev/null +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-micrometer.adoc @@ -0,0 +1,177 @@ += Semantic Micrometer Component +:doctitle: Semantic Micrometer +:shortname: semantic-micrometer +:artifactid: camel-semantic-micrometer +:description: Export semantic assessments and expert measurements with Micrometer +:since: 4.24 +:supportlevel: Preview +:tabs-sync-option: + +*Since Camel {since}* + +This module exports typed assessment completion events and supported expert measurements from the +xref:languages:semantic-language.adoc[Semantic language] to Micrometer. Add one dependency to bring +both `camel-semantic` and `camel-micrometer` transitively, using the same version as Camel core: + +[source,xml] +---- +<dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-semantic-micrometer</artifactId> + <version>x.x.x</version> +</dependency> +---- + +`camel-semantic` itself has no Micrometer dependency. This integration has no routing endpoint of its own; +adding the dependency does not automatically register a collector. Prometheus is not a runtime dependency +of this module: supply the registry implementation used by your application. + +== Register the collector + +Register one listener before starting the context, passing the application's existing `MeterRegistry`: + +[source,java] +---- +import org.apache.camel.semantic.micrometer.MicrometerSemanticMetrics; + +// registry is the application's existing Micrometer MeterRegistry. +// Optional, increasing score bucket bounds may include negative values. +context.getManagementStrategy().addEventNotifier( + new MicrometerSemanticMetrics(registry, -1, 0, 0.5, 1, 2, 5)); +---- + +The listener does not create or close a registry or install an HTTP server. The application/runtime owns +registry configuration, HTTP exposure, authentication and the registry lifecycle. Give contexts sharing a registry +distinct names. Registering more than one listener in a context fails at startup to prevent double counting. +Stop removes meters created by the listener; restarting starts new counters. Existing application meters remain. + +Each completed assessment member is counted once, including individual batch members. Starts, request outcomes +and locally aggregated `typed_results` snapshots are not counted again. Provider attempts/retries are not inferred +from semantic completions. Audit capture, input capture, JMX, the local monitoring cache and UI polling are not +required. Collection failures are isolated from the assessment and reported by the collection health meters. +The first failure since collector start also logs a warning containing only the exception class, without its +message, stack trace or event payload; subsequent failures only increment the error count. + +[cols="2,3",options="header"] +|=== +|Micrometer name |Meaning +|`camel.semantic.evaluations` |Counter by execution `status`: `success`, `failed`, or `not_executed`. +|`camel.semantic.duration` |Timer recording each available execution duration, including failures. Skipped or +missing durations are not zero-duration observations. Percentile histograms use the original durations. +|`camel.semantic.result.availability` |Successful assessments by `availability`: `available` or `unavailable`. +|`camel.semantic.results` |Counter by returned `answer`. Boolean values are final answers, without another +probability threshold. Choice counts the selected answer, not probability entries. Classification counts each +selected label once per assessment, so membership percentages need not sum to 100%. +|`camel.semantic.classification.empty` |Successful classification results with no selected labels. +|`camel.semantic.score.observations` |Count of available finite numeric score observations. +|`camel.semantic.score.sum`, `.minimum`, `.maximum` |Gauges preserving signed, fractional scores since collector +start. The sum can decrease; do not treat it as a monotonic counter. +|`camel.semantic.score.distribution` |Cumulative counters with inclusive `le` bounds supplied to the constructor and a +`+Inf` bucket. Bounds describe numeric values, not severity or policy decisions. +|`camel.semantic.collection.loss` |Counter by fixed `reason`: `population_limit`, `answer_limit`, `invalid_event`, +or `unsupported_measurement`. Loss is excluded from the corresponding result distribution. +|`camel.semantic.collection.errors` |Number of metric collection failures since collector start. +|=== + +Assessment meters have `camelContext`, `expert`, `definition`, `operation`, `origin` and `result.type` tags. +Missing identity is the empty string; direct calls remain distinguishable by origin and the absence of a definition. +The listener retains at most 100 distinct combinations and 64 answer labels of at most 256 characters per combination. +Score histograms accept at most 64 finite, strictly increasing bounds; an empty array keeps count, sum, minimum, +maximum and the overflow bucket. Configure identical bounds on instances you aggregate together. +Each population can have both successful and failed duration timers. Default percentile histograms can add about +70 bucket series per timer: the 100-population limit alone can therefore allow roughly 14,000 duration bucket +series. Configure the registry's meter filter before registering the listener to choose fewer boundaries, for example: + +[source,java] +---- +import java.time.Duration; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; + +registry.config().meterFilter(new MeterFilter() { + @Override + public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) { + if (id.getName().equals("camel.semantic.duration")) { + return DistributionStatisticConfig.builder().percentilesHistogram(false) + .serviceLevelObjectives(Duration.ofMillis(10).toNanos(), Duration.ofMillis(100).toNanos(), + Duration.ofSeconds(1).toNanos(), Duration.ofSeconds(10).toNanos()) + .build().merge(config); + } + return config; + } +}); +---- + +This example exports five duration buckets per timer in Prometheus, including `+Inf`, while retaining every +observation in the count and sum. Choose boundaries for your latency goals: fewer buckets reduce percentile +resolution. The `camelContext` tag matches Camel's other Micrometer metrics. + +Prompts, input, interpretation, exception messages and per-execution identifiers are never metric labels. +Choose stable expert/definition names and result labels. Bounds limit retained series, but collection loss still +means that a distribution may cover only part of the evaluated population. Collection error counts do not prove +that every event was delivered: the shared AI notification contract remains synchronous and best effort. + +== Expert-published scalar gauges + +The same listener accepts `SemanticMonitoringEvent` snapshots with `kind=gauge`, a single finite numeric +`values.value`, and an explicit unit of `1`, `ratio`, `bytes`, `seconds`, `milliseconds`, `nanoseconds`, `count`, +`requests` or `assessments`. If supplied, `availability` must be `available`. It exports +`camel.semantic.expert.value` with `source`, `measurement` (the snapshot ID), `unit` and the supplied +`expert`, `definition`, `operation`, `provider`, `taskId`, `taskVersion`, `revision` dimensions. Missing dimensions +are empty strings. Other dimension keys are rejected, including correlation and message identifiers. + +Values replace earlier values for that identity; repeated publication never increments a counter. At most 100 +such identities are retained, independently of assessment populations. The producer owns meaning and update cadence. +The last value remains until the next publication or collector shutdown; scraping does not refresh it. A publication +with an unavailable or invalid value marks an existing gauge as `NaN`, rather than continuing to expose its old value. +Only published snapshots are consumed: the listener does not call `SemanticMetricSource.snapshots()` or perform I/O +while an assessment completes. Unsupported kinds, units, shapes or dimensions increment `unsupported_measurement`; +aggregated counts are not guessed to be deltas, and snapshot means are never converted to latency observations. + +== Prometheus queries and lifecycle + +Expose the supplied registry through the runtime's normal metrics endpoint. Prometheus scrapes that endpoint and +owns time-series retention; it does not depend on the Camel TUI being connected. Select a scrape interval that fits +your operational needs (for example 15 seconds) and use a rate window containing several scrapes. +The shared listener does not provide automatic Main, Spring Boot or Quarkus registration, a Prometheus query client, +historical TUI views or dashboards. Runtime-specific examples and Quarkus native validation are follow-up work. + +For a Prometheus registry, dots become underscores and counters gain `_total`. These example backend queries aggregate +across instances while retaining the selected expert or definition grouping: + +[source,promql] +---- +# Executed assessment rate per expert; skipped members are excluded. +sum by (expert) (rate(camel_semantic_evaluations_total{status=~"success|failed"}[5m])) + +# Failed execution rate per expert and definition. +sum by (expert, definition) (rate(camel_semantic_evaluations_total{status="failed"}[5m])) + +# Returned-answer distribution per expert and definition. +sum by (expert, definition, result_type, answer) (rate(camel_semantic_results_total[5m])) + +# 95th percentile from actual duration buckets, aggregated across instances. +histogram_quantile(0.95, + sum by (expert, le) (rate(camel_semantic_duration_seconds_bucket[5m]))) + +# Available score distribution using application-declared bounds. +sum by (expert, definition, le) (rate(camel_semantic_score_distribution_total[5m])) + +# Counter resets observed on individual application series. +resets(camel_semantic_evaluations_total[1h]) + +# Collection exclusions over the observation window. +sum by (reason) (increase(camel_semantic_collection_loss_total[5m])) +---- + +Apply `rate` or `increase` before summing across instances so resets are handled per series. A restart resets these +counters; Prometheus history remains in Prometheus. The first scrape after startup cannot reconstruct evaluations +that happened before a counter's first observed sample. Backend labels such as `job` and `instance` distinguish +application instances; they are supplied by the scrape configuration, not unique evaluation IDs. + +Exemplars depend on the runtime's tracing context, registry and exposition format. This listener neither fabricates +trace context nor promises an exemplar for every assessment, particularly for detached or asynchronous completions. +Historical investigations may query independently retained audit records for a matching `[since, until)` interval, +definition/operation and typed result. Audit retention and metric observations can cover different populations. diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties index 350ddcf3fa5d..b4e6e04b1b80 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties @@ -53,6 +53,7 @@ resilience4j-micrometer resourceresolver-github rxjava sanitizer +semantic-micrometer semantic-openai semantic-typesafe-ai semantic-wolf-defender diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/semantic-micrometer.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/semantic-micrometer.json new file mode 100644 index 000000000000..fae2d80a5032 --- /dev/null +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others/semantic-micrometer.json @@ -0,0 +1,15 @@ +{ + "other": { + "kind": "other", + "name": "semantic-micrometer", + "title": "Semantic Micrometer", + "description": "Export semantic assessments and expert measurements with Micrometer", + "deprecated": false, + "firstVersion": "4.24.0", + "label": "ai,monitoring", + "supportLevel": "Preview", + "groupId": "org.apache.camel", + "artifactId": "camel-semantic-micrometer", + "version": "4.24.0-SNAPSHOT" + } +} diff --git a/components/camel-ai/camel-semantic/pom.xml b/components/camel-ai/camel-semantic-micrometer/pom.xml similarity index 55% copy from components/camel-ai/camel-semantic/pom.xml copy to components/camel-ai/camel-semantic-micrometer/pom.xml index 508b8c70d295..a8a4ade8b22b 100644 --- a/components/camel-ai/camel-semantic/pom.xml +++ b/components/camel-ai/camel-semantic-micrometer/pom.xml @@ -25,42 +25,23 @@ <artifactId>camel-ai-parent</artifactId> <version>4.24.0-SNAPSHOT</version> </parent> - <artifactId>camel-semantic</artifactId> - <name>Camel :: AI :: Semantic Evaluation</name> - <description>Invoke named evaluations of message content to produce boolean decisions, categories, scores and label sets through provider adapters</description> + <artifactId>camel-semantic-micrometer</artifactId> + <name>Camel :: AI :: Semantic Micrometer</name> + <description>Export semantic assessments and expert measurements with Micrometer</description> + <properties> + <firstVersion>4.24.0</firstVersion> + <title>Semantic Micrometer</title> + <label>ai,monitoring</label> + <supportLevel>Preview</supportLevel> + </properties> <dependencies> - <dependency> - <groupId>io.micrometer</groupId> - <artifactId>micrometer-core</artifactId> - <optional>true</optional> - </dependency> - <dependency> - <groupId>io.micrometer</groupId> - <artifactId>micrometer-registry-prometheus</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.apache.camel</groupId> - <artifactId>camel-support</artifactId> - </dependency> - <dependency> - <groupId>org.apache.camel</groupId> - <artifactId>camel-core-languages</artifactId> - </dependency> - <dependency> - <groupId>org.apache.camel</groupId> - <artifactId>camel-core-model</artifactId> - <optional>true</optional> - </dependency> <dependency> <groupId>org.apache.camel</groupId> - <artifactId>camel-xml-io</artifactId> - <optional>true</optional> + <artifactId>camel-semantic</artifactId> </dependency> <dependency> <groupId>org.apache.camel</groupId> - <artifactId>camel-yaml-dsl-common</artifactId> - <optional>true</optional> + <artifactId>camel-micrometer</artifactId> </dependency> <dependency> <groupId>org.apache.camel</groupId> @@ -68,23 +49,13 @@ <scope>test</scope> </dependency> <dependency> - <groupId>org.apache.camel</groupId> - <artifactId>camel-xml-io-dsl</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.apache.camel</groupId> - <artifactId>camel-main</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.apache.camel</groupId> - <artifactId>camel-management</artifactId> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> <scope>test</scope> </dependency> <dependency> - <groupId>org.assertj</groupId> - <artifactId>assertj-core</artifactId> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> <scope>test</scope> </dependency> </dependencies> diff --git a/components/camel-ai/camel-semantic-micrometer/src/generated/resources/META-INF/services/org/apache/camel/other.properties b/components/camel-ai/camel-semantic-micrometer/src/generated/resources/META-INF/services/org/apache/camel/other.properties new file mode 100644 index 000000000000..63614db73621 --- /dev/null +++ b/components/camel-ai/camel-semantic-micrometer/src/generated/resources/META-INF/services/org/apache/camel/other.properties @@ -0,0 +1,7 @@ +# Generated by camel build tools - do NOT edit this file! +name=semantic-micrometer +groupId=org.apache.camel +artifactId=camel-semantic-micrometer +version=4.24.0-SNAPSHOT +projectName=Camel :: AI :: Semantic Micrometer +projectDescription=Export semantic assessments and expert measurements with Micrometer diff --git a/components/camel-ai/camel-semantic-micrometer/src/generated/resources/semantic-micrometer.json b/components/camel-ai/camel-semantic-micrometer/src/generated/resources/semantic-micrometer.json new file mode 100644 index 000000000000..fae2d80a5032 --- /dev/null +++ b/components/camel-ai/camel-semantic-micrometer/src/generated/resources/semantic-micrometer.json @@ -0,0 +1,15 @@ +{ + "other": { + "kind": "other", + "name": "semantic-micrometer", + "title": "Semantic Micrometer", + "description": "Export semantic assessments and expert measurements with Micrometer", + "deprecated": false, + "firstVersion": "4.24.0", + "label": "ai,monitoring", + "supportLevel": "Preview", + "groupId": "org.apache.camel", + "artifactId": "camel-semantic-micrometer", + "version": "4.24.0-SNAPSHOT" + } +} diff --git a/components/camel-ai/camel-semantic-micrometer/src/main/docs/semantic-micrometer.adoc b/components/camel-ai/camel-semantic-micrometer/src/main/docs/semantic-micrometer.adoc new file mode 100644 index 000000000000..c3d175dab600 --- /dev/null +++ b/components/camel-ai/camel-semantic-micrometer/src/main/docs/semantic-micrometer.adoc @@ -0,0 +1,177 @@ += Semantic Micrometer Component +:doctitle: Semantic Micrometer +:shortname: semantic-micrometer +:artifactid: camel-semantic-micrometer +:description: Export semantic assessments and expert measurements with Micrometer +:since: 4.24 +:supportlevel: Preview +:tabs-sync-option: + +*Since Camel {since}* + +This module exports typed assessment completion events and supported expert measurements from the +xref:languages:semantic-language.adoc[Semantic language] to Micrometer. Add one dependency to bring +both `camel-semantic` and `camel-micrometer` transitively, using the same version as Camel core: + +[source,xml] +---- +<dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-semantic-micrometer</artifactId> + <version>x.x.x</version> +</dependency> +---- + +`camel-semantic` itself has no Micrometer dependency. This integration has no routing endpoint of its own; +adding the dependency does not automatically register a collector. Prometheus is not a runtime dependency +of this module: supply the registry implementation used by your application. + +== Register the collector + +Register one listener before starting the context, passing the application's existing `MeterRegistry`: + +[source,java] +---- +import org.apache.camel.semantic.micrometer.MicrometerSemanticMetrics; + +// registry is the application's existing Micrometer MeterRegistry. +// Optional, increasing score bucket bounds may include negative values. +context.getManagementStrategy().addEventNotifier( + new MicrometerSemanticMetrics(registry, -1, 0, 0.5, 1, 2, 5)); +---- + +The listener does not create or close a registry or install an HTTP server. The application/runtime owns +registry configuration, HTTP exposure, authentication and the registry lifecycle. Give contexts sharing a registry +distinct names. Registering more than one listener in a context fails at startup to prevent double counting. +Stop removes meters created by the listener; restarting starts new counters. Existing application meters remain. + +Each completed assessment member is counted once, including individual batch members. Starts, request outcomes +and locally aggregated `typed_results` snapshots are not counted again. Provider attempts/retries are not inferred +from semantic completions. Audit capture, input capture, JMX, the local monitoring cache and UI polling are not +required. Collection failures are isolated from the assessment and reported by the collection health meters. +The first failure since collector start also logs a warning containing only the exception class, without its +message, stack trace or event payload; subsequent failures only increment the error count. + +[cols="2,3",options="header"] +|=== +|Micrometer name |Meaning +|`camel.semantic.evaluations` |Counter by execution `status`: `success`, `failed`, or `not_executed`. +|`camel.semantic.duration` |Timer recording each available execution duration, including failures. Skipped or +missing durations are not zero-duration observations. Percentile histograms use the original durations. +|`camel.semantic.result.availability` |Successful assessments by `availability`: `available` or `unavailable`. +|`camel.semantic.results` |Counter by returned `answer`. Boolean values are final answers, without another +probability threshold. Choice counts the selected answer, not probability entries. Classification counts each +selected label once per assessment, so membership percentages need not sum to 100%. +|`camel.semantic.classification.empty` |Successful classification results with no selected labels. +|`camel.semantic.score.observations` |Count of available finite numeric score observations. +|`camel.semantic.score.sum`, `.minimum`, `.maximum` |Gauges preserving signed, fractional scores since collector +start. The sum can decrease; do not treat it as a monotonic counter. +|`camel.semantic.score.distribution` |Cumulative counters with inclusive `le` bounds supplied to the constructor and a +`+Inf` bucket. Bounds describe numeric values, not severity or policy decisions. +|`camel.semantic.collection.loss` |Counter by fixed `reason`: `population_limit`, `answer_limit`, `invalid_event`, +or `unsupported_measurement`. Loss is excluded from the corresponding result distribution. +|`camel.semantic.collection.errors` |Number of metric collection failures since collector start. +|=== + +Assessment meters have `camelContext`, `expert`, `definition`, `operation`, `origin` and `result.type` tags. +Missing identity is the empty string; direct calls remain distinguishable by origin and the absence of a definition. +The listener retains at most 100 distinct combinations and 64 answer labels of at most 256 characters per combination. +Score histograms accept at most 64 finite, strictly increasing bounds; an empty array keeps count, sum, minimum, +maximum and the overflow bucket. Configure identical bounds on instances you aggregate together. +Each population can have both successful and failed duration timers. Default percentile histograms can add about +70 bucket series per timer: the 100-population limit alone can therefore allow roughly 14,000 duration bucket +series. Configure the registry's meter filter before registering the listener to choose fewer boundaries, for example: + +[source,java] +---- +import java.time.Duration; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; + +registry.config().meterFilter(new MeterFilter() { + @Override + public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) { + if (id.getName().equals("camel.semantic.duration")) { + return DistributionStatisticConfig.builder().percentilesHistogram(false) + .serviceLevelObjectives(Duration.ofMillis(10).toNanos(), Duration.ofMillis(100).toNanos(), + Duration.ofSeconds(1).toNanos(), Duration.ofSeconds(10).toNanos()) + .build().merge(config); + } + return config; + } +}); +---- + +This example exports five duration buckets per timer in Prometheus, including `+Inf`, while retaining every +observation in the count and sum. Choose boundaries for your latency goals: fewer buckets reduce percentile +resolution. The `camelContext` tag matches Camel's other Micrometer metrics. + +Prompts, input, interpretation, exception messages and per-execution identifiers are never metric labels. +Choose stable expert/definition names and result labels. Bounds limit retained series, but collection loss still +means that a distribution may cover only part of the evaluated population. Collection error counts do not prove +that every event was delivered: the shared AI notification contract remains synchronous and best effort. + +== Expert-published scalar gauges + +The same listener accepts `SemanticMonitoringEvent` snapshots with `kind=gauge`, a single finite numeric +`values.value`, and an explicit unit of `1`, `ratio`, `bytes`, `seconds`, `milliseconds`, `nanoseconds`, `count`, +`requests` or `assessments`. If supplied, `availability` must be `available`. It exports +`camel.semantic.expert.value` with `source`, `measurement` (the snapshot ID), `unit` and the supplied +`expert`, `definition`, `operation`, `provider`, `taskId`, `taskVersion`, `revision` dimensions. Missing dimensions +are empty strings. Other dimension keys are rejected, including correlation and message identifiers. + +Values replace earlier values for that identity; repeated publication never increments a counter. At most 100 +such identities are retained, independently of assessment populations. The producer owns meaning and update cadence. +The last value remains until the next publication or collector shutdown; scraping does not refresh it. A publication +with an unavailable or invalid value marks an existing gauge as `NaN`, rather than continuing to expose its old value. +Only published snapshots are consumed: the listener does not call `SemanticMetricSource.snapshots()` or perform I/O +while an assessment completes. Unsupported kinds, units, shapes or dimensions increment `unsupported_measurement`; +aggregated counts are not guessed to be deltas, and snapshot means are never converted to latency observations. + +== Prometheus queries and lifecycle + +Expose the supplied registry through the runtime's normal metrics endpoint. Prometheus scrapes that endpoint and +owns time-series retention; it does not depend on the Camel TUI being connected. Select a scrape interval that fits +your operational needs (for example 15 seconds) and use a rate window containing several scrapes. +The shared listener does not provide automatic Main, Spring Boot or Quarkus registration, a Prometheus query client, +historical TUI views or dashboards. Runtime-specific examples and Quarkus native validation are follow-up work. + +For a Prometheus registry, dots become underscores and counters gain `_total`. These example backend queries aggregate +across instances while retaining the selected expert or definition grouping: + +[source,promql] +---- +# Executed assessment rate per expert; skipped members are excluded. +sum by (expert) (rate(camel_semantic_evaluations_total{status=~"success|failed"}[5m])) + +# Failed execution rate per expert and definition. +sum by (expert, definition) (rate(camel_semantic_evaluations_total{status="failed"}[5m])) + +# Returned-answer distribution per expert and definition. +sum by (expert, definition, result_type, answer) (rate(camel_semantic_results_total[5m])) + +# 95th percentile from actual duration buckets, aggregated across instances. +histogram_quantile(0.95, + sum by (expert, le) (rate(camel_semantic_duration_seconds_bucket[5m]))) + +# Available score distribution using application-declared bounds. +sum by (expert, definition, le) (rate(camel_semantic_score_distribution_total[5m])) + +# Counter resets observed on individual application series. +resets(camel_semantic_evaluations_total[1h]) + +# Collection exclusions over the observation window. +sum by (reason) (increase(camel_semantic_collection_loss_total[5m])) +---- + +Apply `rate` or `increase` before summing across instances so resets are handled per series. A restart resets these +counters; Prometheus history remains in Prometheus. The first scrape after startup cannot reconstruct evaluations +that happened before a counter's first observed sample. Backend labels such as `job` and `instance` distinguish +application instances; they are supplied by the scrape configuration, not unique evaluation IDs. + +Exemplars depend on the runtime's tracing context, registry and exposition format. This listener neither fabricates +trace context nor promises an exemplar for every assessment, particularly for detached or asynchronous completions. +Historical investigations may query independently retained audit records for a matching `[since, until)` interval, +definition/operation and typed result. Audit retention and metric observations can cover different populations. diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetrics.java b/components/camel-ai/camel-semantic-micrometer/src/main/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetrics.java similarity index 99% rename from components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetrics.java rename to components/camel-ai/camel-semantic-micrometer/src/main/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetrics.java index 988d99282729..589e1e8bf3ac 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetrics.java +++ b/components/camel-ai/camel-semantic-micrometer/src/main/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetrics.java @@ -40,6 +40,8 @@ import org.apache.camel.support.EventNotifierSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.apache.camel.component.micrometer.MicrometerConstants.CAMEL_CONTEXT_TAG; + /** * Optional Micrometer instrumentation of completed semantic assessment members. Register one notifier with the Camel * management strategy, supplying the application's registry. The registry remains application-owned; this notifier @@ -49,7 +51,6 @@ import org.slf4j.LoggerFactory; */ public final class MicrometerSemanticMetrics extends EventNotifierSupport { private static final Logger LOG = LoggerFactory.getLogger(MicrometerSemanticMetrics.class); - private static final String CAMEL_CONTEXT_TAG = "camelContext"; private static final int MAX_POPULATIONS = 100; private static final int MAX_ANSWERS = 64; private static final Set<String> STATUSES = Set.of("success", "failed", "not_executed"); diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetricsTest.java b/components/camel-ai/camel-semantic-micrometer/src/test/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetricsTest.java similarity index 90% rename from components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetricsTest.java rename to components/camel-ai/camel-semantic-micrometer/src/test/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetricsTest.java index a1d1a3a8cb09..7abf5f1e0adc 100644 --- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetricsTest.java +++ b/components/camel-ai/camel-semantic-micrometer/src/test/java/org/apache/camel/semantic/micrometer/MicrometerSemanticMetricsTest.java @@ -16,9 +16,6 @@ */ package org.apache.camel.semantic.micrometer; -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -60,35 +57,12 @@ import org.apache.logging.log4j.core.Logger; import org.apache.logging.log4j.core.appender.AbstractAppender; import org.apache.logging.log4j.core.config.Property; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.within; class MicrometerSemanticMetricsTest { - @Test - void semanticEvaluationDoesNotRequireMicrometerOnTheApplicationClasspath(@TempDir Path temporary) throws Exception { - List<String> classpath = new ArrayList<>(); - for (String entry : System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")) - .split(File.pathSeparator)) { - if (!Path.of(entry).getFileName().toString().startsWith("micrometer-")) { - classpath.add(entry); - } - } - Path output = temporary.resolve("without-micrometer.log"); - var process = new ProcessBuilder( - Path.of(System.getProperty("java.home"), "bin", "java").toString(), - "-cp", String.join(File.pathSeparator, classpath), WithoutMicrometer.class.getName()) - .redirectErrorStream(true).redirectOutput(output.toFile()).start(); - try { - assertThat(process.waitFor(30, TimeUnit.SECONDS)).as("isolated semantic evaluation completed").isTrue(); - assertThat(process.exitValue()).as(Files.readString(output)).isZero(); - } finally { - process.destroyForcibly(); - } - } - @Test void rejectsInvalidHistogramConfiguration() { var registry = new SimpleMeterRegistry(); @@ -426,27 +400,4 @@ class MicrometerSemanticMetricsTest { } } - public static class WithoutMicrometer { - public static void main(String[] args) throws Exception { - try { - Class.forName("io.micrometer.core.instrument.MeterRegistry"); - throw new IllegalStateException("Micrometer unexpectedly present"); - } catch (ClassNotFoundException expected) { - // This process deliberately omits every Micrometer artifact. - } - try (var context = new DefaultCamelContext()) { - context.disableJMX(); - context.getRegistry().bind("expert", new BooleanExpert()); - context.start(); - var language = (SemanticLanguage) context.resolveLanguage("semantic"); - Object result = language.evaluate(new SemanticEvaluation("containsText", "expert", null, Map.of()), "hello") - .getValue(); - if (!Boolean.TRUE.equals(result)) { - throw new IllegalStateException("Semantic evaluation failed without Micrometer"); - } - } catch (Exception failure) { - throw new IllegalStateException(failure); - } - } - } } diff --git a/components/camel-ai/camel-semantic/pom.xml b/components/camel-ai/camel-semantic/pom.xml index 508b8c70d295..b3571086b700 100644 --- a/components/camel-ai/camel-semantic/pom.xml +++ b/components/camel-ai/camel-semantic/pom.xml @@ -29,16 +29,6 @@ <name>Camel :: AI :: Semantic Evaluation</name> <description>Invoke named evaluations of message content to produce boolean decisions, categories, scores and label sets through provider adapters</description> <dependencies> - <dependency> - <groupId>io.micrometer</groupId> - <artifactId>micrometer-core</artifactId> - <optional>true</optional> - </dependency> - <dependency> - <groupId>io.micrometer</groupId> - <artifactId>micrometer-registry-prometheus</artifactId> - <scope>test</scope> - </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-support</artifactId> diff --git a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc index 445e1ccc4955..5175ac896296 100644 --- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc +++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc @@ -1507,7 +1507,7 @@ camel semantic monitoring my-app --source=departmentMetrics The Semantic *Monitoring* sibling, `tui_get_monitoring`, `semantic-monitoring` console and CLI read this same current-value cache. The TUI offers expert/definition grouping, filters and an inspector for counts, durations, distributions, meaning, observation times and collection health. It has no historical-period, reference or -comparison controls. Pausing affects the display only. The optional Micrometer listener below exports +comparison controls. Pausing affects the display only. The optional Micrometer integration exports measurements independently of this viewer. Runtime-specific automatic registration and scrape endpoint integration are separate from the shared listener. @@ -1520,155 +1520,8 @@ Facades are removed at context shutdown. Monitoring also works with JMX disabled === Optional Micrometer instrumentation -`MicrometerSemanticMetrics` consumes typed assessment completion events directly. Add `io.micrometer:micrometer-core` -to your application, or use the registry dependency supplied by your runtime. Micrometer is an optional dependency -of `camel-semantic`; applications that do not register this listener do not need it. Prometheus is not a runtime -dependency of the listener. - -Register one listener before starting the context, passing the application's existing `MeterRegistry`: - -[source,java] ----- -import org.apache.camel.semantic.micrometer.MicrometerSemanticMetrics; - -// registry is the application's existing Micrometer MeterRegistry. -// Optional, increasing score bucket bounds may include negative values. -context.getManagementStrategy().addEventNotifier( - new MicrometerSemanticMetrics(registry, -1, 0, 0.5, 1, 2, 5)); ----- - -The listener does not create or close a registry or install an HTTP server. The application/runtime owns -registry configuration, HTTP exposure, authentication and the registry lifecycle. Give contexts sharing a registry -distinct names. Registering more than one listener in a context fails at startup to prevent double counting. -Stop removes meters created by the listener; restarting starts new counters. Existing application meters remain. - -Each completed assessment member is counted once, including individual batch members. Starts, request outcomes -and locally aggregated `typed_results` snapshots are not counted again. Provider attempts/retries are not inferred -from semantic completions. Audit capture, input capture, JMX, the local monitoring cache and UI polling are not -required. Collection failures are isolated from the assessment and reported by the collection health meters. -The first failure since collector start also logs a warning containing only the exception class, without its -message, stack trace or event payload; subsequent failures only increment the error count. - -[cols="2,3",options="header"] -|=== -|Micrometer name |Meaning -|`camel.semantic.evaluations` |Counter by execution `status`: `success`, `failed`, or `not_executed`. -|`camel.semantic.duration` |Timer recording each available execution duration, including failures. Skipped or -missing durations are not zero-duration observations. Percentile histograms use the original durations. -|`camel.semantic.result.availability` |Successful assessments by `availability`: `available` or `unavailable`. -|`camel.semantic.results` |Counter by returned `answer`. Boolean values are final answers, without another -probability threshold. Choice counts the selected answer, not probability entries. Classification counts each -selected label once per assessment, so membership percentages need not sum to 100%. -|`camel.semantic.classification.empty` |Successful classification results with no selected labels. -|`camel.semantic.score.observations` |Count of available finite numeric score observations. -|`camel.semantic.score.sum`, `.minimum`, `.maximum` |Gauges preserving signed, fractional scores since collector -start. The sum can decrease; do not treat it as a monotonic counter. -|`camel.semantic.score.distribution` |Cumulative counters with inclusive `le` bounds supplied to the constructor and a -`+Inf` bucket. Bounds describe numeric values, not severity or policy decisions. -|`camel.semantic.collection.loss` |Counter by fixed `reason`: `population_limit`, `answer_limit`, `invalid_event`, -or `unsupported_measurement`. Loss is excluded from the corresponding result distribution. -|`camel.semantic.collection.errors` |Number of metric collection failures since collector start. -|=== - -Assessment meters have `camelContext`, `expert`, `definition`, `operation`, `origin` and `result.type` tags. -Missing identity is the empty string; direct calls remain distinguishable by origin and the absence of a definition. -The listener retains at most 100 distinct combinations and 64 answer labels of at most 256 characters per combination. -Score histograms accept at most 64 finite, strictly increasing bounds; an empty array keeps count, sum, minimum, -maximum and the overflow bucket. Configure identical bounds on instances you aggregate together. -Each population can have both successful and failed duration timers. Default percentile histograms can add about -70 bucket series per timer: the 100-population limit alone can therefore allow roughly 14,000 duration bucket -series. Configure the registry's meter filter before registering the listener to choose fewer boundaries, for example: - -[source,java] ----- -import java.time.Duration; - -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.config.MeterFilter; -import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; - -registry.config().meterFilter(new MeterFilter() { - @Override - public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) { - if (id.getName().equals("camel.semantic.duration")) { - return DistributionStatisticConfig.builder().percentilesHistogram(false) - .serviceLevelObjectives(Duration.ofMillis(10).toNanos(), Duration.ofMillis(100).toNanos(), - Duration.ofSeconds(1).toNanos(), Duration.ofSeconds(10).toNanos()) - .build().merge(config); - } - return config; - } -}); ----- - -This example exports five duration buckets per timer in Prometheus, including `+Inf`, while retaining every -observation in the count and sum. Choose boundaries for your latency goals: fewer buckets reduce percentile -resolution. The `camelContext` tag matches Camel's other Micrometer metrics. - -Prompts, input, interpretation, exception messages and per-execution identifiers are never metric labels. -Choose stable expert/definition names and result labels. Bounds limit retained series, but collection loss still -means that a distribution may cover only part of the evaluated population. Collection error counts do not prove -that every event was delivered: the shared AI notification contract remains synchronous and best effort. - -==== Expert-published scalar gauges - -The same listener accepts `SemanticMonitoringEvent` snapshots with `kind=gauge`, a single finite numeric -`values.value`, and an explicit unit of `1`, `ratio`, `bytes`, `seconds`, `milliseconds`, `nanoseconds`, `count`, -`requests` or `assessments`. If supplied, `availability` must be `available`. It exports -`camel.semantic.expert.value` with `source`, `measurement` (the snapshot ID), `unit` and the supplied -`expert`, `definition`, `operation`, `provider`, `taskId`, `taskVersion`, `revision` dimensions. Missing dimensions -are empty strings. Other dimension keys are rejected, including correlation and message identifiers. - -Values replace earlier values for that identity; repeated publication never increments a counter. At most 100 -such identities are retained, independently of assessment populations. The producer owns meaning and update cadence. -The last value remains until the next publication or collector shutdown; scraping does not refresh it. A publication -with an unavailable or invalid value marks an existing gauge as `NaN`, rather than continuing to expose its old value. -Only published snapshots are consumed: the listener does not call `SemanticMetricSource.snapshots()` or perform I/O -while an assessment completes. Unsupported kinds, units, shapes or dimensions increment `unsupported_measurement`; -aggregated counts are not guessed to be deltas, and snapshot means are never converted to latency observations. - -==== Prometheus queries and lifecycle - -Expose the supplied registry through the runtime's normal metrics endpoint. Prometheus scrapes that endpoint and -owns time-series retention; it does not depend on the Camel TUI being connected. Select a scrape interval that fits -your operational needs (for example 15 seconds) and use a rate window containing several scrapes. -The shared listener does not provide automatic Main, Spring Boot or Quarkus registration, a Prometheus query client, -historical TUI views or dashboards. Runtime-specific examples and Quarkus native validation are follow-up work. - -For a Prometheus registry, dots become underscores and counters gain `_total`. These example backend queries aggregate -across instances while retaining the selected expert or definition grouping: - -[source,promql] ----- -# Executed assessment rate per expert; skipped members are excluded. -sum by (expert) (rate(camel_semantic_evaluations_total{status=~"success|failed"}[5m])) - -# Failed execution rate per expert and definition. -sum by (expert, definition) (rate(camel_semantic_evaluations_total{status="failed"}[5m])) - -# Returned-answer distribution per expert and definition. -sum by (expert, definition, result_type, answer) (rate(camel_semantic_results_total[5m])) - -# 95th percentile from actual duration buckets, aggregated across instances. -histogram_quantile(0.95, - sum by (expert, le) (rate(camel_semantic_duration_seconds_bucket[5m]))) - -# Available score distribution using application-declared bounds. -sum by (expert, definition, le) (rate(camel_semantic_score_distribution_total[5m])) - -# Counter resets observed on individual application series. -resets(camel_semantic_evaluations_total[1h]) - -# Collection exclusions over the observation window. -sum by (reason) (increase(camel_semantic_collection_loss_total[5m])) ----- - -Apply `rate` or `increase` before summing across instances so resets are handled per series. A restart resets these -counters; Prometheus history remains in Prometheus. The first scrape after startup cannot reconstruct evaluations -that happened before a counter's first observed sample. Backend labels such as `job` and `instance` distinguish -application instances; they are supplied by the scrape configuration, not unique evaluation IDs. - -Exemplars depend on the runtime's tracing context, registry and exposition format. This listener neither fabricates -trace context nor promises an exemplar for every assessment, particularly for detached or asynchronous completions. -Historical investigations may query independently retained audit records for a matching `[since, until)` interval, -definition/operation and typed result. Audit retention and metric observations can cover different populations. +Add xref:others:semantic-micrometer.adoc[Semantic Micrometer] (`camel-semantic-micrometer`) to export +assessment counts, durations, typed result distributions and supported expert measurements to the +application's Micrometer registry. The integration brings `camel-semantic` and `camel-micrometer` +transitively and documents registration, metric names, cardinality limits, lifecycle and PromQL queries. +`camel-semantic` itself has no Micrometer dependency. diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticWithoutMicrometerTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticWithoutMicrometerTest.java new file mode 100644 index 000000000000..084d749b1ddb --- /dev/null +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticWithoutMicrometerTest.java @@ -0,0 +1,60 @@ +/* + * 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.semantic; + +import java.util.Map; + +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.language.semantic.SemanticLanguage; +import org.apache.camel.semantic.SemanticExpert.InputType; +import org.apache.camel.semantic.SemanticExpert.ResultType; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SemanticWithoutMicrometerTest { + @Test + void evaluationWorksWithoutMicrometerOnTheModuleClasspath() throws Exception { + assertThatThrownBy(() -> Class.forName("io.micrometer.core.instrument.MeterRegistry")) + .isInstanceOf(ClassNotFoundException.class); + try (var context = new DefaultCamelContext()) { + context.disableJMX(); + context.getRegistry().bind("expert", new BooleanExpert()); + context.start(); + var language = (SemanticLanguage) context.resolveLanguage("semantic"); + assertThat(language.evaluate(new SemanticEvaluation("containsText", "expert", null, Map.of()), "hello") + .getValue()).isEqualTo(true); + } + } + + @SemanticExpert(name = "text", description = "Checks for nonempty text", provider = "local", artifactId = "test", + operations = @SemanticOperation(name = "containsText", description = "Has text", + inputTypes = InputType.TEXT, inputRequirements = "Text", + resultType = ResultType.BOOLEAN, resultMeaning = "Contains text")) + static class BooleanExpert implements SemanticAdapter { + @Override + public void validate(SemanticEvaluation evaluation) { + SemanticCapabilities.from(getClass()).validate(evaluation); + } + + @Override + public SemanticResult evaluate(SemanticEvaluation evaluation, Object state) { + return new SemanticResult(!state.toString().isBlank(), null, Map.of(), null, Map.of()); + } + } +} diff --git a/components/camel-ai/pom.xml b/components/camel-ai/pom.xml index 95c7db233415..51cd19a87d3c 100644 --- a/components/camel-ai/pom.xml +++ b/components/camel-ai/pom.xml @@ -45,6 +45,7 @@ <module>camel-docling</module> <module>camel-huggingface</module> <module>camel-semantic</module> + <module>camel-semantic-micrometer</module> <module>camel-semantic-openai</module> <module>camel-semantic-typesafe-ai</module> <module>camel-typesafe-ai</module> diff --git a/design/semantic-audit.adoc b/design/semantic-audit.adoc index ee569eac24f0..e4c1de0ccb80 100644 --- a/design/semantic-audit.adoc +++ b/design/semantic-audit.adoc @@ -133,6 +133,8 @@ There are no historical-period or baseline controls. Register a bounded number o facades per source, never one MBean per event or answer. JMX-disabled deployments retain the console. CAMEL-25648 adds a shared Micrometer instrumentation/export bridge for Camel Main, Spring Boot and Quarkus. +The bridge lives in `camel-semantic-micrometer`, which depends on `camel-semantic` and `camel-micrometer`; +the semantic runtime publishes its events without depending on Micrometer. It reuses the runtime registry; Prometheus or another registry backend owns monitoring history and queries. Correlation, exchange, breadcrumb and invocation IDs are not metric labels. They remain audit/log/trace context; standard tracing exemplars may supply sampled metric-to-trace references where supported. Audit diff --git a/docs/components/modules/others/examples/json/semantic-micrometer.json b/docs/components/modules/others/examples/json/semantic-micrometer.json new file mode 120000 index 000000000000..50be138d1453 --- /dev/null +++ b/docs/components/modules/others/examples/json/semantic-micrometer.json @@ -0,0 +1 @@ +../../../../../../components/camel-ai/camel-semantic-micrometer/src/generated/resources/semantic-micrometer.json \ No newline at end of file diff --git a/docs/components/modules/others/nav.adoc b/docs/components/modules/others/nav.adoc index 144269846820..eb122b25a6ba 100644 --- a/docs/components/modules/others/nav.adoc +++ b/docs/components/modules/others/nav.adoc @@ -89,6 +89,7 @@ ** xref:salesforce-rest-api.adoc[Salesforce - REST API] ** xref:salesforce-streaming.adoc[Salesforce - Streaming and Pub/Sub] ** xref:sanitizer.adoc[Sanitizer] +** xref:semantic-micrometer.adoc[Semantic Micrometer] ** xref:semantic-openai.adoc[Semantic OpenAI] ** xref:semantic-typesafe-ai.adoc[Semantic TypeSafe AI] ** xref:semantic-wolf-defender.adoc[Semantic Wolf Defender] diff --git a/docs/components/modules/others/pages/semantic-micrometer.adoc b/docs/components/modules/others/pages/semantic-micrometer.adoc new file mode 120000 index 000000000000..465c487ff8b7 --- /dev/null +++ b/docs/components/modules/others/pages/semantic-micrometer.adoc @@ -0,0 +1 @@ +../../../../../components/camel-ai/camel-semantic-micrometer/src/main/docs/semantic-micrometer.adoc \ No newline at end of file diff --git a/parent/pom.xml b/parent/pom.xml index a421f5d81cd4..98225275501e 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -2744,6 +2744,11 @@ <artifactId>camel-semantic</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-semantic-micrometer</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-semantic-openai</artifactId> diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java index 552f6cc72686..3ac9fd2a27b4 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java +++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/MojoHelper.java @@ -42,7 +42,8 @@ public final class MojoHelper { dir.resolve("camel-ai-prompt"), dir.resolve("camel-ai-resource"), dir.resolve("camel-ai-tool"), dir.resolve("camel-chatscript"), dir.resolve("camel-djl"), dir.resolve("camel-huggingface"), - dir.resolve("camel-semantic"), dir.resolve("camel-semantic-openai"), + dir.resolve("camel-semantic"), dir.resolve("camel-semantic-micrometer"), + dir.resolve("camel-semantic-openai"), dir.resolve("camel-semantic-typesafe-ai"), dir.resolve("camel-semantic-wolf-defender"), dir.resolve("camel-typesafe-ai"), dir.resolve("camel-langchain4j-agent"), dir.resolve("camel-langchain4j-chat"),
