From 88baa93a07bda9616eb1107235e3aa63fca57527 Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Wed, 25 Feb 2026 22:21:46 +0800 Subject: [PATCH 1/3] Add Spring AI 1.x plugin and GenAI layer. --- .github/workflows/plugins-jdk17-test.0.yaml | 1 + .../trace/component/ComponentsDefine.java | 30 ++ apm-protocol/apm-network/src/main/proto | 2 +- .../apm/agent/core/context/tag/Tags.java | 90 ++++++ .../agent/core/context/trace/SpanLayer.java | 7 +- .../apm/agent/core/util/GsonUtil.java | 36 +++ .../apm-sdk-plugin/spring-plugins/pom.xml | 1 + .../spring-ai-1.x-plugin/pom.xml | 47 +++ .../ai/v1/ChatModelCallInterceptor.java | 179 ++++++++++++ .../ai/v1/ChatModelStreamInterceptor.java | 266 +++++++++++++++++ .../DefaultToolCallingManagerInterceptor.java | 63 ++++ .../ai/v1/ToolCallbackCallInterceptor.java | 79 +++++ .../v1/common/ChatModelMetadataResolver.java | 147 ++++++++++ .../ai/v1/config/SpringAiPluginConfig.java | 74 +++++ .../spring/ai/v1/contant/Constants.java | 31 ++ .../v1/define/ChatModelInstrumentation.java | 92 ++++++ ...aultToolCallingManagerInstrumentation.java | 72 +++++ .../define/ToolCallbackInstrumentation.java | 73 +++++ .../provider/AnthropicApiInstrumentation.java | 64 +++++ .../provider/DeepSeekApiInstrumentation.java | 64 +++++ .../HuggingfaceChatModelInstrumentation.java | 64 +++++ .../provider/MiniMaxApiInstrumentation.java | 64 +++++ .../provider/MistralAiApiInstrumentation.java | 64 +++++ .../provider/OllamaApiInstrumentation.java | 64 +++++ .../provider/OpenAiApiInstrumentation.java | 64 +++++ .../provider/ZhiPuAiApiInstrumentation.java | 64 +++++ .../spring/ai/v1/enums/AiProviderEnum.java | 68 +++++ .../spring/ai/v1/messages/InputMessages.java | 271 ++++++++++++++++++ .../spring/ai/v1/messages/OutputMessages.java | 121 ++++++++ .../AnthropicApiConstructorInterceptor.java | 38 +++ .../DeepSeekApiConstructorInterceptor.java | 38 +++ ...ngfaceChatModelConstructorInterceptor.java | 37 +++ .../MiniMaxApiConstructorInterceptor.java | 38 +++ .../MistralAiApiConstructorInterceptor.java | 40 +++ .../OllamaApiConstructorInterceptor.java | 38 +++ .../OpenAiApiConstructorInterceptor.java | 38 +++ .../ZhiPuAiApiConstructorInterceptor.java | 38 +++ .../src/main/resources/skywalking-plugin.def | 27 ++ apm-sniffer/config/agent.config | 22 ++ .../service-agent/java-agent/Plugin-list.md | 1 + pom.xml | 2 +- .../spring-ai-1.x-scenario/bin/startup.sh | 21 ++ .../config/expectedData.yaml | 156 ++++++++++ .../spring-ai-1.x-scenario/configuration.yml | 21 ++ .../scenarios/spring-ai-1.x-scenario/pom.xml | 156 ++++++++++ .../src/main/assembly/assembly.xml | 41 +++ .../testcase/jdk/httpclient/Application.java | 30 ++ .../httpclient/config/ChatClientConfig.java | 32 +++ .../httpclient/controller/CaseController.java | 68 +++++ .../controller/LLMMockController.java | 230 +++++++++++++++ .../jdk/httpclient/tool/WeatherTool.java | 33 +++ .../src/main/resources/application.yaml | 35 +++ .../support-version.list | 18 ++ 53 files changed, 3457 insertions(+), 3 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/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/ChatModelCallInterceptor.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/ChatModelStreamInterceptor.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/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/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/common/ChatModelMetadataResolver.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/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 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 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/DefaultToolCallingManagerInstrumentation.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/java/org/apache/skywalking/apm/plugin/spring/ai/v1/define/provider/AnthropicApiInstrumentation.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/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 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 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/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/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/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 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/AnthropicApiConstructorInterceptor.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/DeepSeekApiConstructorInterceptor.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/HuggingfaceChatModelConstructorInterceptor.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/MiniMaxApiConstructorInterceptor.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/MistralAiApiConstructorInterceptor.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/OllamaApiConstructorInterceptor.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/OpenAiApiConstructorInterceptor.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 create mode 100644 apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/resources/skywalking-plugin.def 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/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/.github/workflows/plugins-jdk17-test.0.yaml b/.github/workflows/plugins-jdk17-test.0.yaml index cfec09ac3c..092fa2daae 100644 --- a/.github/workflows/plugins-jdk17-test.0.yaml +++ b/.github/workflows/plugins-jdk17-test.0.yaml @@ -79,6 +79,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-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..a8b57ba7f8 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,4 +265,34 @@ public class ComponentsDefine { public static final OfficialComponent DMDB_JDBC_DRIVER = new OfficialComponent(163, "Dmdb-jdbc-driver"); + 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_ZHIPU_AI = new OfficialComponent(177, "spring-ai-zhipu-ai"); + + public static final OfficialComponent SPRING_AI = new OfficialComponent(178, "spring-ai"); + } diff --git a/apm-protocol/apm-network/src/main/proto b/apm-protocol/apm-network/src/main/proto index 16c51358eb..07882d57be 160000 --- a/apm-protocol/apm-network/src/main/proto +++ b/apm-protocol/apm-network/src/main/proto @@ -1 +1 @@ -Subproject commit 16c51358ebcf42629bf4ffdf952253971f20eb25 +Subproject commit 07882d57becb37e341f7fc492c11f9f5a5f311cf 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..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 @@ -160,6 +160,96 @@ 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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(40, "gen_ai.stream.ttfr"); + + /** + * 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-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/pom.xml b/apm-sniffer/apm-sdk-plugin/spring-plugins/pom.xml index 609220e725..5a307810b3 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..b6ea5c9863 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/pom.xml @@ -0,0 +1,47 @@ + + + + + + spring-plugins + org.apache.skywalking + 9.7.0-SNAPSHOT + + 4.0.0 + + spring-ai-1.x-plugin + jar + + spring-ai-1.x-plugin + http://maven.apache.org + + + 17 + + + + + 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/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 new file mode 100644 index 0000000000..302db3c407 --- /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/ChatModelCallInterceptor.java @@ -0,0 +1,179 @@ +/* + * 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.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.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; +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 ChatModelCallInterceptor implements InstanceMethodsAroundInterceptor { + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, MethodInterceptResult result) throws Throwable { + ChatModelMetadataResolver.ApiMetadata apiMetadata = ChatModelMetadataResolver.getMetadata(objInst); + AbstractSpan span = ContextManager.createExitSpan("Spring-ai/" + apiMetadata.getProviderName() + "/call", apiMetadata.getPeer()); + SpanLayer.asGenAI(span); + span.setComponent(apiMetadata.getComponent()); + 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(); + if (chatOptions == null) { + return; + } + + if (chatOptions.getModel() != null) { + Tags.GEN_AI_REQUEST_MODEL.set(span, chatOptions.getModel()); + } + if (chatOptions.getTemperature() != null) { + Tags.GEN_AI_TEMPERATURE.set(span, String.valueOf(chatOptions.getTemperature())); + } + if (chatOptions.getTopK() != null) { + Tags.GEN_AI_TOP_K.set(span, String.valueOf(chatOptions.getTopK())); + } + if (chatOptions.getTopP() != null) { + 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 { + if (!(ret instanceof ChatResponse)) { + return ret; + } + + ChatResponse response = (ChatResponse) ret; + + AbstractSpan span = ContextManager.activeSpan(); + ChatResponseMetadata metadata = response.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_CLIENT_TOKEN_USAGE.set(span, String.valueOf(totalTokens)); + } + } + } + + Generation generation = response.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, response, 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, ChatResponse response, long totalTokens) { + int tokenThreshold = SpringAiPluginConfig.Plugin.SpringAi.CONTENT_COLLECT_THRESHOLD_TOKENS; + + if (tokenThreshold >= 0 && totalTokens < tokenThreshold) { + return; + } + + if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_INPUT_MESSAGES) { + collectPrompt(span, allArguments); + } + + if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_OUTPUT_MESSAGES) { + collectCompletion(span, response); + } + } + + private void collectPrompt(AbstractSpan span, Object[] allArguments) { + Prompt prompt = (Prompt) allArguments[0]; + if (prompt == null) { + return; + } + + InputMessages inputMessages = InputMessages.fromPrompt(prompt); + String inputMessagesJson = inputMessages.toJson(); + int limit = SpringAiPluginConfig.Plugin.SpringAi.INPUT_MESSAGES_LENGTH_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, ChatResponse response) { + + OutputMessages outputMessages = OutputMessages.fromChatResponse(response); + String outputMessagesJson = outputMessages.toJson(); + int limit = SpringAiPluginConfig.Plugin.SpringAi.OUTPUT_MESSAGES_LENGTH_LIMIT; + + if (limit > 0 && outputMessagesJson.length() > limit) { + outputMessagesJson = outputMessagesJson.substring(0, limit); + } + 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 new file mode 100644 index 0000000000..9abb852187 --- /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/ChatModelStreamInterceptor.java @@ -0,0 +1,266 @@ +/* + * 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.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.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.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; +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.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +public class ChatModelStreamInterceptor implements InstanceMethodsAroundInterceptor { + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, MethodInterceptResult result) throws Throwable { + ChatModelMetadataResolver.ApiMetadata apiMetadata = ChatModelMetadataResolver.getMetadata(objInst); + AbstractSpan span = ContextManager.createExitSpan("Spring-ai/" + apiMetadata.getProviderName() + "/stream", apiMetadata.getPeer()); + SpanLayer.asGenAI(span); + + span.setComponent(apiMetadata.getComponent()); + Tags.GEN_AI_OPERATION_NAME.set(span, Constants.CHAT); + Tags.GEN_AI_PROVIDER_NAME.set(span, apiMetadata.getProviderName()); + + Prompt prompt = (Prompt) allArguments[0]; + if (prompt == null) { + return; + } + + ChatOptions chatOptions = prompt.getOptions(); + if (chatOptions == null) { + return; + } + + 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()); + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Object ret) throws Throwable { + if (!ContextManager.isActive()) { + return ret; + } + + final AbstractSpan span = ContextManager.activeSpan(); + final ContextSnapshot snapshot = ContextManager.capture(); + + span.prepareForAsync(); + ContextManager.stopSpan(); + + @SuppressWarnings("unchecked") final Flux flux = (Flux) ret; + + final StreamState state = new StreamState(readAndClearStartTime()); + + 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)); + } + + private void onStreamNext(AbstractSpan span, ChatResponse response, StreamState state) { + state.lastResponseRef.set(response); + + final Generation generation = response.getResult(); + if (generation == null) { + return; + } + + recordTtfrIfFirstToken(span, generation, state); + recordFinishReason(generation, state); + appendCompletionChunk(generation, state); + } + + private void onStreamFinally(AbstractSpan span, Object[] allArguments, StreamState state) { + try { + ChatResponse finalResponse = state.lastResponseRef.get(); + long totalTokens = 0; + + if (finalResponse != null && finalResponse.getMetadata() != null) { + ChatResponseMetadata metadata = finalResponse.getMetadata(); + collectResponseTags(span, metadata, state); + totalTokens = collectUsageTags(span, metadata.getUsage()); + } + + int tokenThreshold = SpringAiPluginConfig.Plugin.SpringAi.CONTENT_COLLECT_THRESHOLD_TOKENS; + if (tokenThreshold >= 0 && totalTokens < tokenThreshold) { + return; + } + + if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_INPUT_MESSAGES) { + collectPrompt(span, allArguments); + } + + if (SpringAiPluginConfig.Plugin.SpringAi.COLLECT_OUTPUT_MESSAGES) { + 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; + } + + 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())); + } + + 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; + } + + InputMessages inputMessages = InputMessages.fromPrompt(prompt); + String inputMessagesJson = inputMessages.toJson(); + + int limit = SpringAiPluginConfig.Plugin.SpringAi.INPUT_MESSAGES_LENGTH_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) { + + 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)); + } + + OutputMessages outputMessages = OutputMessages.create().append(OutputMessages.OutputMessage.create("assistant", parts, finishReason)); + String outputMessagesJson = outputMessages.toJson(); + + int limit = SpringAiPluginConfig.Plugin.SpringAi.OUTPUT_MESSAGES_LENGTH_LIMIT; + if (limit > 0 && outputMessagesJson.length() > limit) { + outputMessagesJson = outputMessagesJson.substring(0, limit); + } + + Tags.GEN_AI_OUTPUT_MESSAGES.set(span, outputMessagesJson); + } + + 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 + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, Throwable t) { + if (ContextManager.isActive()) { + 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/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..929fd9bbd8 --- /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/tool/call"); + 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 new file mode 100644 index 0000000000..637599da57 --- /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,79 @@ +/* + * 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.tool.ToolCallback; +import org.springframework.ai.tool.definition.ToolDefinition; + +import java.lang.reflect.Method; + +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.name(); + + AbstractSpan span = ContextManager.createLocalSpan("Spring-ai/tool/execute/" + toolName); + span.setComponent(ComponentsDefine.SPRING_AI); + + 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]; + Tags.GEN_AI_TOOL_CALL_ARGUMENTS.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_CALL_RESULT.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); + } + } +} 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..501f5e026c --- /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,147 @@ +/* + * 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.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 javax.validation.constraints.NotNull; +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 { + for (AiProviderEnum provider : AiProviderEnum.values()) { + if (provider.getModelClassName() != null && provider.getValue() != null) { + MODEL_METADATA_MAP.put( + provider.getModelClassName(), + new ApiMetadata(provider.getValue(), matchComponent(provider)) + ); + } + } + } + + 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; + case ZHIPU_AI: + return ComponentsDefine.SPRING_AI_ZHIPU_AI; + case UNKNOWN: + default: + return ComponentsDefine.SPRING_AI_UNKNOWN; + } + } + + @NotNull + public static ApiMetadata getMetadata(Object chatModelInstance) { + ApiMetadata metadata = MODEL_METADATA_MAP.get(chatModelInstance.getClass().getName()); + if (metadata == null) { + MODEL_METADATA_MAP.get(AiProviderEnum.UNKNOWN); + } + return metadata; + } + + public static ApiMetadata getMetadata(String modelClassName) { + try { + return MODEL_METADATA_MAP.get(modelClassName); + } catch (Exception e) { + LOGGER.error("spring-ai plugin get modelMetadata error: ", e); + return null; + } + } + + public static class ApiMetadata { + + private final String providerName; + private final OfficialComponent component; + private volatile String baseUrl; + private volatile String completionsPath; + + 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; + } + + 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; + } + } +} \ 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/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..d2d5eef6f6 --- /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,74 @@ +/* + * 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_INPUT_MESSAGES = false; + + /** + * Whether to collect the completion content (output text) of the GenAI response. + */ + public static boolean COLLECT_OUTPUT_MESSAGES = 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. + */ + public static int INPUT_MESSAGES_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 OUTPUT_MESSAGES_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_INPUT_MESSAGES} or {@link #COLLECT_OUTPUT_MESSAGES} to be enabled first. + * 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/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..7348a0c11f --- /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,31 @@ +/* + * 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"; + + public static final String SKYWALKING_CONTEXT_SNAPSHOT = "SKYWALKING_CONTEXT_SNAPSHOT"; + + 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 new file mode 100644 index 0000000000..13aa1d381d --- /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); + } + + @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 true; + } + } + }; + } +} 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/DefaultToolCallingManagerInstrumentation.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 new file mode 100644 index 0000000000..595472834f --- /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/DefaultToolCallingManagerInstrumentation.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.apm.plugin.spring.ai.v1.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; +import org.apache.skywalking.apm.agent.core.plugin.match.NameMatch; + +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 DefaultToolCallingManagerInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { + + 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.DefaultToolCallingManagerInterceptor"; + + private static final String INTERCEPT_METHOD = "executeToolCall"; + + @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(3)).and(takesArgumentWithType(0, "org.springframework.ai.chat.prompt.Prompt")); + } + + @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..99a4029b00 --- /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,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.HierarchyMatch; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; +import static org.apache.skywalking.apm.agent.core.plugin.bytebuddy.ArgumentTypeNameMatch.takesArgumentWithType; + +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.v1.ToolCallbackCallInterceptor"; + + @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("call") + .and(takesArguments(2)) + .and(takesArgumentWithType(0, "java.lang.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/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 new file mode 100644 index 0000000000..e226864906 --- /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/AnthropicApiInstrumentation.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 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.provider.AnthropicApiConstructorInterceptor"; + + @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(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/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..17382e75eb --- /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.provider.DeepSeekApiConstructorInterceptor"; + + @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..93f7b57af4 --- /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.provider.HuggingfaceChatModelConstructorInterceptor"; + + @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..c8a5ca3ad1 --- /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.MiniMaxApiConstructorInterceptor"; + + @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/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..ffcaee9198 --- /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.MistralAiApiConstructorInterceptor"; + + @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/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 new file mode 100644 index 0000000000..624262322e --- /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,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 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.OpenAiApiConstructorInterceptor"; + + @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/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/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..5137ab7d7e --- /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 { + + UNKNOWN("unknown", 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("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/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> partMaps = parts.stream() + .map(MessagePart::toMap) + .collect(Collectors.toList()); + + Map map = new HashMap<>(); + map.put("role", role != null ? role : "unknown"); + map.put("parts", partMaps); + return map; + } + } + + public interface MessagePart { + Map toMap(); + } + + public static class TextPart implements MessagePart { + private final String content; + + public TextPart(String content) { + this.content = content; + } + + @Override + public Map toMap() { + return Map.of("type", "text", "content", content != null ? content : ""); + } + } + + public static class ToolCallPart implements MessagePart { + private final String id; + private final String name; + private final String arguments; + + public ToolCallPart(String id, String name, String arguments) { + this.id = id; + this.name = name; + this.arguments = arguments; + } + + @Override + public Map toMap() { + Map map = new java.util.LinkedHashMap<>(); + map.put("type", "tool_call"); + if (id != null) { + map.put("id", id); + } + map.put("name", name != null ? name : ""); + map.put("arguments", arguments != null ? arguments : ""); + return map; + } + } + + public static class ToolCallResponsePart implements MessagePart { + private final String id; + private final String result; + + public ToolCallResponsePart(String id, String result) { + this.id = id; + this.result = result; + } + + @Override + public Map toMap() { + Map map = new LinkedHashMap<>(); + map.put("type", "tool_call_response"); + if (id != null) { + map.put("id", id); + } + map.put("result", result != null ? result : ""); + return map; + } + } + + public static InputMessages fromPrompt(Prompt prompt) { + InputMessages inputMessages = InputMessages.create(); + + if (prompt == null || prompt.getInstructions() == null) { + return inputMessages; + } + + for (Message message : prompt.getInstructions()) { + MessageType type = message.getMessageType(); + + switch (type) { + case SYSTEM: + inputMessages.append(InputMessage.create( + type.getValue(), + textParts(message.getText()) + )); + break; + + case USER: + inputMessages.append(InputMessage.create( + type.getValue(), + userMessageParts(message) + )); + break; + + case ASSISTANT: + inputMessages.append(InputMessage.create( + type.getValue(), + assistantMessageParts(message) + )); + break; + + case TOOL: + inputMessages.append(InputMessage.create( + type.getValue(), + toolMessageParts(message) + )); + break; + default: + inputMessages.append(InputMessage.create( + type.getValue(), + textParts(message.getText()) + )); + break; + } + } + + return inputMessages; + } + + private static List textParts(String text) { + List parts = new ArrayList<>(); + if (text != null && !text.isEmpty()) { + parts.add(new TextPart(text)); + } + return parts; + } + + private static List userMessageParts(Message message) { + List parts = new ArrayList<>(); + + String text = message.getText(); + if (text != null && !text.isEmpty()) { + parts.add(new TextPart(text)); + } + + if (message instanceof UserMessage) { + UserMessage userMessage = (UserMessage) message; + if (userMessage.getMedia() != null) { + for (Media media : userMessage.getMedia()) { + parts.add(new TextPart("[media: " + media.getMimeType() + "]")); + } + } + } + + return parts; + } + + private static List assistantMessageParts(Message message) { + List parts = new ArrayList<>(); + + String text = message.getText(); + if (text != null && !text.isEmpty()) { + parts.add(new TextPart(text)); + } + + if (message instanceof AssistantMessage) { + AssistantMessage assistantMessage = (AssistantMessage) message; + List toolCalls = assistantMessage.getToolCalls(); + if (toolCalls != null) { + for (AssistantMessage.ToolCall toolCall : toolCalls) { + parts.add(new ToolCallPart( + toolCall.id(), + toolCall.name(), + toolCall.arguments() + )); + } + } + } + + return parts; + } + + private static List toolMessageParts(Message message) { + List parts = new ArrayList<>(); + + if (message instanceof ToolResponseMessage) { + ToolResponseMessage toolResponse = (ToolResponseMessage) message; + List responses = toolResponse.getResponses(); + if (responses != null) { + for (ToolResponseMessage.ToolResponse response : responses) { + parts.add(new ToolCallResponsePart( + response.id(), + response.responseData() + )); + } + } + } else { + String text = message.getText(); + if (text != null && !text.isEmpty()) { + parts.add(new ToolCallResponsePart(null, text)); + } + } + + return parts; + } +} 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/OutputMessages.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/OutputMessages.java new file mode 100644 index 0000000000..bc73165fc0 --- /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/OutputMessages.java @@ -0,0 +1,121 @@ +/* + * 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.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class OutputMessages { + private final List messages = new ArrayList<>(); + + public static OutputMessages create() { + return new OutputMessages(); + } + + public OutputMessages append(OutputMessage message) { + this.messages.add(message); + return this; + } + + public String toJson() { + return GsonUtil.toJson( + messages.stream() + .map(OutputMessage::toMap) + .collect(Collectors.toList()) + ); + } + + public static class OutputMessage { + private final String role; + private final List parts; + private final String finishReason; + + private OutputMessage(String role, List parts, String finishReason) { + this.role = role; + this.parts = parts; + this.finishReason = finishReason; + } + + public static OutputMessage create(String role, List parts, String finishReason) { + return new OutputMessage(role, parts, finishReason); + } + + public Map toMap() { + Map map = new LinkedHashMap<>(); + map.put("role", role != null ? role : "assistant"); + map.put("parts", parts.stream() + .map(InputMessages.MessagePart::toMap) + .collect(Collectors.toList())); + if (finishReason != null && !finishReason.isEmpty()) { + map.put("finish_reason", finishReason); + } + return map; + } + } + + public static OutputMessages fromChatResponse(ChatResponse chatResponse) { + OutputMessages outputMessages = OutputMessages.create(); + + if (chatResponse == null || chatResponse.getResults() == null) { + return outputMessages; + } + + for (Generation generation : chatResponse.getResults()) { + List messageParts = new ArrayList<>(); + + AssistantMessage assistantMessage = generation.getOutput(); + if (assistantMessage != null) { + // Text content + String text = assistantMessage.getText(); + if (text != null && !text.isEmpty()) { + messageParts.add(new InputMessages.TextPart(text)); + } + + // Tool calls + List toolCalls = assistantMessage.getToolCalls(); + if (toolCalls != null) { + for (AssistantMessage.ToolCall toolCall : toolCalls) { + messageParts.add(new InputMessages.ToolCallPart( + toolCall.id(), + toolCall.name(), + toolCall.arguments() + )); + } + } + } + + String finishReason = ""; + if (generation.getMetadata() != null && generation.getMetadata().getFinishReason() != null) { + finishReason = generation.getMetadata().getFinishReason().toLowerCase(); + } + + outputMessages.append(OutputMessage.create("assistant", messageParts, finishReason)); + } + + return outputMessages; + } +} 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/AnthropicApiConstructorInterceptor.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 new file mode 100644 index 0000000000..4f9e73ca7e --- /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/AnthropicApiConstructorInterceptor.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 AnthropicApiConstructorInterceptor 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((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/DeepSeekApiConstructorInterceptor.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 new file mode 100644 index 0000000000..e1f26e5e8b --- /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/DeepSeekApiConstructorInterceptor.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 DeepSeekApiConstructorInterceptor 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/HuggingfaceChatModelConstructorInterceptor.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 new file mode 100644 index 0000000000..234b876434 --- /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/HuggingfaceChatModelConstructorInterceptor.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 HuggingfaceChatModelConstructorInterceptor implements InstanceConstructorInterceptor { + + @Override + public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable { + ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.HUGGINGFACE.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/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/MistralAiApiConstructorInterceptor.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 new file mode 100644 index 0000000000..07eac44d5b --- /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/MistralAiApiConstructorInterceptor.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 MistralAiApiConstructorInterceptor implements InstanceConstructorInterceptor { + + @Override + public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable { + ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.MISTRAL_AI.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/OllamaApiConstructorInterceptor.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 new file mode 100644 index 0000000000..abdc2d9f5a --- /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/OllamaApiConstructorInterceptor.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 OllamaApiConstructorInterceptor implements InstanceConstructorInterceptor { + + @Override + public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable { + 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/OpenAiApiConstructorInterceptor.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 new file mode 100644 index 0000000000..da0f3bbf2f --- /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/OpenAiApiConstructorInterceptor.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 OpenAiApiConstructorInterceptor implements InstanceConstructorInterceptor { + + @Override + public void onConstruct(EnhancedInstance objInst, Object[] allArguments) throws Throwable { + ChatModelMetadataResolver.ApiMetadata metadata = ChatModelMetadataResolver.getMetadata(AiProviderEnum.OPENAI.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/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 new file mode 100644 index 0000000000..5c7eec110c --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ai-1.x-plugin/src/main/resources/skywalking-plugin.def @@ -0,0 +1,27 @@ +# 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.ChatModelInstrumentation +spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.ToolCallbackInstrumentation +spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.DefaultToolCallingManagerInstrumentation +spring-ai-1.x=org.apache.skywalking.apm.plugin.spring.ai.v1.define.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.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/apm-sniffer/config/agent.config b/apm-sniffer/config/agent.config index 9056993626..84b5c41622 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 input messages of the GenAI request. +plugin.springai.collect_input_messages=${SW_PLUGIN_SPRINGAI_COLLECT_INPUT_MESSAGES:false} +# Whether to collect the output messages of the GenAI response. +plugin.springai.collect_output_messages=${SW_PLUGIN_SPRINGAI_COLLECT_OUTPUT_MESSAGES: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/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 diff --git a/pom.xml b/pom.xml index 8260a17784..b675688a4a 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..8cb423ece2 --- /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 -Dskywalking.plugin.springai.collect_input_messages=true -Dskywalking.plugin.springai.collect_output_messages=true -Dskywalking.plugin.springai.collect_tool_input=true -Dskywalking.plugin.springai.collect_tool_output=true -jar ${agent_opts} ${home}/../libs/spring-ai-1.x-scenario.jar & \ No newline at end of file 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..3d543ad27f --- /dev/null +++ b/test/plugin/scenarios/spring-ai-1.x-scenario/config/expectedData.yaml @@ -0,0 +1,156 @@ +# 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: spring-ai-1.x-scenario + segmentSize: gt 0 + segments: + - segmentId: not null + spans: + - operationName: Spring-ai/tool/execute/get_weather + parentSpanId: 0 + spanId: 1 + spanLayer: Unknown + startTime: not null + endTime: not null + componentId: 178 + isError: false + spanType: Local + peer: '' + skipAnalysis: false + tags: + - { key: gen_ai.tool.name, value: get_weather } + - { key: gen_ai.operation.name, value: execute_tool } + - { key: gen_ai.tool.call.arguments, value: '{"arg0":"new york"}' } + - { key: gen_ai.tool.call.result, value: '"Sunny, 10°C"' } + + - operationName: Spring-ai/tool/call + parentSpanId: -1 + spanId: 0 + spanLayer: Unknown + startTime: not null + endTime: not null + componentId: 178 + isError: false + spanType: Local + peer: '' + skipAnalysis: false + refs: + - { parentEndpoint: 'GET:/spring-ai-1.x-scenario/case/spring-ai-1.x-scenario-case', + networkAddress: '', refType: CrossThread, parentSpanId: 4, parentTraceSegmentId: not null, + parentServiceInstance: not null, parentService: spring-ai-1.x-scenario, + traceId: not null } + + - segmentId: not null + spans: + - operationName: Spring-ai/tool/execute/get_weather + parentSpanId: 2 + spanId: 3 + spanLayer: Unknown + spanType: Local + startTime: not null + endTime: not null + tags: + - { key: gen_ai.tool.name, value: get_weather } + - { key: gen_ai.operation.name, value: execute_tool } + - { key: gen_ai.tool.call.arguments, value: '{"arg0":"new york"}' } + - { key: gen_ai.tool.call.result, value: '"Sunny, 10°C"' } + + - operationName: Spring-ai/tool/call + parentSpanId: 1 + spanId: 2 + spanLayer: Unknown + startTime: not null + endTime: not null + spanType: Local + + - operationName: Spring-ai/openai/call + parentSpanId: 0 + spanId: 1 + spanLayer: not null + startTime: not null + endTime: not null + componentId: 173 + spanType: Exit + peer: http://localhost:8080/spring-ai-1.x-scenario/llm/v1/chat/completions + tags: + - { key: gen_ai.operation.name, value: chat } + - { key: gen_ai.provider.name, value: openai } + - { key: gen_ai.request.model, value: gpt-4.1-2025-04-14 } + - { key: gen_ai.request.temperature, value: '0.7' } + - { key: gen_ai.request.top_p, value: '0.9' } + - { key: gen_ai.response.id, value: 'chatcmpl-CyJXJt7gxwDgz' } + - { key: gen_ai.response.model, value: gpt-4.1-2025-04-14 } + - { key: gen_ai.usage.input_tokens, value: '52' } + - { key: gen_ai.usage.output_tokens, value: '17' } + - { key: gen_ai.client.token.usage, value: '69' } + - { key: gen_ai.response.finish_reasons, value: STOP } + - { key: gen_ai.input.messages, value: not null } + - { key: gen_ai.output.messages, value: not null } + + - operationName: Spring-ai/openai/stream + parentSpanId: 0 + spanId: 4 + spanLayer: not null + startTime: not null + endTime: not null + componentId: 173 + spanType: Exit + peer: http://localhost:8080/spring-ai-1.x-scenario/llm/v1/chat/completions + tags: + - { key: gen_ai.operation.name, value: chat } + - { key: gen_ai.provider.name, value: openai } + - { key: gen_ai.request.model, value: gpt-4.1-2025-04-14 } + - { key: gen_ai.request.temperature, value: '0.7' } + - { key: gen_ai.request.top_k, value: null } + - { key: gen_ai.request.top_p, value: '0.9' } + - { key: gen_ai.stream.ttfr, value: not null } + - { key: gen_ai.response.id, value: 'chatcmpl-fc1b64d3' } + - { key: gen_ai.response.model, value: gpt-4.1-2025-04-14 } + - { key: gen_ai.response.finish_reasons, value: STOP } + - { key: gen_ai.usage.input_tokens, value: '104' } + - { key: gen_ai.usage.output_tokens, value: '34' } + - { key: gen_ai.client.token.usage, value: '138' } + - { key: gen_ai.input.messages, value: not null } + - { key: gen_ai.output.messages, value: not null } + + - operationName: /spring-ai-1.x-scenario/llm/v1/chat/completions + parentSpanId: 0 + spanId: 5 + spanLayer: Http + startTime: not null + endTime: not null + componentId: 99 + isError: false + spanType: Exit + peer: localhost:8080 + skipAnalysis: false + tags: + - { key: url, value: 'http://localhost:8080/spring-ai-1.x-scenario/llm/v1/chat/completions' } + - { key: http.method, value: POST } + - { key: http.status_code, value: '200' } + + - 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 + spanType: Entry + componentId: 1 + 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' } \ No newline at end of file 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..073d9271d5 --- /dev/null +++ b/test/plugin/scenarios/spring-ai-1.x-scenario/configuration.yml @@ -0,0 +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. + +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 +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..b9d39f538a --- /dev/null +++ b/test/plugin/scenarios/spring-ai-1.x-scenario/pom.xml @@ -0,0 +1,156 @@ + + + + 4.0.0 + + org.apache.skywalking + spring-ai-1.x-scenario + 5.0.0 + + + UTF-8 + 17 + 3.8.1 + + + spring-ai-1.x-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.projectlombok + lombok + 1.18.42 + + + + + + + + org.springframework.ai + spring-ai-bom + 1.1.0 + pom + import + + + + + + spring-ai-1.x-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..deed40fcfe --- /dev/null +++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/assembly/assembly.xml @@ -0,0 +1,41 @@ + + + + + zip + + + + + ./bin + 0775 + + + + + + ./target/spring-ai-1.x-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/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..d6512b726e --- /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,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 test.apache.skywalking.apm.testcase.jdk.httpclient.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.ai.chat.client.ChatClient; +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") +@RequiredArgsConstructor +public class CaseController { + + private final WeatherTool weatherTool; + private final ChatClient chatClient; + + @GetMapping("/healthCheck") + public String healthCheck() { + return "Success"; + } + + @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(); + + 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..5221245280 --- /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,230 @@ +/* + * 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 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; + +@RestController +@RequestMapping("/llm") +public class LLMMockController { + @RequestMapping("/v1/chat/completions") + 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-fc1b64d3"; + 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": [ + { + "finish_reason": "tool_calls", + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "function": { + "arguments": "{\\"arg0\\":\\"new york\\"}", + "name": "get_weather" + }, + "id": "call_iV4bvFIZujbb", + "type": "function" + } + ] + } + } + ], + "created": 1768490813, + "id": "chatcmpl-CyJXJt7gxwDgz", + "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 + }, + "model": "gpt-4.1-2025-04-14", + "object": "chat.completion" + } + """; + + String finalResponse = """ + { + "choices": [ + { + "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, + "id":"chatcmpl-CyJXJt7gxwDgz", + "model": "gpt-4.1-2025-04-14", + "object": "chat.completion" + } + """; + + if ("tool".equals(lastRole)) { + return JSON.parseObject(finalResponse); + } + + 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": { + "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 + }, + "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"); + } +} 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..c4f5c58851 --- /dev/null +++ b/test/plugin/scenarios/spring-ai-1.x-scenario/src/main/resources/application.yaml @@ -0,0 +1,35 @@ +# 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: xxxxxxxxxxxxxxxxxxxxxxxxxxx + base-url: http://localhost:8080/spring-ai-1.x-scenario/llm + 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..9d9826957c --- /dev/null +++ b/test/plugin/scenarios/spring-ai-1.x-scenario/support-version.list @@ -0,0 +1,18 @@ +# 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.0.1 +1.1.1 \ No newline at end of file From a2cc30cb4fff55115e92929ca9d63e8b636e0fbf Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Wed, 25 Feb 2026 22:22:03 +0800 Subject: [PATCH 2/3] add the change --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 29acf896e1..27e5c01fde 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Release Notes. ------------------ * Added support for Lettuce reactive Redis commands. +* Add Spring AI 1.x plugin and GenAI layer. All issues and pull requests are [here](https://github.com/apache/skywalking/milestone/249?closed=1) From 03f147dcd8f96a9c5286cdfe172fe47a7dc5011e Mon Sep 17 00:00:00 2001 From: peachisai <2581009893@qq.com> Date: Wed, 25 Feb 2026 23:43:48 +0800 Subject: [PATCH 3/3] add the GenAI plugin doc --- docs/en/setup/service-agent/java-agent/Supported-list.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/setup/service-agent/java-agent/Supported-list.md b/docs/en/setup/service-agent/java-agent/Supported-list.md index bb841000a4..61f4cfd67f 100644 --- a/docs/en/setup/service-agent/java-agent/Supported-list.md +++ b/docs/en/setup/service-agent/java-agent/Supported-list.md @@ -160,6 +160,8 @@ metrics based on the tracing data. * [MyBatis](https://github.com/mybatis/mybatis-3) 3.4.x -> 3.5.x * Event * [GuavaEventBus](https://github.com/google/guava) 19.x -> 31.x-jre +* GenAI + * [spring-ai](https://github.com/spring-projects/spring-ai) 1.x # Meter Plugins The meter plugin provides the advanced metrics collections, which are not a part of tracing.