diff --git a/.gitignore b/.gitignore index 0c0c62ac..1ff95012 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ target/ ### IntelliJ IDEA ### .idea/* +**/.idea/* .idea/modules.xml .idea/jarRepositories.xml .idea/compiler.xml diff --git a/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java b/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java index 01ba2082..2af8f803 100644 --- a/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java +++ b/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java @@ -26,10 +26,12 @@ import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankModel; import com.alibaba.dashscope.audio.asr.transcription.Transcription; import com.alibaba.dashscope.audio.tts.SpeechSynthesizer; +import io.micrometer.observation.ObservationRegistry; import org.jetbrains.annotations.NotNull; import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackContext; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -96,7 +98,7 @@ public DashScopeChatModel dashscopeChatModel(DashScopeConnectionProperties commo DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, List toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler) { + ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry) { if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); @@ -105,8 +107,8 @@ public DashScopeChatModel dashscopeChatModel(DashScopeConnectionProperties commo var dashscopeApi = dashscopeChatApi(commonProperties, chatProperties, restClientBuilder, webClientBuilder, responseErrorHandler); - return new DashScopeChatModel(dashscopeApi, chatProperties.getOptions(), functionCallbackContext, - retryTemplate); + return new DashScopeChatModel(dashscopeApi, chatProperties.getOptions(), functionCallbackContext, retryTemplate, + observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); } @Bean diff --git a/spring-ai-alibaba-examples/observability-example/compose.yaml b/spring-ai-alibaba-examples/observability-example/compose.yaml new file mode 100644 index 00000000..ff23f8a8 --- /dev/null +++ b/spring-ai-alibaba-examples/observability-example/compose.yaml @@ -0,0 +1,5 @@ +services: + zipkin: + image: 'openzipkin/zipkin:latest' + ports: + - '9411:9411' diff --git a/spring-ai-alibaba-examples/observability-example/pom.xml b/spring-ai-alibaba-examples/observability-example/pom.xml new file mode 100644 index 00000000..f5b7f539 --- /dev/null +++ b/spring-ai-alibaba-examples/observability-example/pom.xml @@ -0,0 +1,120 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.alibaba.cloud.ai + observability-example + 0.0.1-SNAPSHOT + observability-example + Demo project for Spring AI Alibaba + + + 17 + 1.0.0-M3 + 1.0.0-M3.2 + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + io.micrometer + micrometer-core + 1.13.6 + + + + io.micrometer + micrometer-tracing-bridge-otel + 1.3.4 + + + + + + + + io.opentelemetry + opentelemetry-sdk-extension-autoconfigure-spi + 1.37.0 + + + io.opentelemetry + opentelemetry-exporter-common + 1.37.0 + + + io.opentelemetry + opentelemetry-exporter-otlp-common + 1.37.0 + + + io.opentelemetry + opentelemetry-exporter-otlp + 1.37.0 + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter + ${spring-ai-alibaba.version} + + + + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/FileSpanExporterAutoConfiguration.java b/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/FileSpanExporterAutoConfiguration.java new file mode 100644 index 00000000..fcc606a4 --- /dev/null +++ b/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/FileSpanExporterAutoConfiguration.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 com.alibaba.cloud.ai.example.observability; + +import com.alibaba.cloud.ai.example.observability.exporter.oltp.OtlpFileSpanExporter; +import com.alibaba.cloud.ai.example.observability.exporter.oltp.OtlpFileSpanExporterProvider; + +import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass({OtlpFileSpanExporter.class}) +public class FileSpanExporterAutoConfiguration { + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledTracing + OtlpFileSpanExporter outputSpanExporter() { + OtlpFileSpanExporterProvider provider = new OtlpFileSpanExporterProvider(); + return (OtlpFileSpanExporter)provider.createExporter(null); + } +} diff --git a/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/ObservabilityApplication.java b/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/ObservabilityApplication.java new file mode 100644 index 00000000..9f49f676 --- /dev/null +++ b/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/ObservabilityApplication.java @@ -0,0 +1,75 @@ +package com.alibaba.cloud.ai.example.observability; + +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatProperties; +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import io.micrometer.observation.ObservationRegistry; +import io.opentelemetry.api.trace.Span; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.model.function.FunctionCallback; +import org.springframework.ai.model.function.FunctionCallbackContext; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.stereotype.Controller; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.List; +import java.util.Map; + +@SpringBootApplication +public class ObservabilityApplication { + + public static void main(String[] args) { + SpringApplication.run(ObservabilityApplication.class, args); + } + + @Bean + ChatClient chatClient(ChatClient.Builder builder) { + return builder.build(); + } + + @Bean + @ConditionalOnProperty(prefix = DashScopeChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeChatModel dashscopeChatModel(DashScopeChatProperties chatProperties, List toolFunctionCallbacks, + FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate, + ObjectProvider observationRegistry, DashScopeApi dashScopeApi) { + + if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { + chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); + } + + return new DashScopeChatModel(dashScopeApi, chatProperties.getOptions(), functionCallbackContext, retryTemplate, + observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + } +} + +@Controller +@ResponseBody +class JokeController { + + private final ChatClient chatClient; + + JokeController(ChatClient chatClient) { + this.chatClient = chatClient; + } + + @GetMapping("/joke") + Map joke() { + var reply = chatClient + .prompt() + .user(""" + tell me a joke. be concise. don't send anything except the joke. + """) + .call() + .content(); + Span currentSpan = Span.current(); + return Map.of("joke", reply, "traceId", currentSpan.getSpanContext().getTraceId()); + } +} diff --git a/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/exporter/oltp/JsonUtil.java b/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/exporter/oltp/JsonUtil.java new file mode 100755 index 00000000..8522f5d0 --- /dev/null +++ b/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/exporter/oltp/JsonUtil.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.alibaba.cloud.ai.example.observability.exporter.oltp; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.io.SegmentedStringWriter; +import java.io.IOException; + +final class JsonUtil { + + static final JsonFactory JSON_FACTORY = new JsonFactory(); + + static JsonGenerator create(SegmentedStringWriter stringWriter) { + try { + return JSON_FACTORY.createGenerator(stringWriter); + } catch (IOException e) { + throw new IllegalStateException("Unable to create in-memory JsonGenerator, can't happen.", e); + } + } + + private JsonUtil() {} +} diff --git a/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/exporter/oltp/OtlpFileSpanExporter.java b/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/exporter/oltp/OtlpFileSpanExporter.java new file mode 100755 index 00000000..e11c117f --- /dev/null +++ b/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/exporter/oltp/OtlpFileSpanExporter.java @@ -0,0 +1,74 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.alibaba.cloud.ai.example.observability.exporter.oltp; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.io.SegmentedStringWriter; +import io.opentelemetry.exporter.internal.otlp.traces.ResourceSpansMarshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.IOException; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A {@link SpanExporter} which writes {@linkplain SpanData spans} to a {@link Logger} in OTLP JSON + * format. Each log line will include a single {@code ResourceSpans}. + */ +public final class OtlpFileSpanExporter implements SpanExporter { + + private static final Logger logger = + Logger.getLogger(OtlpFileSpanExporter.class.getName()); + + private final AtomicBoolean isShutdown = new AtomicBoolean(); + + /** Returns a new {@link OtlpFileSpanExporter}. */ + public static SpanExporter create() { + return new OtlpFileSpanExporter(); + } + + private OtlpFileSpanExporter() {} + + @Override + public CompletableResultCode export(Collection spans) { + if (isShutdown.get()) { + return CompletableResultCode.ofFailure(); + } + + ResourceSpansMarshaler[] allResourceSpans = ResourceSpansMarshaler.create(spans); + for (ResourceSpansMarshaler resourceSpans : allResourceSpans) { + SegmentedStringWriter sw = + new SegmentedStringWriter(JsonUtil.JSON_FACTORY._getBufferRecycler()); + try (JsonGenerator gen = JsonUtil.create(sw)) { + resourceSpans.writeJsonTo(gen); + } catch (IOException e) { + // Shouldn't happen in practice, just skip it. + continue; + } + try { + logger.log(Level.INFO, sw.getAndClear()); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to read OTLP JSON spans", e); + } + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + if (!isShutdown.compareAndSet(false, true)) { + logger.log(Level.INFO, "Calling shutdown() multiple times."); + } + return CompletableResultCode.ofSuccess(); + } +} diff --git a/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/exporter/oltp/OtlpFileSpanExporterProvider.java b/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/exporter/oltp/OtlpFileSpanExporterProvider.java new file mode 100755 index 00000000..70f2414f --- /dev/null +++ b/spring-ai-alibaba-examples/observability-example/src/main/java/com/alibaba/cloud/ai/example/observability/exporter/oltp/OtlpFileSpanExporterProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.alibaba.cloud.ai.example.observability.exporter.oltp; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.trace.export.SpanExporter; + +/** + * {@link SpanExporter} SPI implementation for {@link OtlpFileSpanExporter}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public class OtlpFileSpanExporterProvider implements ConfigurableSpanExporterProvider { + @Override + public SpanExporter createExporter(ConfigProperties config) { + return OtlpFileSpanExporter.create(); + } + + @Override + public String getName() { + return "logging-otlp"; + } +} diff --git a/spring-ai-alibaba-examples/observability-example/src/main/resources/application.properties b/spring-ai-alibaba-examples/observability-example/src/main/resources/application.properties new file mode 100644 index 00000000..0acb222a --- /dev/null +++ b/spring-ai-alibaba-examples/observability-example/src/main/resources/application.properties @@ -0,0 +1,19 @@ +spring.application.name=ai +# +#export AI_DASHSCOPE_API_KEY=... +spring.ai.dashscope.api-key=${AI_DASHSCOPE_API_KEY} +# +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always +management.tracing.sampling.probability=1.0 +# +spring.threads.virtual.enabled=true + +spring.ai.chat.client.observations.include-input=true +spring.ai.chat.observations.include-completion=true +spring.ai.chat.observations.include-prompt=true +spring.ai.image.observations.include-prompt=true +spring.ai.vectorstore.observations.include-query-response=true + +# +#spring.docker.compose.lifecycle-management=start_only \ No newline at end of file diff --git a/spring-ai-alibaba-examples/observability-example/src/test/java/com/alibaba/cloud/ai/example/observability/ObservabilityApplicationTests.java b/spring-ai-alibaba-examples/observability-example/src/test/java/com/alibaba/cloud/ai/example/observability/ObservabilityApplicationTests.java new file mode 100644 index 00000000..60713ce9 --- /dev/null +++ b/spring-ai-alibaba-examples/observability-example/src/test/java/com/alibaba/cloud/ai/example/observability/ObservabilityApplicationTests.java @@ -0,0 +1,13 @@ +package com.alibaba.cloud.ai.example.observability; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ObservabilityApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/spring-ai-alibaba-examples/pom.xml b/spring-ai-alibaba-examples/pom.xml index 025789bc..45cd64b1 100644 --- a/spring-ai-alibaba-examples/pom.xml +++ b/spring-ai-alibaba-examples/pom.xml @@ -41,6 +41,7 @@ rag-example output-parser-example playground-flight-booking + observability-example