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
44 changes: 34 additions & 10 deletions core/src/main/java/com/volcengine/veadk/trace/OpenTelemetry.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,20 @@
import com.volcengine.veadk.Version;
import com.volcengine.veadk.trace.exporter.AttributeRewritingSpanExporter;
import com.volcengine.veadk.trace.exporter.ExporterFactory;
import com.volcengine.veadk.utils.EnvUtil;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;

public class OpenTelemetry {

Expand All @@ -48,24 +52,26 @@ public static void initOpenTelemetry(List<ExporterFactory> exporterFactories) {
BatchSpanProcessor.builder(rewritingExporter) // 重写一次
.setMaxQueueSize(2048)
.setMaxExportBatchSize(512)
.setScheduleDelay(100, TimeUnit.MILLISECONDS)
// Keep the HTTP root and its Agent/LLM/tool children in one export batch.
// A very short delay lets the platform trace merger finalize a partial
// trace before the long-running HTTP root span has ended.
.setScheduleDelay(30, TimeUnit.SECONDS)
.setExporterTimeout(30, TimeUnit.SECONDS)
.build();

AttributesBuilder resourceAttributes =
Attributes.builder()
.put("service.name", EnvUtil.getOpenTelemetryServiceName())
.put("service.version", Version.JAVA_VEADK_VERSION);
addEnvironmentResourceAttributes(
resourceAttributes, EnvUtil.getOpenTelemetryResourceAttributes());

SdkTracerProvider tracerProvider =
SdkTracerProvider.builder()
.addSpanProcessor(batchProcessor)
.setResource(
Resource.getDefault()
.merge(
Resource.create(
Attributes.of(
AttributeKey.stringKey(
"service.name"),
"veadk_tracing",
AttributeKey.stringKey(
"service.version"),
Version.JAVA_VEADK_VERSION))))
.merge(Resource.create(resourceAttributes.build())))
.build();

OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal();
Expand All @@ -74,4 +80,22 @@ public static void initOpenTelemetry(List<ExporterFactory> exporterFactories) {

Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::close));
}

