From dd0fc35fd2d843f1f1b11283e796bcb1596129a5 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 08:59:53 +0800 Subject: [PATCH 01/20] Support tracing RAG retrieval in Spring AI 1.x plugin --- .../apm/agent/core/context/tag/Tags.java | 15 ++ .../spring-ai-1.x-plugin/pom.xml | 7 + .../ai/v1/ChatModelCallInterceptor.java | 5 +- .../ai/v1/ChatModelStreamInterceptor.java | 12 +- .../spring/ai/v1/contant/Constants.java | 2 + .../src/main/resources/skywalking-plugin.def | 1 + .../scenarios/spring-ai-1.x-scenario/pom.xml | 5 + .../httpclient/config/ChatClientConfig.java | 27 ++++ .../httpclient/controller/CaseController.java | 23 +++ .../controller/LLMMockController.java | 137 ++++++++++++++++++ .../src/main/resources/application.yaml | 3 + 11 files changed, 234 insertions(+), 3 deletions(-) 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 static final class HTTP { */ 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 @@ provided + + org.springframework.ai + spring-ai-vector-store + 1.1.0 + provided + + 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.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 Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA @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.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 Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA 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 @@ private Long readAndClearStartTime() { @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/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/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..6185c8b12f 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 @@ -17,6 +17,7 @@ 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.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/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 @@ spring-ai-starter-model-openai + + org.springframework.ai + spring-ai-vector-store + + org.projectlombok lombok 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 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 vectorStoreProvider; @GetMapping("/healthCheck") public String healthCheck() { @@ -63,6 +70,22 @@ public String testCase() throws Exception { .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..5e35cfada5 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.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 Object completions(@RequestBody JSONObject request, HttpServletResponse r } """; + 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-DknJunZ3tgcSkKivGOyQ6gEkMCrCX", + "model": "gpt-4.1-mini-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 @@ private String escapeJson(String input) { 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 From 1347195d361ae99b935f6ade36f382068ba0e456 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 09:42:59 +0800 Subject: [PATCH 02/20] Add Spring AI error type resolver --- .../ai/v1/common/ErrorTypeResolver.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 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 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..9e76d5a2a4 --- /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,85 @@ +/* + * 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.net.UnknownHostException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; +import java.util.concurrent.TimeoutException; + +public final class ErrorTypeResolver { + + private static final AbstractTag ERROR_TYPE = Tags.ofKey("error.type"); + private static final String TIMEOUT = "timeout"; + private static final String SERVER_CERTIFICATE_INVALID = "server_certificate_invalid"; + private static final String INTERNAL_SERVER_ERROR = "500"; + + 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, UnknownHostException.class::isInstance)) { + return UnknownHostException.class.getName(); + } + if (matches(throwable, ErrorTypeResolver::isCertificateInvalid)) { + return SERVER_CERTIFICATE_INVALID; + } + return INTERNAL_SERVER_ERROR; + } + + 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); + } +} From 81c3b2fbdab3f8e98bbe7953c0dcb5b11866033c Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 10:38:34 +0800 Subject: [PATCH 03/20] Add Spring AI vector store instrumentation --- ...ractObservationVectorStoreInterceptor.java | 190 ++++++++++++++++++ ...ObservationVectorStoreInstrumentation.java | 72 +++++++ 2 files changed, 262 insertions(+) create mode 100644 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 create mode 100644 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 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..014d30bc19 --- /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,190 @@ +/* + * 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.contant.Constants; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingOptions; +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.Field; +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]; + VectorStoreObservationContext context = createObservationContext(objInst); + String dataSourceId = resolveDataSourceId(context, objInst); + + 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_PROVIDER_NAME.set(span, context.getDatabaseSystem()); + Tags.GEN_AI_DATA_SOURCE_ID.set(span, dataSourceId); + String model = resolveEmbeddingModel(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())); + if (StringUtils.hasText(request.getQuery())) { + Tags.GEN_AI_RETRIEVAL_QUERY_TEXT.set(span, request.getQuery()); + } + } + } + + @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) { + 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()) { + ContextManager.activeSpan().log(t); + } + } + + private VectorStoreObservationContext createObservationContext(EnhancedInstance objInst) { + return ((AbstractObservationVectorStore) objInst) + .createObservationContextBuilder(VectorStoreObservationContext.Operation.QUERY.value()) + .build(); + } + + private String resolveEmbeddingModel(EnhancedInstance objInst) { + try { + Field field = AbstractObservationVectorStore.class.getDeclaredField("embeddingModel"); + field.setAccessible(true); + Object embeddingModel = field.get(objInst); + return resolveModelFromEmbeddingModel(embeddingModel); + } catch (Throwable ignored) { + return null; + } + } + + private String resolveModelFromEmbeddingModel(Object embeddingModel) { + if (embeddingModel == null) { + return null; + } + String model = resolveModelFromOptionsMethod(embeddingModel); + if (StringUtils.hasText(model)) { + return model; + } + model = resolveModelFromOptionsField(embeddingModel, "options"); + if (StringUtils.hasText(model)) { + return model; + } + return resolveModelFromOptionsField(embeddingModel, "defaultOptions"); + } + + private String resolveModelFromOptionsMethod(Object embeddingModel) { + try { + Method method = embeddingModel.getClass().getMethod("getOptions"); + return resolveModelFromOptions(method.invoke(embeddingModel)); + } catch (Throwable ignored) { + return null; + } + } + + private String resolveModelFromOptionsField(Object embeddingModel, String fieldName) { + Class type = embeddingModel.getClass(); + while (type != null) { + try { + Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + return resolveModelFromOptions(field.get(embeddingModel)); + } catch (NoSuchFieldException e) { + type = type.getSuperclass(); + } catch (Throwable ignored) { + return null; + } + } + return null; + } + + private String resolveModelFromOptions(Object options) { + if (options instanceof EmbeddingOptions) { + return ((EmbeddingOptions) options).getModel(); + } + return null; + } + + private String resolveDataSourceId(VectorStoreObservationContext context, EnhancedInstance objInst) { + if (StringUtils.hasText(context.getCollectionName())) { + return context.getCollectionName(); + } + if (StringUtils.hasText(context.getNamespace())) { + return context.getNamespace(); + } + if (StringUtils.hasText(context.getDatabaseSystem())) { + return context.getDatabaseSystem(); + } + return objInst.getClass().getSimpleName(); + } + + private String toDocumentsJson(List documents) { + List> retrievalDocuments = new ArrayList<>(documents.size()); + for (Object item : documents) { + if (!(item instanceof Document)) { + continue; + } + Document document = (Document) item; + Map 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/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..97cb37a58d --- /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,72 @@ +/* + * 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 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"; + + @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 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; + } + } + }; + } +} From 75d09e62b02e8c59a4630903bbd229af667796a0 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 10:38:34 +0800 Subject: [PATCH 04/20] Add Spring AI vector store instrumentation --- ...ractObservationVectorStoreInterceptor.java | 193 ++++++++++++++++++ ...ObservationVectorStoreInstrumentation.java | 72 +++++++ 2 files changed, 265 insertions(+) create mode 100644 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 create mode 100644 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 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..3bf2202eed --- /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,193 @@ +/* + * 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.contant.Constants; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingOptions; +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.Field; +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]; + VectorStoreObservationContext context = createObservationContext(objInst, request); + String dataSourceId = resolveDataSourceId(context, objInst); + + 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_PROVIDER_NAME.set(span, context.getDatabaseSystem()); + 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())); + if (StringUtils.hasText(request.getQuery())) { + Tags.GEN_AI_RETRIEVAL_QUERY_TEXT.set(span, request.getQuery()); + } + } + } + + @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) { + 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()) { + ContextManager.activeSpan().log(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) { + try { + Field field = AbstractObservationVectorStore.class.getDeclaredField("embeddingModel"); + field.setAccessible(true); + Object embeddingModel = field.get(objInst); + return resolveModelFromEmbeddingModel(embeddingModel); + } catch (Throwable ignored) { + return null; + } + } + + private String resolveModelFromEmbeddingModel(Object embeddingModel) { + if (embeddingModel == null) { + return null; + } + String model = resolveModelFromOptionsMethod(embeddingModel); + if (StringUtils.hasText(model)) { + return model; + } + model = resolveModelFromOptionsField(embeddingModel, "options"); + if (StringUtils.hasText(model)) { + return model; + } + return resolveModelFromOptionsField(embeddingModel, "defaultOptions"); + } + + private String resolveModelFromOptionsMethod(Object embeddingModel) { + try { + Method method = embeddingModel.getClass().getMethod("getOptions"); + return resolveModelFromOptions(method.invoke(embeddingModel)); + } catch (Throwable ignored) { + return null; + } + } + + private String resolveModelFromOptionsField(Object embeddingModel, String fieldName) { + Class type = embeddingModel.getClass(); + while (type != null) { + try { + Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + return resolveModelFromOptions(field.get(embeddingModel)); + } catch (NoSuchFieldException e) { + type = type.getSuperclass(); + } catch (Throwable ignored) { + return null; + } + } + return null; + } + + private String resolveModelFromOptions(Object options) { + if (options instanceof EmbeddingOptions) { + return ((EmbeddingOptions) options).getModel(); + } + return null; + } + + private String resolveDataSourceId(VectorStoreObservationContext context, EnhancedInstance objInst) { + if (StringUtils.hasText(context.getCollectionName())) { + return context.getCollectionName(); + } + if (StringUtils.hasText(context.getNamespace())) { + return context.getNamespace(); + } + if (StringUtils.hasText(context.getDatabaseSystem())) { + return context.getDatabaseSystem(); + } + return objInst.getClass().getSimpleName(); + } + + private String toDocumentsJson(List documents) { + List> retrievalDocuments = new ArrayList<>(documents.size()); + for (Object item : documents) { + if (!(item instanceof Document)) { + continue; + } + Document document = (Document) item; + Map 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/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..97cb37a58d --- /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,72 @@ +/* + * 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 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"; + + @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 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; + } + } + }; + } +} From f33622d9b99cf49126099826b7139f33133e4e95 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 10:55:01 +0800 Subject: [PATCH 05/20] Fix --- ...ractObservationVectorStoreInterceptor.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) 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 index 3bf2202eed..e8885f421b 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/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 @@ -23,6 +23,7 @@ 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.InstanceConstructorInterceptor; 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; @@ -42,7 +43,14 @@ import java.util.List; import java.util.Map; -public class AbstractObservationVectorStoreInterceptor implements InstanceMethodsAroundInterceptor { +public class AbstractObservationVectorStoreInterceptor implements InstanceConstructorInterceptor, InstanceMethodsAroundInterceptor { + + @Override + public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { + if (allArguments != null && allArguments.length > 0) { + objInst.setSkyWalkingDynamicField(allArguments[0]); + } + } @Override public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, @@ -104,14 +112,7 @@ private VectorStoreObservationContext createObservationContext(EnhancedInstance } private String resolveEmbeddingModelName(EnhancedInstance objInst) { - try { - Field field = AbstractObservationVectorStore.class.getDeclaredField("embeddingModel"); - field.setAccessible(true); - Object embeddingModel = field.get(objInst); - return resolveModelFromEmbeddingModel(embeddingModel); - } catch (Throwable ignored) { - return null; - } + return resolveModelFromEmbeddingModel(objInst.getSkyWalkingDynamicField()); } private String resolveModelFromEmbeddingModel(Object embeddingModel) { From 72685432d1459bf8222b11f57faa2a1092d5fc00 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 11:17:14 +0800 Subject: [PATCH 06/20] Use fallback error type for Spring AI errors --- .../apm/plugin/spring/ai/v1/common/ErrorTypeResolver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 9e76d5a2a4..f5b8e51e7a 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/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 @@ -34,7 +34,7 @@ public final class ErrorTypeResolver { private static final AbstractTag ERROR_TYPE = Tags.ofKey("error.type"); private static final String TIMEOUT = "timeout"; private static final String SERVER_CERTIFICATE_INVALID = "server_certificate_invalid"; - private static final String INTERNAL_SERVER_ERROR = "500"; + private static final String OTHER = "_OTHER"; private ErrorTypeResolver() { } @@ -53,7 +53,7 @@ private static String resolve(Throwable throwable) { if (matches(throwable, ErrorTypeResolver::isCertificateInvalid)) { return SERVER_CERTIFICATE_INVALID; } - return INTERNAL_SERVER_ERROR; + return OTHER; } private static boolean isTimeout(Throwable throwable) { From fa23baf1b3661aa463c99426b96e7f1cc57a810f Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 11:23:07 +0800 Subject: [PATCH 07/20] Cache Spring AI vector store model name --- ...tionVectorStoreConstructorInterceptor.java | 55 ++++++++++++++++++- ...ractObservationVectorStoreInterceptor.java | 51 +---------------- .../ai/v1/VectorStoreEnhanceContext.java | 32 +++++++++++ 3 files changed, 89 insertions(+), 49 deletions(-) create mode 100644 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 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 index 45959f6071..9db95ee33e 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/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 @@ -20,13 +20,66 @@ import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor; +import org.springframework.ai.embedding.EmbeddingOptions; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; public class AbstractObservationVectorStoreConstructorInterceptor implements InstanceConstructorInterceptor { @Override public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { if (allArguments != null && allArguments.length > 0) { - objInst.setSkyWalkingDynamicField(allArguments[0]); + String embeddingModelName = resolveModelFromEmbeddingModel(allArguments[0]); + objInst.setSkyWalkingDynamicField(new VectorStoreEnhanceContext(embeddingModelName)); + } + } + + private String resolveModelFromEmbeddingModel(Object embeddingModel) { + if (embeddingModel == null) { + return null; + } + String model = resolveModelFromOptionsMethod(embeddingModel); + if (StringUtils.hasText(model)) { + return model; + } + model = resolveModelFromOptionsField(embeddingModel, "options"); + if (StringUtils.hasText(model)) { + return model; + } + return resolveModelFromOptionsField(embeddingModel, "defaultOptions"); + } + + private String resolveModelFromOptionsMethod(Object embeddingModel) { + try { + Method method = embeddingModel.getClass().getMethod("getOptions"); + return resolveModelFromOptions(method.invoke(embeddingModel)); + } catch (Throwable ignored) { + return null; + } + } + + private String resolveModelFromOptionsField(Object embeddingModel, String fieldName) { + Class type = embeddingModel.getClass(); + while (type != null) { + try { + Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + return resolveModelFromOptions(field.get(embeddingModel)); + } catch (NoSuchFieldException e) { + type = type.getSuperclass(); + } catch (Throwable ignored) { + return null; + } + } + return null; + } + + private String resolveModelFromOptions(Object options) { + if (options instanceof EmbeddingOptions) { + return ((EmbeddingOptions) options).getModel(); } + return null; } } 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 index d7cbe51f92..2e58e9686f 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/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 @@ -29,13 +29,11 @@ import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants; import org.springframework.ai.document.Document; -import org.springframework.ai.embedding.EmbeddingOptions; 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.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -104,52 +102,9 @@ private VectorStoreObservationContext createObservationContext(EnhancedInstance } private String resolveEmbeddingModelName(EnhancedInstance objInst) { - return resolveModelFromEmbeddingModel(objInst.getSkyWalkingDynamicField()); - } - - private String resolveModelFromEmbeddingModel(Object embeddingModel) { - if (embeddingModel == null) { - return null; - } - String model = resolveModelFromOptionsMethod(embeddingModel); - if (StringUtils.hasText(model)) { - return model; - } - model = resolveModelFromOptionsField(embeddingModel, "options"); - if (StringUtils.hasText(model)) { - return model; - } - return resolveModelFromOptionsField(embeddingModel, "defaultOptions"); - } - - private String resolveModelFromOptionsMethod(Object embeddingModel) { - try { - Method method = embeddingModel.getClass().getMethod("getOptions"); - return resolveModelFromOptions(method.invoke(embeddingModel)); - } catch (Throwable ignored) { - return null; - } - } - - private String resolveModelFromOptionsField(Object embeddingModel, String fieldName) { - Class type = embeddingModel.getClass(); - while (type != null) { - try { - Field field = type.getDeclaredField(fieldName); - field.setAccessible(true); - return resolveModelFromOptions(field.get(embeddingModel)); - } catch (NoSuchFieldException e) { - type = type.getSuperclass(); - } catch (Throwable ignored) { - return null; - } - } - return null; - } - - private String resolveModelFromOptions(Object options) { - if (options instanceof EmbeddingOptions) { - return ((EmbeddingOptions) options).getModel(); + Object context = objInst.getSkyWalkingDynamicField(); + if (context instanceof VectorStoreEnhanceContext) { + return ((VectorStoreEnhanceContext) context).getEmbeddingModelName(); } return null; } 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/VectorStoreEnhanceContext.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 new file mode 100644 index 0000000000..a478d07bd7 --- /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/VectorStoreEnhanceContext.java @@ -0,0 +1,32 @@ +/* + * 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; + +public class VectorStoreEnhanceContext { + + private final String embeddingModelName; + + public VectorStoreEnhanceContext(String embeddingModelName) { + this.embeddingModelName = embeddingModelName; + } + + public String getEmbeddingModelName() { + return embeddingModelName; + } +} From 591427b3ee6e75adfe600da38b2f45ade2e1aeba Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 14:34:21 +0800 Subject: [PATCH 08/20] Fix --- .../config/expectedData.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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..d213c197be 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,24 @@ segmentItems: - { key: http.method, value: POST } - { key: http.status_code, value: '200' } + - operationName: retrieval in-memory-map + parentSpanId: 0 + spanId: 6 + spanLayer: GenAI + startTime: not null + endTime: not null + componentId: 178 + spanType: Exit + peer: in-memory-map + tags: + - { key: gen_ai.operation.name, value: retrieval } + - { key: gen_ai.provider.name, value: simple } + - { key: gen_ai.data_source.id, value: 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: GET:/spring-ai-1.x-scenario/case/spring-ai-1.x-scenario-case parentSpanId: -1 spanId: 0 From 77b4e81bd745cce357959624df70e06987c73333 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 15:50:32 +0800 Subject: [PATCH 09/20] Fix --- .../config/expectedData.yaml | 26 +++++++++++++++++++ .../controller/LLMMockController.java | 4 +-- 2 files changed, 28 insertions(+), 2 deletions(-) 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 d213c197be..e4c6d38b77 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 @@ -160,6 +160,32 @@ segmentItems: - { 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/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 5e35cfada5..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 @@ -194,8 +194,8 @@ public Object completions(@RequestBody JSONObject request, HttpServletResponse r } ], "created": 1780045046, - "id": "chatcmpl-DknJunZ3tgcSkKivGOyQ6gEkMCrCX", - "model": "gpt-4.1-mini-2025-04-14", + "id": "chatcmpl-DknJunZ3tgcSkKiv", + "model": "gpt-4.1-2025-04-14", "object": "chat.completion", "service_tier": "default", "system_fingerprint": "fp_a7294185dc", From d1dd31d0ff90c535d281eb87cd4b10cb8d45c046 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 20:16:50 +0800 Subject: [PATCH 10/20] Fix --- ...ractObservationVectorStoreInterceptor.java | 25 ++++++++++++------- .../config/expectedData.yaml | 5 ++-- 2 files changed, 18 insertions(+), 12 deletions(-) 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 index 2e58e9686f..8a34e695ef 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/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 @@ -49,11 +49,10 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr VectorStoreObservationContext context = createObservationContext(objInst, request); String dataSourceId = resolveDataSourceId(context, objInst); - AbstractSpan span = ContextManager.createExitSpan(Constants.RETRIEVAL + " " + dataSourceId, dataSourceId); + 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_PROVIDER_NAME.set(span, context.getDatabaseSystem()); Tags.GEN_AI_DATA_SOURCE_ID.set(span, dataSourceId); String model = resolveEmbeddingModelName(objInst); if (StringUtils.hasText(model)) { @@ -110,16 +109,24 @@ private String resolveEmbeddingModelName(EnhancedInstance objInst) { } private String resolveDataSourceId(VectorStoreObservationContext context, EnhancedInstance objInst) { - if (StringUtils.hasText(context.getCollectionName())) { - return context.getCollectionName(); + StringBuilder dataSourceId = new StringBuilder(); + appendDataSourcePart(dataSourceId, context.getDatabaseSystem()); + appendDataSourcePart(dataSourceId, context.getNamespace()); + appendDataSourcePart(dataSourceId, context.getCollectionName()); + if (dataSourceId.length() > 0) { + return dataSourceId.toString(); } - if (StringUtils.hasText(context.getNamespace())) { - return context.getNamespace(); + return objInst.getClass().getSimpleName(); + } + + private void appendDataSourcePart(StringBuilder dataSourceId, String value) { + if (!StringUtils.hasText(value)) { + return; } - if (StringUtils.hasText(context.getDatabaseSystem())) { - return context.getDatabaseSystem(); + if (dataSourceId.length() > 0) { + dataSourceId.append('/'); } - return objInst.getClass().getSimpleName(); + dataSourceId.append(value); } private String toDocumentsJson(List documents) { 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 e4c6d38b77..ba3713d433 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,7 +142,7 @@ segmentItems: - { key: http.method, value: POST } - { key: http.status_code, value: '200' } - - operationName: retrieval in-memory-map + - operationName: retrieval/simple/in-memory-map parentSpanId: 0 spanId: 6 spanLayer: GenAI @@ -150,10 +150,9 @@ segmentItems: endTime: not null componentId: 178 spanType: Exit - peer: in-memory-map + peer: simple/in-memory-map tags: - { key: gen_ai.operation.name, value: retrieval } - - { key: gen_ai.provider.name, value: simple } - { key: gen_ai.data_source.id, value: in-memory-map } - { key: gen_ai.request.model, value: text-embedding-3-small } - { key: gen_ai.request.top_k, value: '2' } From a9843a788fb70d0751de35b3e0d6c740fc1fef41 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sat, 30 May 2026 20:21:10 +0800 Subject: [PATCH 11/20] Support tracing RAG retrieval in Spring AI 1.x plugin --- .../apm/agent/core/context/tag/Tags.java | 15 ++ .../spring-ai-1.x-plugin/pom.xml | 7 + ...tionVectorStoreConstructorInterceptor.java | 85 ++++++++++ ...ractObservationVectorStoreInterceptor.java | 148 ++++++++++++++++++ .../ai/v1/ChatModelCallInterceptor.java | 5 +- .../ai/v1/ChatModelStreamInterceptor.java | 12 +- .../ai/v1/VectorStoreEnhanceContext.java | 32 ++++ .../ai/v1/common/ErrorTypeResolver.java | 85 ++++++++++ .../spring/ai/v1/contant/Constants.java | 2 + ...ObservationVectorStoreInstrumentation.java | 92 +++++++++++ .../src/main/resources/skywalking-plugin.def | 1 + .../config/expectedData.yaml | 43 +++++ .../scenarios/spring-ai-1.x-scenario/pom.xml | 5 + .../httpclient/config/ChatClientConfig.java | 27 ++++ .../httpclient/controller/CaseController.java | 23 +++ .../controller/LLMMockController.java | 137 ++++++++++++++++ .../src/main/resources/application.yaml | 3 + 17 files changed, 719 insertions(+), 3 deletions(-) create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 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 static final class HTTP { */ 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 @@ provided + + org.springframework.ai + spring-ai-vector-store + 1.1.0 + provided + + 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..9db95ee33e --- /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,85 @@ +/* + * 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.springframework.ai.embedding.EmbeddingOptions; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class AbstractObservationVectorStoreConstructorInterceptor implements InstanceConstructorInterceptor { + + @Override + public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { + if (allArguments != null && allArguments.length > 0) { + String embeddingModelName = resolveModelFromEmbeddingModel(allArguments[0]); + objInst.setSkyWalkingDynamicField(new VectorStoreEnhanceContext(embeddingModelName)); + } + } + + private String resolveModelFromEmbeddingModel(Object embeddingModel) { + if (embeddingModel == null) { + return null; + } + String model = resolveModelFromOptionsMethod(embeddingModel); + if (StringUtils.hasText(model)) { + return model; + } + model = resolveModelFromOptionsField(embeddingModel, "options"); + if (StringUtils.hasText(model)) { + return model; + } + return resolveModelFromOptionsField(embeddingModel, "defaultOptions"); + } + + private String resolveModelFromOptionsMethod(Object embeddingModel) { + try { + Method method = embeddingModel.getClass().getMethod("getOptions"); + return resolveModelFromOptions(method.invoke(embeddingModel)); + } catch (Throwable ignored) { + return null; + } + } + + private String resolveModelFromOptionsField(Object embeddingModel, String fieldName) { + Class type = embeddingModel.getClass(); + while (type != null) { + try { + Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + return resolveModelFromOptions(field.get(embeddingModel)); + } catch (NoSuchFieldException e) { + type = type.getSuperclass(); + } catch (Throwable ignored) { + return null; + } + } + return null; + } + + private String resolveModelFromOptions(Object options) { + if (options instanceof EmbeddingOptions) { + return ((EmbeddingOptions) options).getModel(); + } + return null; + } +} 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..8a34e695ef --- /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,148 @@ +/* + * 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.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]; + VectorStoreObservationContext context = createObservationContext(objInst, request); + String dataSourceId = resolveDataSourceId(context, objInst); + + 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())); + if (StringUtils.hasText(request.getQuery())) { + Tags.GEN_AI_RETRIEVAL_QUERY_TEXT.set(span, request.getQuery()); + } + } + } + + @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) { + 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()) { + ContextManager.activeSpan().log(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> retrievalDocuments = new ArrayList<>(documents.size()); + for (Object item : documents) { + if (!(item instanceof Document)) { + continue; + } + Document document = (Document) item; + Map 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.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 Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA @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.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 Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA 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 @@ private Long readAndClearStartTime() { @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/VectorStoreEnhanceContext.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 new file mode 100644 index 0000000000..a478d07bd7 --- /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/VectorStoreEnhanceContext.java @@ -0,0 +1,32 @@ +/* + * 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; + +public class VectorStoreEnhanceContext { + + private final String embeddingModelName; + + public VectorStoreEnhanceContext(String embeddingModelName) { + this.embeddingModelName = embeddingModelName; + } + + public String getEmbeddingModelName() { + return 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..f5b8e51e7a --- /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,85 @@ +/* + * 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.net.UnknownHostException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; +import java.util.concurrent.TimeoutException; + +public final class ErrorTypeResolver { + + private static final AbstractTag ERROR_TYPE = Tags.ofKey("error.type"); + private static final String TIMEOUT = "timeout"; + private static final String SERVER_CERTIFICATE_INVALID = "server_certificate_invalid"; + private static final String OTHER = "_OTHER"; + + 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, UnknownHostException.class::isInstance)) { + return UnknownHostException.class.getName(); + } + if (matches(throwable, ErrorTypeResolver::isCertificateInvalid)) { + return SERVER_CERTIFICATE_INVALID; + } + return OTHER; + } + + 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/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..895205832c --- /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 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 EMBEDDING_MODEL_CLASS = "org.springframework.ai.embedding.EmbeddingModel"; + + 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 HierarchyMatch.byHierarchyMatch(ENHANCE_CLASS); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[] { + new ConstructorInterceptPoint() { + @Override + public ElementMatcher getConstructorMatcher() { + return takesArguments(4).and(takesArgumentWithType(0, EMBEDDING_MODEL_CLASS)); + } + + @Override + public String getConstructorInterceptor() { + return CONSTRUCTOR_INTERCEPTOR_CLASS; + } + } + }; + } + + @Override + public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { + return new InstanceMethodsInterceptPoint[] { + new InstanceMethodsInterceptPoint() { + @Override + public ElementMatcher 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/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..6185c8b12f 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 @@ -17,6 +17,7 @@ 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.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/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..ba3713d433 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: 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 @@ spring-ai-starter-model-openai + + org.springframework.ai + spring-ai-vector-store + + org.projectlombok lombok 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 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 vectorStoreProvider; @GetMapping("/healthCheck") public String healthCheck() { @@ -63,6 +70,22 @@ public String testCase() throws Exception { .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.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 Object completions(@RequestBody JSONObject request, HttpServletResponse r } """; + 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 @@ private String escapeJson(String input) { 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 From d7721411c88371037936ff81db3382091038b922 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sun, 31 May 2026 10:50:18 +0800 Subject: [PATCH 12/20] Fix --- ...ObservationVectorStoreConstructorInterceptor.java | 10 +++++++++- ...bstractObservationVectorStoreInstrumentation.java | 12 ++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) 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 index 9db95ee33e..be5ea2c4ee 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/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 @@ -21,6 +21,7 @@ import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor; import org.springframework.ai.embedding.EmbeddingOptions; +import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.util.StringUtils; import java.lang.reflect.Field; @@ -31,11 +32,18 @@ public class AbstractObservationVectorStoreConstructorInterceptor implements Ins @Override public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { if (allArguments != null && allArguments.length > 0) { - String embeddingModelName = resolveModelFromEmbeddingModel(allArguments[0]); + String embeddingModelName = resolveModelFromArgument(allArguments[0]); objInst.setSkyWalkingDynamicField(new VectorStoreEnhanceContext(embeddingModelName)); } } + private String resolveModelFromArgument(Object argument) { + if (argument instanceof EmbeddingModel) { + return resolveModelFromEmbeddingModel(argument); + } + return null; + } + private String resolveModelFromEmbeddingModel(Object embeddingModel) { if (embeddingModel == null) { return null; 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 index 895205832c..f379c0464c 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/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 @@ -25,6 +25,8 @@ 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; @@ -34,8 +36,6 @@ public class AbstractObservationVectorStoreInstrumentation extends ClassInstance private static final String ENHANCE_CLASS = "org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore"; - private static final String EMBEDDING_MODEL_CLASS = "org.springframework.ai.embedding.EmbeddingModel"; - private static final String INTERCEPT_METHOD = "doSimilaritySearch"; private static final String INTERCEPTOR_CLASS = @@ -46,16 +46,16 @@ public class AbstractObservationVectorStoreInstrumentation extends ClassInstance @Override protected ClassMatch enhanceClass() { - return HierarchyMatch.byHierarchyMatch(ENHANCE_CLASS); + return LogicalMatchOperation.or(HierarchyMatch.byHierarchyMatch(ENHANCE_CLASS), MultiClassNameMatch.byMultiClassMatch(ENHANCE_CLASS)); } @Override public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { - return new ConstructorInterceptPoint[] { + return new ConstructorInterceptPoint[]{ new ConstructorInterceptPoint() { @Override public ElementMatcher getConstructorMatcher() { - return takesArguments(4).and(takesArgumentWithType(0, EMBEDDING_MODEL_CLASS)); + return takesArgumentWithType(0, "org.springframework.ai.embedding.EmbeddingModel"); } @Override @@ -68,7 +68,7 @@ public String getConstructorInterceptor() { @Override public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { - return new InstanceMethodsInterceptPoint[] { + return new InstanceMethodsInterceptPoint[]{ new InstanceMethodsInterceptPoint() { @Override public ElementMatcher getMethodsMatcher() { From 3b1acdceec8a70e561815006164745103517c204 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Sun, 31 May 2026 13:37:55 +0800 Subject: [PATCH 13/20] Fix e2e test --- .../scenarios/spring-ai-1.x-scenario/config/expectedData.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ba3713d433..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 @@ -153,7 +153,7 @@ segmentItems: peer: simple/in-memory-map tags: - { key: gen_ai.operation.name, value: retrieval } - - { key: gen_ai.data_source.id, value: in-memory-map } + - { 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?' } From e7923be8fcdf9260a614cfcbe7af40006c2938cf Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Mon, 1 Jun 2026 21:59:59 +0800 Subject: [PATCH 14/20] Fix suggestions --- ...tractObservationVectorStoreInterceptor.java | 18 ++++++++++++++++-- .../spring/ai/v1/common/ErrorTypeResolver.java | 6 +----- 2 files changed, 17 insertions(+), 7 deletions(-) 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 index 8a34e695ef..5f18d5c2eb 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/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 @@ -46,10 +46,24 @@ public class AbstractObservationVectorStoreInterceptor implements InstanceMethod public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, MethodInterceptResult result) throws Throwable { SearchRequest request = (SearchRequest) allArguments[0]; - VectorStoreObservationContext context = createObservationContext(objInst, request); - String dataSourceId = resolveDataSourceId(context, objInst); + 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); 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 index f5b8e51e7a..e0b9c54c18 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/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 @@ -24,7 +24,6 @@ import javax.net.ssl.SSLHandshakeException; import java.net.SocketTimeoutException; -import java.net.UnknownHostException; import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; import java.util.concurrent.TimeoutException; @@ -47,13 +46,10 @@ private static String resolve(Throwable throwable) { if (matches(throwable, ErrorTypeResolver::isTimeout)) { return TIMEOUT; } - if (matches(throwable, UnknownHostException.class::isInstance)) { - return UnknownHostException.class.getName(); - } if (matches(throwable, ErrorTypeResolver::isCertificateInvalid)) { return SERVER_CERTIFICATE_INVALID; } - return OTHER; + return throwable.getClass().getName(); } private static boolean isTimeout(Throwable throwable) { From c852d8ee26d06624d92e2578c69f4b9a713de741 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Mon, 1 Jun 2026 23:57:51 +0800 Subject: [PATCH 15/20] Fix suggestions --- CHANGES.md | 1 + ...tionVectorStoreConstructorInterceptor.java | 65 +++------------- .../ai/v1/EmbeddingModelEnhanceContext.java | 36 +++++++++ .../ai/v1/EmbeddingModelInterceptor.java | 65 ++++++++++++++++ .../ai/v1/VectorStoreEnhanceContext.java | 11 ++- .../define/EmbeddingModelInstrumentation.java | 77 +++++++++++++++++++ 6 files changed, 197 insertions(+), 58 deletions(-) create mode 100644 apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/EmbeddingModelEnhanceContext.java create mode 100644 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 create mode 100644 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 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-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 index be5ea2c4ee..d1895750bb 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/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 @@ -20,74 +20,31 @@ import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor; -import org.springframework.ai.embedding.EmbeddingOptions; import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.util.StringUtils; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; public class AbstractObservationVectorStoreConstructorInterceptor implements InstanceConstructorInterceptor { @Override public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { if (allArguments != null && allArguments.length > 0) { - String embeddingModelName = resolveModelFromArgument(allArguments[0]); - objInst.setSkyWalkingDynamicField(new VectorStoreEnhanceContext(embeddingModelName)); - } - } - - private String resolveModelFromArgument(Object argument) { - if (argument instanceof EmbeddingModel) { - return resolveModelFromEmbeddingModel(argument); - } - return null; - } - - private String resolveModelFromEmbeddingModel(Object embeddingModel) { - if (embeddingModel == null) { - return null; - } - String model = resolveModelFromOptionsMethod(embeddingModel); - if (StringUtils.hasText(model)) { - return model; + objInst.setSkyWalkingDynamicField(new VectorStoreEnhanceContext(resolveContextFromArgument(allArguments[0]))); } - model = resolveModelFromOptionsField(embeddingModel, "options"); - if (StringUtils.hasText(model)) { - return model; - } - return resolveModelFromOptionsField(embeddingModel, "defaultOptions"); } - private String resolveModelFromOptionsMethod(Object embeddingModel) { - try { - Method method = embeddingModel.getClass().getMethod("getOptions"); - return resolveModelFromOptions(method.invoke(embeddingModel)); - } catch (Throwable ignored) { - return null; - } - } - - private String resolveModelFromOptionsField(Object embeddingModel, String fieldName) { - Class type = embeddingModel.getClass(); - while (type != null) { - try { - Field field = type.getDeclaredField(fieldName); - field.setAccessible(true); - return resolveModelFromOptions(field.get(embeddingModel)); - } catch (NoSuchFieldException e) { - type = type.getSuperclass(); - } catch (Throwable ignored) { - return null; - } + private EmbeddingModelEnhanceContext resolveContextFromArgument(Object argument) { + if (argument instanceof EmbeddingModel && argument instanceof EnhancedInstance) { + return getOrCreateContext((EnhancedInstance) argument); } return null; } - private String resolveModelFromOptions(Object options) { - if (options instanceof EmbeddingOptions) { - return ((EmbeddingOptions) options).getModel(); + private EmbeddingModelEnhanceContext getOrCreateContext(EnhancedInstance embeddingModel) { + Object context = embeddingModel.getSkyWalkingDynamicField(); + if (context instanceof EmbeddingModelEnhanceContext) { + return (EmbeddingModelEnhanceContext) context; } - return null; + 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/EmbeddingModelEnhanceContext.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/EmbeddingModelEnhanceContext.java new file mode 100644 index 0000000000..a077fea851 --- /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/EmbeddingModelEnhanceContext.java @@ -0,0 +1,36 @@ +/* + * 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.springframework.util.StringUtils; + +public class EmbeddingModelEnhanceContext { + + private volatile String embeddingModelName; + + public String getEmbeddingModelName() { + return embeddingModelName; + } + + 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/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..cfc6d5da2a --- /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,65 @@ +/* + * 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 java.lang.reflect.Method; + +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.springframework.ai.embedding.EmbeddingOptions; +import org.springframework.util.StringUtils; + +public class EmbeddingModelInterceptor implements InstanceMethodsAroundInterceptor { + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInterceptResult result) { + if (allArguments == null || allArguments.length < 2 || !(allArguments[1] instanceof EmbeddingOptions)) { + return; + } + String model = ((EmbeddingOptions) allArguments[1]).getModel(); + if (!StringUtils.hasText(model)) { + return; + } + EmbeddingModelEnhanceContext context = getOrCreateContext(objInst); + context.setEmbeddingModelNameIfAbsent(model); + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret) { + 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/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 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 index a478d07bd7..b0a7ff6a7a 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/VectorStoreEnhanceContext.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 @@ -20,13 +20,16 @@ public class VectorStoreEnhanceContext { - private final String embeddingModelName; + private final EmbeddingModelEnhanceContext embeddingModelEnhanceContext; - public VectorStoreEnhanceContext(String embeddingModelName) { - this.embeddingModelName = embeddingModelName; + public VectorStoreEnhanceContext(EmbeddingModelEnhanceContext embeddingModelEnhanceContext) { + this.embeddingModelEnhanceContext = embeddingModelEnhanceContext; } public String getEmbeddingModelName() { - return embeddingModelName; + if (embeddingModelEnhanceContext == null) { + return null; + } + return embeddingModelEnhanceContext.getEmbeddingModelName(); } } 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..8ad07907d6 --- /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,77 @@ +/* + * 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 = "embed"; + + 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 getMethodsMatcher() { + return named(INTERCEPT_METHOD) + .and(takesArguments(3)) + .and(takesArgumentWithType(0, "java.util.List")) + .and(takesArgumentWithType(1, "org.springframework.ai.embedding.EmbeddingOptions")) + .and(takesArgumentWithType(2, "org.springframework.ai.embedding.BatchingStrategy")); + } + + @Override + public String getMethodsInterceptor() { + return INTERCEPTOR_CLASS; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } +} From a453e4ed9df120265f8c5a55f9af1958a70d006c Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Mon, 1 Jun 2026 23:58:12 +0800 Subject: [PATCH 16/20] add plugin definition --- .../src/main/resources/skywalking-plugin.def | 1 + 1 file changed, 1 insertion(+) 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 6185c8b12f..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,6 +15,7 @@ # 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 From aeaf35a4b2452978a3c68de275bdd71c7d420fb6 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Tue, 2 Jun 2026 12:40:46 +0800 Subject: [PATCH 17/20] Fix EmbeddingModelInterceptor --- .../ai/v1/EmbeddingModelInterceptor.java | 32 +++++++++---------- .../define/EmbeddingModelInstrumentation.java | 8 ++--- 2 files changed, 19 insertions(+), 21 deletions(-) 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 index cfc6d5da2a..d1767cc1bc 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/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 @@ -18,39 +18,39 @@ package org.apache.skywalking.apm.plugin.spring.ai.v1; -import java.lang.reflect.Method; - 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.springframework.ai.embedding.EmbeddingOptions; +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) { - if (allArguments == null || allArguments.length < 2 || !(allArguments[1] instanceof EmbeddingOptions)) { - return; + 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) { + EmbeddingResponseMetadata metadata = ((EmbeddingResponse) ret).getMetadata(); + if (metadata == null) { + return ret; } - String model = ((EmbeddingOptions) allArguments[1]).getModel(); + String model = metadata.getModel(); if (!StringUtils.hasText(model)) { - return; + return ret; } EmbeddingModelEnhanceContext context = getOrCreateContext(objInst); context.setEmbeddingModelNameIfAbsent(model); - } - - @Override - public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, - Object ret) { return ret; } @Override - public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, - Class[] argumentsTypes, Throwable t) { + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Throwable t) { } private EmbeddingModelEnhanceContext getOrCreateContext(EnhancedInstance objInst) { 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 index 8ad07907d6..12a327ae29 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/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 @@ -34,7 +34,7 @@ public class EmbeddingModelInstrumentation extends ClassInstanceMethodsEnhancePl private static final String ENHANCE_CLASS = "org.springframework.ai.embedding.EmbeddingModel"; - private static final String INTERCEPT_METHOD = "embed"; + private static final String INTERCEPT_METHOD = "call"; private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.EmbeddingModelInterceptor"; @@ -56,10 +56,8 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { @Override public ElementMatcher getMethodsMatcher() { return named(INTERCEPT_METHOD) - .and(takesArguments(3)) - .and(takesArgumentWithType(0, "java.util.List")) - .and(takesArgumentWithType(1, "org.springframework.ai.embedding.EmbeddingOptions")) - .and(takesArgumentWithType(2, "org.springframework.ai.embedding.BatchingStrategy")); + .and(takesArguments(1)) + .and(takesArgumentWithType(0, "org.springframework.ai.embedding.EmbeddingRequest")); } @Override From e92817018576bba213af27e0e8c4b5087a3ebcc2 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Tue, 2 Jun 2026 12:42:54 +0800 Subject: [PATCH 18/20] remove redundant codes --- ...stractObservationVectorStoreConstructorInterceptor.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 index d1895750bb..55a0fee1ab 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/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 @@ -20,19 +20,16 @@ import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor; -import org.springframework.ai.embedding.EmbeddingModel; public class AbstractObservationVectorStoreConstructorInterceptor implements InstanceConstructorInterceptor { @Override public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { - if (allArguments != null && allArguments.length > 0) { - objInst.setSkyWalkingDynamicField(new VectorStoreEnhanceContext(resolveContextFromArgument(allArguments[0]))); - } + objInst.setSkyWalkingDynamicField(new VectorStoreEnhanceContext(resolveContextFromArgument(allArguments[0]))); } private EmbeddingModelEnhanceContext resolveContextFromArgument(Object argument) { - if (argument instanceof EmbeddingModel && argument instanceof EnhancedInstance) { + if (argument instanceof EnhancedInstance) { return getOrCreateContext((EnhancedInstance) argument); } return null; From 2219fe7e8dbb0c3282112ed093463be53176baa3 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Tue, 2 Jun 2026 23:15:38 +0800 Subject: [PATCH 19/20] Fix suggestions --- ...vationVectorStoreConstructorInterceptor.java | 1 + ...stractObservationVectorStoreInterceptor.java | 17 +++++++++++++---- .../spring/ai/v1/EmbeddingModelInterceptor.java | 5 +++++ .../spring/ai/v1/VectorStoreEnhanceContext.java | 2 ++ .../EmbeddingModelEnhanceContext.java | 2 +- .../spring/ai/v1/common/ErrorTypeResolver.java | 1 - .../ai/v1/config/SpringAiPluginConfig.java | 17 +++++++++++++++++ apm-sniffer/config/agent.config | 10 +++++++++- .../spring-ai-1.x-scenario/bin/startup.sh | 3 ++- 9 files changed, 50 insertions(+), 8 deletions(-) rename 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 (95%) 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 index 55a0fee1ab..faefa999bc 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/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 @@ -20,6 +20,7 @@ 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 { 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 index 5f18d5c2eb..4b278b6c87 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/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 @@ -27,6 +27,8 @@ 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; @@ -75,8 +77,13 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr if (request != null) { Tags.GEN_AI_TOP_K.set(span, String.valueOf(request.getTopK())); - if (StringUtils.hasText(request.getQuery())) { - Tags.GEN_AI_RETRIEVAL_QUERY_TEXT.set(span, request.getQuery()); + 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); } } } @@ -88,7 +95,7 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA return ret; } try { - if (ret instanceof List) { + if (ret instanceof List && SpringAiPluginConfig.Plugin.SpringAi.COLLECT_RETRIEVAL_DOCUMENTS) { Tags.GEN_AI_RETRIEVAL_DOCUMENTS.set(ContextManager.activeSpan(), toDocumentsJson((List) ret)); } } finally { @@ -101,7 +108,9 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA 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 index d1767cc1bc..8e2b57fae9 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/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 @@ -21,6 +21,7 @@ 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; @@ -36,6 +37,10 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr @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; 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/VectorStoreEnhanceContext.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 index b0a7ff6a7a..76916a59ea 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/VectorStoreEnhanceContext.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 @@ -18,6 +18,8 @@ package org.apache.skywalking.apm.plugin.spring.ai.v1; +import org.apache.skywalking.apm.plugin.spring.ai.v1.common.EmbeddingModelEnhanceContext; + public class VectorStoreEnhanceContext { private final EmbeddingModelEnhanceContext 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/EmbeddingModelEnhanceContext.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 95% rename from apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/EmbeddingModelEnhanceContext.java rename 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 a077fea851..199bd97cd4 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/EmbeddingModelEnhanceContext.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 @@ -16,7 +16,7 @@ * */ -package org.apache.skywalking.apm.plugin.spring.ai.v1; +package org.apache.skywalking.apm.plugin.spring.ai.v1.common; import org.springframework.util.StringUtils; 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 index e0b9c54c18..1d3172cf3d 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/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 @@ -33,7 +33,6 @@ public final class ErrorTypeResolver { private static final AbstractTag ERROR_TYPE = Tags.ofKey("error.type"); private static final String TIMEOUT = "timeout"; private static final String SERVER_CERTIFICATE_INVALID = "server_certificate_invalid"; - private static final String OTHER = "_OTHER"; private ErrorTypeResolver() { } 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 static class SpringAi { * 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/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..43e2734fd7 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,5 @@ 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 From 9874bd447b3123d1758c3054269a7756da1fd613 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Wed, 3 Jun 2026 00:08:05 +0800 Subject: [PATCH 20/20] remove space --- test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 43e2734fd7..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,5 +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 --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 +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