This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-java.git
The following commit(s) were added to refs/heads/main by this push:
new 9e770bfd1f Support tracing RAG retrieval in Spring AI 1.x plugin (#808)
9e770bfd1f is described below
commit 9e770bfd1f7386d97af1b78cebbf72038a8d4936
Author: peachisai <[email protected]>
AuthorDate: Wed Jun 3 11:57:16 2026 +0800
Support tracing RAG retrieval in Spring AI 1.x plugin (#808)
---
CHANGES.md | 1 +
.../apm/agent/core/context/tag/Tags.java | 15 ++
.../spring-plugins/spring-ai-1.x-plugin/pom.xml | 7 +
...servationVectorStoreConstructorInterceptor.java | 48 ++++++
.../AbstractObservationVectorStoreInterceptor.java | 171 +++++++++++++++++++++
.../spring/ai/v1/ChatModelCallInterceptor.java | 5 +-
.../spring/ai/v1/ChatModelStreamInterceptor.java | 12 +-
.../spring/ai/v1/EmbeddingModelInterceptor.java | 70 +++++++++
.../spring/ai/v1/VectorStoreEnhanceContext.java | 25 +--
.../ai/v1/common/EmbeddingModelEnhanceContext.java | 24 +--
.../spring/ai/v1/common/ErrorTypeResolver.java | 80 ++++++++++
.../spring/ai/v1/config/SpringAiPluginConfig.java | 17 ++
.../apm/plugin/spring/ai/v1/contant/Constants.java | 2 +
...tractObservationVectorStoreInstrumentation.java | 92 +++++++++++
.../v1/define/EmbeddingModelInstrumentation.java | 75 +++++++++
.../src/main/resources/skywalking-plugin.def | 2 +
apm-sniffer/config/agent.config | 10 +-
.../spring-ai-1.x-scenario/bin/startup.sh | 2 +-
.../config/expectedData.yaml | 43 ++++++
.../scenarios/spring-ai-1.x-scenario/pom.xml | 5 +
.../jdk/httpclient/config/ChatClientConfig.java | 27 ++++
.../jdk/httpclient/controller/CaseController.java | 23 +++
.../httpclient/controller/LLMMockController.java | 137 +++++++++++++++++
.../src/main/resources/application.yaml | 3 +
24 files changed, 871 insertions(+), 25 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 3a9514fdc6..0bf6869120 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -25,6 +25,7 @@ Release Notes.
* Only publish `apm-application-toolkit` modules to Maven Central. Agent and
plugins are distributed via download package and Docker images.
* Add unified release script (`tools/releasing/release.sh`) with two-step
flow: `prepare-vote` and `vote-passed`.
* 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.
+- Add tracing support for vector-store retrieval operations.
All issues and pull requests are
[here](https://github.com/apache/skywalking/milestone/249?closed=1)
diff --git
a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/tag/Tags.java
b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/tag/Tags.java
index 3d0b9f37cb..8acb2b9879 100644
---
a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/tag/Tags.java
+++
b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/tag/Tags.java
@@ -250,6 +250,21 @@ public final class Tags {
*/
public static final StringTag GEN_AI_OUTPUT_MESSAGES = new StringTag(42,
"gen_ai.output.messages");
+ /**
+ * GEN_AI_DATA_SOURCE_ID represents the data source identifier.
+ */
+ public static final StringTag GEN_AI_DATA_SOURCE_ID = new StringTag(43,
"gen_ai.data_source.id");
+
+ /**
+ * GEN_AI_RETRIEVAL_DOCUMENTS represents the documents retrieved.
+ */
+ public static final StringTag GEN_AI_RETRIEVAL_DOCUMENTS = new
StringTag(44, "gen_ai.retrieval.documents");
+
+ /**
+ * GEN_AI_RETRIEVAL_QUERY_TEXT represents the query text used for
retrieval.
+ */
+ public static final StringTag GEN_AI_RETRIEVAL_QUERY_TEXT = new
StringTag(45, "gen_ai.retrieval.query.text");
+
/**
* Creates a {@code StringTag} with the given key and cache it, if it's
created before, simply return it without
* creating a new one.
diff --git
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/pom.xml
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/pom.xml
index 295cf9d8ed..583ce9c90c 100644
--- a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/pom.xml
+++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/pom.xml
@@ -46,6 +46,13 @@
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.springframework.ai</groupId>
+ <artifactId>spring-ai-vector-store</artifactId>
+ <version>1.1.0</version>
+ <scope>provided</scope>
+ </dependency>
+
</dependencies>
<build>
diff --git
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/AbstractObservationVectorStoreConstructorInterceptor.java
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/AbstractObservationVectorStoreConstructorInterceptor.java
new file mode 100644
index 0000000000..faefa999bc
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/AbstractObservationVectorStoreConstructorInterceptor.java
@@ -0,0 +1,48 @@
+/*
+ * 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.skywalking.apm.plugin.spring.ai.v1;
+
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor;
+import
org.apache.skywalking.apm.plugin.spring.ai.v1.common.EmbeddingModelEnhanceContext;
+
+public class AbstractObservationVectorStoreConstructorInterceptor implements
InstanceConstructorInterceptor {
+
+ @Override
+ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) {
+ objInst.setSkyWalkingDynamicField(new
VectorStoreEnhanceContext(resolveContextFromArgument(allArguments[0])));
+ }
+
+ private EmbeddingModelEnhanceContext resolveContextFromArgument(Object
argument) {
+ if (argument instanceof EnhancedInstance) {
+ return getOrCreateContext((EnhancedInstance) argument);
+ }
+ return null;
+ }
+
+ private EmbeddingModelEnhanceContext getOrCreateContext(EnhancedInstance
embeddingModel) {
+ Object context = embeddingModel.getSkyWalkingDynamicField();
+ if (context instanceof EmbeddingModelEnhanceContext) {
+ return (EmbeddingModelEnhanceContext) context;
+ }
+ EmbeddingModelEnhanceContext embeddingModelEnhanceContext = new
EmbeddingModelEnhanceContext();
+ embeddingModel.setSkyWalkingDynamicField(embeddingModelEnhanceContext);
+ return embeddingModelEnhanceContext;
+ }
+}
diff --git
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/AbstractObservationVectorStoreInterceptor.java
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/AbstractObservationVectorStoreInterceptor.java
new file mode 100644
index 0000000000..4b278b6c87
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/AbstractObservationVectorStoreInterceptor.java
@@ -0,0 +1,171 @@
+/*
+ * 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.skywalking.apm.plugin.spring.ai.v1;
+
+import org.apache.skywalking.apm.agent.core.context.ContextManager;
+import org.apache.skywalking.apm.agent.core.context.tag.Tags;
+import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
+import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
+import org.apache.skywalking.apm.agent.core.util.GsonUtil;
+import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ErrorTypeResolver;
+import
org.apache.skywalking.apm.plugin.spring.ai.v1.config.SpringAiPluginConfig;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.vectorstore.SearchRequest;
+import
org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
+import
org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
+import org.springframework.util.StringUtils;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class AbstractObservationVectorStoreInterceptor implements
InstanceMethodsAroundInterceptor {
+
+ @Override
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[]
allArguments, Class<?>[] argumentsTypes,
+ MethodInterceptResult result) throws Throwable {
+ SearchRequest request = (SearchRequest) allArguments[0];
+ String dataSourceId = objInst.getClass().getSimpleName();
+
+ try {
+ VectorStoreObservationContext context =
+ createObservationContext(objInst, request);
+
+ String resolved =
+ resolveDataSourceId(context, objInst);
+
+ if (StringUtils.hasText(resolved)) {
+ dataSourceId = resolved;
+ }
+ } catch (Throwable ignored) {
+
+ }
+
+ AbstractSpan span = ContextManager.createExitSpan(Constants.RETRIEVAL
+ "/" + dataSourceId, dataSourceId);
+
+ SpanLayer.asGenAI(span);
+ span.setComponent(ComponentsDefine.SPRING_AI);
+ Tags.GEN_AI_OPERATION_NAME.set(span, Constants.RETRIEVAL);
+ Tags.GEN_AI_DATA_SOURCE_ID.set(span, dataSourceId);
+ String model = resolveEmbeddingModelName(objInst);
+ if (StringUtils.hasText(model)) {
+ Tags.GEN_AI_REQUEST_MODEL.set(span, model);
+ }
+
+ if (request != null) {
+ Tags.GEN_AI_TOP_K.set(span, String.valueOf(request.getTopK()));
+ String query = request.getQuery();
+ if (StringUtils.hasText(query) &&
SpringAiPluginConfig.Plugin.SpringAi.COLLECT_RETRIEVAL_QUERY) {
+ int limit =
SpringAiPluginConfig.Plugin.SpringAi.RETRIEVAL_QUERY_LENGTH_LIMIT;
+ if (limit > 0 && query.length() > limit) {
+ query = query.substring(0, limit);
+ }
+ Tags.GEN_AI_RETRIEVAL_QUERY_TEXT.set(span, query);
+ }
+ }
+ }
+
+ @Override
+ public Object afterMethod(EnhancedInstance objInst, Method method,
Object[] allArguments, Class<?>[] argumentsTypes,
+ Object ret) throws Throwable {
+ if (!ContextManager.isActive()) {
+ return ret;
+ }
+ try {
+ if (ret instanceof List<?> &&
SpringAiPluginConfig.Plugin.SpringAi.COLLECT_RETRIEVAL_DOCUMENTS) {
+
Tags.GEN_AI_RETRIEVAL_DOCUMENTS.set(ContextManager.activeSpan(),
toDocumentsJson((List<?>) ret));
+ }
+ } finally {
+ ContextManager.stopSpan();
+ }
+ return ret;
+ }
+
+ @Override
+ public void handleMethodException(EnhancedInstance objInst, Method method,
Object[] allArguments,
+ Class<?>[] argumentsTypes, Throwable t) {
+ if (ContextManager.isActive()) {
+ AbstractSpan span = ContextManager.activeSpan();
+ span.log(t);
+ ErrorTypeResolver.setErrorType(span, t);
+ }
+ }
+
+ private VectorStoreObservationContext
createObservationContext(EnhancedInstance objInst, SearchRequest request) {
+ VectorStoreObservationContext.Builder builder =
((AbstractObservationVectorStore) objInst)
+
.createObservationContextBuilder(VectorStoreObservationContext.Operation.QUERY.value());
+ if (request != null) {
+ builder.queryRequest(request);
+ }
+ return builder.build();
+ }
+
+ private String resolveEmbeddingModelName(EnhancedInstance objInst) {
+ Object context = objInst.getSkyWalkingDynamicField();
+ if (context instanceof VectorStoreEnhanceContext) {
+ return ((VectorStoreEnhanceContext)
context).getEmbeddingModelName();
+ }
+ return null;
+ }
+
+ private String resolveDataSourceId(VectorStoreObservationContext context,
EnhancedInstance objInst) {
+ StringBuilder dataSourceId = new StringBuilder();
+ appendDataSourcePart(dataSourceId, context.getDatabaseSystem());
+ appendDataSourcePart(dataSourceId, context.getNamespace());
+ appendDataSourcePart(dataSourceId, context.getCollectionName());
+ if (dataSourceId.length() > 0) {
+ return dataSourceId.toString();
+ }
+ return objInst.getClass().getSimpleName();
+ }
+
+ private void appendDataSourcePart(StringBuilder dataSourceId, String
value) {
+ if (!StringUtils.hasText(value)) {
+ return;
+ }
+ if (dataSourceId.length() > 0) {
+ dataSourceId.append('/');
+ }
+ dataSourceId.append(value);
+ }
+
+ private String toDocumentsJson(List<?> documents) {
+ List<Map<String, Object>> retrievalDocuments = new
ArrayList<>(documents.size());
+ for (Object item : documents) {
+ if (!(item instanceof Document)) {
+ continue;
+ }
+ Document document = (Document) item;
+ Map<String, Object> documentMap = new LinkedHashMap<>();
+ documentMap.put("id", document.getId());
+ if (document.getScore() != null) {
+ documentMap.put("score", document.getScore());
+ }
+ retrievalDocuments.add(documentMap);
+ }
+ return GsonUtil.toJson(retrievalDocuments);
+ }
+}
diff --git
a/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
b/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
index 302db3c407..73f62629c1 100644
---
a/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
+++
b/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
@@ -26,6 +26,7 @@ import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedI
import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
import
org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ErrorTypeResolver;
import
org.apache.skywalking.apm.plugin.spring.ai.v1.config.SpringAiPluginConfig;
import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants;
import org.apache.skywalking.apm.plugin.spring.ai.v1.messages.InputMessages;
@@ -129,7 +130,9 @@ public class ChatModelCallInterceptor implements
InstanceMethodsAroundIntercepto
@Override
public void handleMethodException(EnhancedInstance objInst, Method method,
Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
if (ContextManager.isActive()) {
- ContextManager.activeSpan().log(t);
+ AbstractSpan span = ContextManager.activeSpan();
+ span.log(t);
+ ErrorTypeResolver.setErrorType(span, t);
}
}
diff --git
a/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
b/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
index 1674e82011..7d1b380573 100644
---
a/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
+++
b/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
@@ -27,6 +27,7 @@ import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedI
import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
import
org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ErrorTypeResolver;
import
org.apache.skywalking.apm.plugin.spring.ai.v1.config.SpringAiPluginConfig;
import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants;
import org.apache.skywalking.apm.plugin.spring.ai.v1.messages.InputMessages;
@@ -94,11 +95,16 @@ public class ChatModelStreamInterceptor implements
InstanceMethodsAroundIntercep
return flux
.doOnNext(response -> onStreamNext(span, response, state))
- .doOnError(span::log)
+ .doOnError(t -> recordError(span, t))
.doFinally(signalType -> onStreamFinally(span, allArguments,
state))
.contextWrite(c ->
c.put(Constants.SKYWALKING_CONTEXT_SNAPSHOT, snapshot));
}
+ private void recordError(AbstractSpan span, Throwable t) {
+ span.log(t);
+ ErrorTypeResolver.setErrorType(span, t);
+ }
+
private void onStreamNext(AbstractSpan span, ChatResponse response,
StreamState state) {
state.lastResponseRef.set(response);
@@ -248,7 +254,9 @@ public class ChatModelStreamInterceptor implements
InstanceMethodsAroundIntercep
@Override
public void handleMethodException(EnhancedInstance objInst, Method method,
Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
if (ContextManager.isActive()) {
- ContextManager.activeSpan().log(t);
+ AbstractSpan span = ContextManager.activeSpan();
+ span.log(t);
+ ErrorTypeResolver.setErrorType(span, t);
}
}
diff --git
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/EmbeddingModelInterceptor.java
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/EmbeddingModelInterceptor.java
new file mode 100644
index 0000000000..8e2b57fae9
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/EmbeddingModelInterceptor.java
@@ -0,0 +1,70 @@
+/*
+ * 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.skywalking.apm.plugin.spring.ai.v1;
+
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
+import
org.apache.skywalking.apm.plugin.spring.ai.v1.common.EmbeddingModelEnhanceContext;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.embedding.EmbeddingResponseMetadata;
+import org.springframework.util.StringUtils;
+
+import java.lang.reflect.Method;
+
+public class EmbeddingModelInterceptor implements
InstanceMethodsAroundInterceptor {
+
+ @Override
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[]
allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) {
+
+ }
+
+ @Override
+ public Object afterMethod(EnhancedInstance objInst, Method method,
Object[] allArguments, Class<?>[] argumentsTypes, Object ret) {
+ if (!(ret instanceof EmbeddingResponse)) {
+ return ret;
+ }
+
+ EmbeddingResponseMetadata metadata = ((EmbeddingResponse)
ret).getMetadata();
+ if (metadata == null) {
+ return ret;
+ }
+ String model = metadata.getModel();
+ if (!StringUtils.hasText(model)) {
+ return ret;
+ }
+ EmbeddingModelEnhanceContext context = getOrCreateContext(objInst);
+ context.setEmbeddingModelNameIfAbsent(model);
+ return ret;
+ }
+
+ @Override
+ public void handleMethodException(EnhancedInstance objInst, Method method,
Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
+ }
+
+ private EmbeddingModelEnhanceContext getOrCreateContext(EnhancedInstance
objInst) {
+ Object context = objInst.getSkyWalkingDynamicField();
+ if (context instanceof EmbeddingModelEnhanceContext) {
+ return (EmbeddingModelEnhanceContext) context;
+ }
+ EmbeddingModelEnhanceContext embeddingModelEnhanceContext = new
EmbeddingModelEnhanceContext();
+ objInst.setSkyWalkingDynamicField(embeddingModelEnhanceContext);
+ return embeddingModelEnhanceContext;
+ }
+}
diff --git
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/VectorStoreEnhanceContext.java
similarity index 55%
copy from
test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
copy to
apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/VectorStoreEnhanceContext.java
index 79fde137ab..76916a59ea 100644
---
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/VectorStoreEnhanceContext.java
@@ -13,20 +13,25 @@
* 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 test.apache.skywalking.apm.testcase.jdk.httpclient.config;
+package org.apache.skywalking.apm.plugin.spring.ai.v1;
+
+import
org.apache.skywalking.apm.plugin.spring.ai.v1.common.EmbeddingModelEnhanceContext;
-import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.ai.openai.OpenAiChatModel;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
+public class VectorStoreEnhanceContext {
-@Configuration
-public class ChatClientConfig {
+ private final EmbeddingModelEnhanceContext embeddingModelEnhanceContext;
+
+ public VectorStoreEnhanceContext(EmbeddingModelEnhanceContext
embeddingModelEnhanceContext) {
+ this.embeddingModelEnhanceContext = embeddingModelEnhanceContext;
+ }
- @Bean
- public ChatClient openAIChatClient(OpenAiChatModel model) {
- return ChatClient.create(model);
+ public String getEmbeddingModelName() {
+ if (embeddingModelEnhanceContext == null) {
+ return null;
+ }
+ return embeddingModelEnhanceContext.getEmbeddingModelName();
}
}
diff --git
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/common/EmbeddingModelEnhanceContext.java
similarity index 60%
copy from
test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
copy to
apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/common/EmbeddingModelEnhanceContext.java
index 79fde137ab..199bd97cd4 100644
---
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/common/EmbeddingModelEnhanceContext.java
@@ -13,20 +13,24 @@
* 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 test.apache.skywalking.apm.testcase.jdk.httpclient.config;
+package org.apache.skywalking.apm.plugin.spring.ai.v1.common;
+
+import org.springframework.util.StringUtils;
-import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.ai.openai.OpenAiChatModel;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
+public class EmbeddingModelEnhanceContext {
-@Configuration
-public class ChatClientConfig {
+ private volatile String embeddingModelName;
+
+ public String getEmbeddingModelName() {
+ return embeddingModelName;
+ }
- @Bean
- public ChatClient openAIChatClient(OpenAiChatModel model) {
- return ChatClient.create(model);
+ public void setEmbeddingModelNameIfAbsent(String embeddingModelName) {
+ if (!StringUtils.hasText(this.embeddingModelName) &&
StringUtils.hasText(embeddingModelName)) {
+ this.embeddingModelName = embeddingModelName;
+ }
}
}
diff --git
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/common/ErrorTypeResolver.java
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/common/ErrorTypeResolver.java
new file mode 100644
index 0000000000..1d3172cf3d
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/common/ErrorTypeResolver.java
@@ -0,0 +1,80 @@
+/*
+ * 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.skywalking.apm.plugin.spring.ai.v1.common;
+
+import org.apache.skywalking.apm.agent.core.context.tag.AbstractTag;
+import org.apache.skywalking.apm.agent.core.context.tag.Tags;
+import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
+
+import javax.net.ssl.SSLHandshakeException;
+import java.net.SocketTimeoutException;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertificateException;
+import java.util.concurrent.TimeoutException;
+
+public final class ErrorTypeResolver {
+
+ private static final AbstractTag<String> ERROR_TYPE =
Tags.ofKey("error.type");
+ private static final String TIMEOUT = "timeout";
+ private static final String SERVER_CERTIFICATE_INVALID =
"server_certificate_invalid";
+
+ private ErrorTypeResolver() {
+ }
+
+ public static void setErrorType(AbstractSpan span, Throwable throwable) {
+ span.tag(ERROR_TYPE, resolve(throwable));
+ }
+
+ private static String resolve(Throwable throwable) {
+ if (matches(throwable, ErrorTypeResolver::isTimeout)) {
+ return TIMEOUT;
+ }
+ if (matches(throwable, ErrorTypeResolver::isCertificateInvalid)) {
+ return SERVER_CERTIFICATE_INVALID;
+ }
+ return throwable.getClass().getName();
+ }
+
+ private static boolean isTimeout(Throwable throwable) {
+ return throwable instanceof SocketTimeoutException
+ || throwable instanceof TimeoutException
+ || throwable.getClass().getName().contains("TimeoutException");
+ }
+
+ private static boolean isCertificateInvalid(Throwable throwable) {
+ return throwable instanceof SSLHandshakeException
+ || throwable instanceof CertificateException
+ || throwable instanceof CertPathValidatorException;
+ }
+
+ private static boolean matches(Throwable throwable, Matcher matcher) {
+ Throwable current = throwable;
+ while (current != null) {
+ if (matcher.matches(current)) {
+ return true;
+ }
+ current = current.getCause();
+ }
+ return false;
+ }
+
+ private interface Matcher {
+ boolean matches(Throwable throwable);
+ }
+}
diff --git
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/config/SpringAiPluginConfig.java
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/config/SpringAiPluginConfig.java
index d2d5eef6f6..6a348c622b 100644
---
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/config/SpringAiPluginConfig.java
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/config/SpringAiPluginConfig.java
@@ -69,6 +69,23 @@ public class SpringAiPluginConfig {
* Whether to collect the execution result (output) of the
tool/function call.
*/
public static boolean COLLECT_TOOL_OUTPUT = false;
+
+ /**
+ * Whether to collect the query of the rag call.
+ */
+ public static boolean COLLECT_RETRIEVAL_QUERY = false;
+
+ /**
+ * The maximum characters of the collected rag query content.
+ * If the content exceeds this limit, it will be truncated.
+ * Use a negative value to represent no limit, but be aware this
could cause OOM.
+ */
+ public static int RETRIEVAL_QUERY_LENGTH_LIMIT = 1024;
+
+ /**
+ * Whether to collect the documents of the rag call.
+ */
+ public static boolean COLLECT_RETRIEVAL_DOCUMENTS = false;
}
}
}
diff --git
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/contant/Constants.java
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/contant/Constants.java
index 7348a0c11f..688f4c321a 100644
---
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/contant/Constants.java
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/contant/Constants.java
@@ -27,5 +27,7 @@ public class Constants {
public static final String EXECUTE_TOOL = "execute_tool";
+ public static final String RETRIEVAL = "retrieval";
+
public static final String DEFAULT_COMPLETIONS_PATH =
"/v1/chat/completions";
}
diff --git
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/AbstractObservationVectorStoreInstrumentation.java
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/AbstractObservationVectorStoreInstrumentation.java
new file mode 100644
index 0000000000..f379c0464c
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/AbstractObservationVectorStoreInstrumentation.java
@@ -0,0 +1,92 @@
+/*
+ * 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.skywalking.apm.plugin.spring.ai.v1.define;
+
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
+import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
+import org.apache.skywalking.apm.agent.core.plugin.match.HierarchyMatch;
+import org.apache.skywalking.apm.agent.core.plugin.match.MultiClassNameMatch;
+import
org.apache.skywalking.apm.agent.core.plugin.match.logical.LogicalMatchOperation;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+import static
org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType;
+
+public class AbstractObservationVectorStoreInstrumentation extends
ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS =
"org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore";
+
+ private static final String INTERCEPT_METHOD = "doSimilaritySearch";
+
+ private static final String INTERCEPTOR_CLASS =
+
"org.apache.skywalking.apm.plugin.spring.ai.v1.AbstractObservationVectorStoreInterceptor";
+
+ private static final String CONSTRUCTOR_INTERCEPTOR_CLASS =
+
"org.apache.skywalking.apm.plugin.spring.ai.v1.AbstractObservationVectorStoreConstructorInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return
LogicalMatchOperation.or(HierarchyMatch.byHierarchyMatch(ENHANCE_CLASS),
MultiClassNameMatch.byMultiClassMatch(ENHANCE_CLASS));
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[]{
+ new ConstructorInterceptPoint() {
+ @Override
+ public ElementMatcher<MethodDescription>
getConstructorMatcher() {
+ return takesArgumentWithType(0,
"org.springframework.ai.embedding.EmbeddingModel");
+ }
+
+ @Override
+ public String getConstructorInterceptor() {
+ return CONSTRUCTOR_INTERCEPTOR_CLASS;
+ }
+ }
+ };
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints()
{
+ return new InstanceMethodsInterceptPoint[]{
+ new InstanceMethodsInterceptPoint() {
+ @Override
+ public ElementMatcher<MethodDescription>
getMethodsMatcher() {
+ return named(INTERCEPT_METHOD)
+ .and(takesArguments(1))
+ .and(takesArgumentWithType(0,
"org.springframework.ai.vectorstore.SearchRequest"));
+ }
+
+ @Override
+ public String getMethodsInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+
+ @Override
+ public boolean isOverrideArgs() {
+ return false;
+ }
+ }
+ };
+ }
+}
diff --git
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/EmbeddingModelInstrumentation.java
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/EmbeddingModelInstrumentation.java
new file mode 100644
index 0000000000..12a327ae29
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/EmbeddingModelInstrumentation.java
@@ -0,0 +1,75 @@
+/*
+ * 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.skywalking.apm.plugin.spring.ai.v1.define;
+
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
+import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
+import org.apache.skywalking.apm.agent.core.plugin.match.HierarchyMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+import static
org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType;
+
+public class EmbeddingModelInstrumentation extends
ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS =
"org.springframework.ai.embedding.EmbeddingModel";
+
+ private static final String INTERCEPT_METHOD = "call";
+
+ private static final String INTERCEPTOR_CLASS =
+
"org.apache.skywalking.apm.plugin.spring.ai.v1.EmbeddingModelInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return HierarchyMatch.byHierarchyMatch(ENHANCE_CLASS);
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[0];
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints()
{
+ return new InstanceMethodsInterceptPoint[]{
+ new InstanceMethodsInterceptPoint() {
+ @Override
+ public ElementMatcher<MethodDescription>
getMethodsMatcher() {
+ return named(INTERCEPT_METHOD)
+ .and(takesArguments(1))
+ .and(takesArgumentWithType(0,
"org.springframework.ai.embedding.EmbeddingRequest"));
+ }
+
+ @Override
+ public String getMethodsInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+
+ @Override
+ public boolean isOverrideArgs() {
+ return false;
+ }
+ }
+ };
+ }
+}
diff --git
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/resources/skywalking-plugin.def
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/resources/skywalking-plugin.def
index 5c7eec110c..62e1f6af3e 100644
---
a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/resources/skywalking-plugin.def
+++
b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/resources/skywalking-plugin.def
@@ -15,8 +15,10 @@
# limitations under the License.
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.ChatModelInstrumentation
+spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.EmbeddingModelInstrumentation
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.ToolCallbackInstrumentation
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.DefaultToolCallingManagerInstrumentation
+spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.AbstractObservationVectorStoreInstrumentation
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.AnthropicApiInstrumentation
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.DeepSeekApiInstrumentation
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.HuggingfaceChatModelInstrumentation
diff --git a/apm-sniffer/config/agent.config b/apm-sniffer/config/agent.config
index 38c9bbf8d5..146d0d94fc 100755
--- a/apm-sniffer/config/agent.config
+++ b/apm-sniffer/config/agent.config
@@ -362,4 +362,12 @@
plugin.springai.content_collect_threshold_tokens=${SW_PLUGIN_SPRINGAI_CONTENT_CO
# Whether to collect the arguments (input parameters) of the tool/function
call.
plugin.springai.collect_tool_input=${SW_PLUGIN_SPRINGAI_COLLECT_TOOL_INPUT:false}
# Whether to collect the execution result (output) of the tool/function call.
-plugin.springai.collect_tool_output=${SW_PLUGIN_SPRINGAI_COLLECT_TOOL_OUTPUT:false}
\ No newline at end of file
+plugin.springai.collect_tool_output=${SW_PLUGIN_SPRINGAI_COLLECT_TOOL_OUTPUT:false}
+# Whether to collect the query of the rag call.
+plugin.springai.collect_retrieval_query=${SW_PLUGIN_SPRINGAI_COLLECT_RETRIEVAL_QUERY:false}
+# The maximum characters of the collected rag query.
+# If the content exceeds this limit, it will be truncated.
+# Use a negative value to represent no limit, but be aware this could cause
OOM.
+plugin.springai.retrieval_query=${SW_PLUGIN_SPRINGAI_RETRIEVAL_QUERY_LENGTH_LIMIT:1024}
+# Whether to collect the documents of the rag call.
+plugin.springai.collect_retrieval_documents=${SW_PLUGIN_SPRINGAI_COLLECT_RETRIEVAL_DOCUMENTS:false}
\ No newline at end of file
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
b/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
index 8cb423ece2..9950f6fed6 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
@@ -18,4 +18,4 @@
home="$(cd "$(dirname $0)"; pwd)"
-java -Dskywalking.plugin.springai.collect_input_messages=true
-Dskywalking.plugin.springai.collect_output_messages=true
-Dskywalking.plugin.springai.collect_tool_input=true
-Dskywalking.plugin.springai.collect_tool_output=true -jar ${agent_opts}
${home}/../libs/spring-ai-1.x-scenario.jar &
\ No newline at end of file
+java -Dskywalking.plugin.springai.collect_input_messages=true
-Dskywalking.plugin.springai.collect_output_messages=true
-Dskywalking.plugin.springai.collect_tool_input=true
-Dskywalking.plugin.springai.collect_tool_output=true
-Dskywalking.plugin.springai.collect_retrieval_query=true
-Dskywalking.plugin.springai.collect_retrieval_documents=true -jar
${agent_opts} ${home}/../libs/spring-ai-1.x-scenario.jar &
\ No newline at end of file
diff --git
a/test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml
b/test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml
index 5f40e79a48..d714ae2542 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml
@@ -142,6 +142,49 @@ segmentItems:
- { key: http.method, value: POST }
- { key: http.status_code, value: '200' }
+ - operationName: retrieval/simple/in-memory-map
+ parentSpanId: 0
+ spanId: 6
+ spanLayer: GenAI
+ startTime: not null
+ endTime: not null
+ componentId: 178
+ spanType: Exit
+ peer: simple/in-memory-map
+ tags:
+ - { key: gen_ai.operation.name, value: retrieval }
+ - { key: gen_ai.data_source.id, value: simple/in-memory-map }
+ - { key: gen_ai.request.model, value: text-embedding-3-small }
+ - { key: gen_ai.request.top_k, value: '2' }
+ - { key: gen_ai.retrieval.query.text, value: 'What is Apache
SkyWalking?' }
+ - { key: gen_ai.retrieval.documents, value: not null }
+
+ - operationName: Spring-ai/openai/call
+ parentSpanId: 0
+ spanId: 7
+ spanLayer: GenAI
+ startTime: not null
+ endTime: not null
+ componentId: 173
+ isError: false
+ spanType: Exit
+ peer:
http://localhost:8080/spring-ai-1.x-scenario/llm/v1/chat/completions
+ skipAnalysis: false
+ tags:
+ - { key: gen_ai.operation.name, value: chat }
+ - { key: gen_ai.provider.name, value: openai }
+ - { key: gen_ai.request.model, value: gpt-4.1-2025-04-14 }
+ - { key: gen_ai.request.temperature, value: '0.7' }
+ - { key: gen_ai.request.top_p, value: '0.9' }
+ - { key: gen_ai.response.id, value: chatcmpl-DknJunZ3tgcSkKiv }
+ - { key: gen_ai.response.model, value: gpt-4.1-2025-04-14 }
+ - { key: gen_ai.usage.input_tokens, value: '72' }
+ - { key: gen_ai.usage.output_tokens, value: '25' }
+ - { key: gen_ai.client.token.usage, value: '97' }
+ - { key: gen_ai.response.finish_reasons, value: STOP }
+ - { key: gen_ai.input.messages, value: not null }
+ - { key: gen_ai.output.messages, value: not null }
+
- operationName:
GET:/spring-ai-1.x-scenario/case/spring-ai-1.x-scenario-case
parentSpanId: -1
spanId: 0
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
index b9d39f538a..0d508db8b0 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
@@ -55,6 +55,11 @@
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.springframework.ai</groupId>
+ <artifactId>spring-ai-vector-store</artifactId>
+ </dependency>
+
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
diff --git
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
index 79fde137ab..02d0f3466b 100644
---
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
+++
b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
@@ -17,10 +17,18 @@
package test.apache.skywalking.apm.testcase.jdk.httpclient.config;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.vectorstore.SimpleVectorStore;
+import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+import java.util.ArrayList;
+import java.util.List;
@Configuration
public class ChatClientConfig {
@@ -29,4 +37,23 @@ public class ChatClientConfig {
public ChatClient openAIChatClient(OpenAiChatModel model) {
return ChatClient.create(model);
}
+
+ @Bean
+ @Lazy
+ public VectorStore vectorStore(EmbeddingModel embeddingModel) {
+ SimpleVectorStore vectorStore =
SimpleVectorStore.builder(embeddingModel).build();
+
+ List<Document> documentList = new ArrayList<>();
+ documentList.add(new Document("The 2025 AI Summit is scheduled for
October 10-12 in San Francisco. "
+ + "The event will focus on Generative AI and Autonomous
Agents."));
+ documentList.add(new Document("Apache SkyWalking is an open-source
Application Performance Management system "
+ + "designed for microservices, cloud native, and
container-based architectures."));
+ documentList.add(new Document("Spring AI provides a unified interface
for interacting with different "
+ + "AI Models, allowing developers to switch between providers
with minimal code changes."));
+ documentList.add(new Document("A new distributed tracing protocol,
TraceContext v2, was proposed "
+ + "on August 25, 2025, to improve cross-cloud
observability."));
+
+ vectorStore.add(documentList);
+ return vectorStore;
+ }
}
diff --git
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/CaseController.java
b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/CaseController.java
index d6512b726e..73dc1b15bc 100644
---
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/CaseController.java
+++
b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/CaseController.java
@@ -18,12 +18,18 @@
package test.apache.skywalking.apm.testcase.jdk.httpclient.controller;
import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.vectorstore.SearchRequest;
+import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import test.apache.skywalking.apm.testcase.jdk.httpclient.tool.WeatherTool;
+import java.util.stream.Collectors;
+
@RestController
@RequestMapping("/case")
@RequiredArgsConstructor
@@ -31,6 +37,7 @@ public class CaseController {
private final WeatherTool weatherTool;
private final ChatClient chatClient;
+ private final ObjectProvider<VectorStore> vectorStoreProvider;
@GetMapping("/healthCheck")
public String healthCheck() {
@@ -63,6 +70,22 @@ public class CaseController {
.doOnNext(System.out::println)
.blockLast();
+ String question = "What is Apache SkyWalking?";
+ VectorStore vectorStore = vectorStoreProvider.getObject();
+ String context = vectorStore.similaritySearch(SearchRequest.builder()
+ .query(question)
+ .topK(2)
+ .build())
+ .stream()
+ .map(Document::getText)
+ .collect(Collectors.joining("\n"));
+
+ chatClient
+ .prompt(question)
+ .system("Answer using only the following context:\n" + context)
+ .call()
+ .content();
+
return "success";
}
}
diff --git
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/LLMMockController.java
b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/LLMMockController.java
index 5221245280..d63482c5fd 100644
---
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/LLMMockController.java
+++
b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/LLMMockController.java
@@ -21,6 +21,7 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -177,6 +178,60 @@ public class LLMMockController {
}
""";
+ String ragLlmResponse = """
+ {
+ "choices": [
+ {
+ "finish_reason": "stop",
+ "index": 0,
+ "logprobs": null,
+ "message": {
+ "annotations": [],
+ "content": "Apache SkyWalking is an
open-source Application Performance Management system designed for
microservices, cloud native, and container-based architectures.",
+ "refusal": null,
+ "role": "assistant"
+ }
+ }
+ ],
+ "created": 1780045046,
+ "id": "chatcmpl-DknJunZ3tgcSkKiv",
+ "model": "gpt-4.1-2025-04-14",
+ "object": "chat.completion",
+ "service_tier": "default",
+ "system_fingerprint": "fp_a7294185dc",
+ "usage": {
+ "completion_tokens": 25,
+ "completion_tokens_details": {
+ "accepted_prediction_tokens": 0,
+ "audio_tokens": 0,
+ "reasoning_tokens": 0,
+ "rejected_prediction_tokens": 0
+ },
+ "latency_checkpoint": {
+ "engine_tbt_ms": 23,
+ "engine_ttft_ms": 672,
+ "engine_ttlt_ms": 1259,
+ "pre_inference_ms": 157,
+ "service_tbt_ms": 610,
+ "service_ttft_ms": 12302,
+ "service_ttlt_ms": 27518,
+ "total_duration_ms": 27381,
+ "user_visible_ttft_ms": 12145
+ },
+ "prompt_tokens": 72,
+ "prompt_tokens_details": {
+ "audio_tokens": 0,
+ "cached_tokens": 0
+ },
+ "total_tokens": 97
+ }
+ }
+ """;
+
+ if (isRagLlmRequest(messages)) {
+ return JSON.parseObject(ragLlmResponse);
+ }
+
if ("tool".equals(lastRole)) {
return JSON.parseObject(finalResponse);
}
@@ -227,4 +282,86 @@ public class LLMMockController {
if (input == null) return "";
return input.replace("\"", "\\\"").replace("\n", "\\n").replace("\r",
"\\r");
}
+
+ private boolean isRagLlmRequest(JSONArray messages) {
+ if (messages == null || messages.size() < 2) {
+ return false;
+ }
+
+ JSONObject lastMessage = messages.getJSONObject(messages.size() - 1);
+ if (!"user".equals(lastMessage.getString("role"))
+ || !"What is Apache
SkyWalking?".equals(lastMessage.getString("content"))) {
+ return false;
+ }
+
+ for (int i = 0; i < messages.size() - 1; i++) {
+ JSONObject message = messages.getJSONObject(i);
+ if (!"system".equals(message.getString("role"))) {
+ continue;
+ }
+ String content = message.getString("content");
+ if (content != null
+ && content.startsWith("Answer using only the following
context:")
+ && content.contains("Apache SkyWalking is an open-source
Application Performance Management system")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @PostMapping("/v1/embeddings")
+ public Object embeddings(@RequestBody JSONObject request) {
+ Object input = request.get("input");
+ JSONArray inputs = input instanceof JSONArray ? (JSONArray) input :
new JSONArray();
+ if (!(input instanceof JSONArray)) {
+ inputs.add(String.valueOf(input));
+ }
+
+ JSONArray data = new JSONArray();
+ for (int i = 0; i < inputs.size(); i++) {
+ JSONObject item = new JSONObject();
+ item.put("object", "embedding");
+ item.put("index", i);
+ item.put("embedding", embeddingFor(inputs.getString(i)));
+ data.add(item);
+ }
+
+ JSONObject usage = new JSONObject();
+ usage.put("prompt_tokens", inputs.size());
+ usage.put("total_tokens", inputs.size());
+
+ JSONObject response = new JSONObject();
+ response.put("object", "list");
+ response.put("model", "text-embedding-3-small");
+ response.put("data", data);
+ response.put("usage", usage);
+ return response;
+ }
+
+ private JSONArray embeddingFor(String input) {
+ String text = input == null ? "" : input.toLowerCase();
+ double[] values = new double[]{
+ score(text, "summit", "san francisco", "generative",
"autonomous"),
+ score(text, "skywalking", "apm", "microservices", "cloud
native"),
+ score(text, "spring ai", "models", "providers"),
+ score(text, "tracecontext", "tracing", "observability"),
+ 0.1
+ };
+
+ JSONArray embedding = new JSONArray();
+ for (double value : values) {
+ embedding.add(value);
+ }
+ return embedding;
+ }
+
+ private double score(String text, String... keywords) {
+ double value = 0.0;
+ for (String keyword : keywords) {
+ if (text.contains(keyword)) {
+ value += 1.0;
+ }
+ }
+ return value;
+ }
}
diff --git
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/resources/application.yaml
b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/resources/application.yaml
index c4f5c58851..fceabe447e 100644
---
a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/resources/application.yaml
+++
b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/resources/application.yaml
@@ -30,6 +30,9 @@ spring:
temperature: 0.7
max-tokens: 1000
top-p: 0.9
+ embedding:
+ options:
+ model: text-embedding-3-small