private static void addEnvironmentResourceAttributes(
AttributesBuilder builder, String configuredAttributes) {
if (StringUtils.isBlank(configuredAttributes)) {
return;
}
for (String entry : configuredAttributes.split(",")) {
int separator = entry.indexOf('=');
if (separator <= 0 || separator == entry.length() - 1) {
continue;
}
String key = entry.substring(0, separator).trim();
String value = entry.substring(separator + 1).trim();
if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
builder.put(AttributeKey.stringKey(key.toLowerCase(Locale.ROOT)), value);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.volcengine.veadk.trace.exporter;

import com.volcengine.veadk.utils.EnvUtil;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import org.apache.commons.lang3.StringUtils;

/** Exports Agent traces to the APMPlus OTLP endpoint injected by AgentKit Runtime. */
public class APMPlusExporter implements ExporterFactory {

@Override
public SpanExporter create() {
String standardEndpoint = EnvUtil.getOpenTelemetryTracesEndpoint();
if (StringUtils.isNotBlank(standardEndpoint)) {
String protocol = EnvUtil.getOpenTelemetryTracesProtocol();
if (StringUtils.isBlank(protocol) || "http/protobuf".equalsIgnoreCase(protocol)) {
return OtlpHttpSpanExporter.builder().setEndpoint(standardEndpoint).build();
}
if ("grpc".equalsIgnoreCase(protocol)) {
return OtlpGrpcSpanExporter.builder().setEndpoint(standardEndpoint).build();
}
throw new IllegalStateException(
"Unsupported OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: " + protocol);
}
return OtlpGrpcSpanExporter.builder()
.setEndpoint(EnvUtil.getAPMPlusEndpoint())
.addHeader("x-byteapm-appkey", EnvUtil.getAPMPlusApiKey())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@ private static class RewrittenSpanData implements SpanData {
}

private Attributes rewriteAttributes(Attributes attributes) {
if (attributes.isEmpty()) {
return attributes;
}

AttributesBuilder builder = Attributes.builder();
attributes.forEach(
(key, value) -> {
Expand Down Expand Up @@ -123,9 +119,64 @@ private Attributes rewriteAttributes(Attributes attributes) {
break;
}
});
addPlatformAttributes(builder, delegate.getName(), attributes);
return builder.build();
}

private void addPlatformAttributes(
AttributesBuilder builder, String spanName, Attributes originalAttributes) {
if (spanName == null) {
return;
}
if (spanName.startsWith("invocation")) {
builder.put("gen_ai.operation.name", "chain");
builder.put("gen_ai.span.kind", "workflow");
} else if (spanName.startsWith("agent_run") || spanName.startsWith("invoke_agent")) {
builder.put("gen_ai.operation.name", "agent");
builder.put("gen_ai.span.kind", "agent");
} else if (spanName.startsWith("call_llm")) {
builder.put("gen_ai.operation.name", "chat");
builder.put("gen_ai.span.kind", "llm");
copyStringAttribute(
builder,
originalAttributes,
"gcp.vertex.agent.llm_request",
"gen_ai.input");
copyStringAttribute(
builder,
originalAttributes,
"gcp.vertex.agent.llm_response",
"gen_ai.output");
} else if (spanName.startsWith("tool_call")) {
builder.put("gen_ai.operation.name", "execute_tool");
builder.put("gen_ai.span.kind", "tool");
copyStringAttribute(
builder,
originalAttributes,
"gcp.vertex.agent.tool_call_args",
"gen_ai.input");
} else if (spanName.startsWith("tool_response")) {
builder.put("gen_ai.operation.name", "execute_tool");
builder.put("gen_ai.span.kind", "tool");
copyStringAttribute(
builder,
originalAttributes,
"gcp.vertex.agent.tool_response",
"gen_ai.output");
}
}

private void copyStringAttribute(
AttributesBuilder builder,
Attributes originalAttributes,
String sourceKey,
String targetKey) {
String value = originalAttributes.get(AttributeKey.stringKey(sourceKey));
if (value != null) {
builder.put(targetKey, value);
}
}

@Override
public String getName() {
return delegate.getName();
Expand Down
53 changes: 53 additions & 0 deletions core/src/main/java/com/volcengine/veadk/utils/EnvUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ public class EnvUtil {
private static final String TLS_ENDPOINT = "OBSERVABILITY_OPENTELEMETRY_TLS_ENDPOINT";
private static final String TLS_SERVICE_NAME = "OBSERVABILITY_OPENTELEMETRY_TLS_SERVICE_NAME";
private static final String TLS_REGION = "OBSERVABILITY_OPENTELEMETRY_TLS_REGION";
private static final String APMPLUS_ENDPOINT = "OBSERVABILITY_OPENTELEMETRY_APMPLUS_ENDPOINT";
private static final String APMPLUS_API_KEY = "OBSERVABILITY_OPENTELEMETRY_APMPLUS_API_KEY";
private static final String APMPLUS_SERVICE_NAME =
"OBSERVABILITY_OPENTELEMETRY_APMPLUS_SERVICE_NAME";
private static final String OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME";
private static final String OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES";
private static final String OTEL_EXPORTER_OTLP_TRACES_ENDPOINT =
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
private static final String OTEL_EXPORTER_OTLP_TRACES_PROTOCOL =
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
private static final String VIKINGMEM_MEMORY_TYPE = "DATABASE_VIKINGMEM_MEMORY_TYPE";
private static final String MODEL_AGENT_API_KEY = "MODEL_AGENT_API_KEY";
private static final String TOOL_CODE_SANDBOX_URL = "TOOL_CODE_SANDBOX_URL";
Expand All @@ -39,6 +49,7 @@ public class EnvUtil {
private static final String DEFAULT_VIKING_MEMORY_TYPE = "sys_event_v1";
private static final String DEFAULT_AGENTKIT_SERVICE = "agentkit";
private static final String DEFAULT_AGENTKIT_REGION = "cn-beijing";
private static final String DEFAULT_AGENTKIT_SCHEME = "https";

private EnvUtil() {}

Expand Down Expand Up @@ -116,6 +127,48 @@ public static String getTLSRegion() {
return tlsRegion;
}

public static boolean isAPMPlusConfigured() {
return StringUtils.isNotBlank(getOpenTelemetryTracesEndpoint())
|| (StringUtils.isNotBlank(System.getenv(APMPLUS_ENDPOINT))
&& StringUtils.isNotBlank(System.getenv(APMPLUS_API_KEY)));
}

public static String getOpenTelemetryTracesEndpoint() {
return System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT);
}

public static String getOpenTelemetryTracesProtocol() {
return System.getenv(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL);
}

public static String getAPMPlusEndpoint() {
String endpoint = System.getenv(APMPLUS_ENDPOINT);
if (StringUtils.isBlank(endpoint)) {
throw getIllegalStateException(APMPLUS_ENDPOINT);
}
return endpoint;
}

public static String getAPMPlusApiKey() {
String apiKey = System.getenv(APMPLUS_API_KEY);
if (StringUtils.isBlank(apiKey)) {
throw getIllegalStateException(APMPLUS_API_KEY);
}
return apiKey;
}

public static String getOpenTelemetryServiceName() {
String serviceName = System.getenv(APMPLUS_SERVICE_NAME);
if (StringUtils.isBlank(serviceName)) {
serviceName = System.getenv(OTEL_SERVICE_NAME);
}
return StringUtils.isBlank(serviceName) ? "veadk_tracing" : serviceName;
}

public static String getOpenTelemetryResourceAttributes() {
return System.getenv(OTEL_RESOURCE_ATTRIBUTES);
}

public static String getVikingMmemoryType() {
String memoryType = System.getenv(VIKINGMEM_MEMORY_TYPE);
if (StringUtils.isBlank(memoryType)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.volcengine.veadk.trace.exporter;

import static org.assertj.core.api.Assertions.assertThat;

import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.ClearEnvironmentVariable;
import org.junitpioneer.jupiter.SetEnvironmentVariable;

class APMPlusExporterTest {

@Test
@SetEnvironmentVariable(
key = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
value = "http://collector/path/v1/traces")
@SetEnvironmentVariable(key = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", value = "http/protobuf")
@ClearEnvironmentVariable(key = "OBSERVABILITY_OPENTELEMETRY_APMPLUS_ENDPOINT")
@ClearEnvironmentVariable(key = "OBSERVABILITY_OPENTELEMETRY_APMPLUS_API_KEY")
void create_shouldPreferRuntimeStandardHttpExporter() {
SpanExporter exporter = new APMPlusExporter().create();
try {
assertThat(exporter).isInstanceOf(OtlpHttpSpanExporter.class);
} finally {
exporter.close();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ void export_rewritesAttributes_correctly() {
.build();

SpanData inputSpan = Mockito.mock(SpanData.class);
when(inputSpan.getName()).thenReturn("call_llm");
when(inputSpan.getAttributes()).thenReturn(input);

SpanExporter delegate = Mockito.mock(SpanExporter.class);
Expand Down Expand Up @@ -70,11 +71,41 @@ void export_rewritesAttributes_correctly() {
assertEquals(List.of(true, false), out.get(AttributeKey.booleanArrayKey("normal.barr")));
assertEquals(List.of(1L, 2L), out.get(AttributeKey.longArrayKey("normal.larr")));
assertEquals(List.of(1.1, 2.2), out.get(AttributeKey.doubleArrayKey("normal.darr")));
assertEquals("chat", out.get(AttributeKey.stringKey("gen_ai.operation.name")));
assertEquals("llm", out.get(AttributeKey.stringKey("gen_ai.span.kind")));

// Attribute count reflects rewritten set
assertEquals(out.size(), rewritten.getTotalAttributeCount());
}

@Test
void export_addsAgentKitPlatformAttributes() {
Attributes input =
Attributes.builder()
.put("gcp.vertex.agent.tool_call_args", "{\"code\":\"print(1)\"}")
.build();
SpanData inputSpan = Mockito.mock(SpanData.class);
when(inputSpan.getName()).thenReturn("tool_call [run_code]");
when(inputSpan.getAttributes()).thenReturn(input);

SpanExporter delegate = Mockito.mock(SpanExporter.class);
when(delegate.export(anyList())).thenReturn(CompletableResultCode.ofSuccess());

new AttributeRewritingSpanExporter(delegate).export(List.of(inputSpan));

ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
verify(delegate).export(captor.capture());
SpanData rewritten = (SpanData) captor.getValue().get(0);
assertEquals(
"execute_tool",
rewritten.getAttributes().get(AttributeKey.stringKey("gen_ai.operation.name")));
assertEquals(
"tool", rewritten.getAttributes().get(AttributeKey.stringKey("gen_ai.span.kind")));
assertEquals(
"{\"code\":\"print(1)\"}",
rewritten.getAttributes().get(AttributeKey.stringKey("gen_ai.input")));
}

@Test
void flush_and_shutdown_delegate() {
SpanExporter delegate = Mockito.mock(SpanExporter.class);
Expand Down
32 changes: 32 additions & 0 deletions core/src/test/java/com/volcengine/veadk/utils/EnvUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,38 @@

class EnvUtilTest {

@Test
@SetEnvironmentVariable(
key = "OBSERVABILITY_OPENTELEMETRY_APMPLUS_ENDPOINT",
value = "http://apmplus:4317")
@SetEnvironmentVariable(key = "OBSERVABILITY_OPENTELEMETRY_APMPLUS_API_KEY", value = "test-key")
void apmPlusConfiguration_shouldReadRuntimeEnvironment() {
assertThat(EnvUtil.isAPMPlusConfigured()).isTrue();
assertThat(EnvUtil.getAPMPlusEndpoint()).isEqualTo("http://apmplus:4317");
assertThat(EnvUtil.getAPMPlusApiKey()).isEqualTo("test-key");
}

@Test
@SetEnvironmentVariable(
key = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
value = "http://collector/path/v1/traces")
@SetEnvironmentVariable(key = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", value = "http/protobuf")
@ClearEnvironmentVariable(key = "OBSERVABILITY_OPENTELEMETRY_APMPLUS_ENDPOINT")
@ClearEnvironmentVariable(key = "OBSERVABILITY_OPENTELEMETRY_APMPLUS_API_KEY")
void apmPlusConfiguration_shouldPreferStandardOtlpEnvironment() {
assertThat(EnvUtil.isAPMPlusConfigured()).isTrue();
assertThat(EnvUtil.getOpenTelemetryTracesEndpoint())
.isEqualTo("http://collector/path/v1/traces");
assertThat(EnvUtil.getOpenTelemetryTracesProtocol()).isEqualTo("http/protobuf");
}

@Test
@ClearEnvironmentVariable(key = "OBSERVABILITY_OPENTELEMETRY_APMPLUS_SERVICE_NAME")
@SetEnvironmentVariable(key = "OTEL_SERVICE_NAME", value = "runtime.agent")
void getOpenTelemetryServiceName_shouldUseRuntimeServiceName() {
assertThat(EnvUtil.getOpenTelemetryServiceName()).isEqualTo("runtime.agent");
}

@Test
@SetEnvironmentVariable(key = "MODEL_AGENT_API_KEY", value = "test_api_key")
void getAgentApiKey() {
Expand Down
Loading