Skip to content

Commit 3fea39f

Browse files
authored
Merge pull request #162 from quarkiverse/#142
2 parents 5bd3e7b + 0b56686 commit 3fea39f

File tree

7 files changed

+284
-50
lines changed

7 files changed

+284
-50
lines changed

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

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
import io.quarkiverse.langchain4j.runtime.aiservice.AiServiceMethodImplementationSupport;
5555
import io.quarkiverse.langchain4j.runtime.aiservice.ChatMemoryRemovable;
5656
import io.quarkiverse.langchain4j.runtime.aiservice.DeclarativeAiServiceCreateInfo;
57-
import io.quarkiverse.langchain4j.runtime.aiservice.MetricsWrapper;
57+
import io.quarkiverse.langchain4j.runtime.aiservice.MetricsCountedWrapper;
58+
import io.quarkiverse.langchain4j.runtime.aiservice.MetricsTimedWrapper;
5859
import io.quarkiverse.langchain4j.runtime.aiservice.QuarkusAiServiceContext;
5960
import io.quarkiverse.langchain4j.runtime.aiservice.SpanWrapper;
6061
import io.quarkus.arc.Arc;
@@ -95,6 +96,7 @@ public class AiServicesProcessor {
9596

9697
private static final DotName V = DotName.createSimple(V.class);
9798
public static final DotName MICROMETER_TIMED = DotName.createSimple("io.micrometer.core.annotation.Timed");
99+
public static final DotName MICROMETER_COUNTED = DotName.createSimple("io.micrometer.core.annotation.Counted");
98100
private static final String DEFAULT_DELIMITER = "\n";
99101
private static final Predicate<AnnotationInstance> IS_METHOD_PARAMETER_ANNOTATION = ai -> ai.target()
100102
.kind() == AnnotationTarget.Kind.METHOD_PARAMETER;
@@ -116,6 +118,7 @@ public class AiServicesProcessor {
116118
QuarkusAiServiceContext.class, "removeChatMemoryIds", void.class, Object[].class);
117119
public static final DotName CDI_INSTANCE = DotName.createSimple(Instance.class);
118120
private static final String[] EMPTY_STRING_ARRAY = new String[0];
121+
private static final String METRICS_DEFAULT_NAME = "langchain4j.aiservices";
119122

120123
@BuildStep
121124
public void nativeSupport(CombinedIndexBuildItem indexBuildItem,
@@ -474,7 +477,8 @@ public void handleAiServices(AiServicesRecorder recorder,
474477
var addMicrometerMetrics = metricsCapability.isPresent()
475478
&& metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER);
476479
if (addMicrometerMetrics) {
477-
additionalBeanProducer.produce(AdditionalBeanBuildItem.builder().addBeanClass(MetricsWrapper.class).build());
480+
additionalBeanProducer.produce(AdditionalBeanBuildItem.builder().addBeanClass(MetricsTimedWrapper.class).build());
481+
additionalBeanProducer.produce(AdditionalBeanBuildItem.builder().addBeanClass(MetricsCountedWrapper.class).build());
478482
}
479483

480484
var addOpenTelemetrySpan = capabilities.isPresent(Capability.OPENTELEMETRY_TRACER);
@@ -679,12 +683,15 @@ private AiServiceMethodCreateInfo gatherMethodMetadata(MethodInfo method, boolea
679683
AiServiceMethodCreateInfo.UserMessageInfo userMessageInfo = gatherUserMessageInfo(method, templateParams,
680684
returnType);
681685
Optional<Integer> memoryIdParamPosition = gatherMemoryIdParamName(method);
682-
Optional<AiServiceMethodCreateInfo.MetricsInfo> metricsInfo = gatherMetricsInfo(method, addMicrometerMetrics);
686+
Optional<AiServiceMethodCreateInfo.MetricsTimedInfo> metricsTimedInfo = gatherMetricsTimedInfo(method,
687+
addMicrometerMetrics);
688+
Optional<AiServiceMethodCreateInfo.MetricsCountedInfo> metricsCountedInfo = gatherMetricsCountedInfo(method,
689+
addMicrometerMetrics);
683690
Optional<AiServiceMethodCreateInfo.SpanInfo> spanInfo = gatherSpanInfo(method, addOpenTelemetrySpans);
684691

685692
return new AiServiceMethodCreateInfo(method.declaringClass().name().toString(), method.name(), systemMessageInfo,
686693
userMessageInfo, memoryIdParamPosition, requiresModeration,
687-
returnType, metricsInfo, spanInfo);
694+
returnType, metricsTimedInfo, metricsCountedInfo, spanInfo);
688695
}
689696

690697
private List<TemplateParameterInfo> gatherTemplateParamInfo(List<MethodParameterInfo> params) {
@@ -817,18 +824,14 @@ private AiServiceMethodCreateInfo.UserMessageInfo gatherUserMessageInfo(MethodIn
817824
}
818825
}
819826

820-
private Optional<AiServiceMethodCreateInfo.MetricsInfo> gatherMetricsInfo(MethodInfo method,
827+
private Optional<AiServiceMethodCreateInfo.MetricsTimedInfo> gatherMetricsTimedInfo(MethodInfo method,
821828
boolean addMicrometerMetrics) {
822829
if (!addMicrometerMetrics) {
823830
return Optional.empty();
824831
}
825832

826-
String name = "langchain4j.aiservices";
827-
List<String> tags = new ArrayList<>();
828-
tags.add("aiservice");
829-
tags.add(method.declaringClass().name().withoutPackagePrefix());
830-
tags.add("method");
831-
tags.add(method.name());
833+
String name = METRICS_DEFAULT_NAME;
834+
List<String> tags = defaultMetricsTags(method);
832835

833836
AnnotationInstance timedInstance = method.annotation(MICROMETER_TIMED);
834837
if (timedInstance == null) {
@@ -837,7 +840,7 @@ private Optional<AiServiceMethodCreateInfo.MetricsInfo> gatherMetricsInfo(Method
837840

838841
if (timedInstance == null) {
839842
// we default to having all AiServices being timed
840-
return Optional.of(new AiServiceMethodCreateInfo.MetricsInfo.Builder(name)
843+
return Optional.of(new AiServiceMethodCreateInfo.MetricsTimedInfo.Builder(name)
841844
.setExtraTags(tags.toArray(EMPTY_STRING_ARRAY)).build());
842845
}
843846

@@ -849,7 +852,7 @@ private Optional<AiServiceMethodCreateInfo.MetricsInfo> gatherMetricsInfo(Method
849852
}
850853
}
851854

852-
var builder = new AiServiceMethodCreateInfo.MetricsInfo.Builder(name);
855+
var builder = new AiServiceMethodCreateInfo.MetricsTimedInfo.Builder(name);
853856

854857
AnnotationValue extraTagsValue = timedInstance.value("extraTags");
855858
if (extraTagsValue != null) {
@@ -880,6 +883,64 @@ private Optional<AiServiceMethodCreateInfo.MetricsInfo> gatherMetricsInfo(Method
880883
return Optional.of(builder.build());
881884
}
882885

886+
private Optional<AiServiceMethodCreateInfo.MetricsCountedInfo> gatherMetricsCountedInfo(MethodInfo method,
887+
boolean addMicrometerMetrics) {
888+
if (!addMicrometerMetrics) {
889+
return Optional.empty();
890+
}
891+
892+
String name = METRICS_DEFAULT_NAME;
893+
List<String> tags = defaultMetricsTags(method);
894+
895+
AnnotationInstance timedInstance = method.annotation(MICROMETER_COUNTED);
896+
if (timedInstance == null) {
897+
timedInstance = method.declaringClass().declaredAnnotation(MICROMETER_COUNTED);
898+
}
899+
900+
if (timedInstance == null) {
901+
// we default to having all AiServices being timed
902+
return Optional.of(new AiServiceMethodCreateInfo.MetricsCountedInfo.Builder(name)
903+
.setExtraTags(tags.toArray(EMPTY_STRING_ARRAY)).build());
904+
}
905+
906+
AnnotationValue nameValue = timedInstance.value();
907+
if (nameValue != null) {
908+
String nameStr = nameValue.asString();
909+
if (nameStr != null && !nameStr.isEmpty()) {
910+
name = nameStr;
911+
}
912+
}
913+
914+
var builder = new AiServiceMethodCreateInfo.MetricsCountedInfo.Builder(name);
915+
916+
AnnotationValue extraTagsValue = timedInstance.value("extraTags");
917+
if (extraTagsValue != null) {
918+
tags.addAll(Arrays.asList(extraTagsValue.asStringArray()));
919+
}
920+
builder.setExtraTags(tags.toArray(EMPTY_STRING_ARRAY));
921+
922+
AnnotationValue recordFailuresOnlyValue = timedInstance.value("recordFailuresOnly");
923+
if (recordFailuresOnlyValue != null) {
924+
builder.setRecordFailuresOnly(recordFailuresOnlyValue.asBoolean());
925+
}
926+
927+
AnnotationValue descriptionValue = timedInstance.value("description");
928+
if (descriptionValue != null) {
929+
builder.setDescription(descriptionValue.asString());
930+
}
931+
932+
return Optional.of(builder.build());
933+
}
934+
935+
private List<String> defaultMetricsTags(MethodInfo method) {
936+
List<String> tags = new ArrayList<>(4);
937+
tags.add("aiservice");
938+
tags.add(method.declaringClass().name().withoutPackagePrefix());
939+
tags.add("method");
940+
tags.add(method.name());
941+
return tags;
942+
}
943+
883944
private Optional<AiServiceMethodCreateInfo.SpanInfo> gatherSpanInfo(MethodInfo method,
884945
boolean addOpenTelemetrySpans) {
885946
if (!addOpenTelemetrySpans) {

core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/aiservice/AiServiceMethodCreateInfo.java

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,17 @@ public class AiServiceMethodCreateInfo {
1818
private final boolean requiresModeration;
1919
private final Class<?> returnType;
2020

21-
private final Optional<MetricsInfo> metricsInfo;
22-
21+
private final Optional<MetricsTimedInfo> metricsTimedInfo;
22+
private final Optional<MetricsCountedInfo> metricsCountedInfo;
2323
private final Optional<SpanInfo> spanInfo;
2424

2525
@RecordableConstructor
2626
public AiServiceMethodCreateInfo(String interfaceName, String methodName,
2727
Optional<TemplateInfo> systemMessageInfo, UserMessageInfo userMessageInfo,
2828
Optional<Integer> memoryIdParamPosition,
2929
boolean requiresModeration, Class<?> returnType,
30-
Optional<MetricsInfo> metricsInfo,
30+
Optional<MetricsTimedInfo> metricsTimedInfo,
31+
Optional<MetricsCountedInfo> metricsCountedInfo,
3132
Optional<SpanInfo> spanInfo) {
3233
this.interfaceName = interfaceName;
3334
this.methodName = methodName;
@@ -36,7 +37,8 @@ public AiServiceMethodCreateInfo(String interfaceName, String methodName,
3637
this.memoryIdParamPosition = memoryIdParamPosition;
3738
this.requiresModeration = requiresModeration;
3839
this.returnType = returnType;
39-
this.metricsInfo = metricsInfo;
40+
this.metricsTimedInfo = metricsTimedInfo;
41+
this.metricsCountedInfo = metricsCountedInfo;
4042
this.spanInfo = spanInfo;
4143
}
4244

@@ -68,8 +70,12 @@ public Class<?> getReturnType() {
6870
return returnType;
6971
}
7072

71-
public Optional<MetricsInfo> getMetricsInfo() {
72-
return metricsInfo;
73+
public Optional<MetricsTimedInfo> getMetricsTimedInfo() {
74+
return metricsTimedInfo;
75+
}
76+
77+
public Optional<MetricsCountedInfo> getMetricsCountedInfo() {
78+
return metricsCountedInfo;
7379
}
7480

7581
public Optional<SpanInfo> getSpanInfo() {
@@ -138,7 +144,7 @@ public Map<String, Integer> getNameToParamPosition() {
138144
}
139145
}
140146

141-
public static class MetricsInfo {
147+
public static class MetricsTimedInfo {
142148
private final String name;
143149
private final boolean longTask;
144150
private final String[] extraTags;
@@ -147,7 +153,7 @@ public static class MetricsInfo {
147153
private final String description;
148154

149155
@RecordableConstructor
150-
public MetricsInfo(String name, boolean longTask, String[] extraTags, double[] percentiles, boolean histogram,
156+
public MetricsTimedInfo(String name, boolean longTask, String[] extraTags, double[] percentiles, boolean histogram,
151157
String description) {
152158
this.name = name;
153159
this.longTask = longTask;
@@ -218,13 +224,75 @@ public Builder setDescription(String description) {
218224
return this;
219225
}
220226

221-
public AiServiceMethodCreateInfo.MetricsInfo build() {
222-
return new AiServiceMethodCreateInfo.MetricsInfo(name, longTask, extraTags, percentiles, histogram,
227+
public MetricsTimedInfo build() {
228+
return new MetricsTimedInfo(name, longTask, extraTags, percentiles, histogram,
223229
description);
224230
}
225231
}
226232
}
227233

234+
public static class MetricsCountedInfo {
235+
private final String name;
236+
private final boolean recordFailuresOnly;
237+
private final String[] extraTags;
238+
private final String description;
239+
240+
@RecordableConstructor
241+
public MetricsCountedInfo(String name, String[] extraTags,
242+
boolean recordFailuresOnly, String description) {
243+
this.name = name;
244+
this.extraTags = extraTags;
245+
this.recordFailuresOnly = recordFailuresOnly;
246+
this.description = description;
247+
}
248+
249+
public String getName() {
250+
return name;
251+
}
252+
253+
public String[] getExtraTags() {
254+
return extraTags;
255+
}
256+
257+
public boolean isRecordFailuresOnly() {
258+
return recordFailuresOnly;
259+
}
260+
261+
public String getDescription() {
262+
return description;
263+
}
264+
265+
public static class Builder {
266+
private final String name;
267+
private String[] extraTags = {};
268+
private boolean recordFailuresOnly = false;
269+
private String description = "";
270+
271+
public Builder(String name) {
272+
this.name = name;
273+
}
274+
275+
public Builder setExtraTags(String[] extraTags) {
276+
this.extraTags = extraTags;
277+
return this;
278+
}
279+
280+
public Builder setRecordFailuresOnly(boolean recordFailuresOnly) {
281+
this.recordFailuresOnly = recordFailuresOnly;
282+
return this;
283+
}
284+
285+
public Builder setDescription(String description) {
286+
this.description = description;
287+
return this;
288+
}
289+
290+
public MetricsCountedInfo build() {
291+
return new MetricsCountedInfo(name, extraTags, recordFailuresOnly, description);
292+
}
293+
}
294+
}
295+
228296
public static class SpanInfo {
229297
private final String name;
230298

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.quarkiverse.langchain4j.runtime.aiservice;
2+
3+
import java.util.Optional;
4+
import java.util.function.Function;
5+
6+
import io.micrometer.core.instrument.Counter;
7+
import io.micrometer.core.instrument.Metrics;
8+
9+
public class MetricsCountedWrapper implements AiServiceMethodImplementationSupport.Wrapper {
10+
11+
private static final String RESULT_TAG_FAILURE_VALUE = "failure";
12+
private static final String RESULT_TAG_SUCCESS_VALUE = "success";
13+
private static final String DEFAULT_EXCEPTION_TAG_VALUE = "none";
14+
15+
@Override
16+
public Object wrap(AiServiceMethodImplementationSupport.Input input,
17+
Function<AiServiceMethodImplementationSupport.Input, Object> fun) {
18+
Optional<AiServiceMethodCreateInfo.MetricsCountedInfo> metricsInfoOpt = input.createInfo.getMetricsCountedInfo();
19+
if (metricsInfoOpt.isPresent()) {
20+
AiServiceMethodCreateInfo.MetricsCountedInfo metricsCountedInfo = metricsInfoOpt.get();
21+
try {
22+
Object result = fun.apply(input);
23+
if (!metricsCountedInfo.isRecordFailuresOnly()) {
24+
record(metricsCountedInfo, null);
25+
}
26+
return result;
27+
} catch (Throwable e) {
28+
record(metricsCountedInfo, e);
29+
throw e;
30+
}
31+
} else {
32+
return fun.apply(input);
33+
}
34+
}
35+
36+
private void record(AiServiceMethodCreateInfo.MetricsCountedInfo metricsCountedInfo, Throwable throwable) {
37+
Counter.Builder builder = Counter.builder(metricsCountedInfo.getName())
38+
.tags(metricsCountedInfo.getExtraTags())
39+
.tag("exception", getExceptionTag(throwable))
40+
.tag("result", throwable == null ? RESULT_TAG_SUCCESS_VALUE : RESULT_TAG_FAILURE_VALUE);
41+
String description = metricsCountedInfo.getDescription();
42+
if (!description.isEmpty()) {
43+
builder.description(description);
44+
}
45+
builder.register(Metrics.globalRegistry).increment();
46+
}
47+
48+
private String getExceptionTag(Throwable throwable) {
49+
if (throwable == null) {
50+
return DEFAULT_EXCEPTION_TAG_VALUE;
51+
}
52+
if (throwable.getCause() == null) {
53+
return throwable.getClass().getSimpleName();
54+
}
55+
return throwable.getCause().getClass().getSimpleName();
56+
}
57+
}

0 commit comments

Comments
 (0)