diff --git a/.github/workflows/skywalking.yaml b/.github/workflows/skywalking.yaml index f65c5afa7c4f..2a807063dac0 100644 --- a/.github/workflows/skywalking.yaml +++ b/.github/workflows/skywalking.yaml @@ -627,6 +627,8 @@ jobs: config: test/e2e-v2/cases/zipkin/kafka/e2e.yaml - name: Zipkin BanyanDB config: test/e2e-v2/cases/zipkin/banyandb/e2e.yaml + - name: Virtual GenAI + config: test/e2e-v2/cases/virtual-genai/e2e.yaml - name: Nginx config: test/e2e-v2/cases/nginx/e2e.yaml diff --git a/apm-dist/src/main/assembly/binary.xml b/apm-dist/src/main/assembly/binary.xml index f6dbf452d329..81f7b2a281c4 100644 --- a/apm-dist/src/main/assembly/binary.xml +++ b/apm-dist/src/main/assembly/binary.xml @@ -75,6 +75,7 @@ log-mal-rules/** telegraf-rules/* cilium-rules/* + gen-ai-config.yml config diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md index 190162ba19ec..81afedca6561 100644 --- a/docs/en/changes/changes.md +++ b/docs/en/changes/changes.md @@ -166,6 +166,7 @@ * Update hierarchy rule documentation: `auto-matching-rules` in `hierarchy-definition.yml` no longer use Groovy scripts. Rules now use a dedicated expression grammar supporting property access, String methods, if/else, comparisons, and logical operators. All shipped rules are fully compatible. * Activate `otlp-traces` handler in `receiver-otel` by default. * Update Istio E2E test versions: remove EOL 1.20.0, add 1.25.0–1.29.0 for ALS/Metrics/Ambient tests. Update Rover with Istio Process test from 1.15.0 to 1.28.0 with Kubernetes 1.28. +* Support Virtual-GenAI monitoring. #### UI * Fix the missing icon in new native trace view. diff --git a/docs/en/setup/service-agent/virtual-genai.md b/docs/en/setup/service-agent/virtual-genai.md new file mode 100644 index 000000000000..86b9d5376429 --- /dev/null +++ b/docs/en/setup/service-agent/virtual-genai.md @@ -0,0 +1,63 @@ +# Virtual GenAI + +Virtual GenAI represents the Generative AI service nodes detected by [server agents' plugins](server-agents.md). The performance +metrics of the GenAI operations are from the GenAI client-side perspective. + +For example, a Spring AI plugin in the Java agent could detect the latency of a chat completion request. +As a result, SkyWalking would show traffic, latency, success rate, token usage (input/output), and estimated cost in the GenAI dashboard. + +## Span Contract + +The GenAI operation span should have the following properties: +- It is an **Exit** span +- **Span's layer == GENAI** +- Tag key = `gen_ai.provider.name`, value = The Generative AI provider, e.g. openai, anthropic, ollama +- Tag key = `gen_ai.response.model`, value = The name of the GenAI model, e.g. gpt-4o, claude-3-5-sonnet +- Tag key = `gen_ai.usage.input_tokens`, value = The number of tokens used in the GenAI input (prompt) +- Tag key = `gen_ai.usage.output_tokens`, value = The number of tokens used in the GenAI response (completion) +- Tag key = `gen_ai.server.time_to_first_token`, value = The duration in milliseconds until the first token is received (streaming requests only) +- If the GenAI service is a remote API (e.g. OpenAI), the span's peer should be the network address (IP or domain) of the GenAI server. + +## Provider Configuration + +SkyWalking uses `gen-ai-config.yml` to map model names to providers and configure cost estimation. + +When the `gen_ai.provider.name` tag is present in the span, it is used directly. Otherwise, SkyWalking matches the model name +against `prefix-match` rules to identify the provider. For example, a model name starting with `gpt` is mapped to `openai`. + +To configure cost estimation, add `models` with pricing under the provider: + + +```yaml +providers: +- provider: openai + prefix-match: + - gpt + models: + - name: gpt-4o + input-estimated-cost-per-m: 2.5 # estimated cost per 1,000,000 input tokens + output-estimated-cost-per-m: 10 # estimated cost per 1,000,000 output tokens +``` + +## Metrics + +The following metrics are available at the **provider** (service) level: +- `gen_ai_provider_cpm` - Calls per minute +- `gen_ai_provider_sla` - Success rate +- `gen_ai_provider_resp_time` - Average response time +- `gen_ai_provider_latency_percentile` - Latency percentiles +- `gen_ai_provider_input_tokens_sum / avg` - Input token usage +- `gen_ai_provider_output_tokens_sum / avg` - Output token usage +- `gen_ai_provider_total_estimated_cost / avg_estimated_cost` - Estimated cost + +The following metrics are available at the **model** (service instance) level: +- `gen_ai_model_call_cpm` - Calls per minute +- `gen_ai_model_sla` - Success rate +- `gen_ai_model_latency_avg / percentile` - Latency +- `gen_ai_model_ttft_avg / percentile` - Time to first token (streaming only) +- `gen_ai_model_input_tokens_sum / avg` - Input token usage +- `gen_ai_model_output_tokens_sum / avg` - Output token usage +- `gen_ai_model_total_estimated_cost / avg_estimated_cost` - Estimated cost + +## Requirement +`SkyWalking Java Agent` version >= 9.7 \ No newline at end of file diff --git a/docs/menu.yml b/docs/menu.yml index 38ad052ada37..c7843cf6e38d 100644 --- a/docs/menu.yml +++ b/docs/menu.yml @@ -158,6 +158,10 @@ catalog: path: "/en/setup/backend/dashboards-so11y-java-agent" - name: "SkyWalking Go Agent self telemetry" path: "/en/setup/backend/dashboards-so11y-go-agent" + - name: "GenAI" + catalog: + - name: "Virtual GenAI" + path: "/en/setup/service-agent/virtual-genai" - name: "Configuration Vocabulary" path: "/en/setup/backend/configuration-vocabulary" - name: "Advanced Setup" diff --git a/oap-server/analyzer/agent-analyzer/pom.xml b/oap-server/analyzer/agent-analyzer/pom.xml index 5a281800589b..1fe05db26720 100644 --- a/oap-server/analyzer/agent-analyzer/pom.xml +++ b/oap-server/analyzer/agent-analyzer/pom.xml @@ -43,6 +43,11 @@ meter-analyzer ${project.version} + + org.apache.skywalking + gen-ai-analyzer + ${project.version} + org.apache.skywalking server-testing diff --git a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/VirtualServiceAnalysisListener.java b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/VirtualServiceAnalysisListener.java index 95c0ac47fccb..861f699ce830 100644 --- a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/VirtualServiceAnalysisListener.java +++ b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/VirtualServiceAnalysisListener.java @@ -20,12 +20,16 @@ import java.util.Arrays; import java.util.List; + import lombok.RequiredArgsConstructor; import org.apache.skywalking.apm.network.language.agent.v3.SegmentObject; import org.apache.skywalking.apm.network.language.agent.v3.SpanObject; +import org.apache.skywalking.oap.analyzer.genai.module.GenAIAnalyzerModule; +import org.apache.skywalking.oap.analyzer.genai.service.IGenAIMeterAnalyzerService; import org.apache.skywalking.oap.server.analyzer.provider.AnalyzerModuleConfig; import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice.VirtualCacheProcessor; import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice.VirtualDatabaseProcessor; +import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice.VirtualGenAIProcessor; import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice.VirtualMQProcessor; import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.vservice.VirtualServiceProcessor; import org.apache.skywalking.oap.server.core.CoreModule; @@ -71,23 +75,28 @@ public void parseEntry(final SpanObject span, final SegmentObject segmentObject) public static class Factory implements AnalysisListenerFactory { private final SourceReceiver sourceReceiver; private final NamingControl namingControl; + private final IGenAIMeterAnalyzerService genAIMeterAnalyzerService; public Factory(ModuleManager moduleManager) { this.sourceReceiver = moduleManager.find(CoreModule.NAME).provider().getService(SourceReceiver.class); this.namingControl = moduleManager.find(CoreModule.NAME) .provider() .getService(NamingControl.class); + this.genAIMeterAnalyzerService = moduleManager.find(GenAIAnalyzerModule.NAME) + .provider() + .getService(IGenAIMeterAnalyzerService.class); } @Override public AnalysisListener create(ModuleManager moduleManager, AnalyzerModuleConfig config) { return new VirtualServiceAnalysisListener( - sourceReceiver, - Arrays.asList( - new VirtualCacheProcessor(namingControl, config), - new VirtualDatabaseProcessor(namingControl, config), - new VirtualMQProcessor(namingControl) - ) + sourceReceiver, + Arrays.asList( + new VirtualCacheProcessor(namingControl, config), + new VirtualDatabaseProcessor(namingControl, config), + new VirtualMQProcessor(namingControl), + new VirtualGenAIProcessor(namingControl, genAIMeterAnalyzerService) + ) ); } } diff --git a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/vservice/VirtualGenAIProcessor.java b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/vservice/VirtualGenAIProcessor.java new file mode 100644 index 000000000000..246e1e41c0ec --- /dev/null +++ b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/vservice/VirtualGenAIProcessor.java @@ -0,0 +1,115 @@ +/* + * 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.oap.server.analyzer.provider.trace.parser.listener.vservice; + +import lombok.RequiredArgsConstructor; +import org.apache.skywalking.apm.network.language.agent.v3.SegmentObject; +import org.apache.skywalking.apm.network.language.agent.v3.SpanLayer; +import org.apache.skywalking.apm.network.language.agent.v3.SpanObject; +import org.apache.skywalking.oap.analyzer.genai.service.IGenAIMeterAnalyzerService; +import org.apache.skywalking.oap.server.core.analysis.Layer; +import org.apache.skywalking.oap.server.core.config.NamingControl; +import org.apache.skywalking.oap.server.core.source.GenAIMetrics; +import org.apache.skywalking.oap.server.core.source.GenAIModelAccess; +import org.apache.skywalking.oap.server.core.source.GenAIProviderAccess; +import org.apache.skywalking.oap.server.core.source.ServiceInstance; +import org.apache.skywalking.oap.server.core.source.ServiceMeta; +import org.apache.skywalking.oap.server.core.source.Source; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +@RequiredArgsConstructor +public class VirtualGenAIProcessor implements VirtualServiceProcessor { + + private final NamingControl namingControl; + + private final IGenAIMeterAnalyzerService meterAnalyzerService; + + private final List recordList = new ArrayList<>(); + + @Override + public void prepareVSIfNecessary(SpanObject span, SegmentObject segmentObject) { + if (span.getSpanLayer() != SpanLayer.GenAI) { + return; + } + + GenAIMetrics metrics = meterAnalyzerService.extractMetricsFromSWSpan(span, segmentObject); + if (metrics == null) { + return; + } + + recordList.add(toServiceMeta(metrics)); + recordList.add(toInstance(metrics)); + recordList.add(toProviderAccess(metrics)); + recordList.add(toModelAccess(metrics)); + } + + private ServiceMeta toServiceMeta(GenAIMetrics metrics) { + ServiceMeta service = new ServiceMeta(); + service.setName(namingControl.formatServiceName(metrics.getProviderName())); + service.setLayer(Layer.VIRTUAL_GENAI); + service.setTimeBucket(metrics.getTimeBucket()); + return service; + } + + private Source toInstance(GenAIMetrics metrics) { + ServiceInstance instance = new ServiceInstance(); + instance.setTimeBucket(metrics.getTimeBucket()); + instance.setName(namingControl.formatInstanceName(metrics.getModelName())); + instance.setServiceLayer(Layer.VIRTUAL_GENAI); + instance.setServiceName(metrics.getProviderName()); + return instance; + } + + private GenAIProviderAccess toProviderAccess(GenAIMetrics metrics) { + GenAIProviderAccess source = new GenAIProviderAccess(); + source.setName(namingControl.formatServiceName(metrics.getProviderName())); + source.setInputTokens(metrics.getInputTokens()); + source.setOutputTokens(metrics.getOutputTokens()); + source.setTotalEstimatedCost(metrics.getTotalEstimatedCost()); + source.setLatency(metrics.getLatency()); + source.setStatus(metrics.isStatus()); + source.setTimeBucket(metrics.getTimeBucket()); + return source; + } + + private GenAIModelAccess toModelAccess(GenAIMetrics metrics) { + GenAIModelAccess source = new GenAIModelAccess(); + source.setServiceName(namingControl.formatServiceName(metrics.getProviderName())); + source.setModelName(namingControl.formatInstanceName(metrics.getModelName())); + source.setInputTokens(metrics.getInputTokens()); + source.setOutputTokens(metrics.getOutputTokens()); + source.setTotalEstimatedCost(metrics.getTotalEstimatedCost()); + source.setTimeToFirstToken(metrics.getTimeToFirstToken()); + source.setLatency(metrics.getLatency()); + source.setStatus(metrics.isStatus()); + source.setTimeBucket(metrics.getTimeBucket()); + return source; + } + + @Override + public void emitTo(Consumer consumer) { + for (Source source : recordList) { + if (source != null) { + consumer.accept(source); + } + } + } +} diff --git a/oap-server/analyzer/gen-ai-analyzer/pom.xml b/oap-server/analyzer/gen-ai-analyzer/pom.xml new file mode 100644 index 000000000000..be35fa818e9e --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/pom.xml @@ -0,0 +1,37 @@ + + + + + + analyzer + org.apache.skywalking + ${revision} + + 4.0.0 + + gen-ai-analyzer + + + + org.apache.skywalking + server-core + ${project.version} + + + diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/GenAIAnalyzerModuleProvider.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/GenAIAnalyzerModuleProvider.java new file mode 100644 index 000000000000..6248a9b1b329 --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/GenAIAnalyzerModuleProvider.java @@ -0,0 +1,95 @@ +/* + * 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.oap.analyzer.genai; + +import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfig; +import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfigLoader; +import org.apache.skywalking.oap.analyzer.genai.config.GenAIOALDefine; +import org.apache.skywalking.oap.analyzer.genai.matcher.GenAIProviderPrefixMatcher; +import org.apache.skywalking.oap.analyzer.genai.module.GenAIAnalyzerModule; +import org.apache.skywalking.oap.analyzer.genai.service.GenAIMeterAnalyzer; +import org.apache.skywalking.oap.analyzer.genai.service.IGenAIMeterAnalyzerService; +import org.apache.skywalking.oap.server.core.CoreModule; +import org.apache.skywalking.oap.server.core.oal.rt.OALEngineLoaderService; +import org.apache.skywalking.oap.server.library.module.ModuleConfig; +import org.apache.skywalking.oap.server.library.module.ModuleDefine; +import org.apache.skywalking.oap.server.library.module.ModuleProvider; +import org.apache.skywalking.oap.server.library.module.ModuleStartException; +import org.apache.skywalking.oap.server.library.module.ServiceNotProvidedException; + +public class GenAIAnalyzerModuleProvider extends ModuleProvider { + + private GenAIConfig config; + + @Override + public String name() { + return "default"; + } + + @Override + public Class module() { + return GenAIAnalyzerModule.class; + } + + @Override + public ConfigCreator newConfigCreator() { + return new ConfigCreator() { + @Override + public Class type() { + return GenAIConfig.class; + } + + @Override + public void onInitialized(final GenAIConfig initialized) { + config = initialized; + } + }; + } + + @Override + public void prepare() throws ServiceNotProvidedException, ModuleStartException { + GenAIConfigLoader loader = new GenAIConfigLoader(config); + config = loader.loadConfig(); + GenAIProviderPrefixMatcher matcher = GenAIProviderPrefixMatcher.build(config); + this.registerServiceImplementation( + IGenAIMeterAnalyzerService.class, + new GenAIMeterAnalyzer(matcher) + ); + } + + @Override + public void start() throws ServiceNotProvidedException, ModuleStartException { + getManager().find(CoreModule.NAME) + .provider() + .getService(OALEngineLoaderService.class) + .load(GenAIOALDefine.INSTANCE); + } + + @Override + public void notifyAfterCompleted() throws ServiceNotProvidedException, ModuleStartException { + + } + + @Override + public String[] requiredModules() { + return new String[] { + CoreModule.NAME + }; + } +} diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfig.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfig.java new file mode 100644 index 000000000000..a4a667a80ff2 --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfig.java @@ -0,0 +1,50 @@ +/* + * 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.oap.analyzer.genai.config; + +import lombok.Getter; +import lombok.Setter; +import org.apache.skywalking.oap.server.library.module.ModuleConfig; + +import java.util.ArrayList; +import java.util.List; + +public class GenAIConfig extends ModuleConfig { + + @Getter + @Setter + private List providers = new ArrayList<>(); + + @Getter + @Setter + public static class Provider { + private String provider; + private String baseUrl; + private List prefixMatch = new ArrayList<>(); + private List models = new ArrayList<>(); + } + + @Getter + @Setter + public static class Model { + private String name; + private double inputEstimatedCostPerM; + private double outputEstimatedCostPerM; + } +} diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfigLoader.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfigLoader.java new file mode 100644 index 000000000000..c39d126b8c86 --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIConfigLoader.java @@ -0,0 +1,110 @@ +/* + * 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.oap.analyzer.genai.config; + +import org.apache.skywalking.oap.server.library.module.ModuleStartException; +import org.apache.skywalking.oap.server.library.util.ResourceUtils; +import org.apache.skywalking.oap.server.library.util.StringUtil; +import org.yaml.snakeyaml.Yaml; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; +import java.util.List; +import java.util.Map; + +public class GenAIConfigLoader { + + private final GenAIConfig config; + + public GenAIConfigLoader(GenAIConfig config) { + this.config = config; + } + + public GenAIConfig loadConfig() throws ModuleStartException { + Map>> configMap; + try (Reader applicationReader = ResourceUtils.read("gen-ai-config.yml")) { + Yaml yaml = new Yaml(); + configMap = yaml.loadAs(applicationReader, Map.class); + } catch (FileNotFoundException e) { + throw new ModuleStartException( + "Cannot find the GenAI configuration file [gen-ai-config.yml].", e); + } catch (IOException e) { + throw new ModuleStartException( + "Failed to read the GenAI configuration file [gen-ai-config.yml].", e); + } + + if (configMap == null || !configMap.containsKey("providers")) { + return config; + } + + List> providersConfig = configMap.get("providers"); + for (Map providerMap : providersConfig) { + GenAIConfig.Provider provider = new GenAIConfig.Provider(); + + Object name = providerMap.get("provider"); + if (name == null) { + throw new ModuleStartException("Provider name is missing in [gen-ai-config.yml]."); + } + provider.setProvider(name.toString()); + + Object baseUrl = providerMap.get("base-url"); + if (baseUrl != null && StringUtil.isNotBlank(baseUrl.toString())) { + provider.setBaseUrl(baseUrl.toString()); + } + + Object prefixMatch = providerMap.get("prefix-match"); + if (prefixMatch instanceof List) { + provider.getPrefixMatch().addAll((List) prefixMatch); + } else if (prefixMatch != null) { + throw new ModuleStartException("prefix-match must be a list in [gen-ai-config.yml] for provider: " + name); + } + + // Parse specific model overrides + Object modelsConfig = providerMap.get("models"); + if (modelsConfig instanceof List) { + for (Object modelObj : (List) modelsConfig) { + if (modelObj instanceof Map) { + Map modelMap = (Map) modelObj; + GenAIConfig.Model model = new GenAIConfig.Model(); + model.setName(String.valueOf(modelMap.get("name"))); + model.setInputEstimatedCostPerM(parseCost(modelMap.get("input-estimated-cost-per-m"))); + model.setOutputEstimatedCostPerM(parseCost(modelMap.get("output-estimated-cost-per-m"))); + provider.getModels().add(model); + } + } + } + + config.getProviders().add(provider); + } + + return config; + } + + private double parseCost(Object value) { + if (value == null) { + return 0.0; + } + try { + return Double.parseDouble(value.toString()); + } catch (NumberFormatException e) { + return 0.0; + } + } +} diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIOALDefine.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIOALDefine.java new file mode 100644 index 000000000000..2509b3772581 --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAIOALDefine.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 org.apache.skywalking.oap.analyzer.genai.config; + +import org.apache.skywalking.oap.server.core.oal.rt.OALDefine; + +public class GenAIOALDefine extends OALDefine { + + public static final GenAIOALDefine INSTANCE = new GenAIOALDefine(); + + private GenAIOALDefine() { + super( + "oal/virtual-gen-ai.oal", + "org.apache.skywalking.oap.server.core.source" + ); + } +} diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAITagKeys.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAITagKeys.java new file mode 100644 index 000000000000..e98b49d55a15 --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/config/GenAITagKeys.java @@ -0,0 +1,28 @@ +/* + * 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.oap.analyzer.genai.config; + +public class GenAITagKeys { + + public static final String PROVIDER_NAME = "gen_ai.provider.name"; + + public static final String RESPONSE_MODEL = "gen_ai.response.model"; + public static final String INPUT_TOKENS = "gen_ai.usage.input_tokens"; + public static final String OUTPUT_TOKENS = "gen_ai.usage.output_tokens"; + public static final String SERVER_TIME_TO_FIRST_TOKEN = "gen_ai.server.time_to_first_token"; +} diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/matcher/GenAIProviderPrefixMatcher.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/matcher/GenAIProviderPrefixMatcher.java new file mode 100644 index 000000000000..6bbd71d390bb --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/matcher/GenAIProviderPrefixMatcher.java @@ -0,0 +1,115 @@ +/* + * 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.oap.analyzer.genai.matcher; + +import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfig; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class GenAIProviderPrefixMatcher { + private static final String UNKNOWN = "unknown"; + private final TrieNode root; + private final Map modelMap; + + private static final MatchResult UNKNOWN_RESULT = new MatchResult(UNKNOWN, null); + + private GenAIProviderPrefixMatcher(TrieNode root, Map modelMap) { + this.root = root; + this.modelMap = modelMap; + } + + private static class TrieNode { + final Map children = new HashMap<>(); + String providerName; + } + + public static class MatchResult { + private final String provider; + private final GenAIConfig.Model modelConfig; + + public MatchResult(String provider, GenAIConfig.Model modelConfig) { + this.provider = provider; + this.modelConfig = modelConfig; + } + + public String getProvider() { + return provider; + } + + public GenAIConfig.Model getModelConfig() { + return modelConfig; + } + } + + public static GenAIProviderPrefixMatcher build(GenAIConfig config) { + TrieNode root = new TrieNode(); + Map modelMap = new HashMap<>(); + + for (GenAIConfig.Provider p : config.getProviders()) { + List prefixes = p.getPrefixMatch(); + if (prefixes != null) { + for (String prefix : prefixes) { + if (prefix == null || prefix.isEmpty()) continue; + + TrieNode current = root; + for (int i = 0; i < prefix.length(); i++) { + char c = prefix.charAt(i); + current = current.children.computeIfAbsent(c, k -> new TrieNode()); + } + current.providerName = p.getProvider(); + } + } + + List models = p.getModels(); + if (models != null) { + for (GenAIConfig.Model model : models) { + if (model.getName() != null) { + modelMap.put(model.getName(), model); + } + } + } + } + + return new GenAIProviderPrefixMatcher(root, modelMap); + } + + public MatchResult match(String modelName) { + if (modelName == null || modelName.isEmpty()) { + return UNKNOWN_RESULT; + } + + TrieNode current = root; + String matchedProvider = null; + + for (int i = 0; i < modelName.length(); i++) { + current = current.children.get(modelName.charAt(i)); + if (current == null) break; + if (current.providerName != null) { + matchedProvider = current.providerName; + } + } + + String provider = matchedProvider != null ? matchedProvider : UNKNOWN; + GenAIConfig.Model modelConfig = modelMap.get(modelName); + + return new MatchResult(provider, modelConfig); + } +} diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/module/GenAIAnalyzerModule.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/module/GenAIAnalyzerModule.java new file mode 100644 index 000000000000..833cb30e8e5b --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/module/GenAIAnalyzerModule.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.oap.analyzer.genai.module; + +import org.apache.skywalking.oap.analyzer.genai.service.IGenAIMeterAnalyzerService; +import org.apache.skywalking.oap.server.library.module.ModuleDefine; + +public class GenAIAnalyzerModule extends ModuleDefine { + + public static final String NAME = "gen-ai-analyzer"; + + public GenAIAnalyzerModule() { + super(NAME); + } + + @Override + public Class[] services() { + return new Class[] { + IGenAIMeterAnalyzerService.class, + }; + } +} diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/GenAIMeterAnalyzer.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/GenAIMeterAnalyzer.java new file mode 100644 index 000000000000..41b13dd0652f --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/GenAIMeterAnalyzer.java @@ -0,0 +1,128 @@ +/* + * 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.oap.analyzer.genai.service; + +import lombok.extern.slf4j.Slf4j; +import org.apache.skywalking.apm.network.common.v3.KeyStringValuePair; +import org.apache.skywalking.apm.network.language.agent.v3.SegmentObject; +import org.apache.skywalking.apm.network.language.agent.v3.SpanObject; +import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfig; +import org.apache.skywalking.oap.analyzer.genai.config.GenAITagKeys; +import org.apache.skywalking.oap.analyzer.genai.matcher.GenAIProviderPrefixMatcher; +import org.apache.skywalking.oap.server.core.analysis.IDManager; +import org.apache.skywalking.oap.server.core.analysis.Layer; +import org.apache.skywalking.oap.server.core.analysis.TimeBucket; +import org.apache.skywalking.oap.server.core.source.GenAIMetrics; +import org.apache.skywalking.oap.server.library.util.StringUtil; + +import java.util.Map; + +import static java.util.stream.Collectors.toMap; + +@Slf4j +public class GenAIMeterAnalyzer implements IGenAIMeterAnalyzerService { + + private final GenAIProviderPrefixMatcher matcher; + + public GenAIMeterAnalyzer(GenAIProviderPrefixMatcher matcher) { + this.matcher = matcher; + } + + @Override + public GenAIMetrics extractMetricsFromSWSpan(SpanObject span, SegmentObject segment) { + Map tags = span.getTagsList().stream() + .collect(toMap( + KeyStringValuePair::getKey, + KeyStringValuePair::getValue, + (v1, v2) -> v1 + )); + + String modelName = tags.get(GenAITagKeys.RESPONSE_MODEL); + + if (StringUtil.isBlank(modelName)) { + if (log.isDebugEnabled()) { + log.debug("Model name is missing in span [{}], skipping GenAI analysis", span.getOperationName()); + } + return null; + } + String provider = tags.get(GenAITagKeys.PROVIDER_NAME); + GenAIProviderPrefixMatcher.MatchResult matchResult = matcher.match(modelName); + + if (StringUtil.isBlank(provider)) { + provider = matchResult.getProvider(); + } + + GenAIConfig.Model modelConfig = matchResult.getModelConfig(); + + long inputTokens = parseSafeLong(tags.get(GenAITagKeys.INPUT_TOKENS)); + long outputTokens = parseSafeLong(tags.get(GenAITagKeys.OUTPUT_TOKENS)); + + // calculate the total cost by the cost configs + double totalCost = 0.0D; + if (modelConfig != null) { + if (modelConfig.getInputEstimatedCostPerM() > 0) { + totalCost += inputTokens * modelConfig.getInputEstimatedCostPerM(); + } + if (modelConfig.getOutputEstimatedCostPerM() > 0) { + totalCost += outputTokens * modelConfig.getOutputEstimatedCostPerM(); + } + } + + GenAIMetrics metrics = new GenAIMetrics(); + + metrics.setServiceId(IDManager.ServiceID.buildId(provider, Layer.VIRTUAL_GENAI.isNormal())); + metrics.setProviderName(provider); + metrics.setModelName(modelName); + metrics.setInputTokens(inputTokens); + metrics.setOutputTokens(outputTokens); + + metrics.setTimeToFirstToken(parseSafeInt(tags.get(GenAITagKeys.SERVER_TIME_TO_FIRST_TOKEN))); + metrics.setTotalEstimatedCost(Math.round(totalCost)); + + long latency = span.getEndTime() - span.getStartTime(); + metrics.setLatency(latency); + metrics.setStatus(!span.getIsError()); + metrics.setTimeBucket(TimeBucket.getMinuteTimeBucket(span.getStartTime())); + + return metrics; + } + + private long parseSafeLong(String value) { + if (StringUtil.isEmpty(value)) { + return 0; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + log.warn("Failed to parse value to long: {}", value); + return 0; + } + } + + private int parseSafeInt(String value) { + if (StringUtil.isEmpty(value)) { + return 0; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + log.warn("Failed to parse value to int: {}", value); + return 0; + } + } +} diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/IGenAIMeterAnalyzerService.java b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/IGenAIMeterAnalyzerService.java new file mode 100644 index 000000000000..efbf2192b030 --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/java/org/apache/skywalking/oap/analyzer/genai/service/IGenAIMeterAnalyzerService.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 org.apache.skywalking.oap.analyzer.genai.service; + +import org.apache.skywalking.apm.network.language.agent.v3.SegmentObject; +import org.apache.skywalking.apm.network.language.agent.v3.SpanObject; +import org.apache.skywalking.oap.server.core.source.GenAIMetrics; +import org.apache.skywalking.oap.server.library.module.Service; + +public interface IGenAIMeterAnalyzerService extends Service { + + GenAIMetrics extractMetricsFromSWSpan(SpanObject span, SegmentObject segment); + +} diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine b/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine new file mode 100644 index 000000000000..ea7d0042b839 --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine @@ -0,0 +1,19 @@ +# +# 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. +# +# + +org.apache.skywalking.oap.analyzer.genai.module.GenAIAnalyzerModule diff --git a/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleProvider b/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleProvider new file mode 100644 index 000000000000..b0256f986574 --- /dev/null +++ b/oap-server/analyzer/gen-ai-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleProvider @@ -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. +# + +org.apache.skywalking.oap.analyzer.genai.GenAIAnalyzerModuleProvider diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/pom.xml index 8cad4dff5a9d..091eea2f35f6 100644 --- a/oap-server/analyzer/pom.xml +++ b/oap-server/analyzer/pom.xml @@ -34,6 +34,7 @@ meter-analyzer log-analyzer hierarchy + gen-ai-analyzer diff --git a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4 b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4 index 75a37da627db..497822d54c24 100644 --- a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4 +++ b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4 @@ -63,6 +63,8 @@ SRC_CILIUM_ENDPOINT: 'CiliumEndpoint'; SRC_CILIUM_SERVICE_RELATION: 'CiliumServiceRelation'; SRC_CILIUM_SERVICE_INSTANCE_RELATION: 'CiliumServiceInstanceRelation'; SRC_CILIUM_ENDPOINT_RELATION: 'CiliumEndpointRelation'; +SRC_GEN_AI_PROVIDER_ACCESS: 'GenAIProviderAccess'; +SRC_GEN_AI_MODEL_ACCESS: 'GenAIModelAccess'; DECORATOR: 'decorator'; diff --git a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4 b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4 index 38713ff208ca..fcefe29d53c8 100644 --- a/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4 +++ b/oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4 @@ -63,7 +63,8 @@ source SRC_BROWSER_APP_TRAFFIC | SRC_BROWSER_APP_PAGE_TRAFFIC | SRC_BROWSER_APP_SINGLE_VERSION_TRAFFIC | SRC_EVENT | SRC_MQ_ACCESS | SRC_MQ_ENDPOINT_ACCESS | SRC_K8S_SERVICE | SRC_K8S_SERVICE_INSTANCE | SRC_K8S_ENDPOINT | SRC_K8S_SERVICE_RELATION | SRC_K8S_SERVICE_INSTANCE_RELATION | - SRC_CILIUM_SERVICE | SRC_CILIUM_SERVICE_INSTANCE | SRC_CILIUM_ENDPOINT | SRC_CILIUM_SERVICE_RELATION | SRC_CILIUM_SERVICE_INSTANCE_RELATION | SRC_CILIUM_ENDPOINT_RELATION + SRC_CILIUM_SERVICE | SRC_CILIUM_SERVICE_INSTANCE | SRC_CILIUM_ENDPOINT | SRC_CILIUM_SERVICE_RELATION | SRC_CILIUM_SERVICE_INSTANCE_RELATION | SRC_CILIUM_ENDPOINT_RELATION | + SRC_GEN_AI_PROVIDER_ACCESS | SRC_GEN_AI_MODEL_ACCESS ; disableSource diff --git a/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/v2/generator/RuntimeOALGenerationTest.java b/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/v2/generator/RuntimeOALGenerationTest.java index 27e63c884a05..1665c5550e99 100644 --- a/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/v2/generator/RuntimeOALGenerationTest.java +++ b/oap-server/oal-rt/src/test/java/org/apache/skywalking/oal/v2/generator/RuntimeOALGenerationTest.java @@ -98,6 +98,8 @@ public static void setup() { // DisableOALDefine - no catalog registerOALDefine("disable", createOALDefine("oal/disable.oal", SOURCE_PACKAGE, "")); + registerOALDefine("virtual-gen-ai", createOALDefine("oal/virtual-gen-ai.oal", SOURCE_PACKAGE, "")); + // Set generated file path for IDE inspection OALClassGeneratorV2.setGeneratedFilePath("target/test-classes"); } @@ -185,6 +187,10 @@ private static void initializeScopes() { notifyClass(listener, SOURCE_PACKAGE, "Process"); notifyClass(listener, SOURCE_PACKAGE, "ProcessRelation"); + // gen_ai + notifyClass(listener, SOURCE_PACKAGE, "GenAIProviderAccess"); + notifyClass(listener, SOURCE_PACKAGE, "GenAIModelAccess"); + // Register decorators registerDecorator(SOURCE_PACKAGE, "ServiceDecorator"); registerDecorator(SOURCE_PACKAGE, "EndpointDecorator"); diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java index dd01e97c19d8..525ccf11e2fc 100644 --- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java +++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/management/ui/template/UITemplateInitializer.java @@ -81,6 +81,7 @@ public class UITemplateInitializer { Layer.SO11Y_GO_AGENT.name(), Layer.FLINK.name(), Layer.BANYANDB.name(), + Layer.VIRTUAL_GENAI.name(), "custom" }; private final UITemplateManagementService uiTemplateManagementService; diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java index 2650babbb125..0916fe4fdd59 100644 --- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java +++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java @@ -156,6 +156,8 @@ public class DefaultScopeDefine { public static final int PPROF_PROFILING_DATA = 93; public static final int PPROF_TASK_LOG = 94; public static final int ALARM_RECOVERY = 95; + public static final int GEN_AI_PROVIDER_ACCESS = 96; + public static final int GEN_AI_MODEL_ACCESS = 97; /** * Catalog of scope, the metrics processor could use this to group all generated metrics by oal rt. diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIMetrics.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIMetrics.java new file mode 100644 index 000000000000..2ef4562143bb --- /dev/null +++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIMetrics.java @@ -0,0 +1,50 @@ +/* + * 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.oap.server.core.source; + +import lombok.Data; + +@Data +public class GenAIMetrics { + + private String serviceId; + + private String providerName; + + private String modelName; + + private long inputTokens; + + private long outputTokens; + + /** + * The total estimated cost of GenAI model calls. + * This value is amplified by 10^6 (multiplied by 1,000,000) to be stored as a long + * and to avoid precision issues with double in SumMetrics. + */ + private long totalEstimatedCost; + + private int timeToFirstToken; + + private long latency; + + private boolean status; + + private long timeBucket; +} diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIModelAccess.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIModelAccess.java new file mode 100644 index 000000000000..0fbb752ccb07 --- /dev/null +++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIModelAccess.java @@ -0,0 +1,71 @@ +/* + * 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.oap.server.core.source; + +import lombok.Data; +import org.apache.skywalking.oap.server.core.analysis.IDManager; +import org.apache.skywalking.oap.server.core.analysis.Layer; + +import static org.apache.skywalking.oap.server.core.source.DefaultScopeDefine.GEN_AI_MODEL_ACCESS; +import static org.apache.skywalking.oap.server.core.source.DefaultScopeDefine.SERVICE_INSTANCE_CATALOG_NAME; + +@Data +@ScopeDeclaration(id = GEN_AI_MODEL_ACCESS, name = "GenAIModelAccess", catalog = SERVICE_INSTANCE_CATALOG_NAME) +@ScopeDefaultColumn.VirtualColumnDefinition(fieldName = "entityId", columnName = "entity_id", isID = true, type = String.class) +public class GenAIModelAccess extends Source { + + @Override + public int scope() { + return GEN_AI_MODEL_ACCESS; + } + + @Override + public String getEntityId() { + return entityId; + } + + private String entityId; + + @ScopeDefaultColumn.DefinedByField(columnName = "service_name", requireDynamicActive = true) + private String serviceName; + + @ScopeDefaultColumn.DefinedByField(columnName = "service_id") + private String serviceId; + + @ScopeDefaultColumn.DefinedByField(columnName = "name") + private String modelName; + + private long inputTokens; + + private long outputTokens; + + private long totalEstimatedCost; + + private int timeToFirstToken; + + private long latency; + + private boolean status; + + @Override + public void prepare() { + serviceId = IDManager.ServiceID.buildId(serviceName, Layer.VIRTUAL_GENAI.isNormal()); + entityId = IDManager.ServiceInstanceID.buildId(serviceId, modelName); + } +} diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIProviderAccess.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIProviderAccess.java new file mode 100644 index 000000000000..f16634ffe2e2 --- /dev/null +++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/GenAIProviderAccess.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.oap.server.core.source; + +import lombok.Data; +import org.apache.skywalking.oap.server.core.analysis.IDManager; +import org.apache.skywalking.oap.server.core.analysis.Layer; + +import static org.apache.skywalking.oap.server.core.source.DefaultScopeDefine.GEN_AI_PROVIDER_ACCESS; +import static org.apache.skywalking.oap.server.core.source.DefaultScopeDefine.SERVICE_CATALOG_NAME; + +@Data +@ScopeDeclaration(id = GEN_AI_PROVIDER_ACCESS, name = "GenAIProviderAccess", catalog = SERVICE_CATALOG_NAME) +@ScopeDefaultColumn.VirtualColumnDefinition(fieldName = "entityId", columnName = "entity_id", isID = true, type = String.class) +public class GenAIProviderAccess extends Source { + + @Override + public int scope() { + return GEN_AI_PROVIDER_ACCESS; + } + + @Override + public String getEntityId() { + return entityId; + } + + private String entityId; + + @ScopeDefaultColumn.DefinedByField(columnName = "name", requireDynamicActive = true) + private String name; + + private long inputTokens; + + private long outputTokens; + + private long totalEstimatedCost; + + private long latency; + + private boolean status; + + @Override + public void prepare() { + entityId = IDManager.ServiceID.buildId(name, Layer.VIRTUAL_GENAI.isNormal()); + } +} diff --git a/oap-server/server-starter/pom.xml b/oap-server/server-starter/pom.xml index ce1601659e81..cc510317a504 100644 --- a/oap-server/server-starter/pom.xml +++ b/oap-server/server-starter/pom.xml @@ -346,6 +346,7 @@ log-mal-rules/ telegraf-rules/ cilium-rules/ + gen-ai-config.yml diff --git a/oap-server/server-starter/src/main/resources/application.yml b/oap-server/server-starter/src/main/resources/application.yml index 3d9a06e6c6f6..7f2223f8fc4a 100644 --- a/oap-server/server-starter/src/main/resources/application.yml +++ b/oap-server/server-starter/src/main/resources/application.yml @@ -244,6 +244,10 @@ event-analyzer: selector: ${SW_EVENT_ANALYZER:default} default: +gen-ai-analyzer: + selector: ${SW_GENAI_ANALYZER:default} + default: + receiver-sharing-server: selector: ${SW_RECEIVER_SHARING_SERVER:default} default: diff --git a/oap-server/server-starter/src/main/resources/gen-ai-config.yml b/oap-server/server-starter/src/main/resources/gen-ai-config.yml new file mode 100644 index 000000000000..b3b82c71f4c2 --- /dev/null +++ b/oap-server/server-starter/src/main/resources/gen-ai-config.yml @@ -0,0 +1,375 @@ +# 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. + +# This file configures GenAI provider matching rules and estimated pricing. +# Rules define how to map model names to providers and calculate token costs. + +#Here is a example +#- provider: ollama +# # If a model name starts with these strings, it will be mapped to this provider +# prefix-match: +# - ollama +# models: +# - name: ollama +# # --- PRICING CONFIGURATION --- +# # Costs are defined as the price per 1,000,000 (one million) tokens. +# # The currency depends on your system's default setting (e.g., USD or CNY). +# +# # Estimated cost for every 1,000,000 input (prompt) tokens +# input-estimated-cost-per-m: 1 +# +# # Estimated cost for every 1,000,000 output (completion) tokens +# output-estimated-cost-per-m: 1 + +last-updated: 2026-03-23 + +# --- IMPORTANT NOTES +# 1. ESTIMATED COSTS ONLY: The pricing values below are for estimation purposes +# within the system and MUST NOT be treated as actual billing or invoice amounts. +# +# 2. DATA SOURCE: These figures are based on official provider pricing pages +# available as of the 'last-updated' date above. +# +# 3. MARKET VOLATILITY: GenAI model pricing is subject to frequent and rapid +# changes without prior notice. +# +# 4. USER RESPONSIBILITY & CURRENCY: Users are encouraged to verify current rates regularly. +# Default unit is USD; prices from providers using other currencies have been +# converted based on exchange rates at the time of update. + + +providers: + - provider: openai + prefix-match: + - gpt + models: + - name: gpt-5.4-pro + # Pricing for Short context + input-estimated-cost-per-m: 30.0 + output-estimated-cost-per-m: 180.0 + - name: gpt-5.4 + # Pricing for Short context + input-estimated-cost-per-m: 2.5 + output-estimated-cost-per-m: 15.0 + - name: gpt-5.4-mini + # Pricing for Short context + input-estimated-cost-per-m: 0.75 + output-estimated-cost-per-m: 4.5 + - name: gpt-5.4-nano + # Pricing for Short context + input-estimated-cost-per-m: 0.2 + output-estimated-cost-per-m: 1.25 + - name: gpt-5.2-pro + input-estimated-cost-per-m: 21.0 + output-estimated-cost-per-m: 168.0 + - name: gpt-5.2 + input-estimated-cost-per-m: 1.75 + output-estimated-cost-per-m: 14.0 + - name: gpt-5.1 + input-estimated-cost-per-m: 1.25 + output-estimated-cost-per-m: 10.0 + - name: gpt-5-pro + input-estimated-cost-per-m: 15.0 + output-estimated-cost-per-m: 120.0 + - name: gpt-5 + input-estimated-cost-per-m: 1.25 + output-estimated-cost-per-m: 10.0 + - name: gpt-5-mini + input-estimated-cost-per-m: 0.25 + output-estimated-cost-per-m: 2.0 + - name: gpt-5-nano + input-estimated-cost-per-m: 0.05 + output-estimated-cost-per-m: 0.4 + - name: gpt-4.1 + input-estimated-cost-per-m: 2.0 + output-estimated-cost-per-m: 8.0 + - name: gpt-4.1-mini-2025-04-14 + input-estimated-cost-per-m: 0.4 + output-estimated-cost-per-m: 1.6 + - name: gpt-4.1-nano + input-estimated-cost-per-m: 0.1 + output-estimated-cost-per-m: 0.4 + - name: gpt-4o + input-estimated-cost-per-m: 2.5 + output-estimated-cost-per-m: 10.0 + - name: gpt-4o-mini + input-estimated-cost-per-m: 0.15 + output-estimated-cost-per-m: 0.6 + + - provider: anthropic + prefix-match: + - claude + models: + - name: claude-4.6-opus + # Base Input pricing + input-estimated-cost-per-m: 5.0 + output-estimated-cost-per-m: 25.0 + - name: claude-4.5-opus + # Base Input pricing + input-estimated-cost-per-m: 5.0 + output-estimated-cost-per-m: 25.0 + - name: claude-4.1-opus + # Base Input pricing + input-estimated-cost-per-m: 15.0 + output-estimated-cost-per-m: 75.0 + - name: claude-4-opus + # Base Input pricing + input-estimated-cost-per-m: 15.0 + output-estimated-cost-per-m: 75.0 + - name: claude-4.6-sonnet + # Base Input pricing + input-estimated-cost-per-m: 3.0 + output-estimated-cost-per-m: 15.0 + - name: claude-4.5-sonnet + # Base Input pricing + input-estimated-cost-per-m: 3.0 + output-estimated-cost-per-m: 15.0 + - name: claude-4-sonnet + # Base Input pricing + input-estimated-cost-per-m: 3.0 + output-estimated-cost-per-m: 15.0 + - name: claude-3.7-sonnet + # Deprecated, Base Input pricing + input-estimated-cost-per-m: 3.0 + output-estimated-cost-per-m: 15.0 + - name: claude-4.5-haiku + # Base Input pricing + input-estimated-cost-per-m: 1.0 + output-estimated-cost-per-m: 5.0 + - name: claude-3.5-haiku + # Base Input pricing + input-estimated-cost-per-m: 0.8 + output-estimated-cost-per-m: 4.0 + - name: claude-3-opus + # Deprecated, Base Input pricing + input-estimated-cost-per-m: 15.0 + output-estimated-cost-per-m: 75.0 + - name: claude-3-haiku + # Base Input pricing + input-estimated-cost-per-m: 0.25 + output-estimated-cost-per-m: 1.25 + + - provider: gemini + prefix-match: + - gemini + models: + # --- Gemini 3.1 Series --- + - name: gemini-3.1-pro-preview + + # Pricing for prompt <= 200k (Tier 1) + input-estimated-cost-per-m: 2.0 + output-estimated-cost-per-m: 12.0 + - name: gemini-3.1-pro-preview-customtools + input-estimated-cost-per-m: 2.0 + output-estimated-cost-per-m: 12.0 + + # Pricing for text/picture/video + - name: gemini-3.1-flash-lite-preview + input-estimated-cost-per-m: 0.25 + output-estimated-cost-per-m: 1.5 + - name: gemini-3.1-flash-image-preview + input-estimated-cost-per-m: 0.5 + output-estimated-cost-per-m: 3.0 + + # --- Gemini 3 Series --- + - name: gemini-3-flash-preview + input-estimated-cost-per-m: 0.5 + output-estimated-cost-per-m: 3.0 + - name: gemini-3-pro-image-preview + input-estimated-cost-per-m: 2.0 + output-estimated-cost-per-m: 12.0 + + # --- Gemini 2.5 Series --- + - name: gemini-2.5-pro + # Pricing for prompt <= 200k (Tier 1) + input-estimated-cost-per-m: 1.25 + output-estimated-cost-per-m: 10.0 + - name: gemini-2.5-flash + input-estimated-cost-per-m: 0.3 + output-estimated-cost-per-m: 2.5 + - name: gemini-2.5-flash-lite + input-estimated-cost-per-m: 0.1 + output-estimated-cost-per-m: 0.4 + - name: gemini-2.5-flash-lite-preview-09-2025 + input-estimated-cost-per-m: 0.1 + output-estimated-cost-per-m: 0.4 + - name: gemini-2.5-flash-native-audio-preview-12-2025 + input-estimated-cost-per-m: 0.5 + output-estimated-cost-per-m: 2.0 + - name: gemini-2.5-flash-image + input-estimated-cost-per-m: 0.3 + output-estimated-cost-per-m: 2.5 + - name: gemini-2.5-flash-preview-tts + input-estimated-cost-per-m: 0.5 + output-estimated-cost-per-m: 10.0 + - name: gemini-2.5-pro-preview-tts + input-estimated-cost-per-m: 1.0 + output-estimated-cost-per-m: 20.0 + + # --- Gemini 2.0 Series (Deprecated) --- + - name: gemini-2.0-flash + input-estimated-cost-per-m: 0.1 + output-estimated-cost-per-m: 0.4 + + - provider: mistral + prefix-match: + - mistral + models: + - name: mistral-large-3 + input-estimated-cost-per-m: 0.5 + output-estimated-cost-per-m: 1.5 + - name: mistral-medium-3 + input-estimated-cost-per-m: 0.4 + output-estimated-cost-per-m: 2.0 + - name: mistral-small-4 + input-estimated-cost-per-m: 0.15 + output-estimated-cost-per-m: 0.6 + + - provider: groq + prefix-match: + - llama + models: + - name: llama-4-scout-17bx16e-128k + input-estimated-cost-per-m: 0.11 + output-estimated-cost-per-m: 0.34 + - name: llama-3.3-70b-versatile-128k + input-estimated-cost-per-m: 0.59 + output-estimated-cost-per-m: 0.79 + - name: llama-3.1-8b-instant-128k + input-estimated-cost-per-m: 0.05 + output-estimated-cost-per-m: 0.08 + + - provider: deepseek + prefix-match: + - deepseek + models: + - name: deepseek-chat + # DeepSeek-V3.2 (Non-thinking Mode), 128K context + # Input pricing for cache miss + input-estimated-cost-per-m: 0.28 + output-estimated-cost-per-m: 0.42 + - name: deepseek-reasoner + # DeepSeek-V3.2 (Thinking Mode), 128K context + # Input pricing for cache miss + input-estimated-cost-per-m: 0.28 + output-estimated-cost-per-m: 0.42 + + - provider: bytedance + prefix-match: + - doubao + + - provider: zhipu_ai + prefix-match: + - glm + models: + - name: glm-5-turbo + # Input [0, 32) + input-estimated-cost-per-m: 0.73 + output-estimated-cost-per-m: 3.20 + - name: glm-5 + # Input [0, 32) + input-estimated-cost-per-m: 0.58 + output-estimated-cost-per-m: 2.61 + - name: glm-4.7 + # Input [0, 32) + input-estimated-cost-per-m: 0.29 + output-estimated-cost-per-m: 1.16 + - name: glm-4.5-air + # Input [0, 32) + input-estimated-cost-per-m: 0.12 + output-estimated-cost-per-m: 0.29 + - name: glm-4.7-flashx + # For 200K context + input-estimated-cost-per-m: 0.07 + output-estimated-cost-per-m: 0.44 + + - provider: alibaba + prefix-match: + - qwen + models: + - name: qwen3-max + # Lowest output price + input-estimated-cost-per-m: 0.36 + output-estimated-cost-per-m: 1.45 + - name: qwen3.5-plus + # Lowest output price + input-estimated-cost-per-m: 0.12 + output-estimated-cost-per-m: 0.70 + - name: qwen3.5-flash + # Lowest output price + input-estimated-cost-per-m: 0.03 + output-estimated-cost-per-m: 0.29 + + - provider: tencent + prefix-match: + - hunyuan + - Tencent + models: + - name: hunyuan-2.0-think + # For Input (0, 32k] + input-estimated-cost-per-m: 0.58 + output-estimated-cost-per-m: 2.31 + - name: hunyuan-2.0-instruct + # For Input (0, 32k] + input-estimated-cost-per-m: 0.46 + output-estimated-cost-per-m: 1.15 + - name: hunyuan-t1 + input-estimated-cost-per-m: 0.15 + output-estimated-cost-per-m: 0.58 + - name: hunyuan-turbos + input-estimated-cost-per-m: 0.12 + output-estimated-cost-per-m: 0.29 + - name: hunyuan-a13b + input-estimated-cost-per-m: 0.07 + output-estimated-cost-per-m: 0.29 + + - provider: moonshot + prefix-match: + - kimi + models: + - name: kimi-k2.5 + input-estimated-cost-per-m: 0.58 + output-estimated-cost-per-m: 3.05 + - name: kimi-k2-0905 + input-estimated-cost-per-m: 0.58 + output-estimated-cost-per-m: 2.32 + - name: kimi-k2-thinking + input-estimated-cost-per-m: 0.58 + output-estimated-cost-per-m: 2.32 + + - provider: minimax + prefix-match: + - minimax + models: + - name: minimax-m2.7 + input-estimated-cost-per-m: 0.3 + output-estimated-cost-per-m: 1.2 + - name: minimax-m2.7-highspeed + input-estimated-cost-per-m: 0.6 + output-estimated-cost-per-m: 2.4 + - name: minimax-m2.5 + input-estimated-cost-per-m: 0.3 + output-estimated-cost-per-m: 1.2 + - name: minimax-m2.5-highspeed + input-estimated-cost-per-m: 0.6 + output-estimated-cost-per-m: 2.4 + - name: m2-her + input-estimated-cost-per-m: 0.3 + output-estimated-cost-per-m: 1.2 + + - provider: ollama + # OLLAMA (Local Models): This section serves as a template for self-hosted models. + # Users can define their own prefix-match and specific models here. + prefix-match: diff --git a/oap-server/server-starter/src/main/resources/oal/virtual-gen-ai.oal b/oap-server/server-starter/src/main/resources/oal/virtual-gen-ai.oal new file mode 100644 index 000000000000..6a7b9a1d9a05 --- /dev/null +++ b/oap-server/server-starter/src/main/resources/oal/virtual-gen-ai.oal @@ -0,0 +1,46 @@ +/* + * 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. + * + */ + +gen_ai_provider_resp_time = from(GenAIProviderAccess.latency).longAvg(); +gen_ai_provider_sla = from(GenAIProviderAccess.*).percent(status == true); +gen_ai_provider_cpm = from(GenAIProviderAccess.*).cpm(); +gen_ai_provider_latency_percentile = from(GenAIProviderAccess.latency).percentile2(10); + +gen_ai_provider_input_tokens_sum = from(GenAIProviderAccess.inputTokens).sum(); +gen_ai_provider_input_tokens_avg = from(GenAIProviderAccess.inputTokens).longAvg(); +gen_ai_provider_output_tokens_sum = from(GenAIProviderAccess.outputTokens).sum(); +gen_ai_provider_output_tokens_avg = from(GenAIProviderAccess.outputTokens).longAvg(); + +gen_ai_provider_total_estimated_cost = from(GenAIProviderAccess.totalEstimatedCost).sum(); +gen_ai_provider_avg_estimated_cost = from(GenAIProviderAccess.totalEstimatedCost).doubleAvg(); + +gen_ai_model_call_cpm = from(GenAIModelAccess.*).cpm(); +gen_ai_model_sla = from(GenAIModelAccess.*).percent(status == true); +gen_ai_model_latency_avg = from(GenAIModelAccess.latency).longAvg(); +gen_ai_model_latency_percentile = from(GenAIModelAccess.latency).percentile2(10); + +gen_ai_model_ttft_avg = from(GenAIModelAccess.timeToFirstToken).filter(timeToFirstToken > 0).longAvg(); +gen_ai_model_ttft_percentile = from(GenAIModelAccess.timeToFirstToken).filter(timeToFirstToken > 0).percentile2(10); + +gen_ai_model_input_tokens_sum = from(GenAIModelAccess.inputTokens).sum(); +gen_ai_model_input_tokens_avg = from(GenAIModelAccess.inputTokens).longAvg(); +gen_ai_model_output_tokens_sum = from(GenAIModelAccess.outputTokens).sum(); +gen_ai_model_output_tokens_avg = from(GenAIModelAccess.outputTokens).longAvg(); + +gen_ai_model_total_estimated_cost = from(GenAIModelAccess.totalEstimatedCost).sum(); +gen_ai_model_avg_estimated_cost = from(GenAIModelAccess.totalEstimatedCost).doubleAvg(); \ No newline at end of file diff --git a/oap-server/server-starter/src/main/resources/ui-initialized-templates/menu.yaml b/oap-server/server-starter/src/main/resources/ui-initialized-templates/menu.yaml index ffe5059942d4..2a1c862e8764 100644 --- a/oap-server/server-starter/src/main/resources/ui-initialized-templates/menu.yaml +++ b/oap-server/server-starter/src/main/resources/ui-initialized-templates/menu.yaml @@ -272,3 +272,13 @@ menus: description: The Go Agent for Apache SkyWalking, which provides the native tracing/metrics/logging abilities for Golang projects. documentLink: https://skywalking.apache.org/docs/main/next/en/setup/backend/dashboards-so11y-go-agent/ i18nKey: self_observability_go_agent + - title: GenAI + icon: gen_ai + description: Generative AI (GenAI) observability provides comprehensive monitoring and performance insights for various AI services and large language models (LLM). + i18nKey: virtual_genai + menus: + - title: Virtual GenAI + layer: VIRTUAL_GENAI + description: Observe the virtual GenAI providers and models which are conjectured by language agents through various plugins. + documentLink: https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-genai/ + i18nKey: virtual_gen_ai diff --git a/oap-server/server-starter/src/main/resources/ui-initialized-templates/rocketmq/rocketmq-root.json b/oap-server/server-starter/src/main/resources/ui-initialized-templates/rocketmq/rocketmq-root.json index e89b31d355e4..5602bccbd6fd 100644 --- a/oap-server/server-starter/src/main/resources/ui-initialized-templates/rocketmq/rocketmq-root.json +++ b/oap-server/server-starter/src/main/resources/ui-initialized-templates/rocketmq/rocketmq-root.json @@ -32,7 +32,7 @@ "content": "Provide RocketMQ monitoring through OpenTelemetry's Prometheus Receiver", "fontSize": 14, "textAlign": "left", - "url": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-Rocketmq-monitoring/" + "url": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-rocketmq-monitoring/" } } ], diff --git a/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-model.json b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-model.json new file mode 100644 index 000000000000..5035050dcaf6 --- /dev/null +++ b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-model.json @@ -0,0 +1,394 @@ +[ + { + "id": "Virtual-GenAI-Model", + "configuration": { + "children": [ + { + "x": 0, + "y": 0, + "w": 24, + "h": 42, + "i": "0", + "type": "Tab", + "children": [ + { + "name": "Overview", + "children": [ + { + "x": 0, + "y": 0, + "w": 4, + "h": 11, + "i": "2", + "type": "Widget", + "expressions": [ + "latest(gen_ai_model_total_estimated_cost)/1000000" + ], + "graph": { + "type": "Card", + "fontSize": 24, + "textAlign": "center", + "showUnit": true + }, + "widget": { + "name": "TotalEstimatedCost", + "title": "Total Estimated Cost", + "tips": "The total estimated cost of GenAI model calls." + }, + "id": "0-0-2", + "moved": false, + "typesOfMQE": [ + "SINGLE_VALUE" + ] + }, + { + "x": 4, + "y": 0, + "w": 4, + "h": 11, + "i": "18", + "type": "Widget", + "expressions": [ + "latest(gen_ai_model_output_tokens_sum)" + ], + "graph": { + "type": "Card", + "fontSize": 24, + "textAlign": "center", + "showUnit": true + }, + "metricConfig": [ + { + "label": "Output Tokens" + } + ], + "widget": { + "name": "OutputTokens", + "title": "Output Tokens", + "tips": "The sum of output tokens." + }, + "id": "0-0-18", + "moved": false, + "typesOfMQE": [ + "SINGLE_VALUE" + ] + }, + { + "x": 8, + "y": 0, + "w": 4, + "h": 11, + "i": "19", + "type": "Widget", + "expressions": [ + "latest(gen_ai_model_input_tokens_sum)" + ], + "graph": { + "type": "Card", + "fontSize": 24, + "textAlign": "center", + "showUnit": true + }, + "metricConfig": [ + { + "label": "Input Tokens" + } + ], + "widget": { + "name": "InputTokens", + "title": "Input Tokens", + "tips": "The sum of input tokens." + }, + "id": "0-0-19", + "moved": false, + "typesOfMQE": [ + "SINGLE_VALUE" + ] + }, + { + "x": 12, + "y": 24, + "w": 12, + "h": 12, + "i": "1", + "type": "Widget", + "expressions": [ + "gen_ai_model_sla/100" + ], + "graph": { + "type": "Line", + "showXAxis": true, + "showYAxis": true + }, + "widget": { + "name": "ServiceSLA", + "title": "Service SLA", + "tips": "Successful rate of GenAI model calls." + }, + "metricConfig": [ + { + "label": "SLA", + "unit": "%" + } + ], + "id": "0-0-1", + "moved": false + }, + { + "x": 0, + "y": 24, + "w": 12, + "h": 12, + "i": "0", + "type": "Widget", + "expressions": [ + "gen_ai_model_latency_avg" + ], + "graph": { + "type": "Line", + "showXAxis": true, + "showYAxis": true + }, + "metricConfig": [ + { + "label": "Response Time", + "unit": "ms" + } + ], + "widget": { + "name": "ResponseTime", + "title": "Response Time", + "tips": "The average response time of GenAI model." + }, + "id": "0-0-0", + "moved": false + }, + { + "x": 12, + "y": 36, + "w": 12, + "h": 11, + "i": "5", + "type": "Widget", + "expressions": [ + "gen_ai_model_call_cpm" + ], + "graph": { + "type": "Line", + "showXAxis": true, + "showYAxis": true + }, + "metricConfig": [ + { + "label": "CPM" + } + ], + "widget": { + "name": "CPM", + "title": "Calls Per Minute", + "tips": "The number of calls per minute." + }, + "id": "0-0-5", + "moved": false + }, + { + "x": 12, + "y": 47, + "w": 12, + "h": 12, + "i": "13", + "type": "Widget", + "expressions": [ + "gen_ai_model_latency_percentile" + ], + "graph": { + "type": "Bar", + "showBackground": true + }, + "widget": { + "name": "LatencyPercentile", + "title": "Latency Percentile", + "tips": "The percentile of model access latency." + }, + "id": "0-0-13", + "moved": false + }, + { + "x": 0, + "y": 36, + "w": 12, + "h": 11, + "i": "20", + "type": "Widget", + "widget": { + "name": "AvgTTFT", + "title": "Average TTFT", + "tips": "The average time from the start of the request until the model returns the first token (Time To First Token)." + }, + "metricConfig": [ + { + "label": "Avg TTFT", + "unit": "ms" + } + ], + "graph": { + "type": "Line", + "step": false, + "smooth": false, + "showSymbol": true, + "showXAxis": true, + "showYAxis": true + }, + "id": "0-0-20", + "moved": false, + "expressions": [ + "gen_ai_model_ttft_avg" + ], + "typesOfMQE": [ + "TIME_SERIES_VALUES" + ] + }, + { + "x": 0, + "y": 47, + "w": 12, + "h": 12, + "i": "21", + "type": "Widget", + "widget": { + "name": "TTFTPercentile", + "title": "TTFT Percentile", + "tips": "The percentile distribution of Time to First Token." + }, + "metricConfig": [ + { + "label": "TTFT Percentile", + "unit": "ms" + } + ], + "graph": { + "type": "Bar", + "showBackground": true + }, + "id": "0-0-21", + "moved": false, + "expressions": [ + "gen_ai_model_ttft_percentile" + ], + "typesOfMQE": [ + "TIME_SERIES_VALUES" + ] + }, + { + "x": 0, + "y": 11, + "w": 12, + "h": 13, + "i": "22", + "type": "Widget", + "widget": { + "name": "AvgInputTokens", + "title": "Average Input Tokens", + "tips": "The average number of input tokens used per model call." + }, + "metricConfig": [ + { + "label": "Avg Input Tokens" + } + ], + "graph": { + "type": "Line", + "step": false, + "smooth": false, + "showSymbol": true, + "showXAxis": true, + "showYAxis": true + }, + "id": "0-0-22", + "moved": false, + "expressions": [ + "gen_ai_model_input_tokens_avg" + ], + "typesOfMQE": [ + "TIME_SERIES_VALUES" + ] + }, + { + "x": 12, + "y": 11, + "w": 12, + "h": 13, + "i": "23", + "type": "Widget", + "widget": { + "name": "AvgOutputTokens", + "title": "Average Output Tokens", + "tips": "The average number of output tokens used per model call." + }, + "metricConfig": [ + { + "label": "Avg Output Tokens" + } + ], + "graph": { + "type": "Line", + "step": false, + "smooth": false, + "showSymbol": true, + "showXAxis": true, + "showYAxis": true + }, + "id": "0-0-23", + "moved": false, + "expressions": [ + "gen_ai_model_output_tokens_avg" + ], + "typesOfMQE": [ + "TIME_SERIES_VALUES" + ] + }, + { + "x": 12, + "y": 0, + "w": 12, + "h": 11, + "i": "24", + "type": "Widget", + "widget": { + "name": "AvgEstimatedCost", + "title": "Average Estimated Cost", + "tips": "The average estimated cost of model calls." + }, + "graph": { + "type": "Line", + "step": false, + "smooth": false, + "showSymbol": true, + "showXAxis": true, + "showYAxis": true + }, + "id": "0-0-24", + "moved": false, + "expressions": [ + "gen_ai_model_avg_estimated_cost" + ], + "typesOfMQE": [ + "TIME_SERIES_VALUES" + ] + } + ] + } + ], + "id": "0", + "activedTabIndex": 0, + "moved": false + } + ], + "layer": "VIRTUAL_GENAI", + "entity": "ServiceInstance", + "name": "Virtual-GenAI-Model", + "id": "Virtual-GenAI-Model", + "isRoot": false + } + } +] diff --git a/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-provider.json b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-provider.json new file mode 100644 index 000000000000..7a2d7406f751 --- /dev/null +++ b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-provider.json @@ -0,0 +1,337 @@ +[ + { + "id": "Virtual-GenAI-Provider", + "configuration": { + "children": [ + { + "x": 0, + "y": 0, + "w": 24, + "h": 42, + "i": "0", + "type": "Tab", + "children": [ + { + "name": "Overview", + "children": [ + { + "x": 0, + "y": 0, + "w": 4, + "h": 11, + "i": "2", + "type": "Widget", + "expressions": [ + "latest(gen_ai_provider_total_estimated_cost)/1000000" + ], + "graph": { + "type": "Card", + "fontSize": 24, + "textAlign": "center", + "showUnit": true + }, + "widget": { + "name": "TotalEstimatedCost", + "title": "Total Estimated Cost", + "tips": "The total estimated cost of GenAI model calls." + }, + "id": "0-0-2", + "moved": false, + "typesOfMQE": [ + "SINGLE_VALUE" + ] + }, + { + "x": 4, + "y": 0, + "w": 4, + "h": 11, + "i": "18", + "type": "Widget", + "expressions": [ + "latest(gen_ai_provider_output_tokens_sum)" + ], + "graph": { + "type": "Card", + "fontSize": 24, + "textAlign": "center", + "showUnit": true + }, + "metricConfig": [ + { + "label": "Output Tokens" + } + ], + "widget": { + "name": "OutputTokens", + "title": "Output Tokens", + "tips": "The sum of output tokens." + }, + "id": "0-0-18", + "moved": false, + "typesOfMQE": [ + "SINGLE_VALUE" + ] + }, + { + "x": 8, + "y": 0, + "w": 4, + "h": 11, + "i": "19", + "type": "Widget", + "expressions": [ + "latest(gen_ai_provider_input_tokens_sum)" + ], + "graph": { + "type": "Card", + "fontSize": 24, + "textAlign": "center", + "showUnit": true + }, + "metricConfig": [ + { + "label": "Input Tokens" + } + ], + "widget": { + "name": "InputTokens", + "title": "Input Tokens", + "tips": "The sum of input tokens." + }, + "id": "0-0-19", + "moved": false, + "typesOfMQE": [ + "SINGLE_VALUE" + ] + }, + { + "x": 12, + "y": 0, + "w": 12, + "h": 11, + "i": "1", + "type": "Widget", + "expressions": [ + "gen_ai_provider_sla/100" + ], + "graph": { + "type": "Line", + "showXAxis": true, + "showYAxis": true + }, + "widget": { + "name": "ServiceSLA", + "title": "Service SLA", + "tips": "Successful rate of GenAI provider calls." + }, + "metricConfig": [ + { + "label": "SLA", + "unit": "%" + } + ], + "id": "0-0-1", + "moved": false + }, + { + "x": 0, + "y": 34, + "w": 12, + "h": 12, + "i": "0", + "type": "Widget", + "expressions": [ + "gen_ai_provider_resp_time" + ], + "graph": { + "type": "Line", + "showXAxis": true, + "showYAxis": true + }, + "metricConfig": [ + { + "label": "Response Time", + "unit": "ms" + } + ], + "widget": { + "name": "ResponseTime", + "title": "Response Time", + "tips": "The average response time of GenAI provider." + }, + "id": "0-0-0", + "moved": false + }, + { + "x": 12, + "y": 22, + "w": 12, + "h": 12, + "i": "5", + "type": "Widget", + "expressions": [ + "gen_ai_provider_cpm" + ], + "graph": { + "type": "Line", + "showXAxis": true, + "showYAxis": true + }, + "metricConfig": [ + { + "label": "CPM" + } + ], + "widget": { + "name": "CPM", + "title": "Calls Per Minute", + "tips": "The number of calls per minute." + }, + "id": "0-0-5", + "moved": false + }, + { + "x": 0, + "y": 22, + "w": 12, + "h": 12, + "i": "14", + "type": "Widget", + "expressions": [ + "gen_ai_provider_avg_estimated_cost" + ], + "graph": { + "type": "Line", + "showXAxis": true, + "showYAxis": true + }, + "widget": { + "name": "AvgEstimatedCost", + "title": "Average Estimated Cost", + "tips": "The average estimated cost of provider access." + }, + "id": "0-0-14", + "moved": false, + "typesOfMQE": [ + "UNKNOWN" + ] + }, + { + "x": 12, + "y": 34, + "w": 12, + "h": 12, + "i": "13", + "type": "Widget", + "expressions": [ + "gen_ai_provider_latency_percentile" + ], + "graph": { + "type": "Bar", + "showBackground": true + }, + "widget": { + "name": "LatencyPercentile", + "title": "Latency Percentile", + "tips": "The percentile of model access latency." + }, + "id": "0-0-13", + "moved": false + }, + { + "x": 12, + "y": 11, + "w": 12, + "h": 11, + "i": "20", + "type": "Widget", + "widget": { + "name": "AvgOutputTokens", + "title": "Average Output Tokens", + "tips": "The average number of output tokens used per provider access." + }, + "graph": { + "type": "Line", + "step": false, + "smooth": false, + "showSymbol": true, + "showXAxis": true, + "showYAxis": true + }, + "id": "0-0-20", + "moved": false, + "expressions": [ + "gen_ai_provider_output_tokens_avg" + ], + "typesOfMQE": [ + "TIME_SERIES_VALUES" + ] + }, + { + "x": 0, + "y": 11, + "w": 12, + "h": 11, + "i": "21", + "type": "Widget", + "widget": { + "name": "AvgInputTokens", + "title": "Average Input Tokens", + "tips": "The average number of input tokens used per provider access." + }, + "graph": { + "type": "Line", + "step": false, + "smooth": false, + "showSymbol": true, + "showXAxis": true, + "showYAxis": true + }, + "id": "0-0-21", + "moved": false, + "expressions": [ + "gen_ai_provider_input_tokens_avg" + ], + "typesOfMQE": [ + "TIME_SERIES_VALUES" + ] + } + ] + }, + { + "name": "model", + "children": [ + { + "x": 0, + "y": 0, + "w": 24, + "h": 30, + "i": "0", + "type": "Widget", + "graph": { + "type": "InstanceList", + "dashboardName": "Virtual-GenAI-Model", + "fontSize": 12 + }, + "id": "0-1-0", + "moved": false + } + ] + } + ], + "id": "0", + "activedTabIndex": 0, + "moved": false + } + ], + "layer": "VIRTUAL_GENAI", + "entity": "Service", + "name": "Virtual-GenAI-Provider", + "id": "Virtual-GenAI-Provider", + "isRoot": false + } + } +] + + diff --git a/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-root.json b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-root.json new file mode 100644 index 000000000000..b2e76a1168d2 --- /dev/null +++ b/oap-server/server-starter/src/main/resources/ui-initialized-templates/virtual_genai/virtual-genai-root.json @@ -0,0 +1,58 @@ +[ + { + "id": "Virtual-GenAI-Root", + "configuration": { + "children": [ + { + "x": 0, + "y": 2, + "w": 24, + "h": 52, + "i": "0", + "type": "Widget", + "widget": { + "title": "Virtual GenAI" + }, + "graph": { + "type": "ServiceList", + "dashboardName": "Virtual-GenAI-Provider", + "fontSize": 12, + "showXAxis": false, + "showYAxis": false, + "showGroup": false + }, + "expressions": [ + "avg(gen_ai_provider_resp_time)", + "avg(gen_ai_provider_sla)/100", + "avg(gen_ai_provider_cpm)" + ], + "subExpressions": [ + "gen_ai_provider_resp_time", + "gen_ai_provider_sla/100", + "gen_ai_provider_cpm" + ], + "metricConfig": [ + { + "unit": "ms", + "label": "Access Latency" + }, + { + "label": "Successful Access Rate", + "unit": "%" + }, + { + "label": "Access Traffic", + "unit": "calls / min" + } + ] + } + ], + "id": "Virtual-GenAI-Root", + "layer": "VIRTUAL_GENAI", + "entity": "All", + "name": "Virtual-GenAI-Root", + "isRoot": true + } + } +] + diff --git a/oap-server/server-starter/src/test/java/org/apache/skywalking/oap/server/starter/config/GenAIMeterAnalyzerTest.java b/oap-server/server-starter/src/test/java/org/apache/skywalking/oap/server/starter/config/GenAIMeterAnalyzerTest.java new file mode 100644 index 000000000000..b0ebe05939aa --- /dev/null +++ b/oap-server/server-starter/src/test/java/org/apache/skywalking/oap/server/starter/config/GenAIMeterAnalyzerTest.java @@ -0,0 +1,243 @@ +/* + * 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.oap.server.starter.config; + +import org.apache.skywalking.apm.network.common.v3.KeyStringValuePair; +import org.apache.skywalking.apm.network.language.agent.v3.SegmentObject; +import org.apache.skywalking.apm.network.language.agent.v3.SpanObject; +import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfig; +import org.apache.skywalking.oap.analyzer.genai.config.GenAIConfigLoader; +import org.apache.skywalking.oap.analyzer.genai.config.GenAITagKeys; +import org.apache.skywalking.oap.analyzer.genai.matcher.GenAIProviderPrefixMatcher; +import org.apache.skywalking.oap.analyzer.genai.service.GenAIMeterAnalyzer; +import org.apache.skywalking.oap.server.core.source.GenAIMetrics; +import org.apache.skywalking.oap.server.library.module.ModuleStartException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GenAIMeterAnalyzerTest { + private GenAIConfig loadedConfig; + private GenAIProviderPrefixMatcher matcher; + private GenAIMeterAnalyzer analyzer; + + @BeforeEach + void setUp() throws ModuleStartException { + GenAIConfig config = new GenAIConfig(); + GenAIConfigLoader loader = new GenAIConfigLoader(config); + loadedConfig = loader.loadConfig(); + + matcher = GenAIProviderPrefixMatcher.build(loadedConfig); + analyzer = new GenAIMeterAnalyzer(matcher); + } + + @Test + void testLoadConfig() { + assertNotNull(loadedConfig); + assertFalse(loadedConfig.getProviders().isEmpty(), "Providers list should not be empty after loading config"); + } + + @Test + void testProviderMatching() { + assertEquals("openai", matcher.match("gpt-5.4-pro").getProvider()); + assertEquals("deepseek", matcher.match("deepseek-chat").getProvider()); + assertEquals("anthropic", matcher.match("claude-4.6-opus").getProvider()); + assertEquals("gemini", matcher.match("gemini-3.1-pro-preview").getProvider()); + } + + @Test + void testExtractMetricsWithValidSpan() { + SpanObject span = SpanObject.newBuilder() + .setOperationName("genai_call") + .setStartTime(1000000L) + .setEndTime(1005000L) + .setIsError(false) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.RESPONSE_MODEL) + .setValue("gpt-5.4") + .build()) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.PROVIDER_NAME) + .setValue("openai") + .build()) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.INPUT_TOKENS) + .setValue("1000") + .build()) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.OUTPUT_TOKENS) + .setValue("500") + .build()) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.SERVER_TIME_TO_FIRST_TOKEN) + .setValue("100") + .build()) + .build(); + + SegmentObject segment = SegmentObject.newBuilder().build(); + GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment); + + assertNotNull(metrics); + assertEquals("openai", metrics.getProviderName()); + assertEquals("gpt-5.4", metrics.getModelName()); + assertEquals(1000L, metrics.getInputTokens()); + assertEquals(500L, metrics.getOutputTokens()); + assertEquals(100, metrics.getTimeToFirstToken()); + assertEquals(10000L, metrics.getTotalEstimatedCost()); + assertEquals(5000L, metrics.getLatency()); + assertTrue(metrics.isStatus()); + } + + @Test + void testExtractMetricsWithMissingModelName() { + SpanObject span = SpanObject.newBuilder() + .setOperationName("genai_call") + .setStartTime(1000000L) + .setEndTime(1005000L) + .build(); + + SegmentObject segment = SegmentObject.newBuilder().build(); + GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment); + + assertNull(metrics); + } + + @Test + void testExtractMetricsWithoutProviderName() { + SpanObject span = SpanObject.newBuilder() + .setOperationName("genai_call") + .setStartTime(1000000L) + .setEndTime(1005000L) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.RESPONSE_MODEL) + .setValue("deepseek-chat") + .build()) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.INPUT_TOKENS) + .setValue("2000") + .build()) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.OUTPUT_TOKENS) + .setValue("1000") + .build()) + .build(); + + SegmentObject segment = SegmentObject.newBuilder().build(); + GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment); + + assertNotNull(metrics); + assertEquals("deepseek", metrics.getProviderName()); + assertEquals("deepseek-chat", metrics.getModelName()); + assertEquals(980L, metrics.getTotalEstimatedCost()); + } + + @Test + void testExtractMetricsWithNoModelConfig() { + SpanObject span = SpanObject.newBuilder() + .setOperationName("genai_call") + .setStartTime(1000000L) + .setEndTime(1005000L) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.RESPONSE_MODEL) + .setValue("unknown-model") + .build()) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.INPUT_TOKENS) + .setValue("1000") + .build()) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.OUTPUT_TOKENS) + .setValue("500") + .build()) + .build(); + + SegmentObject segment = SegmentObject.newBuilder().build(); + GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment); + + assertNotNull(metrics); + assertEquals(0L, metrics.getTotalEstimatedCost()); + } + + @Test + void testExtractMetricsWithInvalidTokenValues() { + SpanObject span = SpanObject.newBuilder() + .setOperationName("genai_call") + .setStartTime(1000000L) + .setEndTime(1005000L) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.RESPONSE_MODEL) + .setValue("gpt-5.4") + .build()) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.INPUT_TOKENS) + .setValue("invalid") + .build()) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.OUTPUT_TOKENS) + .setValue("not-a-number") + .build()) + .build(); + + SegmentObject segment = SegmentObject.newBuilder().build(); + GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment); + + assertNotNull(metrics); + assertEquals(0L, metrics.getInputTokens()); + assertEquals(0L, metrics.getOutputTokens()); + } + + @Test + void testExtractMetricsWithErrorSpan() { + SpanObject span = SpanObject.newBuilder() + .setOperationName("genai_call") + .setStartTime(1000000L) + .setEndTime(1005000L) + .setIsError(true) + .addTags(KeyStringValuePair.newBuilder() + .setKey(GenAITagKeys.RESPONSE_MODEL) + .setValue("claude-4-sonnet") + .build()) + .build(); + + SegmentObject segment = SegmentObject.newBuilder().build(); + GenAIMetrics metrics = analyzer.extractMetricsFromSWSpan(span, segment); + + assertNotNull(metrics); + assertFalse(metrics.isStatus()); + } + + @Test + void testEstimatedCost() throws ModuleStartException { + GenAIConfig config = new GenAIConfig(); + GenAIConfigLoader loader = new GenAIConfigLoader(config); + GenAIConfig loadedConfig = loader.loadConfig(); + + GenAIProviderPrefixMatcher matcher = GenAIProviderPrefixMatcher.build(loadedConfig); + + GenAIProviderPrefixMatcher.MatchResult result = matcher.match("gpt-5.4-pro"); + assertNotNull(result.getModelConfig()); + assertEquals(30.0, result.getModelConfig().getInputEstimatedCostPerM(), 0.001); + assertEquals(180.0, result.getModelConfig().getOutputEstimatedCostPerM(), 0.001); + } +} diff --git a/skywalking-ui b/skywalking-ui index 6538cc401d19..8b004ef3167c 160000 --- a/skywalking-ui +++ b/skywalking-ui @@ -1 +1 @@ -Subproject commit 6538cc401d19f768d8b1e075785d991ce7e4739f +Subproject commit 8b004ef3167c44d1e4176db0bdeaf41efbad016b diff --git a/test/e2e-v2/cases/storage/expected/config-dump.yml b/test/e2e-v2/cases/storage/expected/config-dump.yml index e5f259f5ef9d..118fa76f7012 100644 --- a/test/e2e-v2/cases/storage/expected/config-dump.yml +++ b/test/e2e-v2/cases/storage/expected/config-dump.yml @@ -38,6 +38,7 @@ core.default.enableDataKeeperExecutor=true agent-analyzer.default.slowCacheReadThreshold=default:20,redis:10 receiver-ebpf.default.continuousPolicyCacheTimeout=60 receiver-ebpf.default.gRPCSslKeyPath= +gen-ai-analyzer.provider=default receiver-browser.provider=default agent-analyzer.default.segmentStatusAnalysisStrategy=FROM_SPAN_STATUS envoy-metric.default.maxConcurrentCallsPerConnection=0 diff --git a/test/e2e-v2/cases/virtual-genai/Dockerfile.provider b/test/e2e-v2/cases/virtual-genai/Dockerfile.provider new file mode 100644 index 000000000000..93f3935f944a --- /dev/null +++ b/test/e2e-v2/cases/virtual-genai/Dockerfile.provider @@ -0,0 +1,44 @@ +# 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. + +ARG SW_AGENT_JDK_VERSION +ARG SW_AGENT_JAVA_COMMIT + +FROM eclipse-temurin:17-jdk AS builder + +RUN apt-get update && apt-get install -y git maven && rm -rf /var/lib/apt/lists/* + +WORKDIR /source +RUN git clone https://github.com/spring-projects/spring-ai-examples.git \ + && cd spring-ai-examples \ + && git checkout 2a6088db3d18d5fa6fc208b12adf1172d22f77fd + +WORKDIR /source/spring-ai-examples/misc/openai-streaming-response +RUN mvn clean package -DskipTests + +RUN find target/ -maxdepth 1 -name "*.jar" ! -name "*-plain.jar" -exec cp {} /app.jar \; + +FROM ghcr.io/apache/skywalking-java/skywalking-java:${SW_AGENT_JAVA_COMMIT}-java${SW_AGENT_JDK_VERSION} + +WORKDIR /skywalking + +VOLUME /services + +COPY --from=builder /app.jar /services/app.jar + +EXPOSE 8080 + +CMD ["sh", "-c", "java -jar /services/app.jar"] + diff --git a/test/e2e-v2/cases/virtual-genai/docker-compose.yml b/test/e2e-v2/cases/virtual-genai/docker-compose.yml new file mode 100644 index 000000000000..25e44de2d73b --- /dev/null +++ b/test/e2e-v2/cases/virtual-genai/docker-compose.yml @@ -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. + +version: "3" + +services: + oap: + extends: + file: ../../script/docker-compose/base-compose.yml + service: oap + ports: + - "12800:12800" + - "11800:11800" + networks: + - e2e + + banyandb: + extends: + file: ../../script/docker-compose/base-compose.yml + service: banyandb + ports: + - 17912 + + provider: + extends: + file: ../../script/docker-compose/base-compose.yml + service: provider + ports: + - 9090 + depends_on: + oap: + condition: service_healthy + + spring-ai-examples: + build: + context: . + dockerfile: Dockerfile.provider + args: + - SW_AGENT_JDK_VERSION=17 + - SW_AGENT_JAVA_COMMIT=${SW_AGENT_JAVA_COMMIT} + ports: + - "9260:8080" + networks: + - e2e + environment: + OPENAI_API_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + SPRING_AI_OPENAI_BASE_URL: http://provider:9090/llm + SW_AGENT_COLLECTOR_BACKEND_SERVICES: oap:11800 + SW_LOGGING_OUTPUT: CONSOLE + SW_AGENT_NAME: e2e-spring-ai + SW_AGENT_INSTANCE_NAME: spring-ai-examples + healthcheck: + test: [ "CMD", "sh", "-c", "nc -nz 127.0.0.1 8080" ] + interval: 5s + timeout: 60s + retries: 120 + depends_on: + oap: + condition: service_healthy + +networks: + e2e: diff --git a/test/e2e-v2/cases/virtual-genai/e2e.yaml b/test/e2e-v2/cases/virtual-genai/e2e.yaml new file mode 100644 index 000000000000..3416deb91cec --- /dev/null +++ b/test/e2e-v2/cases/virtual-genai/e2e.yaml @@ -0,0 +1,45 @@ +# 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. + +# This file is used to show how to write configuration files and can be used to test. + +setup: + env: compose + file: docker-compose.yml + timeout: 20m + init-system-environment: ../../script/env + steps: + - name: set PATH + command: export PATH=/tmp/skywalking-infra-e2e/bin:$PATH + - name: install yq + command: bash test/e2e-v2/script/prepare/setup-e2e-shell/install.sh yq + - name: install swctl + command: bash test/e2e-v2/script/prepare/setup-e2e-shell/install.sh swctl + +trigger: + action: http + interval: 3s + times: 5 + url: http://localhost:9260/ai/generateStream + method: GET + +verify: + retry: + count: 60 + interval: 3s + cases: + - includes: + - ./virtual-genai.yaml + diff --git a/test/e2e-v2/cases/virtual-genai/expected/instance.yml b/test/e2e-v2/cases/virtual-genai/expected/instance.yml new file mode 100644 index 000000000000..f74704b2b875 --- /dev/null +++ b/test/e2e-v2/cases/virtual-genai/expected/instance.yml @@ -0,0 +1,22 @@ +# 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. + +{{- contains . }} +- id: {{ notEmpty .id }} + name: gpt-4.1-mini-2025-04-14 + instanceuuid: {{ notEmpty .instanceuuid }} + attributes: [] + language: UNKNOWN +{{- end }} diff --git a/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value-label.yml b/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value-label.yml new file mode 100644 index 000000000000..c983c0e19ba3 --- /dev/null +++ b/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value-label.yml @@ -0,0 +1,39 @@ +# 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. + +debuggingtrace: null +type: TIME_SERIES_VALUES +results: + {{- contains .results }} + - metric: + labels: + {{- contains .metric.labels }} + - key: "p" + value: {{ notEmpty .value }} + {{- end}} + values: + {{- contains .values }} + - id: {{ notEmpty .id }} + value: {{ .value }} + traceid: null + owner: null + - id: {{ notEmpty .id }} + value: null + traceid: null + owner: null + {{- end}} + {{- end}} +error: null + diff --git a/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value.yml b/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value.yml new file mode 100644 index 000000000000..979b9b25775c --- /dev/null +++ b/test/e2e-v2/cases/virtual-genai/expected/metrics-has-value.yml @@ -0,0 +1,34 @@ +# 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. + +debuggingtrace: null +type: TIME_SERIES_VALUES +results: + {{- contains .results }} + - metric: + labels: [] + values: + {{- contains .values }} + - id: {{ notEmpty .id }} + value: {{ notEmpty .value }} + traceid: null + owner: null + - id: {{ notEmpty .id }} + value: null + traceid: null + owner: null + {{- end}} + {{- end}} +error: null diff --git a/test/e2e-v2/cases/virtual-genai/expected/service.yml b/test/e2e-v2/cases/virtual-genai/expected/service.yml new file mode 100644 index 000000000000..5471c7d596d8 --- /dev/null +++ b/test/e2e-v2/cases/virtual-genai/expected/service.yml @@ -0,0 +1,24 @@ +# 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. + +{{- contains . }} +- id: {{ b64enc "openai" }}.0 + name: openai + group: "" + shortname: openai + layers: + - VIRTUAL_GENAI + normal: false +{{- end }} diff --git a/test/e2e-v2/cases/virtual-genai/virtual-genai.yaml b/test/e2e-v2/cases/virtual-genai/virtual-genai.yaml new file mode 100644 index 000000000000..7f7274b1fa47 --- /dev/null +++ b/test/e2e-v2/cases/virtual-genai/virtual-genai.yaml @@ -0,0 +1,69 @@ +# 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. + +# This file is used to show how to write configuration files and can be used to test. + +cases: + # service cases + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql service ls + expected: expected/service.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_resp_time --service-id=b3BlbmFp.0 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_sla --service-id=b3BlbmFp.0 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_cpm --service-id=b3BlbmFp.0 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_latency_percentile --service-id=b3BlbmFp.0 + expected: expected/metrics-has-value-label.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_input_tokens_sum --service-id=b3BlbmFp.0 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_input_tokens_avg --service-id=b3BlbmFp.0 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_output_tokens_sum --service-id=b3BlbmFp.0 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_output_tokens_avg --service-id=b3BlbmFp.0 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_total_estimated_cost --service-id=b3BlbmFp.0 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_provider_avg_estimated_cost --service-id=b3BlbmFp.0 + expected: expected/metrics-has-value.yml + + # instance cases + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql instance ls --service-id=b3BlbmFp.0 + expected: expected/instance.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_call_cpm --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_sla --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_latency_avg --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_latency_percentile --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value-label.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_ttft_avg --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_ttft_percentile --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value-label.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_input_tokens_sum --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_input_tokens_avg --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_output_tokens_sum --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_output_tokens_avg --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_total_estimated_cost --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value.yml + - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression=gen_ai_model_avg_estimated_cost --service-id=b3BlbmFp.0 --instance-name=gpt-4.1-mini-2025-04-14 + expected: expected/metrics-has-value.yml \ No newline at end of file diff --git a/test/e2e-v2/java-test-service/e2e-service-provider/pom.xml b/test/e2e-v2/java-test-service/e2e-service-provider/pom.xml index 5f535cc0fd19..2de07cc331c9 100644 --- a/test/e2e-v2/java-test-service/e2e-service-provider/pom.xml +++ b/test/e2e-v2/java-test-service/e2e-service-provider/pom.xml @@ -111,6 +111,7 @@ guava 23.0 + diff --git a/test/e2e-v2/java-test-service/e2e-service-provider/src/main/java/org/apache/skywalking/e2e/controller/LLMMockController.java b/test/e2e-v2/java-test-service/e2e-service-provider/src/main/java/org/apache/skywalking/e2e/controller/LLMMockController.java new file mode 100644 index 000000000000..1d29883ebdd7 --- /dev/null +++ b/test/e2e-v2/java-test-service/e2e-service-provider/src/main/java/org/apache/skywalking/e2e/controller/LLMMockController.java @@ -0,0 +1,105 @@ +/* + * 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.e2e.controller; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.time.Instant; + +@RestController +@RequestMapping("/llm") +public class LLMMockController { + @PostMapping("/v1/chat/completions") + public Object completions(HttpServletResponse response) throws Exception { + + response.setContentType("text/event-stream"); + response.setCharacterEncoding("UTF-8"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Connection", "keep-alive"); + + String id = "chatcmpl-simple-mock-001"; + long created = Instant.now().getEpochSecond(); + String model = "gpt-4.1-mini-2025-04-14"; + + try (PrintWriter writer = response.getWriter()) { + Thread.sleep(1000); + writeStreamChunk(writer, id, created, model, "{\"role\":\"assistant\"}", "null"); + + String fullContent = "Why did the scarecrow win an award? Because he was outstanding in his field!"; + String[] words = fullContent.split(" "); + + for (int i = 0; i < words.length; i++) { + String chunk = words[i] + (i == words.length - 1 ? "" : " "); + Thread.sleep(50); + writeStreamChunk(writer, id, created, model, "{\"content\":\"" + chunk + "\"}", "null"); + } + + writeStreamChunk(writer, id, created, model, "{}", "\"stop\""); + + writer.write("data: [DONE]\n\n"); + writer.flush(); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return null; + } + + private void writeStreamChunk(PrintWriter writer, String id, long created, String model, String delta, String finishReason) { + String json = "{" + + "\"id\": \"%s\"," + + "\"object\": \"chat.completion.chunk\"," + + "\"created\": %d," + + "\"model\": \"%s\"," + + "\"system_fingerprint\": null," + + "\"choices\": [" + + "{" + + "\"index\": 0," + + "\"delta\": %s," + + "\"finish_reason\": %s" + + "}" + + "]," + + "\"usage\": {" + + "\"completion_tokens\": 17," + + "\"completion_tokens_details\": {" + + "\"accepted_prediction_tokens\": 0," + + "\"audio_tokens\": 0," + + "\"reasoning_tokens\": 0," + + "\"rejected_prediction_tokens\": 0" + + "}," + + "\"prompt_tokens\": 52," + + "\"prompt_tokens_details\": {" + + "\"audio_tokens\": 0," + + "\"cached_tokens\": 0" + + "}," + + "\"total_tokens\": 69" + + "}" + + "}"; + + String formattedJson = String.format(json, id, created, model, delta, finishReason); + + String cleanJson = formattedJson.replace("\n", "").replace("\r", ""); + writer.write("data: " + cleanJson + "\n\n"); + writer.flush(); + } +} diff --git a/test/e2e-v2/script/env b/test/e2e-v2/script/env index e59a6923db81..e8b227794ea9 100644 --- a/test/e2e-v2/script/env +++ b/test/e2e-v2/script/env @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -SW_AGENT_JAVA_COMMIT=2a61027e5eb74ed1258c764ae2ffeabd499416a6 +SW_AGENT_JAVA_COMMIT=ac0df43d7140e726eba9e5e5b1b75cf364c71dff SW_AGENT_SATELLITE_COMMIT=ea27a3f4e126a24775fe12e2aa2695bcb23d99c3 SW_AGENT_NGINX_LUA_COMMIT=c3cee4841798a147d83b96a10914d4ac0e11d0aa SW_AGENT_NODEJS_COMMIT=4f9a91dad3dfd8cfe5ba8f7bd06b39e11eb5e65e