davsclaus commented on code in PR #25337: URL: https://github.com/apache/camel/pull/25337#discussion_r3727904650
########## components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservability.java: ########## @@ -0,0 +1,90 @@ +/* + * 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.component.ai.observability; + +import java.util.Optional; + +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; + +/** + * Entry point for GenAI observability in Camel AI producers. + * <p/> + * The concrete tracing and metrics implementation lives in {@code camel-ai-observability} and is loaded via reflection + * when that module is on the classpath. Without it, calls return a no-op observation. + */ +public final class GenAiObservability { + + private static final String IMPL_CLASS = "org.apache.camel.component.ai.observability.GenAiObservabilityImpl"; + private static final GenAiObservation NOOP = new NoopGenAiObservation(); + + private GenAiObservability() { + } + + /** + * Whether GenAI observability is enabled for the given context. + */ + public static boolean isEnabled(CamelContext camelContext) { + if (camelContext == null) { + return false; + } + Optional<String> property + = camelContext.getPropertiesComponent().resolveProperty(GenAiObservabilityProperties.ENABLED); + if (property.isPresent()) { + return Boolean.parseBoolean(property.get().trim()); + } + return true; + } + + /** + * Starts a GenAI observation for a single LLM client call. Returns a no-op when disabled, when + * {@code camel-ai-observability} is absent, or when no backend is available. + */ + public static GenAiObservation start(Exchange exchange, GenAiObservationContext context) { + if (exchange == null || context == null || !isEnabled(exchange.getContext())) { + return NOOP; + } + CamelContext camelContext = exchange.getContext(); + Class<?> implClass = camelContext.getClassResolver().resolveClass(IMPL_CLASS); Review Comment: **[Medium] Uncached reflection on every LLM call** `resolveClass(IMPL_CLASS)` + `getMethod("start", ...).invoke()` runs on every `start()` call. `GenAiObservabilityImpl.resolveMetricsBackend()` similarly does `Class.forName()` + constructor reflection per call, creating a new `GenAiMicrometerSupport` instance each time. For agentic loops with multiple LLM iterations per exchange, this adds avoidable overhead. Consider caching the resolved class and method handle per `CamelContext` (e.g., in a `ConcurrentHashMap<CamelContext, MethodHandle>` or via `CamelContext.getExtension()`). ########## components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiModelResolver.java: ########## @@ -0,0 +1,125 @@ +/* + * 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.component.ai.observability; + +import dev.langchain4j.model.ModelProvider; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.embedding.EmbeddingModel; + +/** + * Resolves GenAI provider and model metadata from LangChain4j model beans. + */ +public final class GenAiModelResolver { + + private static final String UNKNOWN = "unknown"; + + private GenAiModelResolver() { + } + + public static String resolveSystem(Object model) { + if (model == null) { + return UNKNOWN; + } + if (model instanceof ChatModel chatModel) { + return mapProvider(chatModel.provider()); + } + if (model instanceof EmbeddingModel embeddingModel) { + return mapProvider(embeddingModel.provider()); + } + return resolveSystemFromPackage(model.getClass().getPackageName()); + } + + public static String resolveModelName(Object model) { + if (model == null) { + return UNKNOWN; + } + if (model instanceof ChatModel chatModel) { + String modelName = chatModel.defaultRequestParameters().modelName(); + if (modelName != null && !modelName.isBlank()) { + return modelName; + } + } + if (model instanceof EmbeddingModel embeddingModel) { + String modelName = embeddingModel.modelName(); + if (modelName != null && !modelName.isBlank()) { + return modelName; + } + } + return UNKNOWN; + } + + /** + * Resolves the response model from a LangChain4j {@link ChatResponse}, falling back when absent. + */ + public static String resolveResponseModelName(ChatResponse chatResponse, String fallback) { + if (chatResponse == null) { + return fallback; + } + String modelName = chatResponse.modelName(); + return modelName != null && !modelName.isBlank() ? modelName : fallback; + } + + private static String mapProvider(ModelProvider provider) { + if (provider == null) { + return UNKNOWN; + } + return switch (provider) { + case OPEN_AI -> "openai"; + case ANTHROPIC -> "anthropic"; + case OLLAMA -> "ollama"; + case AZURE_OPEN_AI -> "azure.ai.openai"; + case GOOGLE_VERTEX_AI_GEMINI, GOOGLE_VERTEX_AI_ANTHROPIC -> "gcp.vertex_ai"; + case GOOGLE_AI_GEMINI, GOOGLE_GENAI -> "google"; Review Comment: **[Medium] Google AI system name inconsistency** `mapProvider()` maps `GOOGLE_AI_GEMINI` / `GOOGLE_GENAI` → `"google"`, but `resolveSystemFromPackage()` (line 111–112) maps `dev.langchain4j.model.google` → `"gcp.vertex_ai"`. The same Google AI Gemini model returns different system names depending on which resolution path is taken. Should the package fallback for `dev.langchain4j.model.google` return `"google"` instead of `"gcp.vertex_ai"`? ########## components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java: ########## @@ -655,6 +707,32 @@ private static String getFinishReasonString(ChatCompletion.Choice choice) { .orElse("stop"); } + private ChatCompletion createChatCompletion(Exchange exchange, ChatCompletionCreateParams params) { + String requestModel = params.model().toString(); + GenAiObservationContext observationContext = GenAiObservationContext.builder() + .operationName(GenAiOperationName.CHAT) + .system("openai") + .requestModel(requestModel) + .componentScheme("openai") + .build(); + GenAiObservation observation = GenAiObservability.start(exchange, observationContext); + try { + ChatCompletion response = getEndpoint().getClient().chat().completions().create(params); + CompletionUsage usage = response.usage().orElse(null); + observation.recordSuccess(GenAiUsage.of( + usage != null ? (int) usage.promptTokens() : null, Review Comment: **[Low] `long` → `int` narrowing cast** `usage.promptTokens()` and `usage.completionTokens()` return `long`, but are cast to `(int)` here (and at lines 660–661 in the streaming path). While overflow is unlikely in practice, consider either: - Typing `GenAiUsage.inputTokens` / `outputTokens` as `Long` (or `long`) to avoid the lossy cast, or - Using `Math.toIntExact()` for a fail-fast on overflow instead of silent truncation ########## components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservability.java: ########## @@ -0,0 +1,90 @@ +/* + * 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.component.ai.observability; + +import java.util.Optional; + +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; + +/** + * Entry point for GenAI observability in Camel AI producers. + * <p/> + * The concrete tracing and metrics implementation lives in {@code camel-ai-observability} and is loaded via reflection + * when that module is on the classpath. Without it, calls return a no-op observation. + */ +public final class GenAiObservability { + + private static final String IMPL_CLASS = "org.apache.camel.component.ai.observability.GenAiObservabilityImpl"; + private static final GenAiObservation NOOP = new NoopGenAiObservation(); + + private GenAiObservability() { + } + + /** + * Whether GenAI observability is enabled for the given context. + */ + public static boolean isEnabled(CamelContext camelContext) { + if (camelContext == null) { + return false; + } + Optional<String> property + = camelContext.getPropertiesComponent().resolveProperty(GenAiObservabilityProperties.ENABLED); + if (property.isPresent()) { + return Boolean.parseBoolean(property.get().trim()); + } + return true; + } + + /** + * Starts a GenAI observation for a single LLM client call. Returns a no-op when disabled, when + * {@code camel-ai-observability} is absent, or when no backend is available. + */ + public static GenAiObservation start(Exchange exchange, GenAiObservationContext context) { + if (exchange == null || context == null || !isEnabled(exchange.getContext())) { + return NOOP; + } + CamelContext camelContext = exchange.getContext(); + Class<?> implClass = camelContext.getClassResolver().resolveClass(IMPL_CLASS); + if (implClass == null) { + return NOOP; + } + try { + return (GenAiObservation) implClass.getMethod("start", Exchange.class, GenAiObservationContext.class) + .invoke(null, exchange, context); + } catch (ReflectiveOperationException | LinkageError e) { Review Comment: **[Medium] Silent exception swallowing** This catch block (and the equivalent one in `GenAiObservabilityImpl.resolveMetricsBackend()`) silently discards all errors. If the impl class exists but has a method signature mismatch (e.g., after a version skew between API and impl), failures will be invisible. A `DEBUG` or `TRACE` level log would aid troubleshooting. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
