diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1f7d13d411..47ce26c51f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -122,6 +122,7 @@ opencensus-impl = { module = "io.opencensus:opencensus-impl", version.ref = "ope opentelemetry-api = "io.opentelemetry:opentelemetry-api:1.64.0" opentelemetry-exporter-prometheus = "io.opentelemetry:opentelemetry-exporter-prometheus:1.64.0-alpha" opentelemetry-gcp-resources = "io.opentelemetry.contrib:opentelemetry-gcp-resources:1.59.0-alpha" +opentelemetry-exporter-otlp = "io.opentelemetry:opentelemetry-exporter-otlp:1.64.0" opentelemetry-sdk-extension-autoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.64.0" opentelemetry-sdk-testing = "io.opentelemetry:opentelemetry-sdk-testing:1.64.0" perfmark-api = "io.perfmark:perfmark-api:0.27.0" diff --git a/interop-testing/build.gradle b/interop-testing/build.gradle index 5160759460c..eab52afff78 100644 --- a/interop-testing/build.gradle +++ b/interop-testing/build.gradle @@ -26,6 +26,7 @@ dependencies { libraries.opencensus.contrib.grpc.metrics, libraries.google.auth.oauth2Http, libraries.opentelemetry.sdk.extension.autoconfigure, + libraries.opentelemetry.exporter.otlp, libraries.guava.jre // Fix checkUpperBoundDeps using -android api project(':grpc-api'), project(':grpc-stub'), diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/OpenTelemetryUtil.java b/interop-testing/src/main/java/io/grpc/testing/integration/OpenTelemetryUtil.java new file mode 100644 index 00000000000..261772aaff7 --- /dev/null +++ b/interop-testing/src/main/java/io/grpc/testing/integration/OpenTelemetryUtil.java @@ -0,0 +1,66 @@ +/* + * Copyright 2026 The gRPC Authors + * + * 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 io.grpc.testing.integration; + +import io.grpc.opentelemetry.GrpcOpenTelemetry; +import io.grpc.opentelemetry.InternalGrpcOpenTelemetry; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder; +import java.util.HashMap; +import java.util.Map; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; + +/** + * Utility methods for OpenTelemetry configuration in integration testing. + */ +public final class OpenTelemetryUtil { + + private OpenTelemetryUtil() {} + + /** + * Initializes and registers OpenTelemetry tracing for interop client and server. + * + * @param otelCollectorAddress optional collector address (e.g. "localhost:4317") + * @return the configured {@link OpenTelemetrySdk} + */ + @IgnoreJRERequirement // OpenTelemetry uses Java 8+ APIs + public static OpenTelemetrySdk setupOpenTelemetry(String otelCollectorAddress) { + AutoConfiguredOpenTelemetrySdkBuilder sdkBuilder = + AutoConfiguredOpenTelemetrySdk.builder(); + Map properties = new HashMap<>(); + properties.put("otel.traces.exporter", "otlp"); + // Reduce BatchSpanProcessor export delay from default 5000ms to 100ms for fast test runs. + properties.put("otel.bsp.schedule.delay", "100"); + if (otelCollectorAddress != null && !otelCollectorAddress.isEmpty()) { + String endpoint = otelCollectorAddress; + if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) { + endpoint = "http://" + endpoint; + } + properties.put("otel.exporter.otlp.endpoint", endpoint); + } + sdkBuilder.addPropertiesSupplier(() -> properties); + AutoConfiguredOpenTelemetrySdk autoSdk = sdkBuilder.build(); + OpenTelemetrySdk openTelemetrySdk = autoSdk.getOpenTelemetrySdk(); + GrpcOpenTelemetry.Builder grpcOpentelemetryBuilder = GrpcOpenTelemetry.newBuilder() + .sdk(openTelemetrySdk); + InternalGrpcOpenTelemetry.enableTracing(grpcOpentelemetryBuilder, true); + GrpcOpenTelemetry grpcOpenTelemetry = grpcOpentelemetryBuilder.build(); + grpcOpenTelemetry.registerGlobal(); + return openTelemetrySdk; + } +} diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java index 8a5ca05da91..63644d582c5 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java @@ -68,6 +68,7 @@ import io.grpc.testing.integration.Messages.StreamingOutputCallRequest; import io.grpc.testing.integration.Messages.StreamingOutputCallResponse; import io.grpc.testing.integration.Messages.TestOrcaReport; +import io.opentelemetry.sdk.OpenTelemetrySdk; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; @@ -79,6 +80,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; /** * Application that starts a client for the {@link TestServiceGrpc.TestServiceImplBase} and runs @@ -135,6 +137,9 @@ public static void main(String[] args) throws Exception { private int soakRequestSize = 271828; private int soakResponseSize = 314159; private int numThreads = 1; + private boolean enableOpentelemetry; + private String otelCollectorAddress; + private OpenTelemetrySdk openTelemetrySdk; private String additionalMetadata = ""; private static LoadBalancerProvider customBackendMetricsLoadBalancerProvider; @@ -219,6 +224,10 @@ void parseArgs(String[] args) throws Exception { numThreads = Integer.parseInt(value); } else if ("additional_metadata".equals(key)) { additionalMetadata = value; + } else if ("enable_opentelemetry".equals(key)) { + enableOpentelemetry = Boolean.parseBoolean(value); + } else if ("otel_collector_address".equals(key)) { + otelCollectorAddress = value; } else { System.err.println("Unknown argument: " + key); usage = true; @@ -306,7 +315,11 @@ void parseArgs(String[] args) throws Exception { } @VisibleForTesting + @IgnoreJRERequirement // OpenTelemetry uses Java 8+ APIs void setUp() { + if (enableOpentelemetry) { + this.openTelemetrySdk = OpenTelemetryUtil.setupOpenTelemetry(otelCollectorAddress); + } tester.setUp(); } @@ -321,6 +334,10 @@ private synchronized void tearDown() { throw ex; } catch (Exception ex) { throw new RuntimeException(ex); + } finally { + if (openTelemetrySdk != null) { + openTelemetrySdk.close(); + } } } diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java index ee0ecff3ce1..2d966af1c8d 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java @@ -32,6 +32,7 @@ import io.grpc.testing.TlsTesting; import io.grpc.xds.orca.OrcaMetricReportingServerInterceptor; import io.grpc.xds.orca.OrcaServiceImpl; +import io.opentelemetry.sdk.OpenTelemetrySdk; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.List; @@ -39,6 +40,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; /** Server that manages startup/shutdown of a single {@code TestService}. */ public class TestServiceServer { @@ -76,6 +78,9 @@ public void run() { private boolean useTls = true; private boolean useAlts = false; private int mcsLimit = -1; + private boolean enableOpentelemetry = false; + private String otelCollectorAddress; + private OpenTelemetrySdk openTelemetrySdk; private ScheduledExecutorService executor; private Server server; @@ -123,6 +128,10 @@ void parseArgs(String[] args) { mcsLimit = Integer.parseInt(value); // TODO: Make Netty server builder usable for IPV6 as well (not limited to MCS handling) addressType = Util.AddressType.IPV4; // To use NettyServerBuilder + } else if ("enable_opentelemetry".equals(key)) { + enableOpentelemetry = Boolean.parseBoolean(value); + } else if ("otel_collector_address".equals(key)) { + otelCollectorAddress = value; } else { System.err.println("Unknown argument: " + key); usage = true; @@ -155,7 +164,11 @@ void parseArgs(String[] args) { @SuppressWarnings("AddressSelection") @VisibleForTesting + @IgnoreJRERequirement // OpenTelemetry uses Java 8+ APIs void start() throws Exception { + if (enableOpentelemetry) { + this.openTelemetrySdk = OpenTelemetryUtil.setupOpenTelemetry(otelCollectorAddress); + } executor = Executors.newSingleThreadScheduledExecutor(); ServerCredentials serverCreds; if (useAlts) { @@ -224,11 +237,17 @@ void start() throws Exception { @VisibleForTesting void stop() throws Exception { - server.shutdownNow(); - if (!server.awaitTermination(5, TimeUnit.SECONDS)) { - System.err.println("Timed out waiting for server shutdown"); + try { + server.shutdownNow(); + if (!server.awaitTermination(5, TimeUnit.SECONDS)) { + System.err.println("Timed out waiting for server shutdown"); + } + MoreExecutors.shutdownAndAwaitTermination(executor, 5, TimeUnit.SECONDS); + } finally { + if (openTelemetrySdk != null) { + openTelemetrySdk.close(); + } } - MoreExecutors.shutdownAndAwaitTermination(executor, 5, TimeUnit.SECONDS); } @VisibleForTesting