Skip to content

Commit 9e770bf

Browse files
authored
Support tracing RAG retrieval in Spring AI 1.x plugin (#808)
1 parent e0e8b3c commit 9e770bf

24 files changed

Lines changed: 915 additions & 5 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Release Notes.
2525
* Only publish `apm-application-toolkit` modules to Maven Central. Agent and plugins are distributed via download package and Docker images.
2626
* Add unified release script (`tools/releasing/release.sh`) with two-step flow: `prepare-vote` and `vote-passed`.
2727
* Fix an issue where `JDBCPluginConfig.Plugin.JDBC.SQL_BODY_MAX_LENGTH` was not honored by clickhouse-0.3.1 and clickhouse-0.3.2.x plugins.
28+
- Add tracing support for vector-store retrieval operations.
2829

2930
All issues and pull requests are [here](https://github.com/apache/skywalking/milestone/249?closed=1)
3031

apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/tag/Tags.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,21 @@ public static final class HTTP {
250250
*/
251251
public static final StringTag GEN_AI_OUTPUT_MESSAGES = new StringTag(42, "gen_ai.output.messages");
252252

253+
/**
254+
* GEN_AI_DATA_SOURCE_ID represents the data source identifier.
255+
*/
256+
public static final StringTag GEN_AI_DATA_SOURCE_ID = new StringTag(43, "gen_ai.data_source.id");
257+
258+
/**
259+
* GEN_AI_RETRIEVAL_DOCUMENTS represents the documents retrieved.
260+
*/
261+
public static final StringTag GEN_AI_RETRIEVAL_DOCUMENTS = new StringTag(44, "gen_ai.retrieval.documents");
262+
263+
/**
264+
* GEN_AI_RETRIEVAL_QUERY_TEXT represents the query text used for retrieval.
265+
*/
266+
public static final StringTag GEN_AI_RETRIEVAL_QUERY_TEXT = new StringTag(45, "gen_ai.retrieval.query.text");
267+
253268
/**
254269
* Creates a {@code StringTag} with the given key and cache it, if it's created before, simply return it without
255270
* creating a new one.

apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@
4646
<scope>provided</scope>
4747
</dependency>
4848

49+
<dependency>
50+
<groupId>org.springframework.ai</groupId>
51+
<artifactId>spring-ai-vector-store</artifactId>
52+
<version>1.1.0</version>
53+
<scope>provided</scope>
54+
</dependency>
55+
4956
</dependencies>
5057

5158
<build>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package org.apache.skywalking.apm.plugin.spring.ai.v1;
20+
21+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
22+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor;
23+
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.EmbeddingModelEnhanceContext;
24+
25+
public class AbstractObservationVectorStoreConstructorInterceptor implements InstanceConstructorInterceptor {
26+
27+
@Override
28+
public void onConstruct(EnhancedInstance objInst, Object[] allArguments) {
29+
objInst.setSkyWalkingDynamicField(new VectorStoreEnhanceContext(resolveContextFromArgument(allArguments[0])));
30+
}
31+
32+
private EmbeddingModelEnhanceContext resolveContextFromArgument(Object argument) {
33+
if (argument instanceof EnhancedInstance) {
34+
return getOrCreateContext((EnhancedInstance) argument);
35+
}
36+
return null;
37+
}
38+
39+
private EmbeddingModelEnhanceContext getOrCreateContext(EnhancedInstance embeddingModel) {
40+
Object context = embeddingModel.getSkyWalkingDynamicField();
41+
if (context instanceof EmbeddingModelEnhanceContext) {
42+
return (EmbeddingModelEnhanceContext) context;
43+
}
44+
EmbeddingModelEnhanceContext embeddingModelEnhanceContext = new EmbeddingModelEnhanceContext();
45+
embeddingModel.setSkyWalkingDynamicField(embeddingModelEnhanceContext);
46+
return embeddingModelEnhanceContext;
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package org.apache.skywalking.apm.plugin.spring.ai.v1;
20+
21+
import org.apache.skywalking.apm.agent.core.context.ContextManager;
22+
import org.apache.skywalking.apm.agent.core.context.tag.Tags;
23+
import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
24+
import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
25+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
26+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
27+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
28+
import org.apache.skywalking.apm.agent.core.util.GsonUtil;
29+
import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
30+
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ErrorTypeResolver;
31+
import org.apache.skywalking.apm.plugin.spring.ai.v1.config.SpringAiPluginConfig;
32+
import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants;
33+
import org.springframework.ai.document.Document;
34+
import org.springframework.ai.vectorstore.SearchRequest;
35+
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
36+
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
37+
import org.springframework.util.StringUtils;
38+
39+
import java.lang.reflect.Method;
40+
import java.util.ArrayList;
41+
import java.util.LinkedHashMap;
42+
import java.util.List;
43+
import java.util.Map;
44+
45+
public class AbstractObservationVectorStoreInterceptor implements InstanceMethodsAroundInterceptor {
46+
47+
@Override
48+
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
49+
MethodInterceptResult result) throws Throwable {
50+
SearchRequest request = (SearchRequest) allArguments[0];
51+
String dataSourceId = objInst.getClass().getSimpleName();
52+
53+
try {
54+
VectorStoreObservationContext context =
55+
createObservationContext(objInst, request);
56+
57+
String resolved =
58+
resolveDataSourceId(context, objInst);
59+
60+
if (StringUtils.hasText(resolved)) {
61+
dataSourceId = resolved;
62+
}
63+
} catch (Throwable ignored) {
64+
65+
}
66+
67+
AbstractSpan span = ContextManager.createExitSpan(Constants.RETRIEVAL + "/" + dataSourceId, dataSourceId);
68+
69+
SpanLayer.asGenAI(span);
70+
span.setComponent(ComponentsDefine.SPRING_AI);
71+
Tags.GEN_AI_OPERATION_NAME.set(span, Constants.RETRIEVAL);
72+
Tags.GEN_AI_DATA_SOURCE_ID.set(span, dataSourceId);
73+
String model = resolveEmbeddingModelName(objInst);
74+
if (StringUtils.hasText(model)) {
75+
Tags.GEN_AI_REQUEST_MODEL.set(span, model);
76+
}
77+
78+
if (request != null) {
79+
Tags.GEN_AI_TOP_K.set(span, String.valueOf(request.getTopK()));
80+
String query = request.getQuery();
81+
if (StringUtils.hasText(query) && SpringAiPluginConfig.Plugin.SpringAi.COLLECT_RETRIEVAL_QUERY) {
82+
int limit = SpringAiPluginConfig.Plugin.SpringAi.RETRIEVAL_QUERY_LENGTH_LIMIT;
83+
if (limit > 0 && query.length() > limit) {
84+
query = query.substring(0, limit);
85+
}
86+
Tags.GEN_AI_RETRIEVAL_QUERY_TEXT.set(span, query);
87+
}
88+
}
89+
}
90+
91+
@Override
92+
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
93+
Object ret) throws Throwable {
94+
if (!ContextManager.isActive()) {
95+
return ret;
96+
}
97+
try {
98+
if (ret instanceof List<?> && SpringAiPluginConfig.Plugin.SpringAi.COLLECT_RETRIEVAL_DOCUMENTS) {
99+
Tags.GEN_AI_RETRIEVAL_DOCUMENTS.set(ContextManager.activeSpan(), toDocumentsJson((List<?>) ret));
100+
}
101+
} finally {
102+
ContextManager.stopSpan();
103+
}
104+
return ret;
105+
}
106+
107+
@Override
108+
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments,
109+
Class<?>[] argumentsTypes, Throwable t) {
110+
if (ContextManager.isActive()) {
111+
AbstractSpan span = ContextManager.activeSpan();
112+
span.log(t);
113+
ErrorTypeResolver.setErrorType(span, t);
114+
}
115+
}
116+
117+
private VectorStoreObservationContext createObservationContext(EnhancedInstance objInst, SearchRequest request) {
118+
VectorStoreObservationContext.Builder builder = ((AbstractObservationVectorStore) objInst)
119+
.createObservationContextBuilder(VectorStoreObservationContext.Operation.QUERY.value());
120+
if (request != null) {
121+
builder.queryRequest(request);
122+
}
123+
return builder.build();
124+
}
125+
126+
private String resolveEmbeddingModelName(EnhancedInstance objInst) {
127+
Object context = objInst.getSkyWalkingDynamicField();
128+
if (context instanceof VectorStoreEnhanceContext) {
129+
return ((VectorStoreEnhanceContext) context).getEmbeddingModelName();
130+
}
131+
return null;
132+
}
133+
134+
private String resolveDataSourceId(VectorStoreObservationContext context, EnhancedInstance objInst) {
135+
StringBuilder dataSourceId = new StringBuilder();
136+
appendDataSourcePart(dataSourceId, context.getDatabaseSystem());
137+
appendDataSourcePart(dataSourceId, context.getNamespace());
138+
appendDataSourcePart(dataSourceId, context.getCollectionName());
139+
if (dataSourceId.length() > 0) {
140+
return dataSourceId.toString();
141+
}
142+
return objInst.getClass().getSimpleName();
143+
}
144+
145+
private void appendDataSourcePart(StringBuilder dataSourceId, String value) {
146+
if (!StringUtils.hasText(value)) {
147+
return;
148+
}
149+
if (dataSourceId.length() > 0) {
150+
dataSourceId.append('/');
151+
}
152+
dataSourceId.append(value);
153+
}
154+
155+
private String toDocumentsJson(List<?> documents) {
156+
List<Map<String, Object>> retrievalDocuments = new ArrayList<>(documents.size());
157+
for (Object item : documents) {
158+
if (!(item instanceof Document)) {
159+
continue;
160+
}
161+
Document document = (Document) item;
162+
Map<String, Object> documentMap = new LinkedHashMap<>();
163+
documentMap.put("id", document.getId());
164+
if (document.getScore() != null) {
165+
documentMap.put("score", document.getScore());
166+
}
167+
retrievalDocuments.add(documentMap);
168+
}
169+
return GsonUtil.toJson(retrievalDocuments);
170+
}
171+
}

apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/ChatModelCallInterceptor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
2727
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
2828
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
29+
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ErrorTypeResolver;
2930
import org.apache.skywalking.apm.plugin.spring.ai.v1.config.SpringAiPluginConfig;
3031
import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants;
3132
import org.apache.skywalking.apm.plugin.spring.ai.v1.messages.InputMessages;
@@ -129,7 +130,9 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
129130
@Override
130131
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
131132
if (ContextManager.isActive()) {
132-
ContextManager.activeSpan().log(t);
133+
AbstractSpan span = ContextManager.activeSpan();
134+
span.log(t);
135+
ErrorTypeResolver.setErrorType(span, t);
133136
}
134137
}
135138

apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/ChatModelStreamInterceptor.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
2828
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
2929
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
30+
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ErrorTypeResolver;
3031
import org.apache.skywalking.apm.plugin.spring.ai.v1.config.SpringAiPluginConfig;
3132
import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants;
3233
import org.apache.skywalking.apm.plugin.spring.ai.v1.messages.InputMessages;
@@ -94,11 +95,16 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
9495

9596
return flux
9697
.doOnNext(response -> onStreamNext(span, response, state))
97-
.doOnError(span::log)
98+
.doOnError(t -> recordError(span, t))
9899
.doFinally(signalType -> onStreamFinally(span, allArguments, state))
99100
.contextWrite(c -> c.put(Constants.SKYWALKING_CONTEXT_SNAPSHOT, snapshot));
100101
}
101102

103+
private void recordError(AbstractSpan span, Throwable t) {
104+
span.log(t);
105+
ErrorTypeResolver.setErrorType(span, t);
106+
}
107+
102108
private void onStreamNext(AbstractSpan span, ChatResponse response, StreamState state) {
103109
state.lastResponseRef.set(response);
104110

@@ -248,7 +254,9 @@ private Long readAndClearStartTime() {
248254
@Override
249255
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
250256
if (ContextManager.isActive()) {
251-
ContextManager.activeSpan().log(t);
257+
AbstractSpan span = ContextManager.activeSpan();
258+
span.log(t);
259+
ErrorTypeResolver.setErrorType(span, t);
252260
}
253261
}
254262

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package org.apache.skywalking.apm.plugin.spring.ai.v1;
20+
21+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
22+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
23+
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
24+
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.EmbeddingModelEnhanceContext;
25+
import org.springframework.ai.embedding.EmbeddingResponse;
26+
import org.springframework.ai.embedding.EmbeddingResponseMetadata;
27+
import org.springframework.util.StringUtils;
28+
29+
import java.lang.reflect.Method;
30+
31+
public class EmbeddingModelInterceptor implements InstanceMethodsAroundInterceptor {
32+
33+
@Override
34+
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) {
35+
36+
}
37+
38+
@Override
39+
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) {
40+
if (!(ret instanceof EmbeddingResponse)) {
41+
return ret;
42+
}
43+
44+
EmbeddingResponseMetadata metadata = ((EmbeddingResponse) ret).getMetadata();
45+
if (metadata == null) {
46+
return ret;
47+
}
48+
String model = metadata.getModel();
49+
if (!StringUtils.hasText(model)) {
50+
return ret;
51+
}
52+
EmbeddingModelEnhanceContext context = getOrCreateContext(objInst);
53+
context.setEmbeddingModelNameIfAbsent(model);
54+
return ret;
55+
}
56+
57+
@Override
58+
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
59+
}
60+
61+
private EmbeddingModelEnhanceContext getOrCreateContext(EnhancedInstance objInst) {
62+
Object context = objInst.getSkyWalkingDynamicField();
63+
if (context instanceof EmbeddingModelEnhanceContext) {
64+
return (EmbeddingModelEnhanceContext) context;
65+
}
66+
EmbeddingModelEnhanceContext embeddingModelEnhanceContext = new EmbeddingModelEnhanceContext();
67+
objInst.setSkyWalkingDynamicField(embeddingModelEnhanceContext);
68+
return embeddingModelEnhanceContext;
69+
}
70+
}

0 commit comments

Comments
 (0)