Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Release Notes.
* Only publish `apm-application-toolkit` modules to Maven Central. Agent and plugins are distributed via download package and Docker images.
* Add unified release script (`tools/releasing/release.sh`) with two-step flow: `prepare-vote` and `vote-passed`.
* Fix an issue where `JDBCPluginConfig.Plugin.JDBC.SQL_BODY_MAX_LENGTH` was not honored by clickhouse-0.3.1 and clickhouse-0.3.2.x plugins.
- Add tracing support for vector-store retrieval operations.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,21 @@ public static final class HTTP {
*/
public static final StringTag GEN_AI_OUTPUT_MESSAGES = new StringTag(42, "gen_ai.output.messages");

/**
* GEN_AI_DATA_SOURCE_ID represents the data source identifier.
*/
public static final StringTag GEN_AI_DATA_SOURCE_ID = new StringTag(43, "gen_ai.data_source.id");

/**
* GEN_AI_RETRIEVAL_DOCUMENTS represents the documents retrieved.
*/
public static final StringTag GEN_AI_RETRIEVAL_DOCUMENTS = new StringTag(44, "gen_ai.retrieval.documents");

/**
* GEN_AI_RETRIEVAL_QUERY_TEXT represents the query text used for retrieval.
*/
public static final StringTag GEN_AI_RETRIEVAL_QUERY_TEXT = new StringTag(45, "gen_ai.retrieval.query.text");

/**
* Creates a {@code StringTag} with the given key and cache it, if it's created before, simply return it without
* creating a new one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store</artifactId>
<version>1.1.0</version>
<scope>provided</scope>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package org.apache.skywalking.apm.plugin.spring.ai.v1;

import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor;

public class AbstractObservationVectorStoreConstructorInterceptor implements InstanceConstructorInterceptor {

@Override
public void onConstruct(EnhancedInstance objInst, Object[] allArguments) {
objInst.setSkyWalkingDynamicField(new VectorStoreEnhanceContext(resolveContextFromArgument(allArguments[0])));
}

private EmbeddingModelEnhanceContext resolveContextFromArgument(Object argument) {
if (argument instanceof EnhancedInstance) {
return getOrCreateContext((EnhancedInstance) argument);
}
return null;
}

private EmbeddingModelEnhanceContext getOrCreateContext(EnhancedInstance embeddingModel) {
Object context = embeddingModel.getSkyWalkingDynamicField();
if (context instanceof EmbeddingModelEnhanceContext) {
return (EmbeddingModelEnhanceContext) context;
}
EmbeddingModelEnhanceContext embeddingModelEnhanceContext = new EmbeddingModelEnhanceContext();
embeddingModel.setSkyWalkingDynamicField(embeddingModelEnhanceContext);
return embeddingModelEnhanceContext;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package org.apache.skywalking.apm.plugin.spring.ai.v1;

import org.apache.skywalking.apm.agent.core.context.ContextManager;
import org.apache.skywalking.apm.agent.core.context.tag.Tags;
import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
import org.apache.skywalking.apm.agent.core.util.GsonUtil;
import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class AbstractObservationVectorStoreInterceptor implements InstanceMethodsAroundInterceptor {

@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
MethodInterceptResult result) throws Throwable {
SearchRequest request = (SearchRequest) allArguments[0];
String dataSourceId = objInst.getClass().getSimpleName();

try {
VectorStoreObservationContext context =
createObservationContext(objInst, request);

String resolved =
resolveDataSourceId(context, objInst);

if (StringUtils.hasText(resolved)) {
dataSourceId = resolved;
}
} catch (Throwable ignored) {

}

AbstractSpan span = ContextManager.createExitSpan(Constants.RETRIEVAL + "/" + dataSourceId, dataSourceId);

SpanLayer.asGenAI(span);
span.setComponent(ComponentsDefine.SPRING_AI);
Tags.GEN_AI_OPERATION_NAME.set(span, Constants.RETRIEVAL);
Tags.GEN_AI_DATA_SOURCE_ID.set(span, dataSourceId);
String model = resolveEmbeddingModelName(objInst);
if (StringUtils.hasText(model)) {
Tags.GEN_AI_REQUEST_MODEL.set(span, model);
}

if (request != null) {
Tags.GEN_AI_TOP_K.set(span, String.valueOf(request.getTopK()));
if (StringUtils.hasText(request.getQuery())) {
Tags.GEN_AI_RETRIEVAL_QUERY_TEXT.set(span, request.getQuery());
Comment thread
wu-sheng marked this conversation as resolved.
Outdated
}
}
}

@Override
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
Object ret) throws Throwable {
if (!ContextManager.isActive()) {
return ret;
}
try {
if (ret instanceof List<?>) {
Tags.GEN_AI_RETRIEVAL_DOCUMENTS.set(ContextManager.activeSpan(), toDocumentsJson((List<?>) ret));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gen_ai.retrieval.documents is likewise emitted unconditionally, unlike COLLECT_OUTPUT_MESSAGES which gates analogous response content (default false).

Lower risk than the query text since only id + score are serialized (not document bodies), but for consistency consider a COLLECT_RETRIEVAL_DOCUMENTS = false flag. Note the JSON grows with topK and has no size cap.

}
} 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);
Comment thread
wu-sheng marked this conversation as resolved.
Outdated
}
}

private VectorStoreObservationContext createObservationContext(EnhancedInstance objInst, SearchRequest request) {
VectorStoreObservationContext.Builder builder = ((AbstractObservationVectorStore) objInst)
.createObservationContextBuilder(VectorStoreObservationContext.Operation.QUERY.value());
if (request != null) {
builder.queryRequest(request);
}
return builder.build();
}

private String resolveEmbeddingModelName(EnhancedInstance objInst) {
Object context = objInst.getSkyWalkingDynamicField();
if (context instanceof VectorStoreEnhanceContext) {
return ((VectorStoreEnhanceContext) context).getEmbeddingModelName();
}
return null;
}

private String resolveDataSourceId(VectorStoreObservationContext context, EnhancedInstance objInst) {
StringBuilder dataSourceId = new StringBuilder();
appendDataSourcePart(dataSourceId, context.getDatabaseSystem());
appendDataSourcePart(dataSourceId, context.getNamespace());
appendDataSourcePart(dataSourceId, context.getCollectionName());
if (dataSourceId.length() > 0) {
return dataSourceId.toString();
}
return objInst.getClass().getSimpleName();
}

private void appendDataSourcePart(StringBuilder dataSourceId, String value) {
if (!StringUtils.hasText(value)) {
return;
}
if (dataSourceId.length() > 0) {
dataSourceId.append('/');
}
dataSourceId.append(value);
}

private String toDocumentsJson(List<?> documents) {
List<Map<String, Object>> retrievalDocuments = new ArrayList<>(documents.size());
for (Object item : documents) {
if (!(item instanceof Document)) {
continue;
}
Document document = (Document) item;
Map<String, Object> documentMap = new LinkedHashMap<>();
documentMap.put("id", document.getId());
if (document.getScore() != null) {
documentMap.put("score", document.getScore());
}
retrievalDocuments.add(documentMap);
}
return GsonUtil.toJson(retrievalDocuments);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ErrorTypeResolver;
import org.apache.skywalking.apm.plugin.spring.ai.v1.config.SpringAiPluginConfig;
import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants;
import org.apache.skywalking.apm.plugin.spring.ai.v1.messages.InputMessages;
Expand Down Expand Up @@ -129,7 +130,9 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA
@Override
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
if (ContextManager.isActive()) {
ContextManager.activeSpan().log(t);
AbstractSpan span = ContextManager.activeSpan();
span.log(t);
ErrorTypeResolver.setErrorType(span, t);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ChatModelMetadataResolver;
import org.apache.skywalking.apm.plugin.spring.ai.v1.common.ErrorTypeResolver;
import org.apache.skywalking.apm.plugin.spring.ai.v1.config.SpringAiPluginConfig;
import org.apache.skywalking.apm.plugin.spring.ai.v1.contant.Constants;
import org.apache.skywalking.apm.plugin.spring.ai.v1.messages.InputMessages;
Expand Down Expand Up @@ -94,11 +95,16 @@ public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allA

return flux
.doOnNext(response -> onStreamNext(span, response, state))
.doOnError(span::log)
.doOnError(t -> recordError(span, t))
.doFinally(signalType -> onStreamFinally(span, allArguments, state))
.contextWrite(c -> c.put(Constants.SKYWALKING_CONTEXT_SNAPSHOT, snapshot));
}

private void recordError(AbstractSpan span, Throwable t) {
span.log(t);
ErrorTypeResolver.setErrorType(span, t);
}

private void onStreamNext(AbstractSpan span, ChatResponse response, StreamState state) {
state.lastResponseRef.set(response);

Expand Down Expand Up @@ -248,7 +254,9 @@ private Long readAndClearStartTime() {
@Override
public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
if (ContextManager.isActive()) {
ContextManager.activeSpan().log(t);
AbstractSpan span = ContextManager.activeSpan();
span.log(t);
ErrorTypeResolver.setErrorType(span, t);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package org.apache.skywalking.apm.plugin.spring.ai.v1;

import org.springframework.util.StringUtils;

public class EmbeddingModelEnhanceContext {

private volatile String embeddingModelName;

public String getEmbeddingModelName() {
return embeddingModelName;
}

public void setEmbeddingModelNameIfAbsent(String embeddingModelName) {
if (!StringUtils.hasText(this.embeddingModelName) && StringUtils.hasText(embeddingModelName)) {
this.embeddingModelName = embeddingModelName;
}
}
}
Loading
Loading