Skip to content

Commit 541e1d8

Browse files
committed
Refactor guardrail metrics
Introduces GuardrailMetricsObserver to collect guardrail metrics with enhanced tags including aiservice name, operation, guardrail class, type (input/output), and outcome (success/failure/reprompt). Deprecates the previous annotation-based metrics collection in GuardrailObservabilityProcessor.
1 parent e110097 commit 541e1d8

File tree

7 files changed

+1400
-18
lines changed

7 files changed

+1400
-18
lines changed

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

Lines changed: 73 additions & 4 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,13 +38,72 @@
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

103+
/**
104+
* @deprecated These metrics are now collected via the GuardrailMetricsObserver bean.
105+
*/
106+
@Deprecated
38107
@BuildStep
39108
void transformWithMetrics(Optional<MetricsCapabilityBuildItem> metricsCapability,
40109
CombinedIndexBuildItem indexBuildItem,
@@ -48,11 +117,11 @@ void transformWithMetrics(Optional<MetricsCapabilityBuildItem> metricsCapability
48117
AnnotationInstance.builder(MICROMETER_COUNTED)
49118
.add("value", "guardrail.invoked")
50119
.add("description",
51-
"Measures the number of times this guardrail was invoked")
120+
"Measures the number of times this guardrail was invoked (deprecated)")
52121
.build(),
53122
AnnotationInstance.builder(MICROMETER_TIMED)
54123
.add("value", "guardrail.timed")
55-
.add("description", "Measures the runtime of this guardrail")
124+
.add("description", "Measures the runtime of this guardrail (deprecated)")
56125
.add("percentiles", new double[] { 0.75, 0.95, 0.99 })
57126
.add("histogram", true)
58127
.build()),

0 commit comments

Comments
 (0)