Skip to content

Commit e73b05a

Browse files
committed
Conditionally generate a bean receiving CDI event to report the guardrail metrics.
1 parent b79d501 commit e73b05a

File tree

2 files changed

+91
-35
lines changed

2 files changed

+91
-35
lines changed

core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/GuardrailObservabilityProcessor.java

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,30 @@
55
import java.util.Optional;
66
import java.util.function.Consumer;
77

8+
import jakarta.enterprise.event.Observes;
9+
import jakarta.inject.Singleton;
10+
811
import org.jboss.jandex.AnnotationInstance;
912
import org.jboss.jandex.AnnotationTransformation;
1013
import org.jboss.jandex.AnnotationTransformation.TransformationContext;
1114
import org.jboss.jandex.IndexView;
1215
import org.jboss.logging.Logger;
1316

14-
import io.quarkiverse.langchain4j.deployment.GuardrailObservabilityProcessorSupport.TransformType;
17+
import dev.langchain4j.observability.api.event.InputGuardrailExecutedEvent;
18+
import dev.langchain4j.observability.api.event.OutputGuardrailExecutedEvent;
1519
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
20+
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
21+
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
1622
import io.quarkus.deployment.Capabilities;
1723
import io.quarkus.deployment.Capability;
1824
import io.quarkus.deployment.annotations.BuildProducer;
1925
import io.quarkus.deployment.annotations.BuildStep;
2026
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
2127
import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem;
28+
import io.quarkus.gizmo.ClassCreator;
29+
import io.quarkus.gizmo.ClassOutput;
30+
import io.quarkus.gizmo.MethodCreator;
31+
import io.quarkus.gizmo.MethodDescriptor;
2232
import io.quarkus.runtime.metrics.MetricsFactory;
2333

2434
/**
@@ -28,12 +38,67 @@
2838
* <p>
2939
* The main capabilities include:
3040
* <ul>
31-
* <li>Applying Micrometer's `@Timed` and `@Counted` annotations to monitor method execution.</li>
41+
* <li>Registering the guardrail metric observer that collect metrics about guardrail execution</li>
42+
* <li>Applying Micrometer's `@Timed` and `@Counted` annotations to monitor method execution. (deprecated)</li>
3243
* <li>Adding OpenTelemetry's `@WithSpan` annotation for distributed tracing.</li>
3344
* </ul>
3445
*/
46+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
3547
public class GuardrailObservabilityProcessor {
3648
private static final Logger LOG = Logger.getLogger(GuardrailObservabilityProcessor.class);
49+
public static final String GUARDRAIL_METRICS_OBSERVER_SUPPORT_CLASS = "io.quarkiverse.langchain4j.runtime.observability.GuardrailMetricsObserverSupport";
50+
51+
/**
52+
* Metrics collection must only be enabled when Micrometer is available.
53+
* In practice, however, Arc always registers observers, regardless of whether
54+
* Micrometer is on the classpath or whether an observer bean is actually
55+
* registered. As a result, an observer may attempt to record metrics when
56+
* Micrometer is missing, which can trigger runtime errors and even break
57+
* native image compilation.
58+
* <p>
59+
* To prevent such failures, this method conditionally generates a bean when
60+
* Micrometer support is detected.
61+
* The generation logic depends on: {@code io.quarkiverse.langchain4j.runtime.observability.GuardrailMetricsObserverSupport}
62+
* *
63+
* </p>
64+
*
65+
* @param metricsCapability the metrics capability build item
66+
* @param generatedBean the generated bean build item producer
67+
*/
68+
@BuildStep
69+
void addMetricObserver(
70+
Optional<MetricsCapabilityBuildItem> metricsCapability,
71+
BuildProducer<GeneratedBeanBuildItem> generatedBean) {
72+
73+
if (metricsCapability.isPresent() && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER)) {
74+
LOG.debug("Generating GuardrailMetricsObserver bean for guardrail metrics collection");
75+
ClassOutput output = new GeneratedBeanGizmoAdaptor(generatedBean);
76+
ClassCreator.Builder classCreatorBuilder = ClassCreator.builder()
77+
.classOutput(output)
78+
.className("io.quarkiverse.langchain4j.runtime.observability.GuardrailMetricsObserver");
79+
try (ClassCreator classCreator = classCreatorBuilder.build()) {
80+
classCreator.addAnnotation(Singleton.class);
81+
MethodCreator onInputGuardrailExecuted = classCreator.getMethodCreator("onInputGuardrailExecuted", "V",
82+
InputGuardrailExecutedEvent.class);
83+
onInputGuardrailExecuted.getParameterAnnotations(0).addAnnotation(Observes.class);
84+
var support1 = MethodDescriptor.ofMethod(
85+
GUARDRAIL_METRICS_OBSERVER_SUPPORT_CLASS,
86+
"onInputGuardrailExecuted", "V", InputGuardrailExecutedEvent.class);
87+
onInputGuardrailExecuted.invokeStaticMethod(support1, onInputGuardrailExecuted.getMethodParam(0));
88+
onInputGuardrailExecuted.returnVoid();
89+
90+
MethodCreator onOutputGuardrailExecuted = classCreator.getMethodCreator("onOutputGuardrailExecuted", "V",
91+
OutputGuardrailExecutedEvent.class);
92+
onOutputGuardrailExecuted.getParameterAnnotations(0).addAnnotation(Observes.class);
93+
var support2 = MethodDescriptor.ofMethod(
94+
GUARDRAIL_METRICS_OBSERVER_SUPPORT_CLASS,
95+
"onOutputGuardrailExecuted", "V", OutputGuardrailExecutedEvent.class);
96+
onOutputGuardrailExecuted.invokeStaticMethod(support2, onOutputGuardrailExecuted.getMethodParam(0));
97+
onOutputGuardrailExecuted.returnVoid();
98+
}
99+
}
100+
101+
}
37102

