Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/skywalking.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,10 @@ jobs:
config: test/e2e-v2/cases/zipkin/banyandb/e2e.yaml
- name: Virtual GenAI
config: test/e2e-v2/cases/virtual-genai/e2e.yaml
- name: OTLP Virtual GenAI
config: test/e2e-v2/cases/otlp-virtual-genai/e2e.yaml
- name: Zipkin Virtual GenAI
config: test/e2e-v2/cases/zipkin-virtual-genai/e2e.yaml

- name: Nginx
config: test/e2e-v2/cases/nginx/e2e.yaml
Expand Down
2 changes: 1 addition & 1 deletion docs/en/changes/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@
* Support Virtual-GenAI monitoring.
* Fix on-demand pod log parsing failure by replacing invalid `DateTimeFormatter` pattern with `ISO_OFFSET_DATE_TIME`.
* Fix Zipkin receiver compatibility with application/x-protobuf Content-Type.

* Support virtual GenAI analysis for otlp and zipkin traces.

#### UI
* Fix the missing icon in new native trace view.
Expand Down
24 changes: 23 additions & 1 deletion docs/en/setup/service-agent/virtual-genai.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ 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.

# Data Sources
Virtual GenAI metrics are derived from distributed tracing data. SkyWalking OAP can ingest and analyze trace data adhering to GenAI semantic conventions from the following sources:

1. Native SkyWalking Traces via SkyWalking Java Agent
2. OpenTelemetry format trace
3. Zipkin format Traces

## Span Contract

The GenAI operation span should have the following properties:
Expand Down Expand Up @@ -60,4 +67,19 @@ The following metrics are available at the **model** (service instance) level:
- `gen_ai_model_total_estimated_cost / avg_estimated_cost` - Estimated cost

## Requirement
`SkyWalking Java Agent` version >= 9.7

### Version
`SkyWalking Java Agent` version >= 9.7

### Semantic Conventions and Compatibility
The tag keys used in Virtual GenAI follow the **OpenTelemetry GenAI Semantic Conventions**. SkyWalking OAP identifies GenAI-related spans based on the following criteria depending on the data source:

* **SkyWalking Native Agent**: Requires an **Exit** span with `SpanLayer == GENAI` and relevant `gen_ai.*` tags.
* **OTLP / Zipkin Traces**: Any span containing the `gen_ai.response.model` tag will be identified as a GenAI operation.

**Note on OTLP / Zipkin Provider Identification**:
To ensure broad compatibility with different OpenTelemetry instrumentation versions, SkyWalking OAP identifies the GenAI provider using the following prioritized logic:

1. **`gen_ai.provider.name`**: SkyWalking first looks for this tag (the latest OTel semantic convention).
2. **`gen_ai.system`**: If the above is missing, it falls back to this legacy tag for backward compatibility with older instrumentation (e.g., current OTel Python auto-instrumentation).
3. **Prefix Matching**: If neither tag is present, SkyWalking attempts to identify the provider by matching the model name against the `prefix-match` rules defined in the `gen-ai-config.yml`.
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public AnalysisListener create(ModuleManager moduleManager, AnalyzerModuleConfig
new VirtualCacheProcessor(namingControl, config),
new VirtualDatabaseProcessor(namingControl, config),
new VirtualMQProcessor(namingControl),
new VirtualGenAIProcessor(namingControl, genAIMeterAnalyzerService)
new VirtualGenAIProcessor(genAIMeterAnalyzerService)
)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,7 @@
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;
Expand All @@ -38,8 +32,6 @@
@RequiredArgsConstructor
public class VirtualGenAIProcessor implements VirtualServiceProcessor {

private final NamingControl namingControl;

private final IGenAIMeterAnalyzerService meterAnalyzerService;

private final List<Source> recordList = new ArrayList<>();
Expand All @@ -55,53 +47,7 @@ public void prepareVSIfNecessary(SpanObject span, SegmentObject segmentObject) {
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;
recordList.addAll(meterAnalyzerService.transferToSources(metrics));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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.config.NamingControl;
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;
Expand All @@ -37,6 +38,8 @@ public class GenAIAnalyzerModuleProvider extends ModuleProvider {

private GenAIConfig config;

private GenAIMeterAnalyzer analyzer;

@Override
public String name() {
return "default";
Expand Down Expand Up @@ -67,10 +70,11 @@ public void prepare() throws ServiceNotProvidedException, ModuleStartException {
GenAIConfigLoader loader = new GenAIConfigLoader(config);
config = loader.loadConfig();
GenAIProviderPrefixMatcher matcher = GenAIProviderPrefixMatcher.build();

this.analyzer = new GenAIMeterAnalyzer(matcher);
this.registerServiceImplementation(
IGenAIMeterAnalyzerService.class,
new GenAIMeterAnalyzer(matcher)
);
analyzer);
}

@Override
Expand All @@ -79,6 +83,12 @@ public void start() throws ServiceNotProvidedException, ModuleStartException {
.provider()
.getService(OALEngineLoaderService.class)
.load(GenAIOALDefine.INSTANCE);

NamingControl namingControl = getManager().find(CoreModule.NAME)
.provider()
.getService(NamingControl.class);

this.analyzer.setNamingControl(namingControl);
}

@Override
Expand All @@ -88,7 +98,7 @@ public void notifyAfterCompleted() throws ServiceNotProvidedException, ModuleSta

@Override
public String[] requiredModules() {
return new String[] {
return new String[]{
CoreModule.NAME
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ public class GenAITagKeys {
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";

public static final String ESTIMATED_COST = "gen_ai.estimated.cost";

public static final String SYSTEM_NAME = "gen_ai.system";
}
Loading
Loading