From 1261eda6385c75c60d5f6df93d9b3e20002ce6ea Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Thu, 8 Jan 2026 01:33:31 +0800
Subject: [PATCH 01/48] Fix
---
.../trace/component/ComponentsDefine.java | 2 +
.../apm/agent/core/context/tag/Tags.java | 4 +
.../apm-sdk-plugin/spring-plugins/pom.xml | 1 +
.../spring-ai-1.x-plugin/pom.xml | 40 ++++++++++
.../ai/v1/ChatClientResponseInterceptor.java | 67 ++++++++++++++++
.../ai/v1/DocumentRetrieverInterceptor.java | 60 +++++++++++++++
.../ai/v1/ToolCallbackCallInterceptor.java | 64 ++++++++++++++++
.../DefaultChatClientInstrumentation.java | 76 +++++++++++++++++++
.../DocumentRetrieverInstrumentation.java | 59 ++++++++++++++
.../define/ToolCallbackInstrumentation.java | 57 ++++++++++++++
.../src/main/resources/skywalking-plugin.def | 17 +++++
11 files changed, 447 insertions(+)
create mode 100644 apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/pom.xml
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/ChatClientResponseInterceptor.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/DocumentRetrieverInterceptor.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/ToolCallbackCallInterceptor.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/DefaultChatClientInstrumentation.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/DocumentRetrieverInstrumentation.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/ToolCallbackInstrumentation.java
create mode 100644 apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/resources/skywalking-plugin.def
diff --git a/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java b/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java
index fa62c91239..dc5d878859 100755
--- a/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java
+++ b/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java
@@ -263,6 +263,8 @@ public class ComponentsDefine {
public static final OfficialComponent THREAD_PER_TASK_EXECUTOR = new OfficialComponent(161, "ThreadPerTask-executor");
+ public static final OfficialComponent SPRING_AI = new OfficialComponent(162, "Spring-AI");
+
public static final OfficialComponent DMDB_JDBC_DRIVER = new OfficialComponent(163, "Dmdb-jdbc-driver");
}
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 18093a453e..d8eca87d17 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
@@ -160,6 +160,10 @@ public static final class HTTP {
*/
public static final StringTag THREAD_CARRIER = new StringTag(24, "thread.carrier");
+ public static final StringTag MODEL_NAME = new StringTag(25, "model.name");
+
+ public static final StringTag TEMPERATURE = new StringTag(26, "temperature");
+
/**
* 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/pom.xml b/apm-sniffer/apm-sdk-plugin/spring-plugins/pom.xml
index 10b0268892..8aeb72256e 100644
--- a/apm-sniffer/apm-sdk-plugin/spring-plugins/pom.xml
+++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/pom.xml
@@ -45,6 +45,7 @@
spring-webflux-5.x-webclient-plugin
spring-webflux-6.x-webclient-plugin
resttemplate-commons
+ spring-ai-1.x-plugin
pom
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
new file mode 100644
index 0000000000..5af5e776af
--- /dev/null
+++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/pom.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ spring-plugins
+ org.apache.skywalking
+ 9.6.0-SNAPSHOT
+
+ 4.0.0
+
+ spring-ai-1.x-plugin
+
+ http://maven.apache.org
+
+
+
+ org.springframework.ai
+ spring-ai-client-chat
+ 1.1.0
+ provided
+
+
+
\ No newline at end of file
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/ChatClientResponseInterceptor.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/ChatClientResponseInterceptor.java
new file mode 100644
index 0000000000..125451d676
--- /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/ChatClientResponseInterceptor.java
@@ -0,0 +1,67 @@
+/*
+ * 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.network.trace.component.ComponentsDefine;
+import org.springframework.ai.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.prompt.ChatOptions;
+import org.springframework.ai.chat.prompt.Prompt;
+
+import java.lang.reflect.Method;
+
+public class ChatClientResponseInterceptor implements InstanceMethodsAroundInterceptor {
+
+ @Override
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
+ AbstractSpan span = ContextManager.createLocalSpan("spring-ai-chat-call");
+
+ ChatClientRequest request = (ChatClientRequest) allArguments[0];
+ Prompt prompt = request.prompt();
+ ChatOptions chatOptions = prompt.getOptions();
+
+
+ span.setComponent(ComponentsDefine.SPRING_AI);
+ Tags.MODEL_NAME.set(span, chatOptions.getModel());
+ Tags.TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
+ SpanLayer.asHttp(span);
+ }
+
+ @Override
+ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, Object ret) throws Throwable {
+ if (ContextManager.isActive()) {
+ 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);
+ ContextManager.activeSpan().errorOccurred();
+ }
+ }
+}
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/DocumentRetrieverInterceptor.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/DocumentRetrieverInterceptor.java
new file mode 100644
index 0000000000..9598463934
--- /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/DocumentRetrieverInterceptor.java
@@ -0,0 +1,60 @@
+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.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.network.trace.component.ComponentsDefine;
+
+import javax.management.Query;
+import java.lang.reflect.Method;
+
+public class DocumentRetrieverInterceptor implements InstanceMethodsAroundInterceptor {
+ @Override
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
+ MethodInterceptResult result) throws Throwable {
+ Query query = (Query) allArguments[0];
+
+ // 创建 LocalSpan 记录检索过程
+ AbstractSpan span = ContextManager.createLocalSpan("SpringAI/DocumentRetriever/retrieve");
+
+ // 设置组件 ID (建议在 ComponentsDefine 中定义专用的 SPRING_AI ID)
+ span.setComponent(ComponentsDefine.SPRING_MVC_ANNOTATION);
+
+ if (query != null) {
+ // 记录原始查询文本
+ Tags.ofKey("ai.retrieve.query").set(span, query.getText());
+ }
+ }
+
+ @Override
+ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
+ Object ret) throws Throwable {
+ if (ContextManager.isActive()) {
+ AbstractSpan span = ContextManager.activeSpan();
+ if (ret instanceof List) {
+ List> documents = (List>) ret;
+ // 记录检索到的文档数量
+ Tags.ofKey("ai.retrieve.document_count").set(span, String.valueOf(documents.size()));
+
+ // 可选:记录前几个文档的 ID 或部分内容以便追踪
+ if (!documents.isEmpty() && documents.get(0) instanceof Document) {
+ Document firstDoc = (Document) documents.get(0);
+ Tags.ofKey("ai.retrieve.first_doc_id").set(span, firstDoc.getId());
+ }
+ }
+ 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).errorOccurred();
+ }
+ }
+}
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/ToolCallbackCallInterceptor.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/ToolCallbackCallInterceptor.java
new file mode 100644
index 0000000000..f3cdb32bde
--- /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/ToolCallbackCallInterceptor.java
@@ -0,0 +1,64 @@
+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.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.network.trace.component.ComponentsDefine;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.definition.ToolDefinition;
+
+import java.lang.reflect.Method;
+
+/**
+ * @description:
+ * @author: sym
+ * @create: 2026-01-03 09:37
+ **/
+
+public class ToolCallbackCallInterceptor implements InstanceMethodsAroundInterceptor {
+
+ @Override
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
+ MethodInterceptResult result) throws Throwable {
+ ToolCallback toolCallback = (ToolCallback) objInst;
+ ToolDefinition definition = toolCallback.getToolDefinition();
+
+ String toolName = definition != null ? definition.getName() : "unknown-tool";
+ String toolInput = (String) allArguments[0];
+
+ // 创建 LocalSpan 记录工具调用
+ AbstractSpan span = ContextManager.createLocalSpan("SpringAI/Tool/" + toolName);
+ span.setComponent(ComponentsDefine.SPRING_MVC_ANNOTATION); // 建议自定义组件 ID
+
+ // 记录输入参数
+ Tags.ofKey("ai.tool.name").set(span, toolName);
+ if (toolInput != null) {
+ Tags.ofKey("ai.tool.input").set(span, toolInput);
+ }
+ }
+
+ @Override
+ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
+ Object ret) throws Throwable {
+ if (ContextManager.isActive()) {
+ AbstractSpan span = ContextManager.activeSpan();
+ if (ret instanceof String) {
+ // 记录工具输出结果
+ Tags.ofKey("ai.tool.output").set(span, (String) ret);
+ }
+ 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).errorOccurred();
+ }
+ }
+}
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/DefaultChatClientInstrumentation.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/DefaultChatClientInstrumentation.java
new file mode 100644
index 0000000000..3aae21bb12
--- /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/DefaultChatClientInstrumentation.java
@@ -0,0 +1,76 @@
+/*
+ * 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 net.bytebuddy.matcher.ElementMatchers;
+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.NameMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+
+public class DefaultChatClientInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS = "org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec";
+
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.DoGetObservableChatClientResponseInterceptor";
+
+ private static final String INTERCEPT_METHOD = "doGetObservableChatClientResponse";
+
+ private static final String ARGUMENT_TYPE = "org.springframework.ai.chat.client.ChatClientRequest";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return NameMatch.byName(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(takesArgument(0, named(ARGUMENT_TYPE)))
+ .and(ElementMatchers.takesArguments(2));
+ }
+
+ @Override
+ public String getMethodsInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+
+ @Override
+ public boolean isOverrideArgs() {
+ return false;
+ }
+ }
+ };
+ }
+}
diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/DocumentRetrieverInstrumentation.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/DocumentRetrieverInstrumentation.java
new file mode 100644
index 0000000000..2384899ab9
--- /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/DocumentRetrieverInstrumentation.java
@@ -0,0 +1,59 @@
+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.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+/**
+ * @description:
+ * @author: sym
+ * @create: 2026-01-03 09:38
+ **/
+
+public class DocumentRetrieverInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_INTERFACE = "org.springframework.ai.document.DocumentRetriever";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.DocumentRetrieverInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return HierarchyMatch.byHierarchyMatch(ENHANCE_INTERFACE);
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[0];
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
+ return new InstanceMethodsInterceptPoint[]{
+ new InstanceMethodsInterceptPoint() {
+ @Override
+ public ElementMatcher getMethodsMatcher() {
+ return named("retrieve")
+ .and(takesArguments(1))
+ .and(takesArgument(0, named("org.springframework.ai.document.Query")));
+ }
+
+ @Override
+ public String getMethodsInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+
+ @Override
+ public boolean isOverrideArgs() {
+ return false;
+ }
+ }
+ };
+ }
+}
diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/ToolCallbackInstrumentation.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/ToolCallbackInstrumentation.java
new file mode 100644
index 0000000000..b4955978eb
--- /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/ToolCallbackInstrumentation.java
@@ -0,0 +1,57 @@
+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;
+
+/**
+ * @description:
+ * @author: sym
+ * @create: 2026-01-03 09:36
+ **/
+
+public class ToolCallbackInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+ private static final String ENHANCE_INTERFACE = "org.springframework.ai.tool.ToolCallback";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.ToolCallbackCallInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ // 对应 implementsInterface(named(...))
+ return HierarchyMatch.byHierarchyMatch(ENHANCE_INTERFACE);
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[0];
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
+ return new InstanceMethodsInterceptPoint[] {
+ new InstanceMethodsInterceptPoint() {
+ @Override
+ public ElementMatcher getMethodsMatcher() {
+ return named("call")
+ .and(takesArguments(2))
+ .and(takesArgument(0, named("java.lang.String")))
+ // 这里 OTel 代码里还匹配了返回值为 String
+ .and(returns(named("java.lang.String")));
+ }
+
+ @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
new file mode 100644
index 0000000000..bd9fe567df
--- /dev/null
+++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/resources/skywalking-plugin.def
@@ -0,0 +1,17 @@
+# 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.
+
+spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.DefaultChatClientInstrumentation
From 75c22d13cd21c5ff1702a2a7fe0b12473c0a87ee Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Wed, 28 Jan 2026 22:54:45 +0800
Subject: [PATCH 02/48] Add spring ai 1.x plugin for LLM monitoring
---
.../trace/component/ComponentsDefine.java | 4 +-
.../apm/agent/core/context/tag/Tags.java | 36 ++-
.../spring-ai-1.x-plugin/pom.xml | 13 +
.../v1/DefaultChatClientCallInterceptor.java | 173 +++++++++++++
.../DefaultChatClientStreamInterceptor.java | 228 ++++++++++++++++++
.../ai/v1/DocumentRetrieverInterceptor.java | 60 -----
.../ai/v1/ToolCallbackCallInterceptor.java | 45 ++--
...a => VectorStoreRetrieverInterceptor.java} | 56 +++--
.../ai/v1/config/SpringAiPluginConfig.java | 64 +++++
.../spring/ai/v1/contant/Constants.java | 23 ++
...DefaultChatClientCallInstrumentation.java} | 13 +-
...efaultChatClientStreamInstrumentation.java | 73 ++++++
.../define/ToolCallbackInstrumentation.java | 34 ++-
... VectorStoreRetrieverInstrumentation.java} | 35 ++-
.../src/main/resources/skywalking-plugin.def | 5 +-
pom.xml | 2 +-
.../spring-ai-1.x-scenario/bin/startup.sh | 21 ++
.../config/expectedData.yaml | 111 +++++++++
.../spring-ai-1.x-scenario/configuration.yml | 23 ++
.../scenarios/spring-ai-1.x-scenario/pom.xml | 155 ++++++++++++
.../src/main/assembly/assembly.xml | 41 ++++
.../testcase/jdk/httpclient/Application.java | 30 +++
.../httpclient/config/ChatClientConfig.java | 32 +++
.../config/RagScenarioConfiguration.java | 62 +++++
.../httpclient/controller/CaseController.java | 86 +++++++
.../controller/LLMMockController.java | 185 ++++++++++++++
.../jdk/httpclient/tool/WeatherTool.java | 33 +++
.../src/main/resources/application.yaml | 38 +++
.../support-version.list | 17 ++
29 files changed, 1565 insertions(+), 133 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/DefaultChatClientCallInterceptor.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/DefaultChatClientStreamInterceptor.java
delete 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/DocumentRetrieverInterceptor.java
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/{ChatClientResponseInterceptor.java => VectorStoreRetrieverInterceptor.java} (52%)
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/config/SpringAiPluginConfig.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/contant/Constants.java
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/{DefaultChatClientInstrumentation.java => DefaultChatClientCallInstrumentation.java} (83%)
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/DefaultChatClientStreamInstrumentation.java
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/{DocumentRetrieverInstrumentation.java => VectorStoreRetrieverInstrumentation.java} (60%)
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/configuration.yml
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/src/main/assembly/assembly.xml
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/Application.java
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.java
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/CaseController.java
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/LLMMockController.java
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/tool/WeatherTool.java
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/src/main/resources/application.yaml
create mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/support-version.list
diff --git a/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java b/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java
index dc5d878859..4135aab449 100755
--- a/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java
+++ b/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java
@@ -263,8 +263,8 @@ public class ComponentsDefine {
public static final OfficialComponent THREAD_PER_TASK_EXECUTOR = new OfficialComponent(161, "ThreadPerTask-executor");
- public static final OfficialComponent SPRING_AI = new OfficialComponent(162, "Spring-AI");
-
public static final OfficialComponent DMDB_JDBC_DRIVER = new OfficialComponent(163, "Dmdb-jdbc-driver");
+ public static final OfficialComponent SPRING_AI = new OfficialComponent(164, "Spring-ai");
+
}
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 d8eca87d17..92e5dbf9f0 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
@@ -160,9 +160,41 @@ public static final class HTTP {
*/
public static final StringTag THREAD_CARRIER = new StringTag(24, "thread.carrier");
- public static final StringTag MODEL_NAME = new StringTag(25, "model.name");
+ public static final StringTag GEN_AI_REQUEST_MODEL = new StringTag(25, "gen_ai.request.model");
- public static final StringTag TEMPERATURE = new StringTag(26, "temperature");
+ public static final StringTag GEN_AI_TOP_K = new StringTag(26, "gen_ai.top.k");
+
+ public static final StringTag GEN_AI_TOP_P = new StringTag(27, "gen_ai.top.p");
+
+ public static final StringTag GEN_AI_TEMPERATURE = new StringTag(28, "gen_ai.temperature");
+
+ public static final StringTag GEN_AI_TOOL_NAME = new StringTag(29, "gen_ai.tool.name");
+
+ public static final StringTag GEN_AI_TOOL_INPUT = new StringTag(30, "gen_ai.tool.input");
+
+ public static final StringTag GEN_AI_RESPONSE_MODEL = new StringTag(31, "gen_ai.response.model");
+
+ public static final StringTag GEN_AI_RESPONSE_ID = new StringTag(32, "gen_ai.response.id");
+
+ public static final StringTag GEN_AI_USAGE_INPUT_TOKENS = new StringTag(33, "gen_ai.usage.input_tokens");
+
+ public static final StringTag GEN_AI_USAGE_OUTPUT_TOKENS = new StringTag(34, "gen_ai.usage.output_tokens");
+
+ public static final StringTag GEN_AI_USAGE_TOTAL_TOKENS = new StringTag(35, "gen_ai.usage.total_tokens");
+
+ public static final StringTag GEN_AI_RESPONSE_FINISH_REASONS = new StringTag(36, "gen_ai.response.finish_reasons");
+
+ public static final StringTag GEN_AI_PROMPT = new StringTag(37, "gen_ai.prompt");
+
+ public static final StringTag GEN_AI_COMPLETION = new StringTag(38, "gen_ai.completion");
+
+ public static final StringTag GEN_AI_STREAM_TTFR = new StringTag(39, "gen_ai.stream.ttfr");
+
+ public static final StringTag GEN_AI_VECTOR_STORE_TOP_K = new StringTag(40, "gen_ai.vector_store.top_k");
+
+ public static final StringTag GEN_AI_VECTOR_STORE_FILTER_EXPRESSION = new StringTag(41, "gen_ai.vector_store.filter_expression");
+
+ public static final StringTag GEN_AI_VECTOR_STORE_RECORD_IDS = new StringTag(42, "gen_ai.vector_store.record_ids");
/**
* Creates a {@code StringTag} with the given key and cache it, if it's created before, simply return it without
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 5af5e776af..b15ec1c429 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
@@ -26,9 +26,15 @@
4.0.0
spring-ai-1.x-plugin
+ jar
+ spring-ai-1.x-plugin
http://maven.apache.org
+
+ 17
+
+
org.springframework.ai
@@ -36,5 +42,12 @@
1.1.0
provided
+
+
+ org.springframework.ai
+ spring-ai-advisors-vector-store
+ 1.1.0
+ provided
+
\ No newline at end of file
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/DefaultChatClientCallInterceptor.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/DefaultChatClientCallInterceptor.java
new file mode 100644
index 0000000000..7c6ea9fc9f
--- /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/DefaultChatClientCallInterceptor.java
@@ -0,0 +1,173 @@
+/*
+ * 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.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.network.trace.component.ComponentsDefine;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.config.SpringAiPluginConfig;
+import org.springframework.ai.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.client.ChatClientResponse;
+import org.springframework.ai.chat.metadata.ChatResponseMetadata;
+import org.springframework.ai.chat.metadata.Usage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.ChatOptions;
+import org.springframework.ai.chat.prompt.Prompt;
+
+import java.lang.reflect.Method;
+
+public class DefaultChatClientCallInterceptor implements InstanceMethodsAroundInterceptor {
+
+ @Override
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
+ AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/chat-client/call");
+
+ ChatClientRequest request = (ChatClientRequest) allArguments[0];
+ Prompt prompt = request.prompt();
+ ChatOptions chatOptions = prompt.getOptions();
+ if (chatOptions == null) {
+ return;
+ }
+
+ span.setComponent(ComponentsDefine.SPRING_AI);
+ Tags.GEN_AI_REQUEST_MODEL.set(span, chatOptions.getModel());
+ Tags.GEN_AI_TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
+ Tags.GEN_AI_TOP_K.set(span, String.valueOf(chatOptions.getTopK()));
+ Tags.GEN_AI_TOP_P.set(span, String.valueOf(chatOptions.getTopP()));
+ }
+
+ @Override
+ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, Object ret) throws Throwable {
+ if (!ContextManager.isActive()) {
+ return ret;
+ }
+
+ try {
+ AbstractSpan span = ContextManager.activeSpan();
+ ChatClientResponse response = (ChatClientResponse) ret;
+ if (response == null || response.chatResponse() == null) {
+ return ret;
+ }
+
+ ChatResponse chatResponse = response.chatResponse();
+ ChatResponseMetadata metadata = chatResponse.getMetadata();
+
+ long totalTokens = 0;
+
+ if (metadata != null) {
+ if (metadata.getId() != null) {
+ Tags.GEN_AI_RESPONSE_ID.set(span, metadata.getId());
+ }
+ if (metadata.getModel() != null) {
+ Tags.GEN_AI_RESPONSE_MODEL.set(span, metadata.getModel());
+ }
+
+ Usage usage = metadata.getUsage();
+ if (usage != null) {
+ if (usage.getPromptTokens() != null) {
+ Tags.GEN_AI_USAGE_INPUT_TOKENS.set(span, String.valueOf(usage.getPromptTokens()));
+ }
+ if (usage.getCompletionTokens() != null) {
+ Tags.GEN_AI_USAGE_OUTPUT_TOKENS.set(span, String.valueOf(usage.getCompletionTokens()));
+ }
+ if (usage.getTotalTokens() != null) {
+ totalTokens = usage.getTotalTokens();
+ Tags.GEN_AI_USAGE_TOTAL_TOKENS.set(span, String.valueOf(totalTokens));
+ }
+ }
+ }
+
+ Generation generation = chatResponse.getResult();
+ if (generation != null && generation.getMetadata() != null) {
+ String finishReason = generation.getMetadata().getFinishReason();
+ if (finishReason != null) {
+ Tags.GEN_AI_RESPONSE_FINISH_REASONS.set(span, finishReason);
+ }
+ }
+
+ collectContent(span, allArguments, generation, totalTokens);
+ } 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 void collectContent(AbstractSpan span, Object[] allArguments, Generation generation, long totalTokens) {
+ int tokenThreshold = SpringAiPluginConfig.Plugin.SpringAi.CONTENT_COLLECT_THRESHOLD_TOKENS;
+
+ if (tokenThreshold >= 0 && totalTokens < tokenThreshold) {
+ return;
+ }
+
+ if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_PROMPT) {
+ collectPrompt(span, allArguments);
+ }
+
+ if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_COMPLETION) {
+ collectCompletion(span, generation);
+ }
+ }
+
+ private void collectPrompt(AbstractSpan span, Object[] allArguments) {
+ ChatClientRequest request = (ChatClientRequest) allArguments[0];
+ if (request == null || request.prompt() == null) {
+ return;
+ }
+
+ String promptText = request.prompt().getContents();
+ if (promptText == null) {
+ return;
+ }
+
+ int limit = SpringAiPluginConfig.Plugin.SpringAi.PROMPT_LENGTH_LIMIT;
+ if (limit > 0 && promptText.length() > limit) {
+ promptText = promptText.substring(0, limit);
+ }
+ Tags.GEN_AI_PROMPT.set(span, promptText);
+ }
+
+ private void collectCompletion(AbstractSpan span, Generation generation) {
+ if (generation == null || generation.getOutput() == null) {
+ return;
+ }
+
+ String completionText = generation.getOutput().getText();
+ if (completionText == null) {
+ return;
+ }
+
+ int limit = SpringAiPluginConfig.Plugin.SpringAi.COMPLETION_LENGTH_LIMIT;
+ if (limit > 0 && completionText.length() > limit) {
+ completionText = completionText.substring(0, limit);
+ }
+ Tags.GEN_AI_COMPLETION.set(span, completionText);
+ }
+}
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/DefaultChatClientStreamInterceptor.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/DefaultChatClientStreamInterceptor.java
new file mode 100644
index 0000000000..7761e1f273
--- /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/DefaultChatClientStreamInterceptor.java
@@ -0,0 +1,228 @@
+/*
+ * 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.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.network.trace.component.ComponentsDefine;
+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.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.client.ChatClientResponse;
+import org.springframework.ai.chat.metadata.ChatResponseMetadata;
+import org.springframework.ai.chat.metadata.Usage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.ChatOptions;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class DefaultChatClientStreamInterceptor implements InstanceMethodsAroundInterceptor {
+
+ @Override
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
+ AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/stream");
+
+ ChatClientRequest request = (ChatClientRequest) allArguments[0];
+ if (request == null || request.prompt() == null) {
+ return;
+ }
+
+ Prompt prompt = request.prompt();
+ ChatOptions chatOptions = prompt.getOptions();
+ if (chatOptions == null) {
+ return;
+ }
+
+ span.setComponent(ComponentsDefine.SPRING_AI);
+ Tags.GEN_AI_REQUEST_MODEL.set(span, chatOptions.getModel());
+ Tags.GEN_AI_TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
+
+ ContextManager.getRuntimeContext().put(Constants.SPRING_AI_STREAM_START_TIME, System.currentTimeMillis());
+ }
+
+ @Override
+ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, Object ret) throws Throwable {
+ if (!ContextManager.isActive()) {
+ return ret;
+ }
+ AbstractSpan span = ContextManager.activeSpan();
+ span.prepareForAsync();
+
+ ContextManager.stopSpan();
+
+ Flux flux = (Flux) ret;
+
+ AtomicReference lastResponseRef = new AtomicReference<>();
+
+ StringBuilder completionBuilder = new StringBuilder();
+
+ AtomicReference finishReason = new AtomicReference<>("");
+
+ AtomicBoolean firstResponseReceived = new AtomicBoolean(false);
+
+ long startTime = (long) ContextManager.getRuntimeContext().get(Constants.SPRING_AI_STREAM_START_TIME);
+
+ return flux.doOnNext(response -> {
+ if (response.chatResponse() != null) {
+
+ ChatResponse chatResponse = response.chatResponse();
+ lastResponseRef.set(chatResponse);
+
+ Generation generation = chatResponse.getResult();
+ if (generation == null) {
+ return;
+ }
+
+ if (generation.getOutput() != null && StringUtils.hasText(generation.getOutput().getText())) {
+ if (firstResponseReceived.compareAndSet(false, true)) {
+ Tags.GEN_AI_STREAM_TTFR.set(span, String.valueOf(System.currentTimeMillis() - startTime));
+ }
+ }
+
+ String reason = generation.getMetadata().getFinishReason();
+ if (reason != null) {
+ finishReason.set(reason);
+ }
+
+ if (generation.getOutput() != null && generation.getOutput().getText() != null) {
+ completionBuilder.append(generation.getOutput().getText());
+ }
+ }
+ })
+ .doOnError(span::log)
+ .doFinally(signalType -> {
+ try {
+ ChatResponse finalResponse = lastResponseRef.get();
+
+ if (finalResponse != null) {
+ ChatResponseMetadata metadata = finalResponse.getMetadata();
+ if (metadata != null) {
+ if (metadata.getId() != null) {
+ Tags.GEN_AI_RESPONSE_ID.set(span, metadata.getId());
+ }
+ if (metadata.getModel() != null) {
+ Tags.GEN_AI_RESPONSE_MODEL.set(span, metadata.getModel());
+ }
+
+ Usage usage = metadata.getUsage();
+ long totalTokens = 0;
+ if (usage != null) {
+ Tags.GEN_AI_USAGE_INPUT_TOKENS.set(span, String.valueOf(usage.getPromptTokens()));
+ Tags.GEN_AI_USAGE_OUTPUT_TOKENS.set(span, String.valueOf(usage.getCompletionTokens()));
+ totalTokens = usage.getTotalTokens() != null ? usage.getTotalTokens() : 0;
+ Tags.GEN_AI_USAGE_TOTAL_TOKENS.set(span, String.valueOf(usage.getTotalTokens().longValue()));
+ }
+
+ Tags.GEN_AI_RESPONSE_FINISH_REASONS.set(span, finishReason.get());
+
+ int tokenThreshold = SpringAiPluginConfig.Plugin.SpringAi.CONTENT_COLLECT_THRESHOLD_TOKENS;
+ if (tokenThreshold < 0 || totalTokens >= tokenThreshold) {
+
+ if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_PROMPT) {
+ ChatClientRequest request = (ChatClientRequest) allArguments[0];
+ Prompt prompt = request.prompt();
+ String promptText = prompt.getContents();
+ if (promptText != null) {
+ int limit = SpringAiPluginConfig.Plugin.SpringAi.PROMPT_LENGTH_LIMIT;
+ if (limit > 0 && promptText.length() > limit) {
+ promptText = promptText.substring(0, limit);
+ }
+ Tags.GEN_AI_PROMPT.set(span, promptText);
+ }
+ }
+
+ if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_COMPLETION) {
+ Tags.GEN_AI_COMPLETION.set(span, completionBuilder.toString());
+ }
+ }
+ }
+ }
+ } catch (Throwable t) {
+ span.log(t);
+ } finally {
+ span.asyncFinish();
+ ContextManager.getRuntimeContext().remove(Constants.SPRING_AI_STREAM_START_TIME);
+ }
+ });
+ }
+
+ @Override
+ public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, Throwable t) {
+ if (ContextManager.isActive()) {
+ ContextManager.activeSpan().log(t);
+ }
+ }
+
+ private void collectContent(AbstractSpan span, StringBuilder completionBuilder, Object[] allArguments, long totalTokens) {
+ int tokenThreshold = SpringAiPluginConfig.Plugin.SpringAi.CONTENT_COLLECT_THRESHOLD_TOKENS;
+
+ if (tokenThreshold >= 0 && totalTokens < tokenThreshold) {
+ return;
+ }
+
+ if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_PROMPT) {
+ collectPrompt(span, allArguments);
+ }
+
+ if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_COMPLETION) {
+ collectCompletion(span, completionBuilder);
+ }
+ }
+
+ private void collectPrompt(AbstractSpan span, Object[] allArguments) {
+ ChatClientRequest request = (ChatClientRequest) allArguments[0];
+ if (request == null || request.prompt() == null) {
+ return;
+ }
+
+ String promptText = request.prompt().getContents();
+ if (promptText == null) {
+ return;
+ }
+
+ int limit = SpringAiPluginConfig.Plugin.SpringAi.PROMPT_LENGTH_LIMIT;
+ if (limit > 0 && promptText.length() > limit) {
+ promptText = promptText.substring(0, limit);
+ }
+ Tags.GEN_AI_PROMPT.set(span, promptText);
+ }
+
+ private void collectCompletion(AbstractSpan span, StringBuilder completionBuilder) {
+ String completionText = completionBuilder.toString();
+ if (completionText.isEmpty()) {
+ return;
+ }
+
+ int limit = SpringAiPluginConfig.Plugin.SpringAi.COMPLETION_LENGTH_LIMIT;
+ if (limit > 0 && completionText.length() > limit) {
+ completionText = completionText.substring(0, limit);
+ }
+ Tags.GEN_AI_COMPLETION.set(span, completionText);
+ }
+}
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/DocumentRetrieverInterceptor.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/DocumentRetrieverInterceptor.java
deleted file mode 100644
index 9598463934..0000000000
--- a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/DocumentRetrieverInterceptor.java
+++ /dev/null
@@ -1,60 +0,0 @@
-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.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.network.trace.component.ComponentsDefine;
-
-import javax.management.Query;
-import java.lang.reflect.Method;
-
-public class DocumentRetrieverInterceptor implements InstanceMethodsAroundInterceptor {
- @Override
- public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
- MethodInterceptResult result) throws Throwable {
- Query query = (Query) allArguments[0];
-
- // 创建 LocalSpan 记录检索过程
- AbstractSpan span = ContextManager.createLocalSpan("SpringAI/DocumentRetriever/retrieve");
-
- // 设置组件 ID (建议在 ComponentsDefine 中定义专用的 SPRING_AI ID)
- span.setComponent(ComponentsDefine.SPRING_MVC_ANNOTATION);
-
- if (query != null) {
- // 记录原始查询文本
- Tags.ofKey("ai.retrieve.query").set(span, query.getText());
- }
- }
-
- @Override
- public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
- Object ret) throws Throwable {
- if (ContextManager.isActive()) {
- AbstractSpan span = ContextManager.activeSpan();
- if (ret instanceof List) {
- List> documents = (List>) ret;
- // 记录检索到的文档数量
- Tags.ofKey("ai.retrieve.document_count").set(span, String.valueOf(documents.size()));
-
- // 可选:记录前几个文档的 ID 或部分内容以便追踪
- if (!documents.isEmpty() && documents.get(0) instanceof Document) {
- Document firstDoc = (Document) documents.get(0);
- Tags.ofKey("ai.retrieve.first_doc_id").set(span, firstDoc.getId());
- }
- }
- 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).errorOccurred();
- }
- }
-}
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/ToolCallbackCallInterceptor.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/ToolCallbackCallInterceptor.java
index f3cdb32bde..3b6ae10fbc 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/ToolCallbackCallInterceptor.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/ToolCallbackCallInterceptor.java
@@ -1,3 +1,21 @@
+/*
+ * 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;
@@ -12,12 +30,6 @@
import java.lang.reflect.Method;
-/**
- * @description:
- * @author: sym
- * @create: 2026-01-03 09:37
- **/
-
public class ToolCallbackCallInterceptor implements InstanceMethodsAroundInterceptor {
@Override
@@ -26,29 +38,20 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr
ToolCallback toolCallback = (ToolCallback) objInst;
ToolDefinition definition = toolCallback.getToolDefinition();
- String toolName = definition != null ? definition.getName() : "unknown-tool";
+ String toolName = definition.name();
String toolInput = (String) allArguments[0];
- // 创建 LocalSpan 记录工具调用
- AbstractSpan span = ContextManager.createLocalSpan("SpringAI/Tool/" + toolName);
- span.setComponent(ComponentsDefine.SPRING_MVC_ANNOTATION); // 建议自定义组件 ID
+ AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/tool/" + toolName);
+ span.setComponent(ComponentsDefine.SPRING_AI);
- // 记录输入参数
- Tags.ofKey("ai.tool.name").set(span, toolName);
- if (toolInput != null) {
- Tags.ofKey("ai.tool.input").set(span, toolInput);
- }
+ Tags.GEN_AI_TOOL_NAME.set(span, toolName);
+ Tags.GEN_AI_TOOL_INPUT.set(span, toolInput);
}
@Override
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
Object ret) throws Throwable {
if (ContextManager.isActive()) {
- AbstractSpan span = ContextManager.activeSpan();
- if (ret instanceof String) {
- // 记录工具输出结果
- Tags.ofKey("ai.tool.output").set(span, (String) ret);
- }
ContextManager.stopSpan();
}
return ret;
@@ -58,7 +61,7 @@ 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).errorOccurred();
+ ContextManager.activeSpan().log(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/ChatClientResponseInterceptor.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/VectorStoreRetrieverInterceptor.java
similarity index 52%
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/ChatClientResponseInterceptor.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/VectorStoreRetrieverInterceptor.java
index 125451d676..da443dc797 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/ChatClientResponseInterceptor.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/VectorStoreRetrieverInterceptor.java
@@ -21,47 +21,65 @@
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.network.trace.component.ComponentsDefine;
-import org.springframework.ai.chat.client.ChatClientRequest;
-import org.springframework.ai.chat.prompt.ChatOptions;
-import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.vectorstore.SearchRequest;
import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
-public class ChatClientResponseInterceptor implements InstanceMethodsAroundInterceptor {
+public class VectorStoreRetrieverInterceptor implements InstanceMethodsAroundInterceptor {
@Override
- public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
- AbstractSpan span = ContextManager.createLocalSpan("spring-ai-chat-call");
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
+ MethodInterceptResult result) throws Throwable {
- ChatClientRequest request = (ChatClientRequest) allArguments[0];
- Prompt prompt = request.prompt();
- ChatOptions chatOptions = prompt.getOptions();
+ AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/vectorStore/retrieve");
+ span.setComponent(ComponentsDefine.SPRING_AI);
+ SearchRequest searchRequest = (SearchRequest) allArguments[0];
+ Tags.GEN_AI_VECTOR_STORE_TOP_K.set(span, String.valueOf(searchRequest.getTopK()));
- span.setComponent(ComponentsDefine.SPRING_AI);
- Tags.MODEL_NAME.set(span, chatOptions.getModel());
- Tags.TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
- SpanLayer.asHttp(span);
+ if (searchRequest.getFilterExpression() != null) {
+ Tags.GEN_AI_VECTOR_STORE_FILTER_EXPRESSION.set(span, searchRequest.getFilterExpression().toString());
+ }
}
@Override
- public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, Object ret) throws Throwable {
- if (ContextManager.isActive()) {
- ContextManager.stopSpan();
+ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
+ Object ret) throws Throwable {
+ if (!ContextManager.isActive()) {
+ return ret;
+ }
+
+ if (ret != null) {
+ List documents = (List) ret;
+ AbstractSpan span = ContextManager.activeSpan();
+ if (!documents.isEmpty()) {
+ String recordIds = documents.stream()
+ .map(Document::getId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.joining(","));
+
+ if (!recordIds.isEmpty()) {
+ Tags.GEN_AI_VECTOR_STORE_RECORD_IDS.set(span, recordIds);
+ }
+ }
}
+ ContextManager.stopSpan();
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) {
if (ContextManager.isActive()) {
ContextManager.activeSpan().log(t);
- ContextManager.activeSpan().errorOccurred();
}
}
}
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
new file mode 100644
index 0000000000..54e046d2f2
--- /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/config/SpringAiPluginConfig.java
@@ -0,0 +1,64 @@
+/*
+ * 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.config;
+
+import org.apache.skywalking.apm.agent.core.boot.PluginConfig;
+
+public class SpringAiPluginConfig {
+
+ public static class Plugin {
+
+ @PluginConfig(root = SpringAiPluginConfig.class)
+ public static class SpringAi {
+
+ /**
+ * Whether to collect the prompt content (input text) of the GenAI request.
+ */
+ public static boolean COLLECT_PROMPT = true;
+
+ /**
+ * Whether to collect the completion content (output text) of the GenAI response.
+ */
+ public static boolean COLLECT_COMPLETION = true;
+
+ /**
+ * The maximum characters of the collected prompt 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 PROMPT_LENGTH_LIMIT = 1024;
+
+ /**
+ * The maximum characters of the collected completion 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 COMPLETION_LENGTH_LIMIT = 1024;
+
+ /**
+ * The threshold for token usage to trigger content collection.
+ * When set to a positive value, prompt and completion will only be collected
+ * if the total token usage of the request exceeds this threshold.
+ * * This requires {@link #COLLECT_PROMPT} or {@link #COLLECT_COMPLETION} to be enabled first.
+ * Use a negative value to disable this threshold-based filtering (collect all).
+ */
+ public static int CONTENT_COLLECT_THRESHOLD_TOKENS = -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/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
new file mode 100644
index 0000000000..ca23ff6eb2
--- /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/contant/Constants.java
@@ -0,0 +1,23 @@
+/*
+ * 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.contant;
+
+public class Constants {
+ public static final String SPRING_AI_STREAM_START_TIME = "Spring-ai.stream.startTime";
+}
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/DefaultChatClientInstrumentation.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/DefaultChatClientCallInstrumentation.java
similarity index 83%
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/define/DefaultChatClientInstrumentation.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/define/DefaultChatClientCallInstrumentation.java
index 3aae21bb12..308efc04e6 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/DefaultChatClientInstrumentation.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/DefaultChatClientCallInstrumentation.java
@@ -20,26 +20,25 @@
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;
-import net.bytebuddy.matcher.ElementMatchers;
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.NameMatch;
+import org.springframework.ai.chat.client.ChatClientRequest;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
-public class DefaultChatClientInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+public class DefaultChatClientCallInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
private static final String ENHANCE_CLASS = "org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.DoGetObservableChatClientResponseInterceptor";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.DefaultChatClientCallInterceptor";
private static final String INTERCEPT_METHOD = "doGetObservableChatClientResponse";
- private static final String ARGUMENT_TYPE = "org.springframework.ai.chat.client.ChatClientRequest";
-
@Override
protected ClassMatch enhanceClass() {
return NameMatch.byName(ENHANCE_CLASS);
@@ -56,9 +55,7 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher getMethodsMatcher() {
- return named(INTERCEPT_METHOD)
- .and(takesArgument(0, named(ARGUMENT_TYPE)))
- .and(ElementMatchers.takesArguments(2));
+ return named(INTERCEPT_METHOD).and(takesArguments(2)).and(takesArgument(0, ChatClientRequest.class));
}
@Override
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/DefaultChatClientStreamInstrumentation.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/DefaultChatClientStreamInstrumentation.java
new file mode 100644
index 0000000000..2e76fb5654
--- /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/DefaultChatClientStreamInstrumentation.java
@@ -0,0 +1,73 @@
+/*
+ * 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.NameMatch;
+import org.springframework.ai.chat.client.ChatClientRequest;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+public class DefaultChatClientStreamInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS = "org.springframework.ai.chat.client.DefaultChatClient$DefaultStreamResponseSpec";
+
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.DefaultChatClientStreamInterceptor";
+
+ private static final String INTERCEPT_METHOD = "doGetObservableFluxChatResponse";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return NameMatch.byName(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(takesArgument(0, ChatClientRequest.class));
+ }
+
+ @Override
+ public String getMethodsInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+
+ @Override
+ public boolean isOverrideArgs() {
+ return false;
+ }
+ }
+ };
+ }
+}
diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/ToolCallbackInstrumentation.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/ToolCallbackInstrumentation.java
index b4955978eb..88f424dc2a 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/ToolCallbackInstrumentation.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/ToolCallbackInstrumentation.java
@@ -1,3 +1,21 @@
+/*
+ * 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;
@@ -8,19 +26,18 @@
import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
import org.apache.skywalking.apm.agent.core.plugin.match.HierarchyMatch;
-/**
- * @description:
- * @author: sym
- * @create: 2026-01-03 09:36
- **/
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.returns;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
public class ToolCallbackInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
private static final String ENHANCE_INTERFACE = "org.springframework.ai.tool.ToolCallback";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.ToolCallbackCallInterceptor";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.ToolCallbackCallInterceptor";
@Override
protected ClassMatch enhanceClass() {
- // 对应 implementsInterface(named(...))
return HierarchyMatch.byHierarchyMatch(ENHANCE_INTERFACE);
}
@@ -31,14 +48,13 @@ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
- return new InstanceMethodsInterceptPoint[] {
+ return new InstanceMethodsInterceptPoint[]{
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher getMethodsMatcher() {
return named("call")
.and(takesArguments(2))
.and(takesArgument(0, named("java.lang.String")))
- // 这里 OTel 代码里还匹配了返回值为 String
.and(returns(named("java.lang.String")));
}
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/DocumentRetrieverInstrumentation.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/VectorStoreRetrieverInstrumentation.java
similarity index 60%
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/define/DocumentRetrieverInstrumentation.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/define/VectorStoreRetrieverInstrumentation.java
index 2384899ab9..426f42c27b 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/DocumentRetrieverInstrumentation.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/VectorStoreRetrieverInstrumentation.java
@@ -1,3 +1,21 @@
+/*
+ * 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;
@@ -7,21 +25,18 @@
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.springframework.ai.vectorstore.SearchRequest;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
-/**
- * @description:
- * @author: sym
- * @create: 2026-01-03 09:38
- **/
+public class VectorStoreRetrieverInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
-public class DocumentRetrieverInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+ private static final String ENHANCE_INTERFACE = "org.springframework.ai.vectorstore.VectorStoreRetriever";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.VectorStoreRetrieverInterceptor";
- private static final String ENHANCE_INTERFACE = "org.springframework.ai.document.DocumentRetriever";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.DocumentRetrieverInterceptor";
+ private static final String INTERCEPT_METHOD = "doSimilaritySearch";
@Override
protected ClassMatch enhanceClass() {
@@ -39,9 +54,9 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher getMethodsMatcher() {
- return named("retrieve")
+ return named(INTERCEPT_METHOD)
.and(takesArguments(1))
- .and(takesArgument(0, named("org.springframework.ai.document.Query")));
+ .and(takesArgument(0, SearchRequest.class));
}
@Override
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 bd9fe567df..960e977592 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
@@ -14,4 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.DefaultChatClientInstrumentation
+spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.DefaultChatClientCallInstrumentation
+spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.DefaultChatClientStreamInstrumentation
+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.VectorStoreRetrieverInstrumentation
diff --git a/pom.xml b/pom.xml
index 8e85a9cee4..6077be71f5 100755
--- a/pom.xml
+++ b/pom.xml
@@ -119,7 +119,7 @@
2.22.0
3.2.0
3.1.0
- 3.2.4
+ 3.3.0
3.0.0-M2
3.10.1
3.1.0
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
new file mode 100644
index 0000000000..6c3e9e8b21
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# 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.
+
+home="$(cd "$(dirname $0)"; pwd)"
+
+java -jar ${agent_opts} ${home}/../libs/jdk-httpclient-scenario.jar &
\ No newline at end of file
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml b/test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml
new file mode 100644
index 0000000000..f009e03a46
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml
@@ -0,0 +1,111 @@
+# 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.
+segmentItems:
+ - serviceName: jdk-httpclient-scenario
+ segmentSize: gt 0
+ segments:
+ - segmentId: not null
+ spans:
+ - operationName: POST:/user/login
+ parentSpanId: -1
+ spanId: 0
+ spanLayer: Http
+ startTime: not null
+ endTime: not null
+ componentId: 14
+ isError: false
+ spanType: Entry
+ peer: ''
+ skipAnalysis: 'false'
+ tags:
+ - { key: url, value: not null }
+ - { key: http.method, value: POST }
+ - { key: http.status_code, value: '200' }
+ refs:
+ - { parentEndpoint: GET:/case/jdk-httpclient-scenario-case, networkAddress: 'localhost:8080',
+ refType: CrossProcess, parentSpanId: 1, parentTraceSegmentId: not null, parentServiceInstance: not
+ null, parentService: jdk-httpclient-scenario, traceId: not null }
+ - segmentId: not null
+ spans:
+ - operationName: POST:/user/asyncLogin
+ parentSpanId: -1
+ spanId: 0
+ spanLayer: Http
+ startTime: not null
+ endTime: not null
+ componentId: 14
+ isError: false
+ spanType: Entry
+ peer: ''
+ skipAnalysis: 'false'
+ tags:
+ - { key: url, value: not null }
+ - { key: http.method, value: POST }
+ - { key: http.status_code, value: '200' }
+ refs:
+ - { parentEndpoint: GET:/case/jdk-httpclient-scenario-case, networkAddress: 'localhost:8080',
+ refType: CrossProcess, parentSpanId: 2, parentTraceSegmentId: not null, parentServiceInstance: not
+ null, parentService: jdk-httpclient-scenario, traceId: not null }
+
+ - segmentId: not null
+ spans:
+ - operationName: POST:/jdk-httpclient-scenario/user/login
+ parentSpanId: 0
+ spanId: 1
+ spanLayer: Http
+ startTime: not null
+ endTime: not null
+ componentId: 66
+ isError: false
+ spanType: Exit
+ peer: not null
+ skipAnalysis: 'false'
+ tags:
+ - { key: http.method, value: POST }
+ - { key: url, value: http://localhost:8080/jdk-httpclient-scenario/user/login }
+ - { key: http.status_code, value: '200' }
+
+ - operationName: POST:/jdk-httpclient-scenario/user/asyncLogin
+ parentSpanId: 0
+ spanId: 2
+ spanLayer: Http
+ startTime: not null
+ endTime: not null
+ componentId: 66
+ isError: false
+ spanType: Exit
+ peer: not null
+ skipAnalysis: 'false'
+ tags:
+ - { key: http.method, value: POST }
+ - { key: url, value: http://localhost:8080/jdk-httpclient-scenario/user/asyncLogin }
+ - { key: http.status_code, value: '200' }
+
+ - operationName: GET:/case/jdk-httpclient-scenario-case
+ parentSpanId: -1
+ spanId: 0
+ spanLayer: Http
+ startTime: not null
+ endTime: not null
+ componentId: 14
+ isError: false
+ spanType: Entry
+ peer: ''
+ tags:
+ - { key: url, value: http://localhost:8080/jdk-httpclient-scenario/case/jdk-httpclient-scenario-case }
+ - { key: http.method, value: GET }
+ - { key: http.status_code, value: '200' }
+ skipAnalysis: 'false'
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/configuration.yml b/test/plugin/scenarios/spring-ai-1.x-scenario/configuration.yml
new file mode 100644
index 0000000000..acec927aa9
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/configuration.yml
@@ -0,0 +1,23 @@
+# 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.
+
+type: jvm
+entryService: http://localhost:8080/spring-ai-1.x-scenario/case/spring-ai-1.x-scenario-case
+healthCheck: http://localhost:8080/spring-ai-1.x-scenario/case/healthCheck
+runningMode: with_bootstrap
+withPlugins: apm-jdk-httpclient-plugin-*.jar
+startScript: ./bin/startup.sh
+
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
new file mode 100644
index 0000000000..bfadced625
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
@@ -0,0 +1,155 @@
+
+
+
+ 4.0.0
+
+ org.apache.skywalking
+ x-scenario
+ 5.0.0
+
+
+ UTF-8
+ 17
+ 3.8.1
+
+
+ spring-ai-scenario
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ 3.5.7
+
+
+
+ com.alibaba
+ fastjson
+ 1.2.83
+
+
+
+ org.springframework.ai
+ spring-ai-client-chat
+
+
+
+ org.springframework.ai
+ spring-ai-starter-model-openai
+
+
+
+ org.springframework.ai
+ spring-ai-advisors-vector-store
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-bom
+ 1.1.0
+ pom
+ import
+
+
+
+
+
+ jdk-httpclient-scenario
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ 3.5.7
+
+
+
+ repackage
+
+
+
+
+
+ maven-compiler-plugin
+ ${maven-compiler-plugin.version}
+
+ ${compiler.version}
+ ${compiler.version}
+ ${project.build.sourceEncoding}
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ assemble
+ package
+
+ single
+
+
+
+ src/main/assembly/assembly.xml
+
+ ./target/
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+ false
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+ empty-javadoc-jar
+ package
+
+ jar
+
+
+ javadoc
+ ${basedir}/javadoc
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/assembly/assembly.xml b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/assembly/assembly.xml
new file mode 100644
index 0000000000..3ab23573ab
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/assembly/assembly.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ zip
+
+
+
+
+ ./bin
+ 0775
+
+
+
+
+
+ ./target/jdk-httpclient-scenario.jar
+ ./libs
+ 0775
+
+
+
\ No newline at end of file
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/Application.java b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/Application.java
new file mode 100644
index 0000000000..5147a224ee
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/Application.java
@@ -0,0 +1,30 @@
+/*
+ * 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 test.apache.skywalking.apm.testcase.jdk.httpclient;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
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
new file mode 100644
index 0000000000..79fde137ab
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/ChatClientConfig.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 test.apache.skywalking.apm.testcase.jdk.httpclient.config;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ChatClientConfig {
+
+ @Bean
+ public ChatClient openAIChatClient(OpenAiChatModel model) {
+ return ChatClient.create(model);
+ }
+}
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java
new file mode 100644
index 0000000000..650583740e
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java
@@ -0,0 +1,62 @@
+/*
+ * 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 test.apache.skywalking.apm.testcase.jdk.httpclient.config;
+
+import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
+import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+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 java.util.ArrayList;
+import java.util.List;
+
+@Configuration
+public class RagScenarioConfiguration {
+
+ @Bean
+ 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;
+ }
+
+ @Bean
+ public QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore) {
+ return QuestionAnswerAdvisor.builder(vectorStore).build();
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..a073dda732
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/CaseController.java
@@ -0,0 +1,86 @@
+/*
+ * 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 test.apache.skywalking.apm.testcase.jdk.httpclient.controller;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
+import org.springframework.beans.factory.annotation.Qualifier;
+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;
+
+@RestController
+@RequestMapping("/case")
+public class CaseController {
+
+ private final WeatherTool weatherTool;
+
+ private final ChatClient chatClient;
+
+ private final QuestionAnswerAdvisor questionAnswerAdvisor;
+
+ public CaseController(WeatherTool weatherTool, @Qualifier("openAIChatClient") ChatClient chatClient, QuestionAnswerAdvisor questionAnswerAdvisor) {
+ this.weatherTool = weatherTool;
+ this.chatClient = chatClient;
+ this.questionAnswerAdvisor = questionAnswerAdvisor;
+ }
+
+ @GetMapping("/healthCheck")
+ public String healthCheck() {
+ return "Success";
+ }
+
+ @GetMapping("/spring-ai-1.x-scenario-case")
+ public String testCase() throws Exception {
+ String content1 = chatClient
+ .prompt("What's the weather in New York?")
+ .tools(weatherTool)
+ .call()
+ .content();
+
+ System.out.println(content1);
+
+// chatClient
+// .prompt("What is Spring AI?")
+// .system("you are a smart AI")
+// .stream()
+// .content()
+// .doOnNext(System.out::println)
+// .blockLast();
+
+ String systemPrompt = """
+ You are a professional technical assistant.
+ Strictly use the provided context to answer questions.
+ If the information is not in the context, say: "I'm sorry, I don't have that information in my knowledge base."
+ Do not use outside knowledge. Be concise.
+ """;
+
+ String content2 = chatClient
+ .prompt("When is the AI Summit?")
+ .advisors(questionAnswerAdvisor)
+ .system(systemPrompt)
+ .stream()
+ .content()
+ .doOnNext(System.out::println)
+ .blockLast();
+ System.out.println(content2);
+
+ 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
new file mode 100644
index 0000000000..495713d916
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/controller/LLMMockController.java
@@ -0,0 +1,185 @@
+/*
+ * 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 test.apache.skywalking.apm.testcase.jdk.httpclient.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/llm")
+public class LLMMockController {
+
+ @RequestMapping("/v1/chat/completions")
+ public JSONObject completions(@RequestBody JSONObject request) {
+ System.out.println(request.toString());
+
+ JSONArray messages = request.getJSONArray("messages");
+
+ JSONObject lastMessage = messages.getJSONObject(messages.size() - 1);
+ String lastRole = lastMessage.getString("role");
+
+ String toolCallResponse = """
+ {
+ "choices": [
+ {
+ "content_filter_results": {},
+ "finish_reason": "tool_calls",
+ "index": 0,
+ "logprobs": null,
+ "message": {
+ "annotations": [],
+ "content": null,
+ "refusal": null,
+ "role": "assistant",
+ "tool_calls": [
+ {
+ "function": {
+ "arguments": "{\\"arg0\\":\\"beijing\\"}",
+ "name": "get_weather"
+ },
+ "id": "call_iV4bvFIZujbbVZwWbvsUKiiK",
+ "type": "function"
+ }
+ ]
+ }
+ }
+ ],
+ "created": 1768490813,
+ "id": "chatcmpl-CyJXJt7gxwDgz7UjYml95gc54784J",
+ "model": "gpt-4.1-2025-04-14",
+ "object": "chat.completion",
+ "prompt_filter_results": [
+ {
+ "prompt_index": 0,
+ "content_filter_results": {
+ "hate": { "filtered": false, "severity": "safe" },
+ "self_harm": { "filtered": false, "severity": "safe" },
+ "sexual": { "filtered": false, "severity": "safe" },
+ "violence": { "filtered": false, "severity": "safe" }
+ }
+ }
+ ],
+ "system_fingerprint": "fp_b9041e10b4",
+ "usage": {
+ "completion_tokens": 17,
+ "completion_tokens_details": {
+ "accepted_prediction_tokens": 0,
+ "audio_tokens": 0,
+ "reasoning_tokens": 0,
+ "rejected_prediction_tokens": 0
+ },
+ "prompt_tokens": 52,
+ "prompt_tokens_details": {
+ "audio_tokens": 0,
+ "cached_tokens": 0
+ },
+ "total_tokens": 69
+ }
+ }
+ """;
+
+ String finalResponse = """
+ {
+ "choices": [
+ {
+ "content_filter_results": {
+ "hate": {
+ "filtered": false,
+ "severity": "safe"
+ },
+ "self_harm": {
+ "filtered": false,
+ "severity": "safe"
+ },
+ "sexual": {
+ "filtered": false,
+ "severity": "safe"
+ },
+ "violence": {
+ "filtered": false,
+ "severity": "safe"
+ }
+ },
+ "finish_reason": "stop",
+ "index": 0,
+ "logprobs": null,
+ "message": {
+ "annotations": [],
+ "content": "The weather in Beijing is currently sunny with a temperature of 10°C.",
+ "refusal": null,
+ "role": "assistant"
+ }
+ }
+ ],
+ "created": 1768491057,
+ "id": "chatcmpl-CyJbFkZsyhOyHW2Otycoh2s4vrAZE",
+ "model": "gpt-4.1-2025-04-14",
+ "object": "chat.completion",
+ "prompt_filter_results": [
+ {
+ "prompt_index": 0,
+ "content_filter_results": {
+ "hate": {
+ "filtered": false,
+ "severity": "safe"
+ },
+ "self_harm": {
+ "filtered": false,
+ "severity": "safe"
+ },
+ "sexual": {
+ "filtered": false,
+ "severity": "safe"
+ },
+ "violence": {
+ "filtered": false,
+ "severity": "safe"
+ }
+ }
+ }
+ ],
+ "system_fingerprint": "fp_b9041e10b4",
+ "usage": {
+ "completion_tokens": 17,
+ "completion_tokens_details": {
+ "accepted_prediction_tokens": 0,
+ "audio_tokens": 0,
+ "reasoning_tokens": 0,
+ "rejected_prediction_tokens": 0
+ },
+ "prompt_tokens": 83,
+ "prompt_tokens_details": {
+ "audio_tokens": 0,
+ "cached_tokens": 0
+ },
+ "total_tokens": 100
+ }
+ }
+ """;
+
+ if ("tool".equals(lastRole)) {
+ return JSON.parseObject(finalResponse);
+ }
+
+ return JSON.parseObject(toolCallResponse);
+ }
+}
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/tool/WeatherTool.java b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/tool/WeatherTool.java
new file mode 100644
index 0000000000..1606b67436
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/tool/WeatherTool.java
@@ -0,0 +1,33 @@
+/*
+ * 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 test.apache.skywalking.apm.testcase.jdk.httpclient.tool;
+
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.stereotype.Component;
+
+@Component
+public class WeatherTool {
+ @Tool(name = "get_weather", description = "Get weather by city name")
+ public String getWeather(String city) {
+ return switch (city.toLowerCase()) {
+ case "new york" -> "Sunny, 10°C";
+ case "london" -> "Cloudy, 12°C";
+ default -> "Unknown city";
+ };
+ }
+}
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
new file mode 100644
index 0000000000..1142bddf51
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/resources/application.yaml
@@ -0,0 +1,38 @@
+# 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.
+
+server:
+ port: 8080
+ servlet:
+ context-path: /spring-ai-1.x-scenario
+
+spring:
+ ai:
+ openai:
+ api-key: sk-zuxrezdvwLLU9sGRUEW8mdFKkXuFugK7H9uf2b7iS3qCRU9M
+# base-url: http://localhost:8080/spring-ai-1.x-scenario/llm
+ base-url: https://globalai.vip
+ embedding:
+ options:
+ model: text-embedding-ada-002
+ chat:
+ options:
+ model: gpt-4.1-2025-04-14
+ temperature: 0.7
+ max-tokens: 1000
+ top-p: 0.9
+
+
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/support-version.list b/test/plugin/scenarios/spring-ai-1.x-scenario/support-version.list
new file mode 100644
index 0000000000..d2f82818c0
--- /dev/null
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/support-version.list
@@ -0,0 +1,17 @@
+# 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.
+
+1.1.1
\ No newline at end of file
From ab8837fea0517aacf25bb0372c89949516aab09f Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Tue, 3 Feb 2026 23:24:09 +0800
Subject: [PATCH 03/48] fix
---
.github/workflows/plugins-jdk17-test.0.yaml | 1 +
.../apm/agent/core/context/tag/Tags.java | 2 +
.../DefaultChatClientStreamInterceptor.java | 69 +++-----------
.../ai/v1/ToolCallbackCallInterceptor.java | 14 ++-
.../ai/v1/config/SpringAiPluginConfig.java | 14 ++-
apm-sniffer/config/agent.config | 22 +++++
.../spring-ai-1.x-scenario/bin/startup.sh | 2 +-
.../config/expectedData.yaml | 95 -------------------
.../scenarios/spring-ai-1.x-scenario/pom.xml | 5 +
.../config/RagScenarioConfiguration.java | 10 --
.../httpclient/controller/CaseController.java | 43 ++++-----
.../controller/LLMMockController.java | 10 +-
.../src/main/resources/application.yaml | 5 +-
13 files changed, 97 insertions(+), 195 deletions(-)
diff --git a/.github/workflows/plugins-jdk17-test.0.yaml b/.github/workflows/plugins-jdk17-test.0.yaml
index 6d2ad4c2b6..542d98411a 100644
--- a/.github/workflows/plugins-jdk17-test.0.yaml
+++ b/.github/workflows/plugins-jdk17-test.0.yaml
@@ -64,6 +64,7 @@ jobs:
- grizzly-2.3.x-4.x-workthreadpool-scenario
- jetty-11.x-scenario
- jetty-10.x-scenario
+ - spring-ai-1.x-scenario
steps:
- uses: actions/checkout@v2
with:
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 92e5dbf9f0..0b0f9781d0 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
@@ -172,6 +172,8 @@ public static final class HTTP {
public static final StringTag GEN_AI_TOOL_INPUT = new StringTag(30, "gen_ai.tool.input");
+ public static final StringTag GEN_AI_TOOL_OUTPUT = new StringTag(43, "gen_ai.tool.output");
+
public static final StringTag GEN_AI_RESPONSE_MODEL = new StringTag(31, "gen_ai.response.model");
public static final StringTag GEN_AI_RESPONSE_ID = new StringTag(32, "gen_ai.response.id");
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/DefaultChatClientStreamInterceptor.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/DefaultChatClientStreamInterceptor.java
index 7761e1f273..053e55c213 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/DefaultChatClientStreamInterceptor.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/DefaultChatClientStreamInterceptor.java
@@ -47,9 +47,9 @@ public class DefaultChatClientStreamInterceptor implements InstanceMethodsAround
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/stream");
-
+ span.setComponent(ComponentsDefine.SPRING_AI);
ChatClientRequest request = (ChatClientRequest) allArguments[0];
- if (request == null || request.prompt() == null) {
+ if (request == null) {
return;
}
@@ -59,9 +59,10 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr
return;
}
- span.setComponent(ComponentsDefine.SPRING_AI);
Tags.GEN_AI_REQUEST_MODEL.set(span, chatOptions.getModel());
Tags.GEN_AI_TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
+ Tags.GEN_AI_TOP_K.set(span, String.valueOf(chatOptions.getTopK()));
+ Tags.GEN_AI_TOP_P.set(span, String.valueOf(chatOptions.getTopP()));
ContextManager.getRuntimeContext().put(Constants.SPRING_AI_STREAM_START_TIME, System.currentTimeMillis());
}
@@ -80,13 +81,14 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
AtomicReference lastResponseRef = new AtomicReference<>();
- StringBuilder completionBuilder = new StringBuilder();
+ final StringBuilder completionBuilder = new StringBuilder();
AtomicReference finishReason = new AtomicReference<>("");
AtomicBoolean firstResponseReceived = new AtomicBoolean(false);
- long startTime = (long) ContextManager.getRuntimeContext().get(Constants.SPRING_AI_STREAM_START_TIME);
+ Long startTime = (Long) ContextManager.getRuntimeContext().get(Constants.SPRING_AI_STREAM_START_TIME);
+ ContextManager.getRuntimeContext().remove(Constants.SPRING_AI_STREAM_START_TIME);
return flux.doOnNext(response -> {
if (response.chatResponse() != null) {
@@ -100,7 +102,7 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
}
if (generation.getOutput() != null && StringUtils.hasText(generation.getOutput().getText())) {
- if (firstResponseReceived.compareAndSet(false, true)) {
+ if (firstResponseReceived.compareAndSet(false, true) && startTime != null) {
Tags.GEN_AI_STREAM_TTFR.set(span, String.valueOf(System.currentTimeMillis() - startTime));
}
}
@@ -158,7 +160,12 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
}
if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_COMPLETION) {
- Tags.GEN_AI_COMPLETION.set(span, completionBuilder.toString());
+ int limit = SpringAiPluginConfig.Plugin.SpringAi.COMPLETION_LENGTH_LIMIT;
+ String output = completionBuilder.toString();
+ if (limit > 0 && output.length() > limit) {
+ output = output.substring(0, limit);
+ }
+ Tags.GEN_AI_COMPLETION.set(span, output);
}
}
}
@@ -167,7 +174,6 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
span.log(t);
} finally {
span.asyncFinish();
- ContextManager.getRuntimeContext().remove(Constants.SPRING_AI_STREAM_START_TIME);
}
});
}
@@ -178,51 +184,4 @@ public void handleMethodException(EnhancedInstance objInst, Method method, Objec
ContextManager.activeSpan().log(t);
}
}
-
- private void collectContent(AbstractSpan span, StringBuilder completionBuilder, Object[] allArguments, long totalTokens) {
- int tokenThreshold = SpringAiPluginConfig.Plugin.SpringAi.CONTENT_COLLECT_THRESHOLD_TOKENS;
-
- if (tokenThreshold >= 0 && totalTokens < tokenThreshold) {
- return;
- }
-
- if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_PROMPT) {
- collectPrompt(span, allArguments);
- }
-
- if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_COMPLETION) {
- collectCompletion(span, completionBuilder);
- }
- }
-
- private void collectPrompt(AbstractSpan span, Object[] allArguments) {
- ChatClientRequest request = (ChatClientRequest) allArguments[0];
- if (request == null || request.prompt() == null) {
- return;
- }
-
- String promptText = request.prompt().getContents();
- if (promptText == null) {
- return;
- }
-
- int limit = SpringAiPluginConfig.Plugin.SpringAi.PROMPT_LENGTH_LIMIT;
- if (limit > 0 && promptText.length() > limit) {
- promptText = promptText.substring(0, limit);
- }
- Tags.GEN_AI_PROMPT.set(span, promptText);
- }
-
- private void collectCompletion(AbstractSpan span, StringBuilder completionBuilder) {
- String completionText = completionBuilder.toString();
- if (completionText.isEmpty()) {
- return;
- }
-
- int limit = SpringAiPluginConfig.Plugin.SpringAi.COMPLETION_LENGTH_LIMIT;
- if (limit > 0 && completionText.length() > limit) {
- completionText = completionText.substring(0, limit);
- }
- Tags.GEN_AI_COMPLETION.set(span, completionText);
- }
}
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/ToolCallbackCallInterceptor.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/ToolCallbackCallInterceptor.java
index 3b6ae10fbc..0b2afe8014 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/ToolCallbackCallInterceptor.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/ToolCallbackCallInterceptor.java
@@ -25,6 +25,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.network.trace.component.ComponentsDefine;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.config.SpringAiPluginConfig;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
@@ -39,21 +40,30 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr
ToolDefinition definition = toolCallback.getToolDefinition();
String toolName = definition.name();
- String toolInput = (String) allArguments[0];
AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/tool/" + toolName);
span.setComponent(ComponentsDefine.SPRING_AI);
Tags.GEN_AI_TOOL_NAME.set(span, toolName);
- Tags.GEN_AI_TOOL_INPUT.set(span, toolInput);
+
+ if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_TOOL_INPUT) {
+ String toolInput = (String) allArguments[0];
+ Tags.GEN_AI_TOOL_INPUT.set(span, toolInput);
+ }
}
@Override
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
Object ret) throws Throwable {
if (ContextManager.isActive()) {
+ AbstractSpan span = ContextManager.activeSpan();
+ if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_TOOL_OUTPUT && ret != null) {
+ Tags.GEN_AI_TOOL_OUTPUT.set(span, (String) ret);
+ }
+
ContextManager.stopSpan();
}
+
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/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 54e046d2f2..9869ea3ea6 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
@@ -30,12 +30,12 @@ public static class SpringAi {
/**
* Whether to collect the prompt content (input text) of the GenAI request.
*/
- public static boolean COLLECT_PROMPT = true;
+ public static boolean COLLECT_PROMPT = false;
/**
* Whether to collect the completion content (output text) of the GenAI response.
*/
- public static boolean COLLECT_COMPLETION = true;
+ public static boolean COLLECT_COMPLETION = false;
/**
* The maximum characters of the collected prompt content.
@@ -59,6 +59,16 @@ public static class SpringAi {
* Use a negative value to disable this threshold-based filtering (collect all).
*/
public static int CONTENT_COLLECT_THRESHOLD_TOKENS = -1;
+
+ /**
+ * Whether to collect the arguments (input parameters) of the tool/function call.
+ */
+ public static boolean COLLECT_TOOL_INPUT = false;
+
+ /**
+ * Whether to collect the execution result (output) of the tool/function call.
+ */
+ public static boolean COLLECT_TOOL_OUTPUT = false;
}
}
}
diff --git a/apm-sniffer/config/agent.config b/apm-sniffer/config/agent.config
index 9056993626..4e54651a58 100755
--- a/apm-sniffer/config/agent.config
+++ b/apm-sniffer/config/agent.config
@@ -339,3 +339,25 @@ plugin.solon.http_body_length_threshold=${SW_PLUGIN_SOLON_HTTP_BODY_LENGTH_THRES
plugin.caffeine.operation_mapping_write=${SW_PLUGIN_CAFFEINE_OPERATION_MAPPING_WRITE:put,putAll,remove,clear}
# Specify which command should be converted to read operation
plugin.caffeine.operation_mapping_read=${SW_PLUGIN_CAFFEINE_OPERATION_MAPPING_READ:getIfPresent,getAllPresent,computeIfAbsent}
+# Whether to collect the prompt content (input text) of the GenAI request.
+plugin.springai.collect_prompt=${SW_PLUGIN_SPRINGAI_COLLECT_PROMPT:false}
+# Whether to collect the completion content (output text) of the GenAI response.
+plugin.springai.collect_completion=${SW_PLUGIN_SPRINGAI_COLLECT_COMPLETION:false}
+# The maximum characters of the collected prompt 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.
+plugin.springai.prompt_length_limit=${SW_PLUGIN_SPRINGAI_PROMPT_LENGTH_LIMIT:1024}
+# The maximum characters of the collected completion 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.
+plugin.springai.completion_length_limit=${SW_PLUGIN_SPRINGAI_COMPLETION_LENGTH_LIMIT:1024}
+# The threshold for token usage to trigger content collection.
+# When set to a positive value, prompt and completion will only be collected
+# if the total token usage of the request exceeds this threshold.
+# This requires collect_prompt or collect_completion to be enabled first.
+# Use a negative value to disable this threshold-based filtering (collect all).
+plugin.springai.content_collect_threshold_tokens=${SW_PLUGIN_SPRINGAI_CONTENT_COLLECT_THRESHOLD_TOKENS:-1}
+# 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
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 6c3e9e8b21..912d655aeb 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
@@ -18,4 +18,4 @@
home="$(cd "$(dirname $0)"; pwd)"
-java -jar ${agent_opts} ${home}/../libs/jdk-httpclient-scenario.jar &
\ No newline at end of file
+java -Dskywalking.plugin.springai.collect_prompt=true -Dskywalking.plugin.springai.collect_completion=true -Dskywalking.plugin.springai.collect_tool_input=true -Dskywalking.plugin.springai.collect_tool_output=true -jar ${agent_opts} ${home}/../libs/jdk-httpclient-scenario.jar &
\ No newline at end of file
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml b/test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml
index f009e03a46..588a655d98 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
@@ -13,99 +13,4 @@
# 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.
-segmentItems:
- - serviceName: jdk-httpclient-scenario
- segmentSize: gt 0
- segments:
- - segmentId: not null
- spans:
- - operationName: POST:/user/login
- parentSpanId: -1
- spanId: 0
- spanLayer: Http
- startTime: not null
- endTime: not null
- componentId: 14
- isError: false
- spanType: Entry
- peer: ''
- skipAnalysis: 'false'
- tags:
- - { key: url, value: not null }
- - { key: http.method, value: POST }
- - { key: http.status_code, value: '200' }
- refs:
- - { parentEndpoint: GET:/case/jdk-httpclient-scenario-case, networkAddress: 'localhost:8080',
- refType: CrossProcess, parentSpanId: 1, parentTraceSegmentId: not null, parentServiceInstance: not
- null, parentService: jdk-httpclient-scenario, traceId: not null }
- - segmentId: not null
- spans:
- - operationName: POST:/user/asyncLogin
- parentSpanId: -1
- spanId: 0
- spanLayer: Http
- startTime: not null
- endTime: not null
- componentId: 14
- isError: false
- spanType: Entry
- peer: ''
- skipAnalysis: 'false'
- tags:
- - { key: url, value: not null }
- - { key: http.method, value: POST }
- - { key: http.status_code, value: '200' }
- refs:
- - { parentEndpoint: GET:/case/jdk-httpclient-scenario-case, networkAddress: 'localhost:8080',
- refType: CrossProcess, parentSpanId: 2, parentTraceSegmentId: not null, parentServiceInstance: not
- null, parentService: jdk-httpclient-scenario, traceId: not null }
- - segmentId: not null
- spans:
- - operationName: POST:/jdk-httpclient-scenario/user/login
- parentSpanId: 0
- spanId: 1
- spanLayer: Http
- startTime: not null
- endTime: not null
- componentId: 66
- isError: false
- spanType: Exit
- peer: not null
- skipAnalysis: 'false'
- tags:
- - { key: http.method, value: POST }
- - { key: url, value: http://localhost:8080/jdk-httpclient-scenario/user/login }
- - { key: http.status_code, value: '200' }
-
- - operationName: POST:/jdk-httpclient-scenario/user/asyncLogin
- parentSpanId: 0
- spanId: 2
- spanLayer: Http
- startTime: not null
- endTime: not null
- componentId: 66
- isError: false
- spanType: Exit
- peer: not null
- skipAnalysis: 'false'
- tags:
- - { key: http.method, value: POST }
- - { key: url, value: http://localhost:8080/jdk-httpclient-scenario/user/asyncLogin }
- - { key: http.status_code, value: '200' }
-
- - operationName: GET:/case/jdk-httpclient-scenario-case
- parentSpanId: -1
- spanId: 0
- spanLayer: Http
- startTime: not null
- endTime: not null
- componentId: 14
- isError: false
- spanType: Entry
- peer: ''
- tags:
- - { key: url, value: http://localhost:8080/jdk-httpclient-scenario/case/jdk-httpclient-scenario-case }
- - { key: http.method, value: GET }
- - { key: http.status_code, value: '200' }
- skipAnalysis: 'false'
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 bfadced625..05b32a346c 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
@@ -59,6 +59,11 @@
org.springframework.ai
spring-ai-advisors-vector-store
+
+ org.projectlombok
+ lombok
+ 1.18.42
+
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java
index 650583740e..e69c4807b4 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java
@@ -17,7 +17,6 @@
package test.apache.skywalking.apm.testcase.jdk.httpclient.config;
-import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
@@ -41,15 +40,6 @@ public VectorStore vectorStore(EmbeddingModel embeddingModel) {
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 a073dda732..c9fe1e3846 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
@@ -17,6 +17,7 @@
package test.apache.skywalking.apm.testcase.jdk.httpclient.controller;
+import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -27,20 +28,13 @@
@RestController
@RequestMapping("/case")
+@RequiredArgsConstructor
public class CaseController {
private final WeatherTool weatherTool;
-
private final ChatClient chatClient;
-
private final QuestionAnswerAdvisor questionAnswerAdvisor;
- public CaseController(WeatherTool weatherTool, @Qualifier("openAIChatClient") ChatClient chatClient, QuestionAnswerAdvisor questionAnswerAdvisor) {
- this.weatherTool = weatherTool;
- this.chatClient = chatClient;
- this.questionAnswerAdvisor = questionAnswerAdvisor;
- }
-
@GetMapping("/healthCheck")
public String healthCheck() {
return "Success";
@@ -56,14 +50,6 @@ public String testCase() throws Exception {
System.out.println(content1);
-// chatClient
-// .prompt("What is Spring AI?")
-// .system("you are a smart AI")
-// .stream()
-// .content()
-// .doOnNext(System.out::println)
-// .blockLast();
-
String systemPrompt = """
You are a professional technical assistant.
Strictly use the provided context to answer questions.
@@ -71,15 +57,22 @@ public String testCase() throws Exception {
Do not use outside knowledge. Be concise.
""";
- String content2 = chatClient
- .prompt("When is the AI Summit?")
- .advisors(questionAnswerAdvisor)
- .system(systemPrompt)
- .stream()
- .content()
- .doOnNext(System.out::println)
- .blockLast();
- System.out.println(content2);
+// String content = chatClient
+// .prompt("When is the AI Summit?")
+// .system(systemPrompt)
+// .advisors(questionAnswerAdvisor)
+// .call()
+// .content();
+// System.out.println(content);
+
+// chatClient
+// .prompt("When is the AI Summit?")
+// .system(systemPrompt)
+// .advisors(questionAnswerAdvisor)
+// .stream()
+// .content()
+// .doOnNext(System.out::println)
+// .blockLast();
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 495713d916..e535df924f 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
@@ -53,7 +53,7 @@ public JSONObject completions(@RequestBody JSONObject request) {
"tool_calls": [
{
"function": {
- "arguments": "{\\"arg0\\":\\"beijing\\"}",
+ "arguments": "{\\"arg0\\":\\"new york\\"}",
"name": "get_weather"
},
"id": "call_iV4bvFIZujbbVZwWbvsUKiiK",
@@ -124,7 +124,7 @@ public JSONObject completions(@RequestBody JSONObject request) {
"logprobs": null,
"message": {
"annotations": [],
- "content": "The weather in Beijing is currently sunny with a temperature of 10°C.",
+ "content": "The weather in New York is currently sunny with a temperature of 10°C.",
"refusal": null,
"role": "assistant"
}
@@ -182,4 +182,10 @@ public JSONObject completions(@RequestBody JSONObject request) {
return JSON.parseObject(toolCallResponse);
}
+
+ @RequestMapping("/v1/embeddings")
+ public JSONObject embeddings(@RequestBody JSONObject request) {
+ System.out.println(request);
+ return new JSONObject();
+ }
}
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 1142bddf51..d1a7e24df5 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
@@ -22,9 +22,8 @@ server:
spring:
ai:
openai:
- api-key: sk-zuxrezdvwLLU9sGRUEW8mdFKkXuFugK7H9uf2b7iS3qCRU9M
-# base-url: http://localhost:8080/spring-ai-1.x-scenario/llm
- base-url: https://globalai.vip
+ api-key: noaichaPCNAC09DHBWQCOACNacoi
+ base-url: http://localhost:8080/spring-ai-1.x-scenario/llm
embedding:
options:
model: text-embedding-ada-002
From 869e6bb56ea0c68842664e190aaa022483559f6b Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Tue, 3 Feb 2026 23:28:38 +0800
Subject: [PATCH 04/48] fix
---
.../config/expectedData.yaml | 60 +++++++++++++++++++
.../config/RagScenarioConfiguration.java | 52 ----------------
.../httpclient/controller/CaseController.java | 31 +---------
.../controller/LLMMockController.java | 7 ---
.../src/main/resources/application.yaml | 3 -
5 files changed, 61 insertions(+), 92 deletions(-)
delete mode 100644 test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java
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 588a655d98..8e22bdc715 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
@@ -13,4 +13,64 @@
# 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.
+segmentItems:
+ - serviceName: spring-ai-1.x-scenario
+ segmentSize: gt 0
+ segments:
+ - segmentId: not null
+ spans:
+ - operationName: Spring-ai/tool/get_weather
+ parentSpanId: 1
+ spanId: 2
+ spanLayer: Unknown
+ startTime: not null
+ endTime: not null
+ componentId: not null # Spring-ai component
+ isError: false
+ spanType: Local
+ peer: ''
+ skipAnalysis: 'false'
+ tags:
+ - { key: gen_ai.tool.name, value: get_weather }
+ - { key: gen_ai.tool.input, value: '{"arg0":"New York"}' }
+ - { key: gen_ai.tool.output, value: '"Sunny, 10°C"' }
+ - operationName: Spring-ai/chat-client/call
+ parentSpanId: 0
+ spanId: 1
+ spanLayer: Unknown
+ startTime: not null
+ endTime: not null
+ componentId: not null # Spring-ai component
+ isError: false
+ spanType: Local
+ peer: ''
+ skipAnalysis: 'false'
+ tags:
+ - { key: gen_ai.request.model, value: gpt-4.1-2025-04-14 }
+ - { key: gen_ai.temperature, value: '0.7' }
+ - { key: gen_ai.top.p, value: '0.9' }
+ - { key: gen_ai.response.id, value: not null }
+ - { key: gen_ai.response.model, value: gpt-4.1-2025-04-14 }
+ - { key: gen_ai.usage.input_tokens, value: not null }
+ - { key: gen_ai.usage.output_tokens, value: not null }
+ - { key: gen_ai.usage.total_tokens, value: not null }
+ - { key: gen_ai.response.finish_reasons, value: STOP }
+ - { key: gen_ai.prompt, value: "What's the weather in New York?" }
+ - { key: gen_ai.completion, value: "The weather in New York is currently sunny with a temperature of 10°C." }
+
+ - operationName: GET:/spring-ai-1.x-scenario/case/spring-ai-1.x-scenario-case
+ parentSpanId: -1
+ spanId: 0
+ spanLayer: Http
+ startTime: not null
+ endTime: not null
+ componentId: 14
+ isError: false
+ spanType: Entry
+ peer: ''
+ skipAnalysis: 'false'
+ tags:
+ - { key: url, value: http://localhost:8080/spring-ai-1.x-scenario/case/spring-ai-1.x-scenario-case }
+ - { key: http.method, value: GET }
+ - { key: http.status_code, value: '200' }
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java
deleted file mode 100644
index e69c4807b4..0000000000
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/java/test/apache/skywalking/apm/testcase/jdk/httpclient/config/RagScenarioConfiguration.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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 test.apache.skywalking.apm.testcase.jdk.httpclient.config;
-
-import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
-import org.springframework.ai.document.Document;
-import org.springframework.ai.embedding.EmbeddingModel;
-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 java.util.ArrayList;
-import java.util.List;
-
-@Configuration
-public class RagScenarioConfiguration {
-
- @Bean
- 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."));
-
- vectorStore.add(documentList);
-
- return vectorStore;
- }
-
- @Bean
- public QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore) {
- return QuestionAnswerAdvisor.builder(vectorStore).build();
- }
-}
\ No newline at end of file
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 c9fe1e3846..e8a69d0ded 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
@@ -19,8 +19,6 @@
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
-import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -33,7 +31,6 @@ public class CaseController {
private final WeatherTool weatherTool;
private final ChatClient chatClient;
- private final QuestionAnswerAdvisor questionAnswerAdvisor;
@GetMapping("/healthCheck")
public String healthCheck() {
@@ -42,38 +39,12 @@ public String healthCheck() {
@GetMapping("/spring-ai-1.x-scenario-case")
public String testCase() throws Exception {
- String content1 = chatClient
+ chatClient
.prompt("What's the weather in New York?")
.tools(weatherTool)
.call()
.content();
- System.out.println(content1);
-
- String systemPrompt = """
- You are a professional technical assistant.
- Strictly use the provided context to answer questions.
- If the information is not in the context, say: "I'm sorry, I don't have that information in my knowledge base."
- Do not use outside knowledge. Be concise.
- """;
-
-// String content = chatClient
-// .prompt("When is the AI Summit?")
-// .system(systemPrompt)
-// .advisors(questionAnswerAdvisor)
-// .call()
-// .content();
-// System.out.println(content);
-
-// chatClient
-// .prompt("When is the AI Summit?")
-// .system(systemPrompt)
-// .advisors(questionAnswerAdvisor)
-// .stream()
-// .content()
-// .doOnNext(System.out::println)
-// .blockLast();
-
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 e535df924f..573b3f0c48 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
@@ -30,7 +30,6 @@ public class LLMMockController {
@RequestMapping("/v1/chat/completions")
public JSONObject completions(@RequestBody JSONObject request) {
- System.out.println(request.toString());
JSONArray messages = request.getJSONArray("messages");
@@ -182,10 +181,4 @@ public JSONObject completions(@RequestBody JSONObject request) {
return JSON.parseObject(toolCallResponse);
}
-
- @RequestMapping("/v1/embeddings")
- public JSONObject embeddings(@RequestBody JSONObject request) {
- System.out.println(request);
- return new JSONObject();
- }
}
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 d1a7e24df5..75a26b2bc9 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
@@ -24,9 +24,6 @@ spring:
openai:
api-key: noaichaPCNAC09DHBWQCOACNacoi
base-url: http://localhost:8080/spring-ai-1.x-scenario/llm
- embedding:
- options:
- model: text-embedding-ada-002
chat:
options:
model: gpt-4.1-2025-04-14
From 4a2992d9d4f25a5215fedabc082c0a0f220e5aaf Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Tue, 3 Feb 2026 23:36:03 +0800
Subject: [PATCH 05/48] fix
---
.../jdk/httpclient/controller/LLMMockController.java | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
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 573b3f0c48..70591908d9 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
@@ -55,7 +55,7 @@ public JSONObject completions(@RequestBody JSONObject request) {
"arguments": "{\\"arg0\\":\\"new york\\"}",
"name": "get_weather"
},
- "id": "call_iV4bvFIZujbbVZwWbvsUKiiK",
+ "id": "call_iV4bvFIZujbb",
"type": "function"
}
]
@@ -63,7 +63,7 @@ public JSONObject completions(@RequestBody JSONObject request) {
}
],
"created": 1768490813,
- "id": "chatcmpl-CyJXJt7gxwDgz7UjYml95gc54784J",
+ "id": "chatcmpl-CyJXJt7gxwDgz7Uj",
"model": "gpt-4.1-2025-04-14",
"object": "chat.completion",
"prompt_filter_results": [
@@ -77,7 +77,7 @@ public JSONObject completions(@RequestBody JSONObject request) {
}
}
],
- "system_fingerprint": "fp_b9041e10b4",
+ "system_fingerprint": "fp_b9041e1",
"usage": {
"completion_tokens": 17,
"completion_tokens_details": {
@@ -130,7 +130,7 @@ public JSONObject completions(@RequestBody JSONObject request) {
}
],
"created": 1768491057,
- "id": "chatcmpl-CyJbFkZsyhOyHW2Otycoh2s4vrAZE",
+ "id": "chatcmpl-CyJbFkZsyhOyHW2Otyc",
"model": "gpt-4.1-2025-04-14",
"object": "chat.completion",
"prompt_filter_results": [
@@ -156,7 +156,7 @@ public JSONObject completions(@RequestBody JSONObject request) {
}
}
],
- "system_fingerprint": "fp_b9041e10b4",
+ "system_fingerprint": "fp_b9041e1",
"usage": {
"completion_tokens": 17,
"completion_tokens_details": {
From ff421201fdccd93ec1601af45a70cb7c78f20f50 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Wed, 4 Feb 2026 00:43:35 +0800
Subject: [PATCH 06/48] fix
---
.../ai/v1/define/DefaultChatClientCallInstrumentation.java | 5 ++---
.../ai/v1/define/DefaultChatClientStreamInstrumentation.java | 5 ++---
.../spring/ai/v1/define/ToolCallbackInstrumentation.java | 4 ++--
.../ai/v1/define/VectorStoreRetrieverInstrumentation.java | 5 ++---
docs/en/setup/service-agent/java-agent/Plugin-list.md | 1 +
5 files changed, 9 insertions(+), 11 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/define/DefaultChatClientCallInstrumentation.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/DefaultChatClientCallInstrumentation.java
index 308efc04e6..450fb4ed0d 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/DefaultChatClientCallInstrumentation.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/DefaultChatClientCallInstrumentation.java
@@ -25,11 +25,10 @@
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.NameMatch;
-import org.springframework.ai.chat.client.ChatClientRequest;
import static net.bytebuddy.matcher.ElementMatchers.named;
-import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+import static org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType;
public class DefaultChatClientCallInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
@@ -55,7 +54,7 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher getMethodsMatcher() {
- return named(INTERCEPT_METHOD).and(takesArguments(2)).and(takesArgument(0, ChatClientRequest.class));
+ return named(INTERCEPT_METHOD).and(takesArguments(2)).and(takesArgumentWithType(0, "org.springframework.ai.chat.client.ChatClientRequest"));
}
@Override
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/DefaultChatClientStreamInstrumentation.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/DefaultChatClientStreamInstrumentation.java
index 2e76fb5654..b5fb5d3564 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/DefaultChatClientStreamInstrumentation.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/DefaultChatClientStreamInstrumentation.java
@@ -25,11 +25,10 @@
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.NameMatch;
-import org.springframework.ai.chat.client.ChatClientRequest;
import static net.bytebuddy.matcher.ElementMatchers.named;
-import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+import static org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType;
public class DefaultChatClientStreamInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
@@ -55,7 +54,7 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher getMethodsMatcher() {
- return named(INTERCEPT_METHOD).and(takesArguments(1)).and(takesArgument(0, ChatClientRequest.class));
+ return named(INTERCEPT_METHOD).and(takesArguments(1)).and(takesArgumentWithType(0, "org.springframework.ai.chat.client.ChatClientRequest"));
}
@Override
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/ToolCallbackInstrumentation.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/ToolCallbackInstrumentation.java
index 88f424dc2a..99a4029b00 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/ToolCallbackInstrumentation.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/ToolCallbackInstrumentation.java
@@ -28,8 +28,8 @@
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.returns;
-import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+import static org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType;
public class ToolCallbackInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
@@ -54,7 +54,7 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
public ElementMatcher getMethodsMatcher() {
return named("call")
.and(takesArguments(2))
- .and(takesArgument(0, named("java.lang.String")))
+ .and(takesArgumentWithType(0, "java.lang.String"))
.and(returns(named("java.lang.String")));
}
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/VectorStoreRetrieverInstrumentation.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/VectorStoreRetrieverInstrumentation.java
index 426f42c27b..6c09e083fd 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/VectorStoreRetrieverInstrumentation.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/VectorStoreRetrieverInstrumentation.java
@@ -25,11 +25,10 @@
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.springframework.ai.vectorstore.SearchRequest;
import static net.bytebuddy.matcher.ElementMatchers.named;
-import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+import static org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType;
public class VectorStoreRetrieverInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
@@ -56,7 +55,7 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
public ElementMatcher getMethodsMatcher() {
return named(INTERCEPT_METHOD)
.and(takesArguments(1))
- .and(takesArgument(0, SearchRequest.class));
+ .and(takesArgumentWithType(0, "org.springframework.ai.vectorstore.SearchRequest"));
}
@Override
diff --git a/docs/en/setup/service-agent/java-agent/Plugin-list.md b/docs/en/setup/service-agent/java-agent/Plugin-list.md
index c5a5bfd48f..49d1cb9c8e 100644
--- a/docs/en/setup/service-agent/java-agent/Plugin-list.md
+++ b/docs/en/setup/service-agent/java-agent/Plugin-list.md
@@ -103,6 +103,7 @@
- sharding-sphere-5.0.0
- sofarpc
- solrj-7.x
+- spring-ai-1.x
- spring-annotation
- spring-async-annotation-5.x
- spring-cloud-feign-1.x
From 6c9ce2acfe9d159ae3fe297453a91692d1c69aa6 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Wed, 4 Feb 2026 12:01:42 +0800
Subject: [PATCH 07/48] fix
---
test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 05b32a346c..d504d1ea01 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
@@ -30,7 +30,7 @@
3.8.1
- spring-ai-scenario
+ spring-ai-1.x-scenario
From 142a456e2805e26a691c31cc82e34d91e63eb044 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Wed, 4 Feb 2026 12:41:58 +0800
Subject: [PATCH 08/48] fix
---
test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 912d655aeb..8a1c2e0480 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
@@ -18,4 +18,4 @@
home="$(cd "$(dirname $0)"; pwd)"
-java -Dskywalking.plugin.springai.collect_prompt=true -Dskywalking.plugin.springai.collect_completion=true -Dskywalking.plugin.springai.collect_tool_input=true -Dskywalking.plugin.springai.collect_tool_output=true -jar ${agent_opts} ${home}/../libs/jdk-httpclient-scenario.jar &
\ No newline at end of file
+java -Dskywalking.plugin.springai.collect_prompt=true -Dskywalking.plugin.springai.collect_completion=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
From 44e4589c116d293c4e4312cd526061243c6d04e6 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Wed, 4 Feb 2026 21:47:49 +0800
Subject: [PATCH 09/48] fix
---
test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 d504d1ea01..1afb12d1b9 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
@@ -21,7 +21,7 @@
4.0.0
org.apache.skywalking
- x-scenario
+ spring-ai-1.x-scenario
5.0.0
From 4b25e8fde4f1d13b70ac0dc2f10bb9c5fc99869c Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Wed, 4 Feb 2026 22:56:39 +0800
Subject: [PATCH 10/48] fix
---
test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 1afb12d1b9..540bb0a541 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
@@ -80,7 +80,7 @@
- jdk-httpclient-scenario
+ spring-ai-1.x-scenario
org.springframework.boot
From 175792a086edc8c3be1c3c6a33f792b86edd61f7 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Thu, 5 Feb 2026 12:42:28 +0800
Subject: [PATCH 11/48] fix
---
test/plugin/scenarios/spring-ai-1.x-scenario/configuration.yml | 2 --
1 file changed, 2 deletions(-)
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/configuration.yml b/test/plugin/scenarios/spring-ai-1.x-scenario/configuration.yml
index acec927aa9..073d9271d5 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/configuration.yml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/configuration.yml
@@ -17,7 +17,5 @@
type: jvm
entryService: http://localhost:8080/spring-ai-1.x-scenario/case/spring-ai-1.x-scenario-case
healthCheck: http://localhost:8080/spring-ai-1.x-scenario/case/healthCheck
-runningMode: with_bootstrap
-withPlugins: apm-jdk-httpclient-plugin-*.jar
startScript: ./bin/startup.sh
From 0a5704646262199b2602a32852223f4e2975314a Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Thu, 5 Feb 2026 21:13:18 +0800
Subject: [PATCH 12/48] fix
---
.../spring-ai-1.x-scenario/src/main/assembly/assembly.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/assembly/assembly.xml b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/assembly/assembly.xml
index 3ab23573ab..deed40fcfe 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/assembly/assembly.xml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/assembly/assembly.xml
@@ -33,7 +33,7 @@
- ./target/jdk-httpclient-scenario.jar
+ ./target/spring-ai-1.x-scenario.jar
./libs
0775
From 346a1897145324cea2f9d9732eb9203694006a46 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Thu, 5 Feb 2026 21:37:33 +0800
Subject: [PATCH 13/48] fix
---
.../spring-ai-1.x-scenario/config/expectedData.yaml | 6 +++---
1 file changed, 3 insertions(+), 3 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 8e22bdc715..116fd775c9 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
@@ -25,14 +25,14 @@ segmentItems:
spanLayer: Unknown
startTime: not null
endTime: not null
- componentId: not null # Spring-ai component
+ componentId: 164
isError: false
spanType: Local
peer: ''
skipAnalysis: 'false'
tags:
- { key: gen_ai.tool.name, value: get_weather }
- - { key: gen_ai.tool.input, value: '{"arg0":"New York"}' }
+ - { key: gen_ai.tool.input, value: '{"arg0":"new york"}' }
- { key: gen_ai.tool.output, value: '"Sunny, 10°C"' }
- operationName: Spring-ai/chat-client/call
@@ -41,7 +41,7 @@ segmentItems:
spanLayer: Unknown
startTime: not null
endTime: not null
- componentId: not null # Spring-ai component
+ componentId: 164
isError: false
spanType: Local
peer: ''
From f20a8c6c8444a715525e129446beb4a955621a65 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Thu, 5 Feb 2026 23:13:45 +0800
Subject: [PATCH 14/48] fix
---
.../scenarios/spring-ai-1.x-scenario/config/expectedData.yaml | 1 +
1 file changed, 1 insertion(+)
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 116fd775c9..2e939b4c46 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
@@ -49,6 +49,7 @@ segmentItems:
tags:
- { key: gen_ai.request.model, value: gpt-4.1-2025-04-14 }
- { key: gen_ai.temperature, value: '0.7' }
+ - { key: gen_ai.top.k, value: null }
- { key: gen_ai.top.p, value: '0.9' }
- { key: gen_ai.response.id, value: not null }
- { key: gen_ai.response.model, value: gpt-4.1-2025-04-14 }
From d1a69089d55a012b9bdb62ebf05233bc2e5cfd86 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Fri, 6 Feb 2026 09:18:02 +0800
Subject: [PATCH 15/48] fix
---
.../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 2e939b4c46..7abee75316 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
@@ -66,7 +66,7 @@ segmentItems:
spanLayer: Http
startTime: not null
endTime: not null
- componentId: 14
+ componentId: 1
isError: false
spanType: Entry
peer: ''
From 58b1ca35aeab8df2d39c7a3ddc09c9d6b2990176 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Thu, 12 Feb 2026 22:32:46 +0800
Subject: [PATCH 16/48] fix
---
.../apm/agent/core/context/tag/Tags.java | 94 ++++++++++-
.../agent/core/context/trace/SpanLayer.java | 7 +-
...tor.java => ChatModelCallInterceptor.java} | 24 ++-
...r.java => ChatModelStreamInterceptor.java} | 42 +++--
.../DefaultToolCallingManagerInterceptor.java | 63 ++++++++
.../ai/v1/ToolCallbackCallInterceptor.java | 6 +-
.../v1/common/ChatModelMetadataResolver.java | 146 ++++++++++++++++++
.../spring/ai/v1/contant/Constants.java | 6 +
...ultToolCallingManagerInstrumentation.java} | 10 +-
.../AnthropicApiInstrumentation.java} | 38 ++---
.../provider/OpenAIApiInstrumentation.java | 63 ++++++++
.../spring/ai/v1/enums/AiProviderEnum.java | 68 ++++++++
.../v1/provider/AnthropicApiInterceptor.java | 41 +++++
.../ai/v1/provider/OpenAiApiInterceptor.java | 41 +++++
.../src/main/resources/skywalking-plugin.def | 5 +-
.../spring-ai-1.x-scenario/bin/startup.sh | 2 +-
.../scenarios/spring-ai-1.x-scenario/pom.xml | 6 +-
.../httpclient/config/ChatClientConfig.java | 4 +-
.../httpclient/controller/CaseController.java | 26 ++++
19 files changed, 626 insertions(+), 66 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/{DefaultChatClientCallInterceptor.java => ChatModelCallInterceptor.java} (86%)
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/{DefaultChatClientStreamInterceptor.java => ChatModelStreamInterceptor.java} (85%)
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/DefaultToolCallingManagerInterceptor.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/ChatModelMetadataResolver.java
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/{DefaultChatClientCallInstrumentation.java => DefaultToolCallingManagerInstrumentation.java} (86%)
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/{DefaultChatClientStreamInstrumentation.java => provider/AnthropicApiInstrumentation.java} (67%)
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/provider/OpenAIApiInstrumentation.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/enums/AiProviderEnum.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/provider/AnthropicApiInterceptor.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/provider/OpenAiApiInterceptor.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 0b0f9781d0..c170d0218a 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
@@ -160,44 +160,126 @@ public static final class HTTP {
*/
public static final StringTag THREAD_CARRIER = new StringTag(24, "thread.carrier");
+ /**
+ * GEN_AI_OPERATION_NAME represents the name of the operation being performed
+ */
+ public static final StringTag GEN_AI_OPERATION_NAME = new StringTag(44, "gen_ai.operation.name");
+
+ /**
+ * GEN_AI_PROVIDER_NAME represents the Generative AI provider as identified by the client or server instrumentation.
+ */
+ public static final StringTag GEN_AI_PROVIDER_NAME = new StringTag(46, "gen_ai.provider.name");
+
+ /**
+ * GEN_AI_REQUEST_MODEL represents the name of the GenAI model a request is being made to.
+ */
public static final StringTag GEN_AI_REQUEST_MODEL = new StringTag(25, "gen_ai.request.model");
- public static final StringTag GEN_AI_TOP_K = new StringTag(26, "gen_ai.top.k");
+ /**
+ * GEN_AI_TOP_K represents the top_k sampling setting for the GenAI request.
+ */
+ public static final StringTag GEN_AI_TOP_K = new StringTag(26, "gen_ai.request.top_k");
- public static final StringTag GEN_AI_TOP_P = new StringTag(27, "gen_ai.top.p");
+ /**
+ * GEN_AI_TOP_P represents the top_p sampling setting for the GenAI request.
+ */
+ public static final StringTag GEN_AI_TOP_P = new StringTag(27, "gen_ai.request.top_p");
- public static final StringTag GEN_AI_TEMPERATURE = new StringTag(28, "gen_ai.temperature");
+ /**
+ * GEN_AI_TEMPERATURE represents the temperature setting for the GenAI request.
+ */
+ public static final StringTag GEN_AI_TEMPERATURE = new StringTag(28, "gen_ai.request.temperature");
+ /**
+ * GEN_AI_TOOL_NAME represents the name of the tool utilized by the agent.
+ */
public static final StringTag GEN_AI_TOOL_NAME = new StringTag(29, "gen_ai.tool.name");
- public static final StringTag GEN_AI_TOOL_INPUT = new StringTag(30, "gen_ai.tool.input");
+ /**
+ * GEN_AI_TOOL_CALL_ARGUMENTS represents the parameters passed to the tool call.
+ */
+ public static final StringTag GEN_AI_TOOL_CALL_ARGUMENTS = new StringTag(30, "gen_ai.tool.call.arguments");
- public static final StringTag GEN_AI_TOOL_OUTPUT = new StringTag(43, "gen_ai.tool.output");
+ /**
+ * GEN_AI_TOOL_CALL_RESULT represents the result returned by the tool call (if any and if execution was successful).
+ */
+ public static final StringTag GEN_AI_TOOL_CALL_RESULT = new StringTag(43, "gen_ai.tool.call.result");
+ /**
+ * GEN_AI_RESPONSE_MODEL represents the name of the model that generated the response.
+ */
public static final StringTag GEN_AI_RESPONSE_MODEL = new StringTag(31, "gen_ai.response.model");
+ /**
+ * GEN_AI_RESPONSE_ID represents the unique identifier for the completion.
+ */
public static final StringTag GEN_AI_RESPONSE_ID = new StringTag(32, "gen_ai.response.id");
+ /**
+ * GEN_AI_USAGE_INPUT_TOKENS represents the number of tokens used in the GenAI input (prompt).
+ */
public static final StringTag GEN_AI_USAGE_INPUT_TOKENS = new StringTag(33, "gen_ai.usage.input_tokens");
+ /**
+ * GEN_AI_USAGE_OUTPUT_TOKENS represents the number of tokens used in the GenAI response (completion).
+ */
public static final StringTag GEN_AI_USAGE_OUTPUT_TOKENS = new StringTag(34, "gen_ai.usage.output_tokens");
- public static final StringTag GEN_AI_USAGE_TOTAL_TOKENS = new StringTag(35, "gen_ai.usage.total_tokens");
+ /**
+ * GEN_AI_USAGE_TOTAL_TOKENS represents the total number of tokens used in the GenAI exchange.
+ */
+ public static final StringTag GEN_AI_CLIENT_TOKEN_USAGE = new StringTag(35, "gen_ai.client.token.usage");
+ /**
+ * GEN_AI_RESPONSE_FINISH_REASONS represents the array of reasons the model stopped generating tokens.
+ */
public static final StringTag GEN_AI_RESPONSE_FINISH_REASONS = new StringTag(36, "gen_ai.response.finish_reasons");
+ /**
+ * GEN_AI_PROMPT represents the full prompt text or messages provided to the model.
+ */
public static final StringTag GEN_AI_PROMPT = new StringTag(37, "gen_ai.prompt");
+ /**
+ * GEN_AI_COMPLETION represents the full completion text or messages returned by the model.
+ */
public static final StringTag GEN_AI_COMPLETION = new StringTag(38, "gen_ai.completion");
+ /**
+ * GEN_AI_STREAM_TTFR represents the time to first response (TTFR) for streaming operations.
+ */
public static final StringTag GEN_AI_STREAM_TTFR = new StringTag(39, "gen_ai.stream.ttfr");
+ /**
+ * GEN_AI_VECTOR_STORE_TOP_K represents the target number of top results to return from the vector store.
+ */
public static final StringTag GEN_AI_VECTOR_STORE_TOP_K = new StringTag(40, "gen_ai.vector_store.top_k");
+ /**
+ * GEN_AI_VECTOR_STORE_FILTER_EXPRESSION represents the filter expression used in the vector store search.
+ */
public static final StringTag GEN_AI_VECTOR_STORE_FILTER_EXPRESSION = new StringTag(41, "gen_ai.vector_store.filter_expression");
+ /**
+ * GEN_AI_VECTOR_STORE_RECORD_IDS represents the unique identifiers of the records retrieved from the vector store.
+ */
public static final StringTag GEN_AI_VECTOR_STORE_RECORD_IDS = new StringTag(42, "gen_ai.vector_store.record_ids");
+ /**
+ * GEN_AI_SYSTEM_INSTRUCTIONS represents the system message or instructions provided to the GenAI model separately from the chat history.
+ */
+ public static final StringTag GEN_AI_SYSTEM_INSTRUCTIONS = new StringTag(43, "gen_ai.system_instructions");
+
+ /**
+ * GEN_AI_INPUT_MESSAGES represents the chat history provided to the model as an input.
+ */
+ public static final StringTag GEN_AI_INPUT_MESSAGES = new StringTag(44, "gen_ai.input.messages");
+
+ /**
+ * GEN_AI_OUTPUT_MESSAGES represents the messages returned by the model where each message represents a specific model response (choice, candidate).
+ */
+ public static final StringTag GEN_AI_OUTPUT_MESSAGES = new StringTag(45, "gen_ai.output.messages");
+
/**
* 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-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/trace/SpanLayer.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/trace/SpanLayer.java
index 4ee9395ac9..c31fdc4cc1 100644
--- a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/trace/SpanLayer.java
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/trace/SpanLayer.java
@@ -19,7 +19,8 @@
package org.apache.skywalking.apm.agent.core.context.trace;
public enum SpanLayer {
- DB(1), RPC_FRAMEWORK(2), HTTP(3), MQ(4), CACHE(5);
+ DB(1), RPC_FRAMEWORK(2), HTTP(3), MQ(4), CACHE(5),
+ GEN_AI(7);
private int code;
@@ -50,4 +51,8 @@ public static void asHttp(AbstractSpan span) {
public static void asMQ(AbstractSpan span) {
span.setLayer(SpanLayer.MQ);
}
+
+ public static void asGenAI(AbstractSpan span) {
+ span.setLayer(SpanLayer.GEN_AI);
+ }
}
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/DefaultChatClientCallInterceptor.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
similarity index 86%
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/DefaultChatClientCallInterceptor.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/ChatModelCallInterceptor.java
index 7c6ea9fc9f..a5dd00f604 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/DefaultChatClientCallInterceptor.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
@@ -21,11 +21,15 @@
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.network.trace.component.ComponentsDefine;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
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.enums.AiProviderEnum;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
@@ -37,20 +41,28 @@
import java.lang.reflect.Method;
-public class DefaultChatClientCallInterceptor implements InstanceMethodsAroundInterceptor {
+public class ChatModelCallInterceptor implements InstanceMethodsAroundInterceptor {
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
- AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/chat-client/call");
+ AbstractSpan span = ContextManager.createExitSpan("Spring-ai/client/call", null);
+ ChatModelMetadataResolver.ApiMetadata apiMetadata = ChatModelMetadataResolver.getMetadata(objInst);
+ if (apiMetadata != null) {
+ span.setPeer(apiMetadata.getPeer());
+ Tags.GEN_AI_PROVIDER_NAME.set(span, apiMetadata.getProviderName());
+ } else {
+ Tags.GEN_AI_PROVIDER_NAME.set(span, AiProviderEnum.UNKNOW.getValue());
+ }
- ChatClientRequest request = (ChatClientRequest) allArguments[0];
- Prompt prompt = request.prompt();
+ Prompt prompt = (Prompt) allArguments[0];
ChatOptions chatOptions = prompt.getOptions();
if (chatOptions == null) {
return;
}
span.setComponent(ComponentsDefine.SPRING_AI);
+ SpanLayer.asGenAI(span);
+ Tags.GEN_AI_OPERATION_NAME.set(span, Constants.CHAT);
Tags.GEN_AI_REQUEST_MODEL.set(span, chatOptions.getModel());
Tags.GEN_AI_TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
Tags.GEN_AI_TOP_K.set(span, String.valueOf(chatOptions.getTopK()));
@@ -93,7 +105,7 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
}
if (usage.getTotalTokens() != null) {
totalTokens = usage.getTotalTokens();
- Tags.GEN_AI_USAGE_TOTAL_TOKENS.set(span, String.valueOf(totalTokens));
+ Tags.GEN_AI_CLIENT_TOKEN_USAGE.set(span, String.valueOf(totalTokens));
}
}
}
@@ -138,7 +150,7 @@ private void collectContent(AbstractSpan span, Object[] allArguments, Generation
private void collectPrompt(AbstractSpan span, Object[] allArguments) {
ChatClientRequest request = (ChatClientRequest) allArguments[0];
- if (request == null || request.prompt() == null) {
+ if (request == null) {
return;
}
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/DefaultChatClientStreamInterceptor.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
similarity index 85%
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/DefaultChatClientStreamInterceptor.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/ChatModelStreamInterceptor.java
index 053e55c213..2858f75fa1 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/DefaultChatClientStreamInterceptor.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
@@ -19,16 +19,17 @@
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.ContextSnapshot;
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.network.trace.component.ComponentsDefine;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
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.chat.client.ChatClientRequest;
-import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
@@ -42,23 +43,31 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
-public class DefaultChatClientStreamInterceptor implements InstanceMethodsAroundInterceptor {
+public class ChatModelStreamInterceptor implements InstanceMethodsAroundInterceptor {
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
- AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/stream");
+ AbstractSpan span = ContextManager.createExitSpan("Spring-ai/client/stream", null);
+ ChatModelMetadataResolver.ApiMetadata apiMetadata = ChatModelMetadataResolver.getMetadata(objInst);
+ if (apiMetadata != null) {
+ span.setPeer(apiMetadata.getPeer());
+ Tags.GEN_AI_PROVIDER_NAME.set(span, apiMetadata.getProviderName());
+ }
+
span.setComponent(ComponentsDefine.SPRING_AI);
- ChatClientRequest request = (ChatClientRequest) allArguments[0];
- if (request == null) {
+ SpanLayer.asGenAI(span);
+
+ Prompt prompt = (Prompt) allArguments[0];
+ if (prompt == null) {
return;
}
- Prompt prompt = request.prompt();
ChatOptions chatOptions = prompt.getOptions();
if (chatOptions == null) {
return;
}
+ Tags.GEN_AI_OPERATION_NAME.set(span, Constants.CHAT);
Tags.GEN_AI_REQUEST_MODEL.set(span, chatOptions.getModel());
Tags.GEN_AI_TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
Tags.GEN_AI_TOP_K.set(span, String.valueOf(chatOptions.getTopK()));
@@ -73,11 +82,12 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
return ret;
}
AbstractSpan span = ContextManager.activeSpan();
- span.prepareForAsync();
+ ContextSnapshot contextSnapshot = ContextManager.capture();
+ span.prepareForAsync();
ContextManager.stopSpan();
- Flux flux = (Flux) ret;
+ Flux flux = (Flux) ret;
AtomicReference lastResponseRef = new AtomicReference<>();
@@ -91,12 +101,11 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
ContextManager.getRuntimeContext().remove(Constants.SPRING_AI_STREAM_START_TIME);
return flux.doOnNext(response -> {
- if (response.chatResponse() != null) {
+ if (response != null) {
- ChatResponse chatResponse = response.chatResponse();
- lastResponseRef.set(chatResponse);
+ lastResponseRef.set(response);
- Generation generation = chatResponse.getResult();
+ Generation generation = response.getResult();
if (generation == null) {
return;
}
@@ -138,7 +147,7 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
Tags.GEN_AI_USAGE_INPUT_TOKENS.set(span, String.valueOf(usage.getPromptTokens()));
Tags.GEN_AI_USAGE_OUTPUT_TOKENS.set(span, String.valueOf(usage.getCompletionTokens()));
totalTokens = usage.getTotalTokens() != null ? usage.getTotalTokens() : 0;
- Tags.GEN_AI_USAGE_TOTAL_TOKENS.set(span, String.valueOf(usage.getTotalTokens().longValue()));
+ Tags.GEN_AI_CLIENT_TOKEN_USAGE.set(span, String.valueOf(usage.getTotalTokens().longValue()));
}
Tags.GEN_AI_RESPONSE_FINISH_REASONS.set(span, finishReason.get());
@@ -147,8 +156,7 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
if (tokenThreshold < 0 || totalTokens >= tokenThreshold) {
if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_PROMPT) {
- ChatClientRequest request = (ChatClientRequest) allArguments[0];
- Prompt prompt = request.prompt();
+ Prompt prompt = (Prompt) allArguments[0];
String promptText = prompt.getContents();
if (promptText != null) {
int limit = SpringAiPluginConfig.Plugin.SpringAi.PROMPT_LENGTH_LIMIT;
@@ -175,7 +183,7 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
} finally {
span.asyncFinish();
}
- });
+ }).contextWrite(c -> c.put(Constants.SKYWALKING_CONTEXT_SNAPSHOT, contextSnapshot));
}
@Override
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/DefaultToolCallingManagerInterceptor.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/DefaultToolCallingManagerInterceptor.java
new file mode 100644
index 0000000000..fdf411e9ef
--- /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/DefaultToolCallingManagerInterceptor.java
@@ -0,0 +1,63 @@
+/*
+ * 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.ContextSnapshot;
+import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
+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.network.trace.component.ComponentsDefine;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants;
+import org.springframework.ai.model.tool.internal.ToolCallReactiveContextHolder;
+import reactor.util.context.ContextView;
+
+import java.lang.reflect.Method;
+
+public class DefaultToolCallingManagerInterceptor implements InstanceMethodsAroundInterceptor {
+
+ @Override
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
+ AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/client/executeTool");
+ span.setComponent(ComponentsDefine.SPRING_AI);
+
+ ContextView reactorCtx = ToolCallReactiveContextHolder.getContext();
+
+ if (reactorCtx != null && reactorCtx.hasKey(Constants.SKYWALKING_CONTEXT_SNAPSHOT)) {
+ ContextSnapshot snapshot = reactorCtx.get(Constants.SKYWALKING_CONTEXT_SNAPSHOT);
+ ContextManager.continued(snapshot);
+ }
+ }
+
+ @Override
+ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, Object ret) throws Throwable {
+ if (ContextManager.isActive()) {
+ 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);
+ }
+ }
+}
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/ToolCallbackCallInterceptor.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/ToolCallbackCallInterceptor.java
index 0b2afe8014..30835c1148 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/ToolCallbackCallInterceptor.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/ToolCallbackCallInterceptor.java
@@ -21,6 +21,7 @@
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;
@@ -43,12 +44,13 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr
AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/tool/" + toolName);
span.setComponent(ComponentsDefine.SPRING_AI);
+ SpanLayer.asGenAI(span);
Tags.GEN_AI_TOOL_NAME.set(span, toolName);
if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_TOOL_INPUT) {
String toolInput = (String) allArguments[0];
- Tags.GEN_AI_TOOL_INPUT.set(span, toolInput);
+ Tags.GEN_AI_TOOL_CALL_ARGUMENTS.set(span, toolInput);
}
}
@@ -58,7 +60,7 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
if (ContextManager.isActive()) {
AbstractSpan span = ContextManager.activeSpan();
if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_TOOL_OUTPUT && ret != null) {
- Tags.GEN_AI_TOOL_OUTPUT.set(span, (String) ret);
+ Tags.GEN_AI_TOOL_CALL_RESULT.set(span, (String) ret);
}
ContextManager.stopSpan();
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/ChatModelMetadataResolver.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/ChatModelMetadataResolver.java
new file mode 100644
index 0000000000..e7ca118387
--- /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/ChatModelMetadataResolver.java
@@ -0,0 +1,146 @@
+/*
+ * 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.logging.api.ILog;
+import org.apache.skywalking.apm.agent.core.logging.api.LogManager;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ChatModelMetadataResolver {
+
+ private static final ILog LOGGER = LogManager.getLogger(ChatModelMetadataResolver.class);
+
+ private static final Map MODEL_METADATA_MAP = new HashMap<>();
+
+ static {
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.ANTHROPIC_CLAUDE.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.ANTHROPIC_CLAUDE.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.OPENAI.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.OPENAI.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.AZURE_OPENAI.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.AZURE_OPENAI.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.OPENAI_SDK_OFFICIAL.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.OPENAI_SDK_OFFICIAL.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.AMAZON_BEDROCK_CONVERSE.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.AMAZON_BEDROCK_CONVERSE.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.GOOGLE_VERTEXAI_GEMINI.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.GOOGLE_VERTEXAI_GEMINI.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.GOOGLE_GENAI.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.GOOGLE_GENAI.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.DEEPSEEK_OPENAI_PROXY.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.DEEPSEEK_OPENAI_PROXY.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.HUGGINGFACE.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.HUGGINGFACE.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.MINIMAX.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.MINIMAX.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.MISTRAL_AI.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.MISTRAL_AI.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.OCI_GENAI_COHERE.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.OCI_GENAI_COHERE.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.OLLAMA.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.OLLAMA.getValue()));
+
+ MODEL_METADATA_MAP.put(
+ AiProviderEnum.ZHIPU_AI.getModelClassName(),
+ new ApiMetadata(AiProviderEnum.ZHIPU_AI.getValue()));
+
+ }
+
+ public static ApiMetadata getMetadata(Object chatModelInstance) {
+ try {
+ ApiMetadata metadata = MODEL_METADATA_MAP.get(chatModelInstance.getClass().getName());
+ if (metadata == null) {
+ return null;
+ }
+
+ return metadata;
+ } catch (Exception e) {
+ LOGGER.error("spring-ai plugin get modelMetadata error: ", e);
+ return null;
+ }
+ }
+
+ public static class ApiMetadata {
+
+ private final String providerName;
+ private volatile String baseUrl;
+ private volatile String completionsPath;
+
+ ApiMetadata(String providerName) {
+ this.providerName = providerName;
+ }
+
+ public String getProviderName() {
+ return providerName;
+ }
+
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ public String getCompletionsPath() {
+ return completionsPath;
+ }
+
+ public void setCompletionsPath(String completionsPath) {
+ this.completionsPath = completionsPath;
+ }
+
+ public String getPeer() {
+ if (baseUrl != null && !baseUrl.isEmpty()) {
+ return completionsPath != null && !completionsPath.isEmpty()
+ ? baseUrl + completionsPath
+ : baseUrl;
+ }
+ return providerName;
+ }
+ }
+}
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 ca23ff6eb2..7191e79dec 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
@@ -20,4 +20,10 @@
public class Constants {
public static final String SPRING_AI_STREAM_START_TIME = "Spring-ai.stream.startTime";
+
+ public static final String SKYWALKING_CONTEXT_SNAPSHOT = "SKYWALKING_CONTEXT_SNAPSHOT";
+
+ public static final String CHAT = "chat";
+
+ public static final String EXECUTE_TOOL = "execute_tool";
}
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/DefaultChatClientCallInstrumentation.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/DefaultToolCallingManagerInstrumentation.java
similarity index 86%
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/define/DefaultChatClientCallInstrumentation.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/define/DefaultToolCallingManagerInstrumentation.java
index 450fb4ed0d..595472834f 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/DefaultChatClientCallInstrumentation.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/DefaultToolCallingManagerInstrumentation.java
@@ -30,13 +30,13 @@
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import static org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType;
-public class DefaultChatClientCallInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+public class DefaultToolCallingManagerInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
- private static final String ENHANCE_CLASS = "org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec";
+ private static final String ENHANCE_CLASS = "org.springframework.ai.model.tool.DefaultToolCallingManager";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.DefaultChatClientCallInterceptor";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.DefaultToolCallingManagerInterceptor";
- private static final String INTERCEPT_METHOD = "doGetObservableChatClientResponse";
+ private static final String INTERCEPT_METHOD = "executeToolCall";
@Override
protected ClassMatch enhanceClass() {
@@ -54,7 +54,7 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher getMethodsMatcher() {
- return named(INTERCEPT_METHOD).and(takesArguments(2)).and(takesArgumentWithType(0, "org.springframework.ai.chat.client.ChatClientRequest"));
+ return named(INTERCEPT_METHOD).and(takesArguments(3)).and(takesArgumentWithType(0, "org.springframework.ai.chat.prompt.Prompt"));
}
@Override
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/DefaultChatClientStreamInstrumentation.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/provider/AnthropicApiInstrumentation.java
similarity index 67%
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/define/DefaultChatClientStreamInstrumentation.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/define/provider/AnthropicApiInstrumentation.java
index b5fb5d3564..0ae41f18f4 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/DefaultChatClientStreamInstrumentation.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/provider/AnthropicApiInstrumentation.java
@@ -16,7 +16,7 @@
*
*/
-package org.apache.skywalking.apm.plugin.spring.ai.v1.define;
+package org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;
@@ -27,16 +27,13 @@
import org.apache.skywalking.apm.agent.core.plugin.match.NameMatch;
import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
-import static org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType;
-public class DefaultChatClientStreamInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+public class AnthropicApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
- private static final String ENHANCE_CLASS = "org.springframework.ai.chat.client.DefaultChatClient$DefaultStreamResponseSpec";
-
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.DefaultChatClientStreamInterceptor";
-
- private static final String INTERCEPT_METHOD = "doGetObservableFluxChatResponse";
+ private static final String ENHANCE_CLASS = "org.springframework.ai.anthropic.api.AnthropicApi";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.AnthropicApiConstructorInterceptor";
@Override
protected ClassMatch enhanceClass() {
@@ -45,28 +42,23 @@ protected ClassMatch enhanceClass() {
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
- return new ConstructorInterceptPoint[0];
- }
-
- @Override
- public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
- return new InstanceMethodsInterceptPoint[]{
- new InstanceMethodsInterceptPoint() {
+ return new ConstructorInterceptPoint[]{
+ new ConstructorInterceptPoint() {
@Override
- public ElementMatcher getMethodsMatcher() {
- return named(INTERCEPT_METHOD).and(takesArguments(1)).and(takesArgumentWithType(0, "org.springframework.ai.chat.client.ChatClientRequest"));
+ public ElementMatcher getConstructorMatcher() {
+ return takesArguments(8).and(takesArgument(0, named("java.lang.String"))).and(takesArgument(1, named("java.lang.String")));
}
@Override
- public String getMethodsInterceptor() {
+ public String getConstructorInterceptor() {
return INTERCEPTOR_CLASS;
}
-
- @Override
- public boolean isOverrideArgs() {
- return false;
- }
}
};
}
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
+ return new InstanceMethodsInterceptPoint[0];
+ }
}
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/provider/OpenAIApiInstrumentation.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/provider/OpenAIApiInstrumentation.java
new file mode 100644
index 0000000000..84c71b82b9
--- /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/provider/OpenAIApiInstrumentation.java
@@ -0,0 +1,63 @@
+/*
+ * 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.provider;
+
+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.NameMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+
+public class OpenAIApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS = "org.springframework.ai.openai.api.OpenAiApi";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.OpenAIApiConstructorInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return NameMatch.byName(ENHANCE_CLASS);
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[]{
+ new ConstructorInterceptPoint() {
+ @Override
+ public ElementMatcher getConstructorMatcher() {
+ return takesArgument(0, named("java.lang.String")).and(takesArgument(3, named("java.lang.String")));
+ }
+
+ @Override
+ public String getConstructorInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+ }
+ };
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
+ return new InstanceMethodsInterceptPoint[0];
+ }
+}
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/enums/AiProviderEnum.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/enums/AiProviderEnum.java
new file mode 100644
index 0000000000..4e139486c8
--- /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/enums/AiProviderEnum.java
@@ -0,0 +1,68 @@
+/*
+ * 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.enums;
+
+public enum AiProviderEnum {
+
+ UNKNOW("unknow", null),
+
+ ANTHROPIC_CLAUDE("anthropic", "org.springframework.ai.anthropic.AnthropicChatModel"),
+
+ AMAZON_BEDROCK_CONVERSE("aws.bedrock", "org.springframework.ai.bedrock.converse.BedrockProxyChatModel"),
+
+ AZURE_OPENAI("azure.openai", "org.springframework.ai.azure.openai.AzureOpenAiChatModel"),
+
+ OCI_GENAI_COHERE("cohere", "org.springframework.ai.oci.cohere.OCICohereChatModel"),
+
+ DEEPSEEK_OPENAI_PROXY("deepseek", "org.springframework.ai.deepseek.DeepSeekChatModel"),
+
+ GOOGLE_GENAI("gcp.gen_ai", "org.springframework.ai.google.genai.GoogleGenAiChatModel"),
+
+ GOOGLE_VERTEXAI_GEMINI("gcp.vertex_ai", "org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel"),
+
+ MISTRAL_AI("mistral_ai", "org.springframework.ai.mistralai.MistralAiChatModel"),
+
+ OPENAI("openai", "org.springframework.ai.openai.OpenAiChatModel"),
+
+ HUGGINGFACE("huggingface", "org.springframework.ai.huggingface.HuggingfaceChatModel"),
+
+ MINIMAX("minimax", "org.springframework.ai.minimax.MiniMaxChatModel"),
+
+ OLLAMA("ollama", "org.springframework.ai.ollama.OllamaChatModel"),
+
+ OPENAI_SDK_OFFICIAL("openai", "org.springframework.ai.openaisdk.OpenAiSdkChatModel"),
+
+ ZHIPU_AI("zhipu_ai", "org.springframework.ai.zhipuai.ZhiPuAiChatModel");
+
+ private final String value;
+ private final String modelClassName;
+
+ AiProviderEnum(String value, String modelClassName) {
+ this.value = value;
+ this.modelClassName = modelClassName;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public String getModelClassName() {
+ return modelClassName;
+ }
+}
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/provider/AnthropicApiInterceptor.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/provider/AnthropicApiInterceptor.java
new file mode 100644
index 0000000000..49d9e2b708
--- /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/provider/AnthropicApiInterceptor.java
@@ -0,0 +1,41 @@
+/*
+ * 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.provider;
+
+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.ChatModelMetadataResolver;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
+
+public class AnthropicApiInterceptor implements InstanceConstructorInterceptor {
+
+ @Override
+ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.ANTHROPIC_CLAUDE.getModelClassName());
+ if (metadata == null) {
+ return;
+ }
+
+ String baseUrl = (String) allArguments[0];
+ String completionsPath = (String) allArguments[1];
+
+ metadata.setBaseUrl(baseUrl);
+ metadata.setCompletionsPath(completionsPath);
+ }
+}
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/provider/OpenAiApiInterceptor.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/provider/OpenAiApiInterceptor.java
new file mode 100644
index 0000000000..42e7932055
--- /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/provider/OpenAiApiInterceptor.java
@@ -0,0 +1,41 @@
+/*
+ * 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.provider;
+
+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.ChatModelMetadataResolver;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
+
+public class OpenAiApiInterceptor implements InstanceConstructorInterceptor {
+
+ @Override
+ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.OPENAI.getModelClassName());
+ if (metadata == null) {
+ return;
+ }
+
+ String baseUrl = (String) allArguments[0];
+ String completionsPath = (String) allArguments[3];
+
+ metadata.setBaseUrl(baseUrl);
+ metadata.setCompletionsPath(completionsPath);
+ }
+}
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 960e977592..462c07ad68 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
@@ -14,7 +14,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.DefaultChatClientCallInstrumentation
-spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.DefaultChatClientStreamInstrumentation
+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.VectorStoreRetrieverInstrumentation
+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.provider.OpenAIApiInstrumentation
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 8a1c2e0480..c4075db94b 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
@@ -18,4 +18,4 @@
home="$(cd "$(dirname $0)"; pwd)"
-java -Dskywalking.plugin.springai.collect_prompt=true -Dskywalking.plugin.springai.collect_completion=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 -jar ${agent_opts} ${home}/../libs/spring-ai-1.x-scenario.jar &
\ No newline at end of file
diff --git a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
index 540bb0a541..94052d033c 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
@@ -50,9 +50,13 @@
spring-ai-client-chat
+
+
+
+
org.springframework.ai
- spring-ai-starter-model-openai
+ spring-ai-starter-model-anthropic
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..30bb61c8bb 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,8 +17,8 @@
package test.apache.skywalking.apm.testcase.jdk.httpclient.config;
+import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -26,7 +26,7 @@
public class ChatClientConfig {
@Bean
- public ChatClient openAIChatClient(OpenAiChatModel model) {
+ public ChatClient openAIChatClient(AnthropicChatModel model) {
return ChatClient.create(model);
}
}
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 e8a69d0ded..441b07d075 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
@@ -39,12 +39,38 @@ public String healthCheck() {
@GetMapping("/spring-ai-1.x-scenario-case")
public String testCase() throws Exception {
+
+ String systemPrompt = """
+ You are a professional technical assistant.
+ Strictly use the provided context to answer questions.
+ If the information is not in the context, say: "I'm sorry, I don't have that information in my knowledge base."
+ Do not use outside knowledge. Be concise.
+ """;
+
chatClient
.prompt("What's the weather in New York?")
+ .system(systemPrompt)
.tools(weatherTool)
.call()
.content();
+ chatClient
+ .prompt("What's the weather in New York?")
+ .system(systemPrompt)
+ .tools(weatherTool)
+ .stream()
+ .content()
+ .doOnNext(System.out::println)
+ .blockLast();
+
+// chatClient
+// .prompt("What is Spring AI?")
+// .system(systemPrompt)
+// .stream()
+// .content()
+// .doOnNext(System.out::println)
+// .blockLast();
+
return "success";
}
}
From 31d92b345c7bd82e22c58d44b77527362b6177d2 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Thu, 12 Feb 2026 22:32:51 +0800
Subject: [PATCH 17/48] fix
---
.../v1/define/ChatModelInstrumentation.java | 92 +++++++++++++++++++
1 file changed, 92 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/define/ChatModelInstrumentation.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/define/ChatModelInstrumentation.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/ChatModelInstrumentation.java
new file mode 100644
index 0000000000..6d5a3a1c33
--- /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/ChatModelInstrumentation.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 ChatModelInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS = "org.springframework.ai.chat.model.ChatModel";
+
+ private static final String INTERCEPT_CALL_METHOD = "call";
+
+ private static final String INTERCEPT_STREAM_METHOD = "stream";
+
+ private static final String INTERCEPTOR_CALL_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.ChatModelCallInterceptor";
+
+ private static final String INTERCEPTOR_STREAM_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.ChatModelStreamInterceptor";
+
+ @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_CALL_METHOD).and(takesArguments(1)).and(takesArgumentWithType(0, "org.springframework.ai.chat.prompt.Prompt"));
+ }
+
+ @Override
+ public String getMethodsInterceptor() {
+ return INTERCEPTOR_CALL_CLASS;
+ }
+
+ @Override
+ public boolean isOverrideArgs() {
+ return false;
+ }
+ },
+ new InstanceMethodsInterceptPoint() {
+ @Override
+ public ElementMatcher getMethodsMatcher() {
+ return named(INTERCEPT_STREAM_METHOD).and(takesArguments(1)).and(takesArgumentWithType(0, "org.springframework.ai.chat.prompt.Prompt"));
+ }
+
+ @Override
+ public String getMethodsInterceptor() {
+ return INTERCEPTOR_STREAM_CLASS;
+ }
+
+ @Override
+ public boolean isOverrideArgs() {
+ return false;
+ }
+ }
+ };
+ }
+}
From ac028a8f309101d679a0ccb6cd55b3becd8af5ea Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Sat, 14 Feb 2026 15:13:38 +0800
Subject: [PATCH 18/48] fix
---
.../ai/v1/ChatModelCallInterceptor.java | 3 +-
.../v1/common/ChatModelMetadataResolver.java | 73 +++++--------------
.../spring/ai/v1/contant/Constants.java | 2 +
.../v1/define/ChatModelInstrumentation.java | 2 +-
.../provider/DeepSeekApiInstrumentation.java | 64 ++++++++++++++++
.../HuggingfaceChatModelInstrumentation.java | 64 ++++++++++++++++
.../provider/MiniMaxApiInstrumentation.java | 64 ++++++++++++++++
.../provider/MistralAiApiInstrumentation.java | 64 ++++++++++++++++
...ion.java => OpenAiApiInstrumentation.java} | 7 +-
.../spring/ai/v1/enums/AiProviderEnum.java | 2 +-
.../v1/provider/AnthropicApiInterceptor.java | 7 +-
.../v1/provider/DeepSeekApiInterceptor.java | 38 ++++++++++
.../HuggingfaceChatModelInterceptor.java | 37 ++++++++++
.../ai/v1/provider/MiniMaxApiInterceptor.java | 37 ++++++++++
.../v1/provider/MistralAiApiInterceptor.java | 40 ++++++++++
.../ai/v1/provider/OpenAiApiInterceptor.java | 7 +-
.../src/main/resources/skywalking-plugin.def | 8 +-
.../scenarios/spring-ai-1.x-scenario/pom.xml | 10 +--
.../httpclient/config/ChatClientConfig.java | 9 ++-
.../httpclient/controller/CaseController.java | 20 ++---
20 files changed, 468 insertions(+), 90 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/define/provider/DeepSeekApiInstrumentation.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/provider/HuggingfaceChatModelInstrumentation.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/provider/MiniMaxApiInstrumentation.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/provider/MistralAiApiInstrumentation.java
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/provider/{OpenAIApiInstrumentation.java => OpenAiApiInstrumentation.java} (87%)
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/provider/DeepSeekApiInterceptor.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/provider/HuggingfaceChatModelInterceptor.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/provider/MiniMaxApiInterceptor.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/provider/MistralAiApiInterceptor.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/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 a5dd00f604..1c853d6c5e 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
@@ -46,6 +46,7 @@ public class ChatModelCallInterceptor implements InstanceMethodsAroundIntercepto
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
AbstractSpan span = ContextManager.createExitSpan("Spring-ai/client/call", null);
+ SpanLayer.asGenAI(span);
ChatModelMetadataResolver.ApiMetadata apiMetadata = ChatModelMetadataResolver.getMetadata(objInst);
if (apiMetadata != null) {
span.setPeer(apiMetadata.getPeer());
@@ -61,7 +62,6 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr
}
span.setComponent(ComponentsDefine.SPRING_AI);
- SpanLayer.asGenAI(span);
Tags.GEN_AI_OPERATION_NAME.set(span, Constants.CHAT);
Tags.GEN_AI_REQUEST_MODEL.set(span, chatOptions.getModel());
Tags.GEN_AI_TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
@@ -163,6 +163,7 @@ private void collectPrompt(AbstractSpan span, Object[] allArguments) {
if (limit > 0 && promptText.length() > limit) {
promptText = promptText.substring(0, limit);
}
+
Tags.GEN_AI_PROMPT.set(span, promptText);
}
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/ChatModelMetadataResolver.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/ChatModelMetadataResolver.java
index e7ca118387..0eac0a6157 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/ChatModelMetadataResolver.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/ChatModelMetadataResolver.java
@@ -32,67 +32,28 @@ public class ChatModelMetadataResolver {
private static final Map MODEL_METADATA_MAP = new HashMap<>();
static {
- MODEL_METADATA_MAP.put(
- AiProviderEnum.ANTHROPIC_CLAUDE.getModelClassName(),
- new ApiMetadata(AiProviderEnum.ANTHROPIC_CLAUDE.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.OPENAI.getModelClassName(),
- new ApiMetadata(AiProviderEnum.OPENAI.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.AZURE_OPENAI.getModelClassName(),
- new ApiMetadata(AiProviderEnum.AZURE_OPENAI.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.OPENAI_SDK_OFFICIAL.getModelClassName(),
- new ApiMetadata(AiProviderEnum.OPENAI_SDK_OFFICIAL.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.AMAZON_BEDROCK_CONVERSE.getModelClassName(),
- new ApiMetadata(AiProviderEnum.AMAZON_BEDROCK_CONVERSE.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.GOOGLE_VERTEXAI_GEMINI.getModelClassName(),
- new ApiMetadata(AiProviderEnum.GOOGLE_VERTEXAI_GEMINI.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.GOOGLE_GENAI.getModelClassName(),
- new ApiMetadata(AiProviderEnum.GOOGLE_GENAI.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.DEEPSEEK_OPENAI_PROXY.getModelClassName(),
- new ApiMetadata(AiProviderEnum.DEEPSEEK_OPENAI_PROXY.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.HUGGINGFACE.getModelClassName(),
- new ApiMetadata(AiProviderEnum.HUGGINGFACE.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.MINIMAX.getModelClassName(),
- new ApiMetadata(AiProviderEnum.MINIMAX.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.MISTRAL_AI.getModelClassName(),
- new ApiMetadata(AiProviderEnum.MISTRAL_AI.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.OCI_GENAI_COHERE.getModelClassName(),
- new ApiMetadata(AiProviderEnum.OCI_GENAI_COHERE.getValue()));
-
- MODEL_METADATA_MAP.put(
- AiProviderEnum.OLLAMA.getModelClassName(),
- new ApiMetadata(AiProviderEnum.OLLAMA.getValue()));
+ for (AiProviderEnum provider : AiProviderEnum.values()) {
+ if (provider.getModelClassName() != null && provider.getValue() != null) {
+ MODEL_METADATA_MAP.put(
+ provider.getModelClassName(),
+ new ApiMetadata(provider.getValue())
+ );
+ }
+ }
+ }
- MODEL_METADATA_MAP.put(
- AiProviderEnum.ZHIPU_AI.getModelClassName(),
- new ApiMetadata(AiProviderEnum.ZHIPU_AI.getValue()));
+ public static ApiMetadata getMetadata(Object chatModelInstance) {
+ ApiMetadata metadata = MODEL_METADATA_MAP.get(chatModelInstance.getClass().getName());
+ if (metadata == null) {
+ return null;
+ }
+ return metadata;
}
- public static ApiMetadata getMetadata(Object chatModelInstance) {
+ public static ApiMetadata getMetadata(String modelClassName) {
try {
- ApiMetadata metadata = MODEL_METADATA_MAP.get(chatModelInstance.getClass().getName());
+ ApiMetadata metadata = MODEL_METADATA_MAP.get(modelClassName);
if (metadata == 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/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 7191e79dec..7348a0c11f 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
@@ -26,4 +26,6 @@ public class Constants {
public static final String CHAT = "chat";
public static final String EXECUTE_TOOL = "execute_tool";
+
+ 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/ChatModelInstrumentation.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/ChatModelInstrumentation.java
index 6d5a3a1c33..d084f6a814 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/ChatModelInstrumentation.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/ChatModelInstrumentation.java
@@ -58,7 +58,7 @@ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher getMethodsMatcher() {
- return named(INTERCEPT_CALL_METHOD).and(takesArguments(1)).and(takesArgumentWithType(0, "org.springframework.ai.chat.prompt.Prompt"));
+ return named(INTERCEPT_CALL_METHOD);
}
@Override
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/provider/DeepSeekApiInstrumentation.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/provider/DeepSeekApiInstrumentation.java
new file mode 100644
index 0000000000..07bb11e348
--- /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/provider/DeepSeekApiInstrumentation.java
@@ -0,0 +1,64 @@
+/*
+ * 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.provider;
+
+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.NameMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+public class DeepSeekApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS = "org.springframework.ai.deepseek.api.DeepSeekApi";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.DeepSeekApiInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return NameMatch.byName(ENHANCE_CLASS);
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[]{
+ new ConstructorInterceptPoint() {
+ @Override
+ public ElementMatcher getConstructorMatcher() {
+ return takesArguments(8).and(takesArgument(0, named("java.lang.String"))).and(takesArgument(3, named("java.lang.String")));
+ }
+
+ @Override
+ public String getConstructorInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+ }
+ };
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
+ return new InstanceMethodsInterceptPoint[0];
+ }
+}
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/provider/HuggingfaceChatModelInstrumentation.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/provider/HuggingfaceChatModelInstrumentation.java
new file mode 100644
index 0000000000..0ae105473f
--- /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/provider/HuggingfaceChatModelInstrumentation.java
@@ -0,0 +1,64 @@
+/*
+ * 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.provider;
+
+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.NameMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+public class HuggingfaceChatModelInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS = "org.springframework.ai.huggingface.HuggingfaceChatModel";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.HuggingfaceChatModelInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return NameMatch.byName(ENHANCE_CLASS);
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[]{
+ new ConstructorInterceptPoint() {
+ @Override
+ public ElementMatcher getConstructorMatcher() {
+ return takesArguments(2).and(takesArgument(1, named("java.lang.String")));
+ }
+
+ @Override
+ public String getConstructorInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+ }
+ };
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
+ return new InstanceMethodsInterceptPoint[0];
+ }
+}
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/provider/MiniMaxApiInstrumentation.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/provider/MiniMaxApiInstrumentation.java
new file mode 100644
index 0000000000..108588aee9
--- /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/provider/MiniMaxApiInstrumentation.java
@@ -0,0 +1,64 @@
+/*
+ * 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.provider;
+
+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.NameMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+public class MiniMaxApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS = "org.springframework.ai.minimax.api.MiniMaxApi";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.MiniMaxApiInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return NameMatch.byName(ENHANCE_CLASS);
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[]{
+ new ConstructorInterceptPoint() {
+ @Override
+ public ElementMatcher getConstructorMatcher() {
+ return takesArguments(2).and(takesArgument(0, named("java.lang.String")));
+ }
+
+ @Override
+ public String getConstructorInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+ }
+ };
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
+ return new InstanceMethodsInterceptPoint[0];
+ }
+}
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/provider/MistralAiApiInstrumentation.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/provider/MistralAiApiInstrumentation.java
new file mode 100644
index 0000000000..13ebe59826
--- /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/provider/MistralAiApiInstrumentation.java
@@ -0,0 +1,64 @@
+/*
+ * 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.provider;
+
+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.NameMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+public class MistralAiApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS = "org.springframework.ai.mistralai.api.MistralAiApi";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.MistralAiApiInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return NameMatch.byName(ENHANCE_CLASS);
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[]{
+ new ConstructorInterceptPoint() {
+ @Override
+ public ElementMatcher getConstructorMatcher() {
+ return takesArguments(5).and(takesArgument(0, named("java.lang.String")));
+ }
+
+ @Override
+ public String getConstructorInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+ }
+ };
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
+ return new InstanceMethodsInterceptPoint[0];
+ }
+}
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/provider/OpenAIApiInstrumentation.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/provider/OpenAiApiInstrumentation.java
similarity index 87%
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/define/provider/OpenAIApiInstrumentation.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/define/provider/OpenAiApiInstrumentation.java
index 84c71b82b9..d96f5f805a 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/provider/OpenAIApiInstrumentation.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/provider/OpenAiApiInstrumentation.java
@@ -28,11 +28,12 @@
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
-public class OpenAIApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+public class OpenAiApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
private static final String ENHANCE_CLASS = "org.springframework.ai.openai.api.OpenAiApi";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.OpenAIApiConstructorInterceptor";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.OpenAiApiInterceptor";
@Override
protected ClassMatch enhanceClass() {
@@ -45,7 +46,7 @@ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
new ConstructorInterceptPoint() {
@Override
public ElementMatcher getConstructorMatcher() {
- return takesArgument(0, named("java.lang.String")).and(takesArgument(3, named("java.lang.String")));
+ return takesArguments(8).and(takesArgument(0, named("java.lang.String"))).and(takesArgument(3, named("java.lang.String")));
}
@Override
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/enums/AiProviderEnum.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/enums/AiProviderEnum.java
index 4e139486c8..25a330d31a 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/enums/AiProviderEnum.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/enums/AiProviderEnum.java
@@ -30,7 +30,7 @@ public enum AiProviderEnum {
OCI_GENAI_COHERE("cohere", "org.springframework.ai.oci.cohere.OCICohereChatModel"),
- DEEPSEEK_OPENAI_PROXY("deepseek", "org.springframework.ai.deepseek.DeepSeekChatModel"),
+ DEEPSEEK("deepseek", "org.springframework.ai.deepseek.DeepSeekChatModel"),
GOOGLE_GENAI("gcp.gen_ai", "org.springframework.ai.google.genai.GoogleGenAiChatModel"),
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/provider/AnthropicApiInterceptor.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/provider/AnthropicApiInterceptor.java
index 49d9e2b708..8bd9995fbd 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/provider/AnthropicApiInterceptor.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/provider/AnthropicApiInterceptor.java
@@ -32,10 +32,7 @@ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws
return;
}
- String baseUrl = (String) allArguments[0];
- String completionsPath = (String) allArguments[1];
-
- metadata.setBaseUrl(baseUrl);
- metadata.setCompletionsPath(completionsPath);
+ metadata.setBaseUrl((String) allArguments[0]);
+ metadata.setCompletionsPath((String) allArguments[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/provider/DeepSeekApiInterceptor.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/provider/DeepSeekApiInterceptor.java
new file mode 100644
index 0000000000..e2f9c1abfb
--- /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/provider/DeepSeekApiInterceptor.java
@@ -0,0 +1,38 @@
+/*
+ * 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.provider;
+
+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.ChatModelMetadataResolver;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
+
+public class DeepSeekApiInterceptor implements InstanceConstructorInterceptor {
+
+ @Override
+ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.DEEPSEEK.getModelClassName());
+ if (metadata == null) {
+ return;
+ }
+
+ metadata.setBaseUrl((String) allArguments[0]);
+ metadata.setCompletionsPath((String) allArguments[3]);
+ }
+}
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/provider/HuggingfaceChatModelInterceptor.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/provider/HuggingfaceChatModelInterceptor.java
new file mode 100644
index 0000000000..b2e8add4b4
--- /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/provider/HuggingfaceChatModelInterceptor.java
@@ -0,0 +1,37 @@
+/*
+ * 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.provider;
+
+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.ChatModelMetadataResolver;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
+
+public class HuggingfaceChatModelInterceptor implements InstanceConstructorInterceptor {
+
+ @Override
+ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.ANTHROPIC_CLAUDE.getModelClassName());
+ if (metadata == null) {
+ return;
+ }
+
+ metadata.setBaseUrl((String) allArguments[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/provider/MiniMaxApiInterceptor.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/provider/MiniMaxApiInterceptor.java
new file mode 100644
index 0000000000..d2d722aa44
--- /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/provider/MiniMaxApiInterceptor.java
@@ -0,0 +1,37 @@
+/*
+ * 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.provider;
+
+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.ChatModelMetadataResolver;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
+
+public class MiniMaxApiInterceptor implements InstanceConstructorInterceptor {
+
+ @Override
+ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.ANTHROPIC_CLAUDE.getModelClassName());
+ if (metadata == null) {
+ return;
+ }
+
+ metadata.setBaseUrl((String) allArguments[0]);
+ }
+}
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/provider/MistralAiApiInterceptor.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/provider/MistralAiApiInterceptor.java
new file mode 100644
index 0000000000..d951060ff6
--- /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/provider/MistralAiApiInterceptor.java
@@ -0,0 +1,40 @@
+/*
+ * 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.provider;
+
+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.ChatModelMetadataResolver;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
+
+import static org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants.DEFAULT_COMPLETIONS_PATH;
+
+public class MistralAiApiInterceptor implements InstanceConstructorInterceptor {
+
+ @Override
+ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.ANTHROPIC_CLAUDE.getModelClassName());
+ if (metadata == null) {
+ return;
+ }
+
+ metadata.setBaseUrl((String) allArguments[0]);
+ metadata.setCompletionsPath(DEFAULT_COMPLETIONS_PATH);
+ }
+}
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/provider/OpenAiApiInterceptor.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/provider/OpenAiApiInterceptor.java
index 42e7932055..9801f8aa0b 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/provider/OpenAiApiInterceptor.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/provider/OpenAiApiInterceptor.java
@@ -32,10 +32,7 @@ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws
return;
}
- String baseUrl = (String) allArguments[0];
- String completionsPath = (String) allArguments[3];
-
- metadata.setBaseUrl(baseUrl);
- metadata.setCompletionsPath(completionsPath);
+ metadata.setBaseUrl((String) allArguments[0]);
+ metadata.setCompletionsPath((String) allArguments[3]);
}
}
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 462c07ad68..033ef952d3 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
@@ -18,4 +18,10 @@ spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.ChatModelInst
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.VectorStoreRetrieverInstrumentation
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.provider.OpenAIApiInstrumentation
+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.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
+spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.MiniMaxApiInstrumentation
+spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.MistralAiApiInstrumentation
+spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.OpenAiApiInstrumentation
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 94052d033c..6210a6adb1 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
@@ -50,14 +50,14 @@
spring-ai-client-chat
-
-
-
-
org.springframework.ai
- spring-ai-starter-model-anthropic
+ spring-ai-starter-model-openai
+
+
+
+
org.springframework.ai
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 30bb61c8bb..bff8f01f9c 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,8 +17,8 @@
package test.apache.skywalking.apm.testcase.jdk.httpclient.config;
-import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -26,7 +26,12 @@
public class ChatClientConfig {
@Bean
- public ChatClient openAIChatClient(AnthropicChatModel model) {
+ public ChatClient openAIChatClient(OpenAiChatModel model) {
return ChatClient.create(model);
}
+
+// @Bean
+// public ChatClient anthropicChatClient(AnthropicChatModel model) {
+// return ChatClient.create(model);
+// }
}
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 441b07d075..21f4cde3bb 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
@@ -47,21 +47,21 @@ public String testCase() throws Exception {
Do not use outside knowledge. Be concise.
""";
- chatClient
+ System.out.println(chatClient
.prompt("What's the weather in New York?")
.system(systemPrompt)
.tools(weatherTool)
.call()
- .content();
+ .content());
- chatClient
- .prompt("What's the weather in New York?")
- .system(systemPrompt)
- .tools(weatherTool)
- .stream()
- .content()
- .doOnNext(System.out::println)
- .blockLast();
+// chatClient
+// .prompt("What's the weather in New York?")
+// .system(systemPrompt)
+// .tools(weatherTool)
+// .stream()
+// .content()
+// .doOnNext(System.out::println)
+// .blockLast();
// chatClient
// .prompt("What is Spring AI?")
From 332be222516270b32578758a8a4577b790b6979f Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Tue, 17 Feb 2026 16:35:53 +0800
Subject: [PATCH 19/48] fix
---
.../ai/v1/ChatModelCallInterceptor.java | 21 +-
.../ai/v1/ChatModelStreamInterceptor.java | 13 +-
.../DefaultToolCallingManagerInterceptor.java | 2 +-
.../ai/v1/ToolCallbackCallInterceptor.java | 4 +-
.../provider/AnthropicApiInstrumentation.java | 2 +-
.../provider/DeepSeekApiInstrumentation.java | 2 +-
.../HuggingfaceChatModelInstrumentation.java | 2 +-
.../provider/MiniMaxApiInstrumentation.java | 4 +-
.../provider/MistralAiApiInstrumentation.java | 2 +-
.../provider/OllamaApiInstrumentation.java | 64 +++++
.../provider/OpenAiApiInstrumentation.java | 2 +-
...> AnthropicApiConstructorInterceptor.java} | 2 +-
...=> DeepSeekApiConstructorInterceptor.java} | 2 +-
...gfaceChatModelConstructorInterceptor.java} | 4 +-
.../MiniMaxApiConstructorInterceptor.java | 38 +++
...> MistralAiApiConstructorInterceptor.java} | 4 +-
...a => OllamaApiConstructorInterceptor.java} | 5 +-
...a => OpenAiApiConstructorInterceptor.java} | 2 +-
.../src/main/resources/skywalking-plugin.def | 1 +
.../scenarios/spring-ai-1.x-scenario/pom.xml | 23 +-
.../httpclient/config/ChatClientConfig.java | 15 +-
.../httpclient/controller/CaseController.java | 24 +-
.../controller/LLMMockController.java | 242 ++++++++++--------
23 files changed, 319 insertions(+), 161 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/define/provider/OllamaApiInstrumentation.java
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/provider/{AnthropicApiInterceptor.java => AnthropicApiConstructorInterceptor.java} (94%)
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/provider/{DeepSeekApiInterceptor.java => DeepSeekApiConstructorInterceptor.java} (94%)
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/provider/{HuggingfaceChatModelInterceptor.java => HuggingfaceChatModelConstructorInterceptor.java} (89%)
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/provider/MiniMaxApiConstructorInterceptor.java
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/provider/{MistralAiApiInterceptor.java => MistralAiApiConstructorInterceptor.java} (90%)
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/provider/{MiniMaxApiInterceptor.java => OllamaApiConstructorInterceptor.java} (87%)
rename apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/provider/{OpenAiApiInterceptor.java => OpenAiApiConstructorInterceptor.java} (94%)
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 1c853d6c5e..4702effeef 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
@@ -45,15 +45,18 @@ public class ChatModelCallInterceptor implements InstanceMethodsAroundIntercepto
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
- AbstractSpan span = ContextManager.createExitSpan("Spring-ai/client/call", null);
- SpanLayer.asGenAI(span);
ChatModelMetadataResolver.ApiMetadata apiMetadata = ChatModelMetadataResolver.getMetadata(objInst);
+ String providerName = AiProviderEnum.UNKNOW.getValue();
+ String peer = null;
+
if (apiMetadata != null) {
- span.setPeer(apiMetadata.getPeer());
- Tags.GEN_AI_PROVIDER_NAME.set(span, apiMetadata.getProviderName());
- } else {
- Tags.GEN_AI_PROVIDER_NAME.set(span, AiProviderEnum.UNKNOW.getValue());
+ if (apiMetadata.getProviderName() != null) {
+ providerName = apiMetadata.getProviderName();
+ }
+ peer = apiMetadata.getPeer();
}
+ AbstractSpan span = ContextManager.createExitSpan("Spring-ai/" + providerName + "/call", peer);
+ SpanLayer.asGenAI(span);
Prompt prompt = (Prompt) allArguments[0];
ChatOptions chatOptions = prompt.getOptions();
@@ -164,7 +167,9 @@ private void collectPrompt(AbstractSpan span, Object[] allArguments) {
promptText = promptText.substring(0, limit);
}
- Tags.GEN_AI_PROMPT.set(span, promptText);
+
+
+ Tags.GEN_AI_INPUT_MESSAGES.set(span, promptText);
}
private void collectCompletion(AbstractSpan span, Generation generation) {
@@ -181,6 +186,6 @@ private void collectCompletion(AbstractSpan span, Generation generation) {
if (limit > 0 && completionText.length() > limit) {
completionText = completionText.substring(0, limit);
}
- Tags.GEN_AI_COMPLETION.set(span, completionText);
+ Tags.GEN_AI_OUTPUT_MESSAGES.set(span, completionText);
}
}
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 2858f75fa1..dd8e748bba 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
@@ -30,6 +30,7 @@
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
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.enums.AiProviderEnum;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
@@ -47,12 +48,18 @@ public class ChatModelStreamInterceptor implements InstanceMethodsAroundIntercep
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
- AbstractSpan span = ContextManager.createExitSpan("Spring-ai/client/stream", null);
ChatModelMetadataResolver.ApiMetadata apiMetadata = ChatModelMetadataResolver.getMetadata(objInst);
+ String providerName = AiProviderEnum.UNKNOW.getValue();
+ String peer = null;
+
if (apiMetadata != null) {
- span.setPeer(apiMetadata.getPeer());
- Tags.GEN_AI_PROVIDER_NAME.set(span, apiMetadata.getProviderName());
+ if (apiMetadata.getProviderName() != null) {
+ providerName = apiMetadata.getProviderName();
+ }
+ peer = apiMetadata.getPeer();
}
+ AbstractSpan span = ContextManager.createExitSpan("Spring-ai/" + providerName + "/stream", peer);
+ SpanLayer.asGenAI(span);
span.setComponent(ComponentsDefine.SPRING_AI);
SpanLayer.asGenAI(span);
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/DefaultToolCallingManagerInterceptor.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/DefaultToolCallingManagerInterceptor.java
index fdf411e9ef..929fd9bbd8 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/DefaultToolCallingManagerInterceptor.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/DefaultToolCallingManagerInterceptor.java
@@ -35,7 +35,7 @@ public class DefaultToolCallingManagerInterceptor implements InstanceMethodsArou
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
- AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/client/executeTool");
+ AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/tool/call");
span.setComponent(ComponentsDefine.SPRING_AI);
ContextView reactorCtx = ToolCallReactiveContextHolder.getContext();
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/ToolCallbackCallInterceptor.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/ToolCallbackCallInterceptor.java
index 30835c1148..abcafa4b55 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/ToolCallbackCallInterceptor.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/ToolCallbackCallInterceptor.java
@@ -27,6 +27,7 @@
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
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.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
@@ -42,11 +43,12 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr
String toolName = definition.name();
- AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/tool/" + toolName);
+ AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/tool/execute/" + toolName);
span.setComponent(ComponentsDefine.SPRING_AI);
SpanLayer.asGenAI(span);
Tags.GEN_AI_TOOL_NAME.set(span, toolName);
+ Tags.GEN_AI_OPERATION_NAME.set(span, Constants.EXECUTE_TOOL);
if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_TOOL_INPUT) {
String toolInput = (String) allArguments[0];
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/provider/AnthropicApiInstrumentation.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/provider/AnthropicApiInstrumentation.java
index 0ae41f18f4..e226864906 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/provider/AnthropicApiInstrumentation.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/provider/AnthropicApiInstrumentation.java
@@ -33,7 +33,7 @@
public class AnthropicApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
private static final String ENHANCE_CLASS = "org.springframework.ai.anthropic.api.AnthropicApi";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.AnthropicApiConstructorInterceptor";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.AnthropicApiConstructorInterceptor";
@Override
protected ClassMatch enhanceClass() {
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/provider/DeepSeekApiInstrumentation.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/provider/DeepSeekApiInstrumentation.java
index 07bb11e348..17382e75eb 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/provider/DeepSeekApiInstrumentation.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/provider/DeepSeekApiInstrumentation.java
@@ -33,7 +33,7 @@
public class DeepSeekApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
private static final String ENHANCE_CLASS = "org.springframework.ai.deepseek.api.DeepSeekApi";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.DeepSeekApiInterceptor";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.DeepSeekApiConstructorInterceptor";
@Override
protected ClassMatch enhanceClass() {
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/provider/HuggingfaceChatModelInstrumentation.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/provider/HuggingfaceChatModelInstrumentation.java
index 0ae105473f..93f7b57af4 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/provider/HuggingfaceChatModelInstrumentation.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/provider/HuggingfaceChatModelInstrumentation.java
@@ -33,7 +33,7 @@
public class HuggingfaceChatModelInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
private static final String ENHANCE_CLASS = "org.springframework.ai.huggingface.HuggingfaceChatModel";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.HuggingfaceChatModelInterceptor";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.HuggingfaceChatModelConstructorInterceptor";
@Override
protected ClassMatch enhanceClass() {
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/provider/MiniMaxApiInstrumentation.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/provider/MiniMaxApiInstrumentation.java
index 108588aee9..c8a5ca3ad1 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/provider/MiniMaxApiInstrumentation.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/provider/MiniMaxApiInstrumentation.java
@@ -33,7 +33,7 @@
public class MiniMaxApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
private static final String ENHANCE_CLASS = "org.springframework.ai.minimax.api.MiniMaxApi";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.MiniMaxApiInterceptor";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.MiniMaxApiConstructorInterceptor";
@Override
protected ClassMatch enhanceClass() {
@@ -46,7 +46,7 @@ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
new ConstructorInterceptPoint() {
@Override
public ElementMatcher getConstructorMatcher() {
- return takesArguments(2).and(takesArgument(0, named("java.lang.String")));
+ return takesArguments(4).and(takesArgument(0, named("java.lang.String")));
}
@Override
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/provider/MistralAiApiInstrumentation.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/provider/MistralAiApiInstrumentation.java
index 13ebe59826..ffcaee9198 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/provider/MistralAiApiInstrumentation.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/provider/MistralAiApiInstrumentation.java
@@ -33,7 +33,7 @@
public class MistralAiApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
private static final String ENHANCE_CLASS = "org.springframework.ai.mistralai.api.MistralAiApi";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.MistralAiApiInterceptor";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.MistralAiApiConstructorInterceptor";
@Override
protected ClassMatch enhanceClass() {
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/provider/OllamaApiInstrumentation.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/provider/OllamaApiInstrumentation.java
new file mode 100644
index 0000000000..7e8fd5ebd8
--- /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/provider/OllamaApiInstrumentation.java
@@ -0,0 +1,64 @@
+/*
+ * 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.provider;
+
+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.NameMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+public class OllamaApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS = "org.springframework.ai.ollama.api.OllamaApi";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.OllamaApiConstructorInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return NameMatch.byName(ENHANCE_CLASS);
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[]{
+ new ConstructorInterceptPoint() {
+ @Override
+ public ElementMatcher getConstructorMatcher() {
+ return takesArguments(4).and(takesArgument(0, named("java.lang.String")));
+ }
+
+ @Override
+ public String getConstructorInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+ }
+ };
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
+ return new InstanceMethodsInterceptPoint[0];
+ }
+}
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/provider/OpenAiApiInstrumentation.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/provider/OpenAiApiInstrumentation.java
index d96f5f805a..624262322e 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/provider/OpenAiApiInstrumentation.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/provider/OpenAiApiInstrumentation.java
@@ -33,7 +33,7 @@
public class OpenAiApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
private static final String ENHANCE_CLASS = "org.springframework.ai.openai.api.OpenAiApi";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.OpenAiApiInterceptor";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.OpenAiApiConstructorInterceptor";
@Override
protected ClassMatch enhanceClass() {
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/provider/AnthropicApiInterceptor.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/provider/AnthropicApiConstructorInterceptor.java
similarity index 94%
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/provider/AnthropicApiInterceptor.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/provider/AnthropicApiConstructorInterceptor.java
index 8bd9995fbd..4f9e73ca7e 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/provider/AnthropicApiInterceptor.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/provider/AnthropicApiConstructorInterceptor.java
@@ -23,7 +23,7 @@
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
-public class AnthropicApiInterceptor implements InstanceConstructorInterceptor {
+public class AnthropicApiConstructorInterceptor implements InstanceConstructorInterceptor {
@Override
public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws 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/provider/DeepSeekApiInterceptor.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/provider/DeepSeekApiConstructorInterceptor.java
similarity index 94%
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/provider/DeepSeekApiInterceptor.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/provider/DeepSeekApiConstructorInterceptor.java
index e2f9c1abfb..e1f26e5e8b 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/provider/DeepSeekApiInterceptor.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/provider/DeepSeekApiConstructorInterceptor.java
@@ -23,7 +23,7 @@
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
-public class DeepSeekApiInterceptor implements InstanceConstructorInterceptor {
+public class DeepSeekApiConstructorInterceptor implements InstanceConstructorInterceptor {
@Override
public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws 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/provider/HuggingfaceChatModelInterceptor.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/provider/HuggingfaceChatModelConstructorInterceptor.java
similarity index 89%
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/provider/HuggingfaceChatModelInterceptor.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/provider/HuggingfaceChatModelConstructorInterceptor.java
index b2e8add4b4..234b876434 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/provider/HuggingfaceChatModelInterceptor.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/provider/HuggingfaceChatModelConstructorInterceptor.java
@@ -23,11 +23,11 @@
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
-public class HuggingfaceChatModelInterceptor implements InstanceConstructorInterceptor {
+public class HuggingfaceChatModelConstructorInterceptor implements InstanceConstructorInterceptor {
@Override
public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
- ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.ANTHROPIC_CLAUDE.getModelClassName());
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.HUGGINGFACE.getModelClassName());
if (metadata == null) {
return;
}
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/provider/MiniMaxApiConstructorInterceptor.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/provider/MiniMaxApiConstructorInterceptor.java
new file mode 100644
index 0000000000..939e440ae1
--- /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/provider/MiniMaxApiConstructorInterceptor.java
@@ -0,0 +1,38 @@
+/*
+ * 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.provider;
+
+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.ChatModelMetadataResolver;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
+
+public class MiniMaxApiConstructorInterceptor implements InstanceConstructorInterceptor {
+
+ @Override
+ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.MINIMAX.getModelClassName());
+ if (metadata == null) {
+ return;
+ }
+
+ metadata.setBaseUrl((String) allArguments[0]);
+ metadata.setCompletionsPath("/v1/text/chatcompletion_v2");
+ }
+}
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/provider/MistralAiApiInterceptor.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/provider/MistralAiApiConstructorInterceptor.java
similarity index 90%
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/provider/MistralAiApiInterceptor.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/provider/MistralAiApiConstructorInterceptor.java
index d951060ff6..07eac44d5b 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/provider/MistralAiApiInterceptor.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/provider/MistralAiApiConstructorInterceptor.java
@@ -25,11 +25,11 @@
import static org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants.DEFAULT_COMPLETIONS_PATH;
-public class MistralAiApiInterceptor implements InstanceConstructorInterceptor {
+public class MistralAiApiConstructorInterceptor implements InstanceConstructorInterceptor {
@Override
public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
- ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.ANTHROPIC_CLAUDE.getModelClassName());
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.MISTRAL_AI.getModelClassName());
if (metadata == null) {
return;
}
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/provider/MiniMaxApiInterceptor.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/provider/OllamaApiConstructorInterceptor.java
similarity index 87%
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/provider/MiniMaxApiInterceptor.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/provider/OllamaApiConstructorInterceptor.java
index d2d722aa44..abdc2d9f5a 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/provider/MiniMaxApiInterceptor.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/provider/OllamaApiConstructorInterceptor.java
@@ -23,15 +23,16 @@
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
-public class MiniMaxApiInterceptor implements InstanceConstructorInterceptor {
+public class OllamaApiConstructorInterceptor implements InstanceConstructorInterceptor {
@Override
public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
- ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.ANTHROPIC_CLAUDE.getModelClassName());
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.OLLAMA.getModelClassName());
if (metadata == null) {
return;
}
metadata.setBaseUrl((String) allArguments[0]);
+ metadata.setCompletionsPath("/api/chat");
}
}
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/provider/OpenAiApiInterceptor.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/provider/OpenAiApiConstructorInterceptor.java
similarity index 94%
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/provider/OpenAiApiInterceptor.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/provider/OpenAiApiConstructorInterceptor.java
index 9801f8aa0b..da0f3bbf2f 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/provider/OpenAiApiInterceptor.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/provider/OpenAiApiConstructorInterceptor.java
@@ -23,7 +23,7 @@
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
-public class OpenAiApiInterceptor implements InstanceConstructorInterceptor {
+public class OpenAiApiConstructorInterceptor implements InstanceConstructorInterceptor {
@Override
public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
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 033ef952d3..43a015aedb 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
@@ -24,4 +24,5 @@ spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.Deep
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.HuggingfaceChatModelInstrumentation
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.MiniMaxApiInstrumentation
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.MistralAiApiInstrumentation
+spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.OllamaApiInstrumentation
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.OpenAiApiInstrumentation
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 6210a6adb1..9e6ec15daa 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
@@ -54,10 +54,25 @@
org.springframework.ai
spring-ai-starter-model-openai
-
-
-
-
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-starter-model-minimax
+
+
+
+ org.springframework.ai
+ spring-ai-starter-model-mistral-ai
+
+
+
+ org.springframework.ai
+ spring-ai-starter-model-ollama
+
org.springframework.ai
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 bff8f01f9c..16e6b7f5a1 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
@@ -18,6 +18,9 @@
package test.apache.skywalking.apm.testcase.jdk.httpclient.config;
import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.minimax.MiniMaxChatModel;
+import org.springframework.ai.mistralai.MistralAiChatModel;
+import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -31,7 +34,17 @@ public ChatClient openAIChatClient(OpenAiChatModel model) {
}
// @Bean
-// public ChatClient anthropicChatClient(AnthropicChatModel model) {
+// public ChatClient anthropicChatClient(MiniMaxChatModel model) {
+// return ChatClient.create(model);
+// }
+
+// @Bean
+// public ChatClient anthropicChatClient(MistralAiChatModel model) {
+// return ChatClient.create(model);
+// }
+
+// @Bean
+// public ChatClient anthropicChatClient(OllamaChatModel model) {
// return ChatClient.create(model);
// }
}
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 21f4cde3bb..d939c16844 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
@@ -54,22 +54,14 @@ public String testCase() throws Exception {
.call()
.content());
-// chatClient
-// .prompt("What's the weather in New York?")
-// .system(systemPrompt)
-// .tools(weatherTool)
-// .stream()
-// .content()
-// .doOnNext(System.out::println)
-// .blockLast();
-
-// chatClient
-// .prompt("What is Spring AI?")
-// .system(systemPrompt)
-// .stream()
-// .content()
-// .doOnNext(System.out::println)
-// .blockLast();
+ chatClient
+ .prompt("What's the weather in New York?")
+ .system(systemPrompt)
+ .tools(weatherTool)
+ .stream()
+ .content()
+ .doOnNext(System.out::println)
+ .blockLast();
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 70591908d9..864d5b2ea4 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
@@ -20,35 +20,114 @@
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
+import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.util.UUID;
+
@RestController
@RequestMapping("/llm")
public class LLMMockController {
-
@RequestMapping("/v1/chat/completions")
- public JSONObject completions(@RequestBody JSONObject request) {
+ public Object completions(@RequestBody JSONObject request, HttpServletResponse response) throws IOException {
+ Boolean isStream = request.getBoolean("stream");
+ if (isStream == null) isStream = false;
JSONArray messages = request.getJSONArray("messages");
-
JSONObject lastMessage = messages.getJSONObject(messages.size() - 1);
String lastRole = lastMessage.getString("role");
+ if (isStream) {
+ response.setContentType("text/event-stream");
+ response.setCharacterEncoding("UTF-8");
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("Connection", "keep-alive");
+
+ PrintWriter writer = response.getWriter();
+ String id = "chatcmpl-" + UUID.randomUUID();
+ long created = Instant.now().getEpochSecond();
+ String model = "gpt-4.1-2025-04-14";
+
+ try {
+ if ("tool".equals(lastRole)) {
+ String fullContent = "The weather in New York is currently sunny with a temperature of 10°C.";
+ writeStreamChunk(writer, id, created, model, "{\"role\":\"assistant\"}", "null");
+
+ int len = fullContent.length();
+ String[] parts = {
+ fullContent.substring(0, len / 3),
+ fullContent.substring(len / 3, len * 2 / 3),
+ fullContent.substring(len * 2 / 3)
+ };
+
+ for (String part : parts) {
+ Thread.sleep(50);
+ writeStreamChunk(writer, id, created, model, "{\"content\":\"" + escapeJson(part) + "\"}", "null");
+ }
+
+ writeStreamChunk(writer, id, created, model, "{}", "\"stop\"");
+ } else {
+ writeStreamChunk(writer, id, created, model, "{\"role\":\"assistant\"}", "null");
+
+ String toolCallDelta = """
+ {
+ "tool_calls": [
+ {
+ "index": 0,
+ "id": "call_iV4bvFIZujbb",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": ""
+ }
+ }
+ ]
+ }
+ """;
+ writeStreamChunk(writer, id, created, model, toolCallDelta, "null");
+
+ String args = "{\\\"arg0\\\":\\\"new york\\\"}";
+ String argsDelta = """
+ {
+ "tool_calls": [
+ {
+ "index": 0,
+ "function": {
+ "arguments": "%s"
+ }
+ }
+ ]
+ }
+ """.formatted(args);
+ Thread.sleep(50);
+ writeStreamChunk(writer, id, created, model, argsDelta, "null");
+
+ writeStreamChunk(writer, id, created, model, "{}", "\"tool_calls\"");
+ }
+
+ writer.write("data: [DONE]\n\n");
+ writer.flush();
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return null;
+ }
+
String toolCallResponse = """
{
"choices": [
{
- "content_filter_results": {},
"finish_reason": "tool_calls",
"index": 0,
- "logprobs": null,
"message": {
- "annotations": [],
- "content": null,
- "refusal": null,
"role": "assistant",
+ "content": null,
"tool_calls": [
{
"function": {
@@ -63,116 +142,27 @@ public JSONObject completions(@RequestBody JSONObject request) {
}
],
"created": 1768490813,
- "id": "chatcmpl-CyJXJt7gxwDgz7Uj",
"model": "gpt-4.1-2025-04-14",
- "object": "chat.completion",
- "prompt_filter_results": [
- {
- "prompt_index": 0,
- "content_filter_results": {
- "hate": { "filtered": false, "severity": "safe" },
- "self_harm": { "filtered": false, "severity": "safe" },
- "sexual": { "filtered": false, "severity": "safe" },
- "violence": { "filtered": false, "severity": "safe" }
- }
- }
- ],
- "system_fingerprint": "fp_b9041e1",
- "usage": {
- "completion_tokens": 17,
- "completion_tokens_details": {
- "accepted_prediction_tokens": 0,
- "audio_tokens": 0,
- "reasoning_tokens": 0,
- "rejected_prediction_tokens": 0
- },
- "prompt_tokens": 52,
- "prompt_tokens_details": {
- "audio_tokens": 0,
- "cached_tokens": 0
- },
- "total_tokens": 69
- }
+ "object": "chat.completion"
}
""";
String finalResponse = """
+ {
+ "choices": [
{
- "choices": [
- {
- "content_filter_results": {
- "hate": {
- "filtered": false,
- "severity": "safe"
- },
- "self_harm": {
- "filtered": false,
- "severity": "safe"
- },
- "sexual": {
- "filtered": false,
- "severity": "safe"
- },
- "violence": {
- "filtered": false,
- "severity": "safe"
- }
- },
- "finish_reason": "stop",
- "index": 0,
- "logprobs": null,
- "message": {
- "annotations": [],
- "content": "The weather in New York is currently sunny with a temperature of 10°C.",
- "refusal": null,
- "role": "assistant"
- }
- }
- ],
- "created": 1768491057,
- "id": "chatcmpl-CyJbFkZsyhOyHW2Otyc",
- "model": "gpt-4.1-2025-04-14",
- "object": "chat.completion",
- "prompt_filter_results": [
- {
- "prompt_index": 0,
- "content_filter_results": {
- "hate": {
- "filtered": false,
- "severity": "safe"
- },
- "self_harm": {
- "filtered": false,
- "severity": "safe"
- },
- "sexual": {
- "filtered": false,
- "severity": "safe"
- },
- "violence": {
- "filtered": false,
- "severity": "safe"
- }
- }
- }
- ],
- "system_fingerprint": "fp_b9041e1",
- "usage": {
- "completion_tokens": 17,
- "completion_tokens_details": {
- "accepted_prediction_tokens": 0,
- "audio_tokens": 0,
- "reasoning_tokens": 0,
- "rejected_prediction_tokens": 0
- },
- "prompt_tokens": 83,
- "prompt_tokens_details": {
- "audio_tokens": 0,
- "cached_tokens": 0
- },
- "total_tokens": 100
+ "finish_reason": "stop",
+ "index": 0,
+ "message": {
+ "content": "The weather in New York is currently sunny with a temperature of 10°C.",
+ "role": "assistant"
}
- }
+ }
+ ],
+ "created": 1768491057,
+ "model": "gpt-4.1-2025-04-14",
+ "object": "chat.completion"
+ }
""";
if ("tool".equals(lastRole)) {
@@ -181,4 +171,34 @@ public JSONObject completions(@RequestBody JSONObject request) {
return JSON.parseObject(toolCallResponse);
}
+
+ private void writeStreamChunk(PrintWriter writer, String id, long created, String model, String delta, String finishReason) {
+ String json = """
+ {
+ "choices": [
+ {
+ "delta": %s,
+ "finish_reason": %s,
+ "index": 0,
+ "logprobs": null
+ }
+ ],
+ "object": "chat.completion.chunk",
+ "usage": null,
+ "created": %d,
+ "system_fingerprint": null,
+ "model": "%s",
+ "id": "%s"
+ }
+ """.formatted(delta, finishReason, created, model, id);
+
+ String cleanJson = json.replace("\n", "").replace("\r", "");
+ writer.write("data: " + cleanJson + "\n\n");
+ writer.flush();
+ }
+
+ private String escapeJson(String input) {
+ if (input == null) return "";
+ return input.replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
+ }
}
From 5f9a95082ada56b8c47483839ebf251dc21dddc5 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Wed, 18 Feb 2026 16:45:32 +0800
Subject: [PATCH 20/48] fix
---
.../apm/agent/core/context/tag/Tags.java | 53 ++--
.../ai/v1/ChatModelCallInterceptor.java | 28 ++-
.../ai/v1/ChatModelStreamInterceptor.java | 236 +++++++++++-------
.../provider/ZhiPuAiApiInstrumentation.java | 64 +++++
.../ZhiPuAiApiConstructorInterceptor.java | 38 +++
.../src/main/resources/skywalking-plugin.def | 1 +
.../spring-ai-1.x-scenario/bin/startup.sh | 2 +-
.../scenarios/spring-ai-1.x-scenario/pom.xml | 23 --
.../httpclient/config/ChatClientConfig.java | 18 --
.../src/main/resources/application.yaml | 3 +-
10 files changed, 288 insertions(+), 178 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/define/provider/ZhiPuAiApiInstrumentation.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/provider/ZhiPuAiApiConstructorInterceptor.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 c170d0218a..074dffdb57 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
@@ -163,112 +163,97 @@ public static final class HTTP {
/**
* GEN_AI_OPERATION_NAME represents the name of the operation being performed
*/
- public static final StringTag GEN_AI_OPERATION_NAME = new StringTag(44, "gen_ai.operation.name");
+ public static final StringTag GEN_AI_OPERATION_NAME = new StringTag(25, "gen_ai.operation.name");
/**
* GEN_AI_PROVIDER_NAME represents the Generative AI provider as identified by the client or server instrumentation.
*/
- public static final StringTag GEN_AI_PROVIDER_NAME = new StringTag(46, "gen_ai.provider.name");
+ public static final StringTag GEN_AI_PROVIDER_NAME = new StringTag(26, "gen_ai.provider.name");
/**
* GEN_AI_REQUEST_MODEL represents the name of the GenAI model a request is being made to.
*/
- public static final StringTag GEN_AI_REQUEST_MODEL = new StringTag(25, "gen_ai.request.model");
+ public static final StringTag GEN_AI_REQUEST_MODEL = new StringTag(27, "gen_ai.request.model");
/**
* GEN_AI_TOP_K represents the top_k sampling setting for the GenAI request.
*/
- public static final StringTag GEN_AI_TOP_K = new StringTag(26, "gen_ai.request.top_k");
+ public static final StringTag GEN_AI_TOP_K = new StringTag(28, "gen_ai.request.top_k");
/**
* GEN_AI_TOP_P represents the top_p sampling setting for the GenAI request.
*/
- public static final StringTag GEN_AI_TOP_P = new StringTag(27, "gen_ai.request.top_p");
+ public static final StringTag GEN_AI_TOP_P = new StringTag(29, "gen_ai.request.top_p");
/**
* GEN_AI_TEMPERATURE represents the temperature setting for the GenAI request.
*/
- public static final StringTag GEN_AI_TEMPERATURE = new StringTag(28, "gen_ai.request.temperature");
+ public static final StringTag GEN_AI_TEMPERATURE = new StringTag(30, "gen_ai.request.temperature");
/**
* GEN_AI_TOOL_NAME represents the name of the tool utilized by the agent.
*/
- public static final StringTag GEN_AI_TOOL_NAME = new StringTag(29, "gen_ai.tool.name");
+ public static final StringTag GEN_AI_TOOL_NAME = new StringTag(31, "gen_ai.tool.name");
/**
* GEN_AI_TOOL_CALL_ARGUMENTS represents the parameters passed to the tool call.
*/
- public static final StringTag GEN_AI_TOOL_CALL_ARGUMENTS = new StringTag(30, "gen_ai.tool.call.arguments");
+ public static final StringTag GEN_AI_TOOL_CALL_ARGUMENTS = new StringTag(32, "gen_ai.tool.call.arguments");
/**
* GEN_AI_TOOL_CALL_RESULT represents the result returned by the tool call (if any and if execution was successful).
*/
- public static final StringTag GEN_AI_TOOL_CALL_RESULT = new StringTag(43, "gen_ai.tool.call.result");
+ public static final StringTag GEN_AI_TOOL_CALL_RESULT = new StringTag(33, "gen_ai.tool.call.result");
/**
* GEN_AI_RESPONSE_MODEL represents the name of the model that generated the response.
*/
- public static final StringTag GEN_AI_RESPONSE_MODEL = new StringTag(31, "gen_ai.response.model");
+ public static final StringTag GEN_AI_RESPONSE_MODEL = new StringTag(34, "gen_ai.response.model");
/**
* GEN_AI_RESPONSE_ID represents the unique identifier for the completion.
*/
- public static final StringTag GEN_AI_RESPONSE_ID = new StringTag(32, "gen_ai.response.id");
+ public static final StringTag GEN_AI_RESPONSE_ID = new StringTag(35, "gen_ai.response.id");
/**
* GEN_AI_USAGE_INPUT_TOKENS represents the number of tokens used in the GenAI input (prompt).
*/
- public static final StringTag GEN_AI_USAGE_INPUT_TOKENS = new StringTag(33, "gen_ai.usage.input_tokens");
+ public static final StringTag GEN_AI_USAGE_INPUT_TOKENS = new StringTag(36, "gen_ai.usage.input_tokens");
/**
* GEN_AI_USAGE_OUTPUT_TOKENS represents the number of tokens used in the GenAI response (completion).
*/
- public static final StringTag GEN_AI_USAGE_OUTPUT_TOKENS = new StringTag(34, "gen_ai.usage.output_tokens");
+ public static final StringTag GEN_AI_USAGE_OUTPUT_TOKENS = new StringTag(37, "gen_ai.usage.output_tokens");
/**
* GEN_AI_USAGE_TOTAL_TOKENS represents the total number of tokens used in the GenAI exchange.
*/
- public static final StringTag GEN_AI_CLIENT_TOKEN_USAGE = new StringTag(35, "gen_ai.client.token.usage");
+ public static final StringTag GEN_AI_CLIENT_TOKEN_USAGE = new StringTag(38, "gen_ai.client.token.usage");
/**
* GEN_AI_RESPONSE_FINISH_REASONS represents the array of reasons the model stopped generating tokens.
*/
- public static final StringTag GEN_AI_RESPONSE_FINISH_REASONS = new StringTag(36, "gen_ai.response.finish_reasons");
-
- /**
- * GEN_AI_PROMPT represents the full prompt text or messages provided to the model.
- */
- public static final StringTag GEN_AI_PROMPT = new StringTag(37, "gen_ai.prompt");
-
- /**
- * GEN_AI_COMPLETION represents the full completion text or messages returned by the model.
- */
- public static final StringTag GEN_AI_COMPLETION = new StringTag(38, "gen_ai.completion");
+ public static final StringTag GEN_AI_RESPONSE_FINISH_REASONS = new StringTag(39, "gen_ai.response.finish_reasons");
/**
* GEN_AI_STREAM_TTFR represents the time to first response (TTFR) for streaming operations.
*/
- public static final StringTag GEN_AI_STREAM_TTFR = new StringTag(39, "gen_ai.stream.ttfr");
+ public static final StringTag GEN_AI_STREAM_TTFR = new StringTag(40, "gen_ai.stream.ttfr");
/**
* GEN_AI_VECTOR_STORE_TOP_K represents the target number of top results to return from the vector store.
*/
- public static final StringTag GEN_AI_VECTOR_STORE_TOP_K = new StringTag(40, "gen_ai.vector_store.top_k");
+ public static final StringTag GEN_AI_VECTOR_STORE_TOP_K = new StringTag(41, "gen_ai.vector_store.top_k");
/**
* GEN_AI_VECTOR_STORE_FILTER_EXPRESSION represents the filter expression used in the vector store search.
*/
- public static final StringTag GEN_AI_VECTOR_STORE_FILTER_EXPRESSION = new StringTag(41, "gen_ai.vector_store.filter_expression");
+ public static final StringTag GEN_AI_VECTOR_STORE_FILTER_EXPRESSION = new StringTag(42, "gen_ai.vector_store.filter_expression");
/**
* GEN_AI_VECTOR_STORE_RECORD_IDS represents the unique identifiers of the records retrieved from the vector store.
*/
- public static final StringTag GEN_AI_VECTOR_STORE_RECORD_IDS = new StringTag(42, "gen_ai.vector_store.record_ids");
-
- /**
- * GEN_AI_SYSTEM_INSTRUCTIONS represents the system message or instructions provided to the GenAI model separately from the chat history.
- */
- public static final StringTag GEN_AI_SYSTEM_INSTRUCTIONS = new StringTag(43, "gen_ai.system_instructions");
+ public static final StringTag GEN_AI_VECTOR_STORE_RECORD_IDS = new StringTag(43, "gen_ai.vector_store.record_ids");
/**
* GEN_AI_INPUT_MESSAGES represents the chat history provided to the model as an input.
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 4702effeef..64c6cfac02 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
@@ -30,8 +30,7 @@
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.enums.AiProviderEnum;
-import org.springframework.ai.chat.client.ChatClientRequest;
-import org.springframework.ai.chat.client.ChatClientResponse;
+import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
@@ -66,6 +65,7 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr
span.setComponent(ComponentsDefine.SPRING_AI);
Tags.GEN_AI_OPERATION_NAME.set(span, Constants.CHAT);
+ Tags.GEN_AI_PROVIDER_NAME.set(span, apiMetadata.getProviderName());
Tags.GEN_AI_REQUEST_MODEL.set(span, chatOptions.getModel());
Tags.GEN_AI_TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
Tags.GEN_AI_TOP_K.set(span, String.valueOf(chatOptions.getTopK()));
@@ -79,14 +79,13 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
}
try {
- AbstractSpan span = ContextManager.activeSpan();
- ChatClientResponse response = (ChatClientResponse) ret;
- if (response == null || response.chatResponse() == null) {
- return ret;
+ ChatResponse response = (ChatResponse) ret;
+ if (response == null) {
+ return null;
}
- ChatResponse chatResponse = response.chatResponse();
- ChatResponseMetadata metadata = chatResponse.getMetadata();
+ AbstractSpan span = ContextManager.activeSpan();
+ ChatResponseMetadata metadata = response.getMetadata();
long totalTokens = 0;
@@ -113,7 +112,7 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
}
}
- Generation generation = chatResponse.getResult();
+ Generation generation = response.getResult();
if (generation != null && generation.getMetadata() != null) {
String finishReason = generation.getMetadata().getFinishReason();
if (finishReason != null) {
@@ -152,12 +151,12 @@ private void collectContent(AbstractSpan span, Object[] allArguments, Generation
}
private void collectPrompt(AbstractSpan span, Object[] allArguments) {
- ChatClientRequest request = (ChatClientRequest) allArguments[0];
- if (request == null) {
+ Prompt prompt = (Prompt) allArguments[0];
+ if (prompt == null) {
return;
}
- String promptText = request.prompt().getContents();
+ String promptText = prompt.getContents();
if (promptText == null) {
return;
}
@@ -167,7 +166,10 @@ private void collectPrompt(AbstractSpan span, Object[] allArguments) {
promptText = promptText.substring(0, limit);
}
-
+ StringBuilder stringBuilder = new StringBuilder();
+ for (Message instruction : prompt.getInstructions()) {
+ stringBuilder.append(instruction.getText());
+ }
Tags.GEN_AI_INPUT_MESSAGES.set(span, promptText);
}
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 dd8e748bba..abaf0ee93b 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
@@ -75,6 +75,7 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr
}
Tags.GEN_AI_OPERATION_NAME.set(span, Constants.CHAT);
+ Tags.GEN_AI_PROVIDER_NAME.set(span, apiMetadata.getProviderName());
Tags.GEN_AI_REQUEST_MODEL.set(span, chatOptions.getModel());
Tags.GEN_AI_TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
Tags.GEN_AI_TOP_K.set(span, String.valueOf(chatOptions.getTopK()));
@@ -88,109 +89,156 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
if (!ContextManager.isActive()) {
return ret;
}
- AbstractSpan span = ContextManager.activeSpan();
- ContextSnapshot contextSnapshot = ContextManager.capture();
+ final AbstractSpan span = ContextManager.activeSpan();
+ final ContextSnapshot snapshot = ContextManager.capture();
span.prepareForAsync();
ContextManager.stopSpan();
- Flux flux = (Flux) ret;
+ @SuppressWarnings("unchecked") final Flux flux = (Flux) ret;
- AtomicReference lastResponseRef = new AtomicReference<>();
+ final StreamState state = new StreamState(readAndClearStartTime());
- final StringBuilder completionBuilder = new StringBuilder();
+ return flux
+ .doOnNext(response -> onStreamNext(span, response, state))
+ .doOnError(span::log)
+ .doFinally(signalType -> onStreamFinally(span, allArguments, state))
+ .contextWrite(c -> c.put(Constants.SKYWALKING_CONTEXT_SNAPSHOT, snapshot));
+ }
- AtomicReference finishReason = new AtomicReference<>("");
+ private void onStreamNext(AbstractSpan span, ChatResponse response, StreamState state) {
+ state.lastResponseRef.set(response);
- AtomicBoolean firstResponseReceived = new AtomicBoolean(false);
+ final Generation generation = response.getResult();
+ if (generation == null) {
+ return;
+ }
- Long startTime = (Long) ContextManager.getRuntimeContext().get(Constants.SPRING_AI_STREAM_START_TIME);
- ContextManager.getRuntimeContext().remove(Constants.SPRING_AI_STREAM_START_TIME);
+ recordTtfrIfFirstToken(span, generation, state);
+ recordFinishReason(generation, state);
+ appendCompletionChunk(generation, state);
+ }
- return flux.doOnNext(response -> {
- if (response != null) {
+ private void onStreamFinally(AbstractSpan span, Object[] allArguments, StreamState state) {
+ try {
+ ChatResponse finalResponse = state.lastResponseRef.get();
+ if (finalResponse == null) {
+ return;
+ }
- lastResponseRef.set(response);
+ ChatResponseMetadata metadata = finalResponse.getMetadata();
+ if (metadata == null) {
+ return;
+ }
- Generation generation = response.getResult();
- if (generation == null) {
- return;
- }
+ collectResponseTags(span, metadata, state);
- if (generation.getOutput() != null && StringUtils.hasText(generation.getOutput().getText())) {
- if (firstResponseReceived.compareAndSet(false, true) && startTime != null) {
- Tags.GEN_AI_STREAM_TTFR.set(span, String.valueOf(System.currentTimeMillis() - startTime));
- }
- }
+ long totalTokens = collectUsageTags(span, metadata.getUsage());
- String reason = generation.getMetadata().getFinishReason();
- if (reason != null) {
- finishReason.set(reason);
- }
+ int tokenThreshold = SpringAiPluginConfig.Plugin.SpringAi.CONTENT_COLLECT_THRESHOLD_TOKENS;
+ if (tokenThreshold >= 0 && totalTokens < tokenThreshold) {
+ return;
+ }
- if (generation.getOutput() != null && generation.getOutput().getText() != null) {
- completionBuilder.append(generation.getOutput().getText());
- }
- }
- })
- .doOnError(span::log)
- .doFinally(signalType -> {
- try {
- ChatResponse finalResponse = lastResponseRef.get();
-
- if (finalResponse != null) {
- ChatResponseMetadata metadata = finalResponse.getMetadata();
- if (metadata != null) {
- if (metadata.getId() != null) {
- Tags.GEN_AI_RESPONSE_ID.set(span, metadata.getId());
- }
- if (metadata.getModel() != null) {
- Tags.GEN_AI_RESPONSE_MODEL.set(span, metadata.getModel());
- }
-
- Usage usage = metadata.getUsage();
- long totalTokens = 0;
- if (usage != null) {
- Tags.GEN_AI_USAGE_INPUT_TOKENS.set(span, String.valueOf(usage.getPromptTokens()));
- Tags.GEN_AI_USAGE_OUTPUT_TOKENS.set(span, String.valueOf(usage.getCompletionTokens()));
- totalTokens = usage.getTotalTokens() != null ? usage.getTotalTokens() : 0;
- Tags.GEN_AI_CLIENT_TOKEN_USAGE.set(span, String.valueOf(usage.getTotalTokens().longValue()));
- }
-
- Tags.GEN_AI_RESPONSE_FINISH_REASONS.set(span, finishReason.get());
-
- int tokenThreshold = SpringAiPluginConfig.Plugin.SpringAi.CONTENT_COLLECT_THRESHOLD_TOKENS;
- if (tokenThreshold < 0 || totalTokens >= tokenThreshold) {
-
- if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_PROMPT) {
- Prompt prompt = (Prompt) allArguments[0];
- String promptText = prompt.getContents();
- if (promptText != null) {
- int limit = SpringAiPluginConfig.Plugin.SpringAi.PROMPT_LENGTH_LIMIT;
- if (limit > 0 && promptText.length() > limit) {
- promptText = promptText.substring(0, limit);
- }
- Tags.GEN_AI_PROMPT.set(span, promptText);
- }
- }
-
- if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_COMPLETION) {
- int limit = SpringAiPluginConfig.Plugin.SpringAi.COMPLETION_LENGTH_LIMIT;
- String output = completionBuilder.toString();
- if (limit > 0 && output.length() > limit) {
- output = output.substring(0, limit);
- }
- Tags.GEN_AI_COMPLETION.set(span, output);
- }
- }
- }
- }
- } catch (Throwable t) {
- span.log(t);
- } finally {
- span.asyncFinish();
- }
- }).contextWrite(c -> c.put(Constants.SKYWALKING_CONTEXT_SNAPSHOT, contextSnapshot));
+ if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_PROMPT) {
+ collectPrompt(span, allArguments);
+ }
+ if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_COMPLETION) {
+ collectCompletion(span, state);
+ }
+ } catch (Throwable t) {
+ span.log(t);
+ } finally {
+ span.asyncFinish();
+ }
+ }
+
+ private void recordTtfrIfFirstToken(AbstractSpan span, Generation generation, StreamState state) {
+ if (state.startTime == null) {
+ return;
+ }
+ if (generation.getOutput() == null || !StringUtils.hasText(generation.getOutput().getText())) {
+ return;
+ }
+ if (state.firstResponseReceived.compareAndSet(false, true)) {
+ Tags.GEN_AI_STREAM_TTFR.set(span, String.valueOf(System.currentTimeMillis() - state.startTime));
+ }
+ }
+
+ private void recordFinishReason(Generation generation, StreamState state) {
+ if (generation.getMetadata() == null) {
+ return;
+ }
+ String reason = generation.getMetadata().getFinishReason();
+ if (reason != null) {
+ state.finishReason.set(reason);
+ }
+ }
+
+ private void appendCompletionChunk(Generation generation, StreamState state) {
+ if (generation.getOutput() == null) {
+ return;
+ }
+ String text = generation.getOutput().getText();
+ if (text != null) {
+ state.completionBuilder.append(text);
+ }
+ }
+
+ private void collectResponseTags(AbstractSpan span, ChatResponseMetadata metadata, StreamState state) {
+ if (metadata.getId() != null) {
+ Tags.GEN_AI_RESPONSE_ID.set(span, metadata.getId());
+ }
+ if (metadata.getModel() != null) {
+ Tags.GEN_AI_RESPONSE_MODEL.set(span, metadata.getModel());
+ }
+ Tags.GEN_AI_RESPONSE_FINISH_REASONS.set(span, state.finishReason.get());
+ }
+
+ private long collectUsageTags(AbstractSpan span, Usage usage) {
+ if (usage == null) {
+ return 0;
+ }
+ Tags.GEN_AI_USAGE_INPUT_TOKENS.set(span, String.valueOf(usage.getPromptTokens()));
+ Tags.GEN_AI_USAGE_OUTPUT_TOKENS.set(span, String.valueOf(usage.getCompletionTokens()));
+
+ long total = usage.getTotalTokens() != null ? usage.getTotalTokens() : 0;
+ Tags.GEN_AI_CLIENT_TOKEN_USAGE.set(span, String.valueOf(total));
+ return total;
+ }
+
+ private void collectPrompt(AbstractSpan span, Object[] allArguments) {
+ Prompt prompt = (Prompt) allArguments[0];
+ if (prompt == null) {
+ return;
+ }
+ String promptText = prompt.getContents();
+ if (promptText == null) {
+ return;
+ }
+ int limit = SpringAiPluginConfig.Plugin.SpringAi.PROMPT_LENGTH_LIMIT;
+ Tags.GEN_AI_INPUT_MESSAGES.set(span, truncate(promptText, limit));
+ }
+
+ private void collectCompletion(AbstractSpan span, StreamState state) {
+ int limit = SpringAiPluginConfig.Plugin.SpringAi.COMPLETION_LENGTH_LIMIT;
+ Tags.GEN_AI_OUTPUT_MESSAGES.set(span, truncate(state.completionBuilder.toString(), limit));
+ }
+
+ private String truncate(String s, int limit) {
+ if (s == null) {
+ return null;
+ }
+ if (limit > 0 && s.length() > limit) {
+ return s.substring(0, limit);
+ }
+ return s;
+ }
+
+ private Long readAndClearStartTime() {
+ Long startTime = (Long) ContextManager.getRuntimeContext().get(Constants.SPRING_AI_STREAM_START_TIME);
+ ContextManager.getRuntimeContext().remove(Constants.SPRING_AI_STREAM_START_TIME);
+ return startTime;
}
@Override
@@ -199,4 +247,16 @@ public void handleMethodException(EnhancedInstance objInst, Method method, Objec
ContextManager.activeSpan().log(t);
}
}
+
+ private static final class StreamState {
+ final AtomicReference lastResponseRef = new AtomicReference<>();
+ final StringBuilder completionBuilder = new StringBuilder();
+ final AtomicReference finishReason = new AtomicReference<>("");
+ final AtomicBoolean firstResponseReceived = new AtomicBoolean(false);
+ final Long startTime;
+
+ StreamState(Long startTime) {
+ this.startTime = startTime;
+ }
+ }
}
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/provider/ZhiPuAiApiInstrumentation.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/provider/ZhiPuAiApiInstrumentation.java
new file mode 100644
index 0000000000..583f045f6b
--- /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/provider/ZhiPuAiApiInstrumentation.java
@@ -0,0 +1,64 @@
+/*
+ * 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.provider;
+
+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.NameMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+public class ZhiPuAiApiInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS = "org.springframework.ai.zhipuai.api.ZhiPuAiApi";
+ private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.provider.ZhiPuAiApiConstructorInterceptor";
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return NameMatch.byName(ENHANCE_CLASS);
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[]{
+ new ConstructorInterceptPoint() {
+ @Override
+ public ElementMatcher getConstructorMatcher() {
+ return takesArguments(8).and(takesArgument(0, named("java.lang.String"))).and(takesArgument(3, named("java.lang.String")));
+ }
+
+ @Override
+ public String getConstructorInterceptor() {
+ return INTERCEPTOR_CLASS;
+ }
+ }
+ };
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
+ return new InstanceMethodsInterceptPoint[0];
+ }
+}
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/provider/ZhiPuAiApiConstructorInterceptor.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/provider/ZhiPuAiApiConstructorInterceptor.java
new file mode 100644
index 0000000000..734c807a6a
--- /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/provider/ZhiPuAiApiConstructorInterceptor.java
@@ -0,0 +1,38 @@
+/*
+ * 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.provider;
+
+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.ChatModelMetadataResolver;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
+
+public class ZhiPuAiApiConstructorInterceptor implements InstanceConstructorInterceptor {
+
+ @Override
+ public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable {
+ ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.ZHIPU_AI.getModelClassName());
+ if (metadata == null) {
+ return;
+ }
+
+ metadata.setBaseUrl((String) allArguments[0]);
+ metadata.setCompletionsPath((String) allArguments[3]);
+ }
+}
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 43a015aedb..47f85834f3 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
@@ -26,3 +26,4 @@ spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.Mini
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.MistralAiApiInstrumentation
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.OllamaApiInstrumentation
spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.OpenAiApiInstrumentation
+spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.provider.ZhiPuAiApiInstrumentation
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 c4075db94b..8a1c2e0480 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/bin/startup.sh
@@ -18,4 +18,4 @@
home="$(cd "$(dirname $0)"; pwd)"
-java -jar ${agent_opts} ${home}/../libs/spring-ai-1.x-scenario.jar &
\ No newline at end of file
+java -Dskywalking.plugin.springai.collect_prompt=true -Dskywalking.plugin.springai.collect_completion=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
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 9e6ec15daa..b9d39f538a 100644
--- a/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
+++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml
@@ -54,30 +54,7 @@
org.springframework.ai
spring-ai-starter-model-openai
-
-
-
-
-
- org.springframework.ai
- spring-ai-starter-model-minimax
-
-
-
- org.springframework.ai
- spring-ai-starter-model-mistral-ai
-
-
-
- org.springframework.ai
- spring-ai-starter-model-ollama
-
-
-
- org.springframework.ai
- spring-ai-advisors-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 16e6b7f5a1..79fde137ab 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
@@ -18,9 +18,6 @@
package test.apache.skywalking.apm.testcase.jdk.httpclient.config;
import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.ai.minimax.MiniMaxChatModel;
-import org.springframework.ai.mistralai.MistralAiChatModel;
-import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -32,19 +29,4 @@ public class ChatClientConfig {
public ChatClient openAIChatClient(OpenAiChatModel model) {
return ChatClient.create(model);
}
-
-// @Bean
-// public ChatClient anthropicChatClient(MiniMaxChatModel model) {
-// return ChatClient.create(model);
-// }
-
-// @Bean
-// public ChatClient anthropicChatClient(MistralAiChatModel model) {
-// return ChatClient.create(model);
-// }
-
-// @Bean
-// public ChatClient anthropicChatClient(OllamaChatModel model) {
-// return ChatClient.create(model);
-// }
}
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 75a26b2bc9..c4f5c58851 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
@@ -22,7 +22,7 @@ server:
spring:
ai:
openai:
- api-key: noaichaPCNAC09DHBWQCOACNacoi
+ api-key: xxxxxxxxxxxxxxxxxxxxxxxxxxx
base-url: http://localhost:8080/spring-ai-1.x-scenario/llm
chat:
options:
@@ -32,3 +32,4 @@ spring:
top-p: 0.9
+
From 9f98420fb48093f4dd268a1432756aa934f26590 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Wed, 18 Feb 2026 16:48:56 +0800
Subject: [PATCH 21/48] fix
---
.../apm/agent/core/context/tag/Tags.java | 15 ----
.../v1/VectorStoreRetrieverInterceptor.java | 85 -------------------
.../VectorStoreRetrieverInstrumentation.java | 73 ----------------
3 files changed, 173 deletions(-)
delete 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/VectorStoreRetrieverInterceptor.java
delete 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/VectorStoreRetrieverInstrumentation.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 074dffdb57..995b91f582 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
@@ -240,21 +240,6 @@ public static final class HTTP {
*/
public static final StringTag GEN_AI_STREAM_TTFR = new StringTag(40, "gen_ai.stream.ttfr");
- /**
- * GEN_AI_VECTOR_STORE_TOP_K represents the target number of top results to return from the vector store.
- */
- public static final StringTag GEN_AI_VECTOR_STORE_TOP_K = new StringTag(41, "gen_ai.vector_store.top_k");
-
- /**
- * GEN_AI_VECTOR_STORE_FILTER_EXPRESSION represents the filter expression used in the vector store search.
- */
- public static final StringTag GEN_AI_VECTOR_STORE_FILTER_EXPRESSION = new StringTag(42, "gen_ai.vector_store.filter_expression");
-
- /**
- * GEN_AI_VECTOR_STORE_RECORD_IDS represents the unique identifiers of the records retrieved from the vector store.
- */
- public static final StringTag GEN_AI_VECTOR_STORE_RECORD_IDS = new StringTag(43, "gen_ai.vector_store.record_ids");
-
/**
* GEN_AI_INPUT_MESSAGES represents the chat history provided to the model as an input.
*/
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/VectorStoreRetrieverInterceptor.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/VectorStoreRetrieverInterceptor.java
deleted file mode 100644
index da443dc797..0000000000
--- a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ai/v1/VectorStoreRetrieverInterceptor.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.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.network.trace.component.ComponentsDefine;
-import org.springframework.ai.document.Document;
-import org.springframework.ai.vectorstore.SearchRequest;
-
-import java.lang.reflect.Method;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-public class VectorStoreRetrieverInterceptor implements InstanceMethodsAroundInterceptor {
-
- @Override
- public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
- MethodInterceptResult result) throws Throwable {
-
- AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/vectorStore/retrieve");
- span.setComponent(ComponentsDefine.SPRING_AI);
- SearchRequest searchRequest = (SearchRequest) allArguments[0];
-
- Tags.GEN_AI_VECTOR_STORE_TOP_K.set(span, String.valueOf(searchRequest.getTopK()));
-
- if (searchRequest.getFilterExpression() != null) {
- Tags.GEN_AI_VECTOR_STORE_FILTER_EXPRESSION.set(span, searchRequest.getFilterExpression().toString());
- }
- }
-
- @Override
- public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes,
- Object ret) throws Throwable {
- if (!ContextManager.isActive()) {
- return ret;
- }
-
- if (ret != null) {
- List documents = (List) ret;
- AbstractSpan span = ContextManager.activeSpan();
- if (!documents.isEmpty()) {
- String recordIds = documents.stream()
- .map(Document::getId)
- .filter(Objects::nonNull)
- .collect(Collectors.joining(","));
-
- if (!recordIds.isEmpty()) {
- Tags.GEN_AI_VECTOR_STORE_RECORD_IDS.set(span, recordIds);
- }
- }
- }
- 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);
- }
- }
-}
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/VectorStoreRetrieverInstrumentation.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/VectorStoreRetrieverInstrumentation.java
deleted file mode 100644
index 6c09e083fd..0000000000
--- 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/VectorStoreRetrieverInstrumentation.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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 VectorStoreRetrieverInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
-
- private static final String ENHANCE_INTERFACE = "org.springframework.ai.vectorstore.VectorStoreRetriever";
- private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.spring.ai.v1.VectorStoreRetrieverInterceptor";
-
- private static final String INTERCEPT_METHOD = "doSimilaritySearch";
-
- @Override
- protected ClassMatch enhanceClass() {
- return HierarchyMatch.byHierarchyMatch(ENHANCE_INTERFACE);
- }
-
- @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 be4827d4abd1d983e39436d3dfcca2cc1d3e0dd1 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Wed, 18 Feb 2026 17:29:45 +0800
Subject: [PATCH 22/48] fix
---
.../testcase/jdk/httpclient/controller/CaseController.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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 d939c16844..d6512b726e 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
@@ -47,12 +47,12 @@ public String testCase() throws Exception {
Do not use outside knowledge. Be concise.
""";
- System.out.println(chatClient
+ chatClient
.prompt("What's the weather in New York?")
.system(systemPrompt)
.tools(weatherTool)
.call()
- .content());
+ .content();
chatClient
.prompt("What's the weather in New York?")
From 43905d52cc394aba0dce08b81f59ab53f81cb7e3 Mon Sep 17 00:00:00 2001
From: peachisai <2581009893@qq.com>
Date: Fri, 20 Feb 2026 17:52:16 +0800
Subject: [PATCH 23/48] fix
---
.../trace/component/ComponentsDefine.java | 32 ++-
.../apm/agent/core/util/GsonUtil.java | 36 +++
.../spring-ai-1.x-plugin/pom.xml | 6 -
.../ai/v1/ChatModelCallInterceptor.java | 52 ++--
.../ai/v1/ChatModelStreamInterceptor.java | 48 ++--
.../v1/common/ChatModelMetadataResolver.java | 61 +++-
.../spring/ai/v1/enums/AiProviderEnum.java | 2 +-
.../spring/ai/v1/messages/InputMessages.java | 271 ++++++++++++++++++
.../spring/ai/v1/messages/OutputMessages.java | 121 ++++++++
.../src/main/resources/skywalking-plugin.def | 1 -
.../config/expectedData.yaml | 127 +++++---
.../controller/LLMMockController.java | 16 ++
12 files changed, 668 insertions(+), 105 deletions(-)
create mode 100644 apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/util/GsonUtil.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/messages/InputMessages.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/messages/OutputMessages.java
diff --git a/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java b/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java
index 4135aab449..9fa2973619 100755
--- a/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java
+++ b/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java
@@ -265,6 +265,36 @@ public class ComponentsDefine {
public static final OfficialComponent DMDB_JDBC_DRIVER = new OfficialComponent(163, "Dmdb-jdbc-driver");
- public static final OfficialComponent SPRING_AI = new OfficialComponent(164, "Spring-ai");
+ public static final OfficialComponent SPRING_AI_UNKNOWN = new OfficialComponent(164, "spring-ai-unknown");
+
+ public static final OfficialComponent SPRING_AI_ANTHROPIC = new OfficialComponent(165, "spring-ai-anthropic");
+
+ public static final OfficialComponent SPRING_AI_BEDROCK = new OfficialComponent(166, "spring-ai-aws-bedrock");
+
+ public static final OfficialComponent SPRING_AI_AZURE_OPENAI = new OfficialComponent(167, "spring-ai-azure-openai");
+
+ public static final OfficialComponent SPRING_AI_COHERE = new OfficialComponent(168, "spring-ai-cohere");
+
+ public static final OfficialComponent SPRING_AI_DEEPSEEK = new OfficialComponent(169, "spring-ai-deepseek");
+
+ public static final OfficialComponent SPRING_AI_GOOGLE_GENAI = new OfficialComponent(170, "spring-ai-gcp-genai");
+
+ public static final OfficialComponent SPRING_AI_VERTEXAI = new OfficialComponent(171, "spring-ai-gcp-vertex-ai");
+
+ public static final OfficialComponent SPRING_AI_MISTRAL_AI = new OfficialComponent(172, "spring-ai-mistral-ai");
+
+ public static final OfficialComponent SPRING_AI_OPENAI = new OfficialComponent(173, "spring-ai-openai");
+
+ public static final OfficialComponent SPRING_AI_HUGGINGFACE = new OfficialComponent(174, "spring-ai-huggingface");
+
+ public static final OfficialComponent SPRING_AI_MINIMAX = new OfficialComponent(175, "spring-ai-minimax");
+
+ public static final OfficialComponent SPRING_AI_OLLAMA = new OfficialComponent(176, "spring-ai-ollama");
+
+ public static final OfficialComponent SPRING_AI_OPENAI_SDK = new OfficialComponent(177, "spring-ai-openai-sdk");
+
+ public static final OfficialComponent SPRING_AI_ZHIPU_AI = new OfficialComponent(178, "spring-ai-zhipu-ai");
+
+ public static final OfficialComponent SPRING_AI = new OfficialComponent(179, "spring-ai");
}
diff --git a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/util/GsonUtil.java b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/util/GsonUtil.java
new file mode 100644
index 0000000000..fb710e4a61
--- /dev/null
+++ b/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/util/GsonUtil.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.agent.core.util;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+public class GsonUtil {
+
+ private static final Gson GSON = new GsonBuilder()
+ .disableHtmlEscaping()
+ .create();
+
+ public static String toJson(Object src) {
+ if (src == null) {
+ return null;
+ }
+ return GSON.toJson(src);
+ }
+}
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 b15ec1c429..a68d7170f2 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
@@ -43,11 +43,5 @@
provided
-
- org.springframework.ai
- spring-ai-advisors-vector-store
- 1.1.0
- provided
-
\ No newline at end of file
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 64c6cfac02..557e696400 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,11 +26,13 @@
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.network.trace.component.ComponentsDefine;
+import org.apache.skywalking.apm.network.trace.component.OfficialComponent;
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
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.enums.AiProviderEnum;
-import org.springframework.ai.chat.messages.Message;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.messages.InputMessages;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.messages.OutputMessages;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
@@ -45,17 +47,22 @@ public class ChatModelCallInterceptor implements InstanceMethodsAroundIntercepto
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
ChatModelMetadataResolver.ApiMetadata apiMetadata = ChatModelMetadataResolver.getMetadata(objInst);
- String providerName = AiProviderEnum.UNKNOW.getValue();
+ String providerName = AiProviderEnum.UNKNOWN.getValue();
+ OfficialComponent component = ComponentsDefine.SPRING_AI_UNKNOWN;
String peer = null;
if (apiMetadata != null) {
if (apiMetadata.getProviderName() != null) {
providerName = apiMetadata.getProviderName();
+ component = apiMetadata.getComponent();
}
peer = apiMetadata.getPeer();
}
AbstractSpan span = ContextManager.createExitSpan("Spring-ai/" + providerName + "/call", peer);
SpanLayer.asGenAI(span);
+ span.setComponent(component);
+ Tags.GEN_AI_OPERATION_NAME.set(span, Constants.CHAT);
+ Tags.GEN_AI_PROVIDER_NAME.set(span, apiMetadata.getProviderName());
Prompt prompt = (Prompt) allArguments[0];
ChatOptions chatOptions = prompt.getOptions();
@@ -63,9 +70,6 @@ public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allAr
return;
}
- span.setComponent(ComponentsDefine.SPRING_AI);
- Tags.GEN_AI_OPERATION_NAME.set(span, Constants.CHAT);
- Tags.GEN_AI_PROVIDER_NAME.set(span, apiMetadata.getProviderName());
Tags.GEN_AI_REQUEST_MODEL.set(span, chatOptions.getModel());
Tags.GEN_AI_TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature()));
Tags.GEN_AI_TOP_K.set(span, String.valueOf(chatOptions.getTopK()));
@@ -120,7 +124,7 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
}
}
- collectContent(span, allArguments, generation, totalTokens);
+ collectContent(span, allArguments, response, totalTokens);
} finally {
ContextManager.stopSpan();
}
@@ -134,7 +138,7 @@ public void handleMethodException(EnhancedInstance objInst, Method method, Objec
}
}
- private void collectContent(AbstractSpan span, Object[] allArguments, Generation generation, long totalTokens) {
+ private void collectContent(AbstractSpan span, Object[] allArguments, ChatResponse response, long totalTokens) {
int tokenThreshold = SpringAiPluginConfig.Plugin.SpringAi.CONTENT_COLLECT_THRESHOLD_TOKENS;
if (tokenThreshold >= 0 && totalTokens < tokenThreshold) {
@@ -146,7 +150,7 @@ private void collectContent(AbstractSpan span, Object[] allArguments, Generation
}
if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_COMPLETION) {
- collectCompletion(span, generation);
+ collectCompletion(span, response);
}
}
@@ -161,33 +165,25 @@ private void collectPrompt(AbstractSpan span, Object[] allArguments) {
return;
}
+ InputMessages inputMessages = InputMessages.fromPrompt(prompt);
+ String inputMessagesJson = inputMessages.toJson();
int limit = SpringAiPluginConfig.Plugin.SpringAi.PROMPT_LENGTH_LIMIT;
- if (limit > 0 && promptText.length() > limit) {
- promptText = promptText.substring(0, limit);
- }
-
- StringBuilder stringBuilder = new StringBuilder();
- for (Message instruction : prompt.getInstructions()) {
- stringBuilder.append(instruction.getText());
+ if (limit > 0 && inputMessagesJson.length() > limit) {
+ inputMessagesJson = inputMessagesJson.substring(0, limit);
}
- Tags.GEN_AI_INPUT_MESSAGES.set(span, promptText);
+ Tags.GEN_AI_INPUT_MESSAGES.set(span, inputMessagesJson);
}
- private void collectCompletion(AbstractSpan span, Generation generation) {
- if (generation == null || generation.getOutput() == null) {
- return;
- }
-
- String completionText = generation.getOutput().getText();
- if (completionText == null) {
- return;
- }
+ private void collectCompletion(AbstractSpan span, ChatResponse response) {
+ OutputMessages outputMessages = OutputMessages.fromChatResponse(response);
+ String outputMessagesJson = outputMessages.toJson();
int limit = SpringAiPluginConfig.Plugin.SpringAi.COMPLETION_LENGTH_LIMIT;
- if (limit > 0 && completionText.length() > limit) {
- completionText = completionText.substring(0, limit);
+
+ if (limit > 0 && outputMessagesJson.length() > limit) {
+ outputMessagesJson = outputMessagesJson.substring(0, limit);
}
- Tags.GEN_AI_OUTPUT_MESSAGES.set(span, completionText);
+ Tags.GEN_AI_OUTPUT_MESSAGES.set(span, outputMessagesJson);
}
}
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 abaf0ee93b..ff597207ee 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,10 +27,13 @@
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.network.trace.component.ComponentsDefine;
+import org.apache.skywalking.apm.network.trace.component.OfficialComponent;
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
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.enums.AiProviderEnum;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.messages.InputMessages;
+import org.apache.skywalking.apm.plugin.spring.ai.v1.messages.OutputMessages;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
@@ -41,6 +44,8 @@
import reactor.core.publisher.Flux;
import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -49,19 +54,21 @@ public class ChatModelStreamInterceptor implements InstanceMethodsAroundIntercep
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
ChatModelMetadataResolver.ApiMetadata apiMetadata = ChatModelMetadataResolver.getMetadata(objInst);
- String providerName = AiProviderEnum.UNKNOW.getValue();
+ String providerName = AiProviderEnum.UNKNOWN.getValue();
+ OfficialComponent component = ComponentsDefine.SPRING_AI_UNKNOWN;
String peer = null;
if (apiMetadata != null) {
if (apiMetadata.getProviderName() != null) {
providerName = apiMetadata.getProviderName();
+ component = apiMetadata.getComponent();
}
peer = apiMetadata.getPeer();
}
AbstractSpan span = ContextManager.createExitSpan("Spring-ai/" + providerName + "/stream", peer);
SpanLayer.asGenAI(span);
- span.setComponent(ComponentsDefine.SPRING_AI);
+ span.setComponent(component);
SpanLayer.asGenAI(span);
Prompt prompt = (Prompt) allArguments[0];
@@ -212,27 +219,36 @@ private void collectPrompt(AbstractSpan span, Object[] allArguments) {
if (prompt == null) {
return;
}
- String promptText = prompt.getContents();
- if (promptText == null) {
- return;
- }
+
+ InputMessages inputMessages = InputMessages.fromPrompt(prompt);
+ String inputMessagesJson = inputMessages.toJson();
+
int limit = SpringAiPluginConfig.Plugin.SpringAi.PROMPT_LENGTH_LIMIT;
- Tags.GEN_AI_INPUT_MESSAGES.set(span, truncate(promptText, limit));
+ if (limit > 0 && inputMessagesJson.length() > limit) {
+ inputMessagesJson = inputMessagesJson.substring(0, limit);
+ }
+
+ Tags.GEN_AI_INPUT_MESSAGES.set(span, inputMessagesJson);
}
private void collectCompletion(AbstractSpan span, StreamState state) {
- int limit = SpringAiPluginConfig.Plugin.SpringAi.COMPLETION_LENGTH_LIMIT;
- Tags.GEN_AI_OUTPUT_MESSAGES.set(span, truncate(state.completionBuilder.toString(), limit));
- }
- private String truncate(String s, int limit) {
- if (s == null) {
- return null;
+ String fullText = state.completionBuilder.toString();
+ String finishReason = state.finishReason.get();
+ List parts = new ArrayList<>();
+ if (fullText != null && !fullText.isEmpty()) {
+ parts.add(new InputMessages.TextPart(fullText));
}
- if (limit > 0 && s.length() > limit) {
- return s.substring(0, limit);
+
+ OutputMessages outputMessages = OutputMessages.create().append(OutputMessages.OutputMessage.create("assistant", parts, finishReason));
+ String outputMessagesJson = outputMessages.toJson();
+
+ int limit = SpringAiPluginConfig.Plugin.SpringAi.COMPLETION_LENGTH_LIMIT;
+ if (limit > 0 && outputMessagesJson.length() > limit) {
+ outputMessagesJson = outputMessagesJson.substring(0, limit);
}
- return s;
+
+ Tags.GEN_AI_OUTPUT_MESSAGES.set(span, outputMessagesJson);
}
private Long readAndClearStartTime() {
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/ChatModelMetadataResolver.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/ChatModelMetadataResolver.java
index 0eac0a6157..e8010d2d0d 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/ChatModelMetadataResolver.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/ChatModelMetadataResolver.java
@@ -20,6 +20,8 @@
import org.apache.skywalking.apm.agent.core.logging.api.ILog;
import org.apache.skywalking.apm.agent.core.logging.api.LogManager;
+import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
+import org.apache.skywalking.apm.network.trace.component.OfficialComponent;
import org.apache.skywalking.apm.plugin.spring.ai.v1.enums.AiProviderEnum;
import java.util.HashMap;
@@ -36,29 +38,56 @@ public class ChatModelMetadataResolver {
if (provider.getModelClassName() != null && provider.getValue() != null) {
MODEL_METADATA_MAP.put(
provider.getModelClassName(),
- new ApiMetadata(provider.getValue())
+ new ApiMetadata(provider.getValue(), matchComponent(provider))
);
}
}
}
- public static ApiMetadata getMetadata(Object chatModelInstance) {
- ApiMetadata metadata = MODEL_METADATA_MAP.get(chatModelInstance.getClass().getName());
- if (metadata == null) {
- return null;
+ private static OfficialComponent matchComponent(AiProviderEnum provider) {
+ switch (provider) {
+ case ANTHROPIC_CLAUDE:
+ return ComponentsDefine.SPRING_AI_ANTHROPIC;
+ case AMAZON_BEDROCK_CONVERSE:
+ return ComponentsDefine.SPRING_AI_BEDROCK;
+ case AZURE_OPENAI:
+ return ComponentsDefine.SPRING_AI_AZURE_OPENAI;
+ case OCI_GENAI_COHERE:
+ return ComponentsDefine.SPRING_AI_COHERE;
+ case DEEPSEEK:
+ return ComponentsDefine.SPRING_AI_DEEPSEEK;
+ case GOOGLE_GENAI:
+ return ComponentsDefine.SPRING_AI_GOOGLE_GENAI;
+ case GOOGLE_VERTEXAI_GEMINI:
+ return ComponentsDefine.SPRING_AI_VERTEXAI;
+ case MISTRAL_AI:
+ return ComponentsDefine.SPRING_AI_MISTRAL_AI;
+ case OPENAI:
+ return ComponentsDefine.SPRING_AI_OPENAI;
+ case HUGGINGFACE:
+ return ComponentsDefine.SPRING_AI_HUGGINGFACE;
+ case MINIMAX:
+ return ComponentsDefine.SPRING_AI_MINIMAX;
+ case OLLAMA:
+ return ComponentsDefine.SPRING_AI_OLLAMA;
+ case OPENAI_SDK_OFFICIAL:
+ return ComponentsDefine.SPRING_AI_OPENAI_SDK;
+ case ZHIPU_AI:
+ return ComponentsDefine.SPRING_AI_ZHIPU_AI;
+ case UNKNOWN:
+ default:
+ return ComponentsDefine.SPRING_AI_UNKNOWN;
}
+ }
+ public static ApiMetadata getMetadata(Object chatModelInstance) {
+ ApiMetadata metadata = MODEL_METADATA_MAP.get(chatModelInstance.getClass().getName());
return metadata;
}
public static ApiMetadata getMetadata(String modelClassName) {
try {
- ApiMetadata metadata = MODEL_METADATA_MAP.get(modelClassName);
- if (metadata == null) {
- return null;
- }
-
- return metadata;
+ return MODEL_METADATA_MAP.get(modelClassName);
} catch (Exception e) {
LOGGER.error("spring-ai plugin get modelMetadata error: ", e);
return null;
@@ -68,17 +97,23 @@ public static ApiMetadata getMetadata(String modelClassName) {
public static class ApiMetadata {
private final String providerName;
+ private final OfficialComponent component;
private volatile String baseUrl;
private volatile String completionsPath;
- ApiMetadata(String providerName) {
+ ApiMetadata(String providerName, OfficialComponent component) {
this.providerName = providerName;
+ this.component = component;
}
public String getProviderName() {
return providerName;
}
+ public OfficialComponent getComponent() {
+ return component;
+ }
+
public String getBaseUrl() {
return baseUrl;
}
@@ -104,4 +139,4 @@ public String getPeer() {
return providerName;
}
}
-}
+}
\ No newline at end of file
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/enums/AiProviderEnum.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/enums/AiProviderEnum.java
index 25a330d31a..5137ab7d7e 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/enums/AiProviderEnum.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/enums/AiProviderEnum.java
@@ -20,7 +20,7 @@
public enum AiProviderEnum {
- UNKNOW("unknow", null),
+ UNKNOWN("unknown", null),
ANTHROPIC_CLAUDE("anthropic", "org.springframework.ai.anthropic.AnthropicChatModel"),
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/messages/InputMessages.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/messages/InputMessages.java
new file mode 100644
index 0000000000..794748bc49
--- /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/messages/InputMessages.java
@@ -0,0 +1,271 @@
+/*
+ * 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.messages;
+
+import org.apache.skywalking.apm.agent.core.util.GsonUtil;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.MessageType;
+import org.springframework.ai.chat.messages.ToolResponseMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.content.Media;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class InputMessages {
+
+ private final List messages = new ArrayList<>();
+
+ public static InputMessages create() {
+ return new InputMessages();
+ }
+
+ public InputMessages append(InputMessage message) {
+ this.messages.add(message);
+ return this;
+ }
+
+ public String toJson() {
+ return GsonUtil.toJson(
+ messages.stream()
+ .map(InputMessage::toMap)
+ .collect(Collectors.toList())
+ );
+ }
+
+ public static class InputMessage {
+ private final String role;
+ private final List parts;
+
+ private InputMessage(String role, List parts) {
+ this.role = role;
+ this.parts = parts;
+ }
+
+ public static InputMessage create(String role, List parts) {
+ return new InputMessage(role, parts);
+ }
+
+ public Map toMap() {
+ List