38103
/**
39104
* @deprecated These metrics are now collected via the GuardrailMetricsObserver bean.
Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,14 @@
22

33
import java.util.concurrent.TimeUnit;
44

5-
import jakarta.enterprise.context.ApplicationScoped;
6-
import jakarta.enterprise.event.Observes;
7-
import jakarta.enterprise.inject.Instance;
8-
import jakarta.inject.Inject;
9-
105
import dev.langchain4j.observability.api.event.InputGuardrailExecutedEvent;
116
import dev.langchain4j.observability.api.event.OutputGuardrailExecutedEvent;
127
import io.micrometer.core.instrument.Counter;
13-
import io.micrometer.core.instrument.MeterRegistry;
8+
import io.micrometer.core.instrument.Metrics;
149
import io.micrometer.core.instrument.Timer;
15-
import io.quarkus.arc.Unremovable;
1610

1711
/**
18-
* Observes guardrail execution events and records metrics with detailed tags.
12+
* Support class to observes guardrail execution events and records metrics with detailed tags.
1913
* <p>
2014
* This observer listens to {@link InputGuardrailExecutedEvent} and {@link OutputGuardrailExecutedEvent}
2115
* and records both counter and timer metrics with the following tags:
@@ -27,25 +21,28 @@
2721
* <li><strong>outcome</strong>: The result of the guardrail execution ({@link GuardrailOutcome})</li>
2822
* </ul>
2923
* <p>
30-
* This observer is only active when Micrometer is available in the application.
24+
* Metrics collection must only be enabled when Micrometer is available.
25+
* In practice, however, Arc always registers observers, regardless of whether
26+
* Micrometer is on the classpath or whether an observer bean is actually
27+
* registered. As a result, an observer may attempt to record metrics when
28+
* Micrometer is missing, which can trigger runtime errors and even break
29+
* native image compilation.
30+
* </p>
31+
*
32+
* <p>
33+
* To prevent such failures, this class is only referenced by a bean that is
34+
* conditionally generated when Micrometer support is detected.
35+
* This class is only providing static methods, so simplify the generation logic.
36+
* </p>
3137
*/
32-
@ApplicationScoped
33-
@Unremovable
34-
public class GuardrailMetricsObserver {
35-
36-
@Inject
37-
Instance<MeterRegistry> registry;
38+
public class GuardrailMetricsObserverSupport {
3839

3940
/**
4041
* Observes input guardrail execution events and records metrics.
4142
*
4243
* @param event the input guardrail executed event
4344
*/
44-
public void onInputGuardrailExecuted(@Observes InputGuardrailExecutedEvent event) {
45-
if (registry.isUnsatisfied()) {
46-
return;
47-
}
48-
45+
public static void onInputGuardrailExecuted(InputGuardrailExecutedEvent event) {
4946
String aiServiceName = event.invocationContext().interfaceName();
5047
String methodName = event.invocationContext().methodName();
5148
String guardrailName = sanitize(event.guardrailClass().getName());
@@ -60,11 +57,7 @@ public void onInputGuardrailExecuted(@Observes InputGuardrailExecutedEvent event
6057
*
6158
* @param event the output guardrail executed event
6259
*/
63-
public void onOutputGuardrailExecuted(@Observes OutputGuardrailExecutedEvent event) {
64-
if (registry.isUnsatisfied()) {
65-
return;
66-
}
67-
60+
public static void onOutputGuardrailExecuted(OutputGuardrailExecutedEvent event) {
6861
String aiServiceName = event.invocationContext().interfaceName();
6962
String methodName = event.invocationContext().methodName();
7063
String guardrailName = sanitize(event.guardrailClass().getName());
@@ -74,7 +67,7 @@ public void onOutputGuardrailExecuted(@Observes OutputGuardrailExecutedEvent eve
7467
recordMetrics(GuardrailType.OUTPUT, aiServiceName, methodName, guardrailName, outcome, durationNanos);
7568
}
7669

77-
private String sanitize(String simpleName) {
70+
private static String sanitize(String simpleName) {
7871
if (simpleName == null) {
7972
return null;
8073
}
@@ -84,11 +77,9 @@ private String sanitize(String simpleName) {
8477
return simpleName;
8578
}
8679

87-
private void recordMetrics(GuardrailType guardrailType, String aiServiceName, String methodName,
80+
private static void recordMetrics(GuardrailType guardrailType, String aiServiceName, String methodName,
8881
String guardrailName, GuardrailOutcome outcome, long durationNanos) {
8982

90-
MeterRegistry meterRegistry = registry.get();
91-
9283
Counter.builder("guardrail.invoked")
9384
.description("Number of guardrail invocations")
9485
.tags(
@@ -97,7 +88,7 @@ private void recordMetrics(GuardrailType guardrailType, String aiServiceName, St
9788
"guardrail", guardrailName,
9889
"guardrail.type", guardrailType.getValue(),
9990
"outcome", outcome.getValue())
100-
.register(meterRegistry)
91+
.register(Metrics.globalRegistry)
10192
.increment();
10293

10394
Timer.builder("guardrail.timed")
@@ -109,11 +100,11 @@ private void recordMetrics(GuardrailType guardrailType, String aiServiceName, St
109100
"outcome", outcome.getValue())
110101
.publishPercentiles(new double[] { 0.75, 0.95, 0.99 })
111102
.publishPercentileHistogram(true)
112-
.register(meterRegistry)
103+
.register(Metrics.globalRegistry)
113104
.record(durationNanos, TimeUnit.NANOSECONDS);
114105
}
115106

116-
private GuardrailOutcome determineInputGuardrailOutcome(InputGuardrailExecutedEvent event) {
107+
private static GuardrailOutcome determineInputGuardrailOutcome(InputGuardrailExecutedEvent event) {
117108
// Check if the guardrail execution was successful
118109
if (event.result().isSuccess()) {
119110
return GuardrailOutcome.SUCCESS;
@@ -122,7 +113,7 @@ private GuardrailOutcome determineInputGuardrailOutcome(InputGuardrailExecutedEv
122113
}
123114
}
124115

125-
private GuardrailOutcome determineOutputGuardrailOutcome(OutputGuardrailExecutedEvent event) {
116+
private static GuardrailOutcome determineOutputGuardrailOutcome(OutputGuardrailExecutedEvent event) {
126117
// Check the result type to determine the outcome
127118
if (event.result().isSuccess()) {
128119
return GuardrailOutcome.SUCCESS;

0 commit comments

Comments
 (0)