diff --git a/.cloudbuild/library_generation/library_generation.Dockerfile b/.cloudbuild/library_generation/library_generation.Dockerfile index e4fdb97650..58b9d96749 100644 --- a/.cloudbuild/library_generation/library_generation.Dockerfile +++ b/.cloudbuild/library_generation/library_generation.Dockerfile @@ -30,7 +30,7 @@ RUN cat /java-formatter-version RUN V=$(cat /java-formatter-version) && curl -o "/google-java-format.jar" "https://maven-central.storage-download.googleapis.com/maven2/com/google/googlejavaformat/google-java-format/${V}/google-java-format-${V}-all-deps.jar" # Compile and install packages -RUN mvn install -B -ntp -DskipTests -Dclirr.skip -Dcheckstyle.skip +RUN mvn install -B -ntp -T 1.5C -DskipTests -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip RUN cp "/root/.m2/repository/com/google/api/gapic-generator-java/${DOCKER_GAPIC_GENERATOR_VERSION}/gapic-generator-java-${DOCKER_GAPIC_GENERATOR_VERSION}.jar" \ "./gapic-generator-java.jar" diff --git a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/model/GapicContext.java b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/model/GapicContext.java index 780890c664..28eb9bccae 100644 --- a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/model/GapicContext.java +++ b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/model/GapicContext.java @@ -95,6 +95,8 @@ static GapicMetadata defaultGapicMetadata() { public abstract Transport transport(); + public abstract Optional repo(); + public static Builder builder() { return new AutoValue_GapicContext.Builder() .setMixinServices(Collections.emptyList()) @@ -130,6 +132,8 @@ public Builder setHelperResourceNames(Set helperResourceNames) { public abstract Builder setTransport(Transport transport); + public abstract Builder setRepo(String repo); + abstract ImmutableMap resourceNames(); abstract ImmutableMap helperResourceNames(); diff --git a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java index 0ff6a71039..602b55ff9e 100644 --- a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java +++ b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java @@ -150,6 +150,7 @@ public static GapicContext parse(CodeGeneratorRequest request) { Optional languageSettingsOpt = GapicLanguageSettingsParser.parse(gapicYamlConfigPathOpt); Optional transportOpt = PluginArgumentParser.parseTransport(request); + Optional repo = PluginArgumentParser.parseRepo(request); boolean willGenerateMetadata = PluginArgumentParser.hasMetadataFlag(request); boolean willGenerateNumericEnum = PluginArgumentParser.hasNumericEnumFlag(request); @@ -253,6 +254,7 @@ public static GapicContext parse(CodeGeneratorRequest request) { .setServiceYamlProto(serviceYamlProtoOpt.orElse(null)) .setTransport(transport) .setRestNumericEnumsEnabled(willGenerateNumericEnum) + .setRepo(repo.orElse(null)) .build(); } diff --git a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/PluginArgumentParser.java b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/PluginArgumentParser.java index f56e0621a9..487aeb440e 100644 --- a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/PluginArgumentParser.java +++ b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/PluginArgumentParser.java @@ -32,6 +32,7 @@ public class PluginArgumentParser { @VisibleForTesting static final String KEY_NUMERIC_ENUM = "rest-numeric-enums"; @VisibleForTesting static final String KEY_SERVICE_YAML_CONFIG = "api-service-config"; @VisibleForTesting static final String KEY_TRANSPORT = "transport"; + @VisibleForTesting static final String KEY_REPO = "repo"; private static final String JSON_FILE_ENDING = "grpc_service_config.json"; private static final String GAPIC_YAML_FILE_ENDING = "gapic.yaml"; @@ -53,6 +54,10 @@ static Optional parseTransport(CodeGeneratorRequest request) { return parseConfigArgument(request.getParameter(), KEY_TRANSPORT); } + static Optional parseRepo(CodeGeneratorRequest request) { + return parseConfigArgument(request.getParameter(), KEY_REPO); + } + static boolean hasMetadataFlag(CodeGeneratorRequest request) { return hasFlag(request.getParameter(), KEY_METADATA); } diff --git a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protowriter/Writer.java b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protowriter/Writer.java index 79c9cbf349..c24c8d935a 100644 --- a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protowriter/Writer.java +++ b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protowriter/Writer.java @@ -71,6 +71,7 @@ protected static CodeGeneratorResponse write( writeMetadataFile(context, writePackageInfo(gapicPackageInfo, codeWriter, jos), jos); writeReflectConfigFile(gapicPackageInfo.packageInfo().pakkage(), reflectConfigInfo, jos); + writeGapicPropertiesFile(context, jos); jos.finish(); jos.flush(); @@ -212,6 +213,22 @@ private static void writeMetadataFile(GapicContext context, String path, JarOutp } } + @VisibleForTesting + static void writeGapicPropertiesFile(GapicContext context, JarOutputStream jos) { + context + .repo() + .ifPresent( + repo -> { + JarEntry jarEntry = new JarEntry("src/main/resources/gapic.properties"); + try { + jos.putNextEntry(jarEntry); + jos.write(String.format("repo=%s\n", repo).getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new GapicWriterException("Could not write repo file", e); + } + }); + } + private static String getPath(String pakkage, String className) { String path = pakkage.replaceAll("\\.", "/"); if (className.startsWith("Mock") || className.endsWith("Test")) { diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protoparser/PluginArgumentParserTest.java b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protoparser/PluginArgumentParserTest.java index 83e75f87f5..ab5fac84f9 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protoparser/PluginArgumentParserTest.java +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protoparser/PluginArgumentParserTest.java @@ -16,6 +16,7 @@ import static com.google.api.generator.gapic.protoparser.PluginArgumentParser.KEY_METADATA; import static com.google.api.generator.gapic.protoparser.PluginArgumentParser.KEY_NUMERIC_ENUM; +import static com.google.api.generator.gapic.protoparser.PluginArgumentParser.KEY_REPO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -269,6 +270,21 @@ void hasFlag_flagFound() { assertTrue(PluginArgumentParser.hasFlag(rawArgument, KEY_METADATA)); } + @Test + void parseRepo_onlyOnePresent() { + String repo = "googleapis/sdk-platform-java"; + CodeGeneratorRequest request = + CodeGeneratorRequest.newBuilder().setParameter(createRepo(repo)).build(); + assertEquals(repo, PluginArgumentParser.parseRepo(request).get()); + } + + @Test + void parseRepo_noneFound() { + CodeGeneratorRequest request = + CodeGeneratorRequest.newBuilder().setParameter("metadata").build(); + assertFalse(PluginArgumentParser.parseRepo(request).isPresent()); + } + private static String createGrpcServiceConfig(String path) { return String.format("%s=%s", PluginArgumentParser.KEY_GRPC_SERVICE_CONFIG, path); } @@ -280,4 +296,8 @@ private static String createGapicConfig(String path) { private static String createServiceConfig(String path) { return String.format("%s=%s", PluginArgumentParser.KEY_SERVICE_YAML_CONFIG, path); } + + private static String createRepo(String repo) { + return String.format("%s=%s", KEY_REPO, repo); + } } diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protowriter/WriterTest.java b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protowriter/WriterTest.java index c366d2085e..9f8275a0fa 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protowriter/WriterTest.java +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protowriter/WriterTest.java @@ -16,6 +16,7 @@ import com.google.gson.Gson; import com.google.protobuf.ByteString; import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; @@ -144,4 +145,26 @@ void productionWrite_emptyGapicContext_succeeds() throws IOException { "temp-codegen.srcjar"); assertNull(result); } + + @Test + void writeRepoFile_isWritten() throws IOException { + String repo = "googleapis/sdk-platform-java"; + GapicContext context = GapicContext.EMPTY.toBuilder().setRepo(repo).build(); + Writer.writeGapicPropertiesFile(context, jarOutputStream); + + closeJarOutputStream(); + + try (JarFile jarFile = new JarFile(file)) { + Enumeration entries = jarFile.entries(); + assertThat(entries.hasMoreElements()).isTrue(); + JarEntry entry = entries.nextElement(); + assertThat(entries.hasMoreElements()).isFalse(); + assertEquals("src/main/resources/gapic.properties", entry.getName()); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(jarFile.getInputStream(entry)))) { + String line = reader.readLine(); + assertEquals("repo=" + repo, line); + } + } + } } diff --git a/gax-java/dependencies.properties b/gax-java/dependencies.properties index f804432d67..662c2822da 100644 --- a/gax-java/dependencies.properties +++ b/gax-java/dependencies.properties @@ -40,6 +40,7 @@ maven.com_google_api_grpc_grpc_google_common_protos=com.google.api.grpc:grpc-goo maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.42.1 maven.com_google_auth_google_auth_library_credentials=com.google.auth:google-auth-library-credentials:1.42.1 maven.io_opentelemetry_opentelemetry_api=io.opentelemetry:opentelemetry-api:1.47.0 +maven.io_opentelemetry_opentelemetry_context=io.opentelemetry:opentelemetry-context:1.47.0 maven.io_opencensus_opencensus_api=io.opencensus:opencensus-api:0.31.1 maven.io_opencensus_opencensus_contrib_grpc_metrics=io.opencensus:opencensus-contrib-grpc-metrics:0.31.1 maven.io_opencensus_opencensus_contrib_http_util=io.opencensus:opencensus-contrib-http-util:0.31.1 diff --git a/gax-java/gax/BUILD.bazel b/gax-java/gax/BUILD.bazel index 80b26ad785..15ed36bcbd 100644 --- a/gax-java/gax/BUILD.bazel +++ b/gax-java/gax/BUILD.bazel @@ -19,6 +19,7 @@ _COMPILE_DEPS = [ "@com_google_errorprone_error_prone_annotations//jar", "@com_google_guava_guava//jar", "@io_opentelemetry_opentelemetry_api//jar", + "@io_opentelemetry_opentelemetry_context//jar", "@io_opencensus_opencensus_api//jar", "@io_opencensus_opencensus_contrib_http_util//jar", "@io_grpc_grpc_java//context:context", diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java index 72d54356b0..d9979ac746 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientContext.java @@ -43,6 +43,7 @@ import com.google.api.gax.rpc.internal.QuotaProjectIdHidingCredentials; import com.google.api.gax.tracing.ApiTracerFactory; import com.google.api.gax.tracing.BaseApiTracerFactory; +import com.google.api.gax.tracing.OpenTelemetryTracingTracer; import com.google.auth.ApiKeyCredentials; import com.google.auth.CredentialTypeForMetrics; import com.google.auth.Credentials; @@ -270,6 +271,25 @@ public static ClientContext create(StubSettings settings) throws IOException { backgroundResources.add(watchdog); } + ApiTracerFactory tracerFactory = settings.getTracerFactory(); + if (tracerFactory != null) { + String rpcSystem = ""; + if ("grpc".equals(transportChannel.getTransportName())) { + rpcSystem = "grpc"; + } else if ("httpjson".equals(transportChannel.getTransportName())) { + rpcSystem = "http"; + } + + tracerFactory = + tracerFactory.withAttributes( + ImmutableMap.of(), + ImmutableMap.of( + OpenTelemetryTracingTracer.SERVER_ADDRESS_ATTRIBUTE, settings.getServerAddress(), + OpenTelemetryTracingTracer.SERVICE_NAME_ATTRIBUTE, settings.getServiceName(), + OpenTelemetryTracingTracer.PORT_ATTRIBUTE, String.valueOf(settings.getPort()), + OpenTelemetryTracingTracer.RPC_SYSTEM_ATTRIBUTE, rpcSystem)); + } + return newBuilder() .setBackgroundResources(backgroundResources.build()) .setExecutor(backgroundExecutor) @@ -284,7 +304,7 @@ public static ClientContext create(StubSettings settings) throws IOException { .setQuotaProjectId(settings.getQuotaProjectId()) .setStreamWatchdog(watchdog) .setStreamWatchdogCheckIntervalDuration(settings.getStreamWatchdogCheckIntervalDuration()) - .setTracerFactory(settings.getTracerFactory()) + .setTracerFactory(tracerFactory) .setEndpointContext(endpointContext) .build(); } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java index c1e41b82e6..f0131d5087 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java @@ -105,6 +105,13 @@ public final String getEndpoint() { return stubSettings.getEndpoint(); } + /** + * @return the fully resolved port used by the client + */ + public final int getPort() { + return stubSettings.getPort(); + } + public final String getQuotaProjectId() { return stubSettings.getQuotaProjectId(); } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java index a2e44d8a8b..cd7de22b1e 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java @@ -133,6 +133,10 @@ public static EndpointContext getDefaultInstance() { public abstract String resolvedEndpoint(); + public abstract int resolvedPort(); + + public abstract String resolvedServerAddress(); + public abstract Builder toBuilder(); public static Builder newBuilder() { @@ -228,6 +232,10 @@ public abstract static class Builder { public abstract Builder setResolvedEndpoint(String resolvedEndpoint); + public abstract Builder setResolvedPort(int resolvedPort); + + public abstract Builder setResolvedServerAddress(String resolvedServerAddress); + public abstract Builder setResolvedUniverseDomain(String resolvedUniverseDomain); abstract Builder setUseS2A(boolean useS2A); @@ -413,10 +421,37 @@ String mtlsEndpointResolver( return endpoint; } + private String parseServerAddress(String endpoint) { + int colonPortIndex = endpoint.lastIndexOf(':'); + int doubleSlashIndex = endpoint.lastIndexOf("//"); + if (colonPortIndex == -1) { + return endpoint; + } + if (doubleSlashIndex != -1 && doubleSlashIndex < colonPortIndex) { + return endpoint.substring(doubleSlashIndex + 2, colonPortIndex); + } + return endpoint.substring(0, colonPortIndex); + } + + private int parsePort(String endpoint) { + int colonIndex = endpoint.lastIndexOf(':'); + if (colonIndex != -1) { + try { + return Integer.parseInt(endpoint.substring(colonIndex + 1)); + } catch (NumberFormatException e) { + // Fallback to default if parsing fails + } + } + return 443; + } + public EndpointContext build() throws IOException { // The Universe Domain is used to resolve the Endpoint. It should be resolved first setResolvedUniverseDomain(determineUniverseDomain()); - setResolvedEndpoint(determineEndpoint()); + String endpoint = determineEndpoint(); + setResolvedEndpoint(endpoint); + setResolvedServerAddress(parseServerAddress(endpoint)); + setResolvedPort(parsePort(endpoint)); setUseS2A(shouldUseS2A()); return autoBuild(); } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java index f97f808ca7..b1482454dc 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java @@ -190,6 +190,20 @@ public String getEndpoint() { return endpointContext.resolvedEndpoint(); } + /** + * @return the fully resolved port used by the client + */ + public final int getPort() { + return endpointContext.resolvedPort(); + } + + /** + * @return the fully resolved server address used by the client + */ + public final String getServerAddress() { + return endpointContext.resolvedServerAddress(); + } + /** * @return the newly created EndpointContext */ diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java index bb8345b88c..0bfe82cee7 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerFactory.java @@ -31,6 +31,7 @@ import com.google.api.core.InternalApi; import com.google.api.core.InternalExtensionOnly; +import java.util.Map; /** * A factory to create new instances of {@link ApiTracer}s. @@ -61,4 +62,16 @@ enum OperationType { * @param operationType the type of operation that the tracer will trace */ ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType); + + /** + * Returns a new {@link ApiTracerFactory} that will add the given attributes to all tracers + * created by the factory. + * + * @param operationAttributes the operation attributes to add to all tracers + * @param attemptAttributes the attempt attributes to add to all tracers + */ + default ApiTracerFactory withAttributes( + Map operationAttributes, Map attemptAttributes) { + return this; + } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java index 16553dd118..9a650137cc 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java @@ -35,13 +35,11 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import com.google.api.core.ObsoleteApi; -import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.StatusCode; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nullable; @@ -218,24 +216,6 @@ public void attemptPermanentFailure(Throwable error) { metricsRecorder.recordAttemptCount(1, attributes); } - /** Function to extract the status of the error as a string */ - @VisibleForTesting - static String extractStatus(@Nullable Throwable error) { - final String statusString; - - if (error == null) { - return StatusCode.Code.OK.toString(); - } else if (error instanceof CancellationException) { - statusString = StatusCode.Code.CANCELLED.toString(); - } else if (error instanceof ApiException) { - statusString = ((ApiException) error).getStatusCode().getCode().toString(); - } else { - statusString = StatusCode.Code.UNKNOWN.toString(); - } - - return statusString; - } - /** * Add attributes that will be attached to all metrics. This is expected to be called by * handwritten client teams to add additional attributes that are not supposed be collected by @@ -260,4 +240,10 @@ public void addAttributes(Map attributes) { Map getAttributes() { return attributes; } + + /** Function to extract the status of the error as a string */ + @VisibleForTesting + String extractStatus(@Nullable Throwable error) { + return TracerUtils.extractStatus(error); + } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingRecorder.java new file mode 100644 index 0000000000..8a7f10f77d --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingRecorder.java @@ -0,0 +1,116 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.tracing; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.util.Map; + +/** + * OpenTelemetry implementation of recording traces. This implementation collects the measurements + * related to the lifecyle of an RPC. + */ +@BetaApi +@InternalApi +public class OpenTelemetryTracingRecorder implements TracingRecorder { + private final Tracer tracer; + + public OpenTelemetryTracingRecorder(OpenTelemetry openTelemetry) { + this.tracer = openTelemetry.getTracer("gax-java"); + } + + @Override + public SpanHandle startSpan(String name, Map attributes) { + return startSpan(name, attributes, null); + } + + @Override + public SpanHandle startSpan(String name, Map attributes, SpanHandle parent) { + SpanBuilder spanBuilder = + tracer.spanBuilder(name).setSpanKind(SpanKind.CLIENT); // Mark as a network-facing call + + if (attributes != null) { + attributes.forEach((k, v) -> spanBuilder.setAttribute(k, v)); + } + + if (parent instanceof OtelSpanHandle) { + spanBuilder.setParent(Context.current().with(((OtelSpanHandle) parent).span)); + } + + Span span = spanBuilder.startSpan(); + + return new OtelSpanHandle(span); + } + + @Override + @SuppressWarnings("MustBeClosedChecker") + public ApiTracer.Scope inScope(SpanHandle handle) { + if (handle instanceof OtelSpanHandle) { + Scope scope = ((OtelSpanHandle) handle).span.makeCurrent(); + return scope::close; + } + return () -> {}; + } + + private static class OtelSpanHandle implements SpanHandle { + private final Span span; + + private OtelSpanHandle(Span span) { + this.span = span; + } + + @Override + public void end() { + span.end(); + } + + @Override + public void recordError(Throwable error) { + span.recordException(error); + span.setAttribute( + OpenTelemetryTracingTracer.ERROR_TYPE_ATTRIBUTE, TracerUtils.extractStatus(error)); + span.setStatus(StatusCode.ERROR); + } + + @Override + public void setAttribute(String key, String value) { + span.setAttribute(key, value); + } + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracer.java new file mode 100644 index 0000000000..f3721a6db1 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracer.java @@ -0,0 +1,168 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.tracing; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import java.util.HashMap; +import java.util.Map; + +@BetaApi +@InternalApi +public class OpenTelemetryTracingTracer extends BaseApiTracer { + public static final String SERVER_ADDRESS_ATTRIBUTE = "server.address"; + public static final String LANGUAGE_ATTRIBUTE = "gcp.client.language"; + public static final String REPO_ATTRIBUTE = "gcp.client.repo"; + public static final String ERROR_TYPE_ATTRIBUTE = "error.type"; + public static final String SERVICE_NAME_ATTRIBUTE = "gcp.client.service"; + public static final String PORT_ATTRIBUTE = "server.port"; + public static final String RPC_SYSTEM_ATTRIBUTE = "rpc.system"; + public static final String GRPC_RESEND_COUNT_ATTRIBUTE = "gcp.grpc.resend_count"; + public static final String HTTP_RESEND_COUNT_ATTRIBUTE = "http.request.resend_count"; + + public static final String DEFAULT_LANGUAGE = "Java"; + + private final TracingRecorder recorder; + private final Map operationAttributes; + private final Map attemptAttributes; + private final String attemptSpanName; + private final TracingRecorder.SpanHandle operationHandle; + private TracingRecorder.SpanHandle attemptHandle; + + public OpenTelemetryTracingTracer( + TracingRecorder recorder, String operationSpanName, String attemptSpanName) { + this.recorder = recorder; + this.attemptSpanName = attemptSpanName; + this.operationAttributes = new HashMap<>(); + this.attemptAttributes = new HashMap<>(); + this.attemptAttributes.put(LANGUAGE_ATTRIBUTE, DEFAULT_LANGUAGE); + this.operationAttributes.put("method", operationSpanName); + + // Start the long-lived operation span. + // TODO(diegomarquezp): This conforms with T3 and is not fully implemented + this.operationHandle = recorder.startSpan(operationSpanName, operationAttributes); + } + + @Override + public Scope inScope() { + // If an attempt is in progress, make it current so downstream spans are its children. + // Otherwise, make the operation span current. + if (attemptHandle != null) { + return recorder.inScope(attemptHandle); + } + return recorder.inScope(operationHandle); + } + + @Override + public void attemptStarted(Object request, int attemptNumber) { + Map attemptAttributes = new HashMap<>(this.attemptAttributes); + String rpcSystem = attemptAttributes.get(RPC_SYSTEM_ATTRIBUTE); + if (attemptNumber > 0 && rpcSystem != null) { + attemptAttributes.put( + rpcSystem.equals("grpc") ? GRPC_RESEND_COUNT_ATTRIBUTE : HTTP_RESEND_COUNT_ATTRIBUTE, + String.valueOf(attemptNumber)); + } + + // Start the specific attempt span with the operation span as parent + this.attemptHandle = recorder.startSpan(attemptSpanName, attemptAttributes, operationHandle); + } + + @Override + public void attemptSucceeded() { + endAttempt(); + } + + @Override + public void attemptCancelled() { + endAttempt(); + } + + @Override + public void attemptFailedDuration(Throwable error, java.time.Duration delay) { + if (attemptHandle != null) { + attemptHandle.recordError(error); + } + endAttempt(); + } + + @Override + public void attemptFailedRetriesExhausted(Throwable error) { + if (attemptHandle != null) { + attemptHandle.recordError(error); + } + endAttempt(); + } + + @Override + public void attemptPermanentFailure(Throwable error) { + if (attemptHandle != null) { + attemptHandle.recordError(error); + } + endAttempt(); + } + + private void endAttempt() { + if (attemptHandle != null) { + attemptHandle.end(); + attemptHandle = null; + } + } + + @Override + public void operationSucceeded() { + operationHandle.end(); + } + + @Override + public void operationCancelled() { + operationHandle.end(); + } + + @Override + public void operationFailed(Throwable error) { + operationHandle.recordError(error); + operationHandle.end(); + } + + public void addOperationAttributes(Map attributes) { + this.operationAttributes.putAll(attributes); + if (operationHandle != null) { + attributes.forEach((k, v) -> operationHandle.setAttribute(k, v)); + } + } + + public void addAttemptAttributes(Map attributes) { + this.attemptAttributes.putAll(attributes); + if (attemptHandle != null) { + attributes.forEach((k, v) -> attemptHandle.setAttribute(k, v)); + } + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerFactory.java new file mode 100644 index 0000000000..6925719c8e --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerFactory.java @@ -0,0 +1,132 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.tracing; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A {@link ApiTracerFactory} to build instances of {@link OpenTelemetryTracingTracer}. + * + *

This class wraps the {@link TracingRecorder} and pass it to {@link + * OpenTelemetryTracingTracer}. It will be used to record traces in {@link + * OpenTelemetryTracingTracer}. + * + *

This class is expected to be initialized once during client initialization. + */ +@BetaApi +@InternalApi +public class OpenTelemetryTracingTracerFactory implements ApiTracerFactory { + private static final Logger LOGGER = + Logger.getLogger(OpenTelemetryTracingTracerFactory.class.getName()); + private static final String GAPIC_PROPERTIES_FILE = "/gapic.properties"; + private static final String REPO_KEY = "repo"; + + private final TracingRecorder tracingRecorder; + + /** Mapping of client attributes that are set for every TracingTracer at operation level */ + private final Map operationAttributes; + + /** Mapping of client attributes that are set for every TracingTracer at attempt level */ + private final Map attemptAttributes; + + /** Creates a TracingTracerFactory with no additional client level attributes. */ + public OpenTelemetryTracingTracerFactory(TracingRecorder tracingRecorder) { + this(tracingRecorder, ImmutableMap.of(), ImmutableMap.of()); + } + + /** + * Pass in a Map of client level attributes which will be added to every single TracingTracer + * created from the ApiTracerFactory. + */ + public OpenTelemetryTracingTracerFactory( + TracingRecorder tracingRecorder, + Map operationAttributes, + Map attemptAttributes) { + this.tracingRecorder = tracingRecorder; + + Map newAttemptAttributes = new HashMap<>(attemptAttributes); + String repo = loadRepoFromProperties(); + if (repo != null) { + newAttemptAttributes.put(OpenTelemetryTracingTracer.REPO_ATTRIBUTE, repo); + } + + this.operationAttributes = ImmutableMap.copyOf(operationAttributes); + this.attemptAttributes = ImmutableMap.copyOf(newAttemptAttributes); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + // TODO(diegomarquezp): use span names from design + String operationSpanName = + spanName.getClientName() + "." + spanName.getMethodName() + "/operation"; + String attemptSpanName = spanName.getClientName() + "/" + spanName.getMethodName() + "/attempt"; + + OpenTelemetryTracingTracer tracingTracer = + new OpenTelemetryTracingTracer(tracingRecorder, operationSpanName, attemptSpanName); + tracingTracer.addOperationAttributes(operationAttributes); + tracingTracer.addAttemptAttributes(attemptAttributes); + return tracingTracer; + } + + @Override + public ApiTracerFactory withAttributes( + Map operationAttributes, Map attemptAttributes) { + Map newOperationAttributes = new HashMap<>(this.operationAttributes); + newOperationAttributes.putAll(operationAttributes); + Map newAttemptAttributes = new HashMap<>(this.attemptAttributes); + newAttemptAttributes.putAll(attemptAttributes); + return new OpenTelemetryTracingTracerFactory( + tracingRecorder, newOperationAttributes, newAttemptAttributes); + } + + private static String loadRepoFromProperties() { + try (InputStream is = + OpenTelemetryTracingTracerFactory.class.getResourceAsStream(GAPIC_PROPERTIES_FILE)) { + if (is != null) { + Properties properties = new Properties(); + properties.load(is); + return properties.getProperty(REPO_KEY); + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not load gapic.properties", e); + } + return null; + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracerUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracerUtils.java new file mode 100644 index 0000000000..433b92ae3f --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracerUtils.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.tracing; + +import com.google.api.core.InternalApi; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode; +import java.util.concurrent.CancellationException; +import javax.annotation.Nullable; + +/** Utility class for tracing. */ +@InternalApi +public class TracerUtils { + + private TracerUtils() {} + + /** Function to extract the status of the error as a string */ + public static String extractStatus(@Nullable Throwable error) { + if (error == null) { + return StatusCode.Code.OK.toString(); + } else if (error instanceof CancellationException) { + return StatusCode.Code.CANCELLED.toString(); + } else if (error instanceof ApiException) { + return ((ApiException) error).getStatusCode().getCode().toString(); + } else { + return StatusCode.Code.UNKNOWN.toString(); + } + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingRecorder.java new file mode 100644 index 0000000000..1566bb8d39 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingRecorder.java @@ -0,0 +1,70 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.tracing; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import java.util.Map; + +/** + * Provides an interface for tracing recording. The implementer is expected to use an observability + * framework, e.g. OpenTelemetry. There should be only one instance of TracingRecorder per client, + * all the methods in this class are expected to be called from multiple threads, hence the + * implementation must be thread safe. + */ +@BetaApi +@InternalApi +public interface TracingRecorder { + /** Starts a span and returns a handle to manage its lifecycle. */ + SpanHandle startSpan(String name, Map attributes); + + /** Starts a span with a parent and returns a handle to manage its lifecycle. */ + default SpanHandle startSpan(String name, Map attributes, SpanHandle parent) { + return startSpan(name, attributes); + } + + /** + * Installs the span into the current thread-local context. + * + * @return a scope that must be closed to remove the span from the context. + */ + default ApiTracer.Scope inScope(SpanHandle handle) { + return () -> {}; + } + + interface SpanHandle { + void end(); + + void recordError(Throwable error); + + void setAttribute(String key, String value); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java index ef64ccd726..cac1bd7da9 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/EndpointContextTest.java @@ -593,4 +593,34 @@ void shouldUseS2A_success() throws IOException { .setUsingGDCH(false); Truth.assertThat(defaultEndpointContextBuilder.shouldUseS2A()).isTrue(); } + + @Test + void endpointContextBuild_resolvesPortAndServerAddress() throws IOException { + String endpoint = "http://localhost:7469"; + EndpointContext endpointContext = + defaultEndpointContextBuilder + .setClientSettingsEndpoint(endpoint) + .setTransportChannelProviderEndpoint(null) + .build(); + Truth.assertThat(endpointContext.resolvedPort()).isEqualTo(7469); + Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("localhost"); + + endpoint = "localhost:7469"; + endpointContext = + defaultEndpointContextBuilder + .setClientSettingsEndpoint(endpoint) + .setTransportChannelProviderEndpoint(null) + .build(); + Truth.assertThat(endpointContext.resolvedPort()).isEqualTo(7469); + Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("localhost"); + + endpoint = "test.googleapis.com:443"; + endpointContext = + defaultEndpointContextBuilder + .setClientSettingsEndpoint(endpoint) + .setTransportChannelProviderEndpoint(null) + .build(); + Truth.assertThat(endpointContext.resolvedPort()).isEqualTo(443); + Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("test.googleapis.com"); + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingRecorderTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingRecorderTest.java new file mode 100644 index 0000000000..455e6c4b50 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingRecorderTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.tracing; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class OpenTelemetryTracingRecorderTest { + @Mock private OpenTelemetry openTelemetry; + @Mock private Tracer tracer; + @Mock private SpanBuilder spanBuilder; + @Mock private Span span; + @Mock private Scope scope; + + private OpenTelemetryTracingRecorder recorder; + + @BeforeEach + void setUp() { + when(openTelemetry.getTracer(anyString())).thenReturn(tracer); + recorder = new OpenTelemetryTracingRecorder(openTelemetry); + } + + @Test + void testStartSpan_recordsSpan() { + String spanName = "test-span"; + Map attributes = ImmutableMap.of("key1", "value1"); + + when(tracer.spanBuilder(spanName)).thenReturn(spanBuilder); + when(spanBuilder.setSpanKind(SpanKind.CLIENT)).thenReturn(spanBuilder); + when(spanBuilder.setAttribute("key1", "value1")).thenReturn(spanBuilder); + when(spanBuilder.startSpan()).thenReturn(span); + + TracingRecorder.SpanHandle handle = recorder.startSpan(spanName, attributes); + handle.end(); + + verify(span).end(); + } + + @Test + void testInScope_managesContext() { + String spanName = "test-span"; + when(tracer.spanBuilder(spanName)).thenReturn(spanBuilder); + when(spanBuilder.setSpanKind(SpanKind.CLIENT)).thenReturn(spanBuilder); + when(spanBuilder.startSpan()).thenReturn(span); + when(span.makeCurrent()).thenReturn(scope); + + TracingRecorder.SpanHandle handle = recorder.startSpan(spanName, null); + try (ApiTracer.Scope ignored = recorder.inScope(handle)) { + // do nothing + } + + verify(span).makeCurrent(); + verify(scope).close(); + } + + @Test + void testRecordError_setsErrorStatus() { + String spanName = "error-span"; + Throwable error = new RuntimeException("test error"); + + when(tracer.spanBuilder(spanName)).thenReturn(spanBuilder); + when(spanBuilder.setSpanKind(SpanKind.CLIENT)).thenReturn(spanBuilder); + when(spanBuilder.startSpan()).thenReturn(span); + + TracingRecorder.SpanHandle handle = recorder.startSpan(spanName, null); + handle.recordError(error); + + verify(span).recordException(error); + verify(span).setStatus(StatusCode.ERROR); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerFactoryTest.java new file mode 100644 index 0000000000..f74bb17a27 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerFactoryTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.tracing; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +class OpenTelemetryTracingTracerFactoryTest { + + @Test + void testNewTracer_createsOpenTelemetryTracingTracer() { + TracingRecorder recorder = mock(TracingRecorder.class); + when(recorder.startSpan(anyString(), anyMap())) + .thenReturn(mock(TracingRecorder.SpanHandle.class)); + + OpenTelemetryTracingTracerFactory factory = new OpenTelemetryTracingTracerFactory(recorder); + ApiTracer tracer = + factory.newTracer( + null, SpanName.of("service", "method"), ApiTracerFactory.OperationType.Unary); + assertThat(tracer).isInstanceOf(OpenTelemetryTracingTracer.class); + } + + @Test + void testNewTracer_addsAttributes() { + TracingRecorder recorder = mock(TracingRecorder.class); + TracingRecorder.SpanHandle operationHandle = mock(TracingRecorder.SpanHandle.class); + when(recorder.startSpan(anyString(), anyMap())).thenReturn(operationHandle); + + OpenTelemetryTracingTracerFactory factory = + new OpenTelemetryTracingTracerFactory( + recorder, ImmutableMap.of(), ImmutableMap.of("server.port", "443")); + ApiTracer tracer = + factory.newTracer( + null, SpanName.of("service", "method"), ApiTracerFactory.OperationType.Unary); + + tracer.attemptStarted(null, 1); + + ArgumentCaptor> attributesCaptor = ArgumentCaptor.forClass(Map.class); + verify(recorder, atLeastOnce()) + .startSpan(anyString(), attributesCaptor.capture(), eq(operationHandle)); + + Map attemptAttributes = attributesCaptor.getValue(); + assertThat(attemptAttributes).containsEntry("server.port", "443"); + // Verify repo attribute from gapic.properties + assertThat(attemptAttributes) + .containsEntry(OpenTelemetryTracingTracer.REPO_ATTRIBUTE, "googleapis/sdk-platform-java"); + } + + @Test + void testWithAttributes_returnsNewFactoryWithMergedAttributes() { + TracingRecorder recorder = mock(TracingRecorder.class); + TracingRecorder.SpanHandle operationHandle = mock(TracingRecorder.SpanHandle.class); + when(recorder.startSpan(anyString(), anyMap())).thenReturn(operationHandle); + + OpenTelemetryTracingTracerFactory factory = + new OpenTelemetryTracingTracerFactory( + recorder, ImmutableMap.of("op1", "v1"), ImmutableMap.of("at1", "v1")); + + ApiTracerFactory factoryWithAttrs = + factory.withAttributes(ImmutableMap.of("op2", "v2"), ImmutableMap.of("at2", "v2")); + + assertThat(factoryWithAttrs).isInstanceOf(OpenTelemetryTracingTracerFactory.class); + + ApiTracer tracer = + factoryWithAttrs.newTracer( + null, SpanName.of("service", "method"), ApiTracerFactory.OperationType.Unary); + + tracer.attemptStarted(null, 1); + + ArgumentCaptor> attributesCaptor = ArgumentCaptor.forClass(Map.class); + verify(recorder, atLeastOnce()) + .startSpan(anyString(), attributesCaptor.capture(), eq(operationHandle)); + assertThat(attributesCaptor.getValue()).containsEntry("at1", "v1"); + assertThat(attributesCaptor.getValue()).containsEntry("at2", "v2"); + // Verify repo attribute from gapic.properties + assertThat(attributesCaptor.getValue()) + .containsEntry(OpenTelemetryTracingTracer.REPO_ATTRIBUTE, "googleapis/sdk-platform-java"); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerTest.java new file mode 100644 index 0000000000..0a48f21ce8 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.tracing; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class OpenTelemetryTracingTracerTest { + @Mock private TracingRecorder recorder; + @Mock private TracingRecorder.SpanHandle operationHandle; + @Mock private TracingRecorder.SpanHandle attemptHandle; + private OpenTelemetryTracingTracer tracer; + private static final String OPERATION_SPAN_NAME = "Service.Method/operation"; + private static final String ATTEMPT_SPAN_NAME = "Service/Method/attempt"; + + @BeforeEach + void setUp() { + when(recorder.startSpan(eq(OPERATION_SPAN_NAME), anyMap())).thenReturn(operationHandle); + tracer = new OpenTelemetryTracingTracer(recorder, OPERATION_SPAN_NAME, ATTEMPT_SPAN_NAME); + } + + @Test + void testOperationSucceeded_endsSpan() { + tracer.operationSucceeded(); + verify(operationHandle).end(); + } + + @Test + void testOperationFailed_recordsErrorAndEndsSpan() { + Throwable error = new RuntimeException("fail"); + tracer.operationFailed(error); + verify(operationHandle).recordError(error); + verify(operationHandle).end(); + } + + @Test + void testAttemptLifecycle_startsAndEndsAttemptSpan() { + when(recorder.startSpan(eq(ATTEMPT_SPAN_NAME), anyMap(), eq(operationHandle))) + .thenReturn(attemptHandle); + tracer.attemptStarted(new Object(), 1); + tracer.attemptSucceeded(); + + verify(attemptHandle).end(); + } + + @Test + void testAddAttemptAttributes_passedToAttemptSpan() { + tracer.addAttemptAttributes(ImmutableMap.of("attempt-key", "attempt-value")); + tracer.addAttemptAttributes( + ImmutableMap.of(OpenTelemetryTracingTracer.RPC_SYSTEM_ATTRIBUTE, "grpc")); + + when(recorder.startSpan(eq(ATTEMPT_SPAN_NAME), anyMap(), eq(operationHandle))) + .thenReturn(attemptHandle); + tracer.attemptStarted(new Object(), 1); + + ArgumentCaptor> attributesCaptor = ArgumentCaptor.forClass(Map.class); + verify(recorder) + .startSpan(eq(ATTEMPT_SPAN_NAME), attributesCaptor.capture(), eq(operationHandle)); + + Map capturedAttributes = attributesCaptor.getValue(); + assertThat(capturedAttributes).containsEntry("attempt-key", "attempt-value"); + assertThat(capturedAttributes) + .containsEntry( + OpenTelemetryTracingTracer.LANGUAGE_ATTRIBUTE, + OpenTelemetryTracingTracer.DEFAULT_LANGUAGE); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracerUtilsTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracerUtilsTest.java new file mode 100644 index 0000000000..c266a7bde0 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracerUtilsTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.tracing; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.api.gax.rpc.testing.FakeStatusCode; +import java.util.concurrent.CancellationException; +import org.junit.jupiter.api.Test; + +class TracerUtilsTest { + + @Test + void testExtractStatus_nullError() { + assertThat(TracerUtils.extractStatus(null)).isEqualTo(Code.OK.toString()); + } + + @Test + void testExtractStatus_cancellationException() { + assertThat(TracerUtils.extractStatus(new CancellationException())) + .isEqualTo(Code.CANCELLED.toString()); + } + + @Test + void testExtractStatus_apiException() { + ApiException error = + new ApiException("fake_error", null, new FakeStatusCode(Code.INVALID_ARGUMENT), false); + assertThat(TracerUtils.extractStatus(error)).isEqualTo(Code.INVALID_ARGUMENT.toString()); + } + + @Test + void testExtractStatus_unknownException() { + assertThat(TracerUtils.extractStatus(new RuntimeException())) + .isEqualTo(Code.UNKNOWN.toString()); + } +} diff --git a/gax-java/gax/src/test/resources/gapic.properties b/gax-java/gax/src/test/resources/gapic.properties new file mode 100644 index 0000000000..ac1d20a460 --- /dev/null +++ b/gax-java/gax/src/test/resources/gapic.properties @@ -0,0 +1 @@ +repo=googleapis/sdk-platform-java diff --git a/hermetic_build/library_generation/generate_composed_library.py b/hermetic_build/library_generation/generate_composed_library.py index ea595a3f98..653df39c09 100755 --- a/hermetic_build/library_generation/generate_composed_library.py +++ b/hermetic_build/library_generation/generate_composed_library.py @@ -84,6 +84,8 @@ def generate_composed_library( gapic=gapic, gapic_inputs=gapic_inputs, temp_destination_path=temp_destination_path, + generation_config=config, + library=library, ) print("arguments: ") print(effective_arguments) @@ -124,6 +126,8 @@ def __construct_effective_arg( gapic: GapicConfig, gapic_inputs: GapicInputs, temp_destination_path: str, + generation_config: LibraryConfig, + library: LibraryConfig, ) -> List[str]: """ Construct arguments consist attributes of a GAPIC library which used in @@ -153,6 +157,8 @@ def __construct_effective_arg( gapic_inputs.service_yaml, "--include_samples", gapic_inputs.include_samples, + "--repo", + util.get_library_repository(generation_config, library), ] arguments += ["--destination_path", temp_destination_path] diff --git a/hermetic_build/library_generation/generate_library.sh b/hermetic_build/library_generation/generate_library.sh index 2625021ea6..d1df2cebd1 100755 --- a/hermetic_build/library_generation/generate_library.sh +++ b/hermetic_build/library_generation/generate_library.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -eo pipefail +set -exo pipefail # parse input parameters while [[ $# -gt 0 ]]; do @@ -50,6 +50,10 @@ case $key in os_architecture="$2" shift ;; + --repo) + repo="$2" + shift + ;; *) echo "Invalid option: [$1]" exit 1 @@ -99,6 +103,10 @@ if [ -z "${os_architecture}" ]; then os_architecture=$(detect_os_architecture) fi +if [ -z "${repo}" ]; then + repo=$(repo) +fi + temp_destination_path="${output_folder}/temp_preprocessed-$RANDOM" mkdir -p "${output_folder}/${destination_path}" if [ -d "${temp_destination_path}" ]; then @@ -179,7 +187,7 @@ if [[ "${proto_only}" == "false" ]]; then "$protoc_path"/protoc --experimental_allow_proto3_optional \ "--plugin=protoc-gen-java_gapic=${script_dir}/gapic-generator-java-wrapper" \ "--java_gapic_out=metadata:${temp_destination_path}/java_gapic_srcjar_raw.srcjar.zip" \ - "--java_gapic_opt=$(get_gapic_opts "${transport}" "${rest_numeric_enums}" "${gapic_yaml}" "${service_config}" "${service_yaml}")" \ + "--java_gapic_opt=$(get_gapic_opts "${transport}" "${rest_numeric_enums}" "${gapic_yaml}" "${service_config}" "${service_yaml}" "${repo}")" \ ${proto_files} ${gapic_additional_protos} unzip -o -q "${temp_destination_path}/java_gapic_srcjar_raw.srcjar.zip" -d "${temp_destination_path}" diff --git a/hermetic_build/library_generation/tests/generate_library_unit_tests.sh b/hermetic_build/library_generation/tests/generate_library_unit_tests.sh index 68eb9ba40e..1093fffe0b 100755 --- a/hermetic_build/library_generation/tests/generate_library_unit_tests.sh +++ b/hermetic_build/library_generation/tests/generate_library_unit_tests.sh @@ -27,10 +27,11 @@ get_gapic_opts_with_rest_test() { local proto_path="${script_dir}/resources/gapic_options" local transport="grpc" local rest_numeric_enums="true" + local repo="googleapis/google-cloud-java" local gapic_opts - gapic_opts="$(get_gapic_opts "${transport}" "${rest_numeric_enums}" "" "" "")" + gapic_opts="$(get_gapic_opts "${transport}" "${rest_numeric_enums}" "" "" "" "${repo}")" assertEquals \ - "transport=grpc,rest-numeric-enums,grpc-service-config=${proto_path}/example_grpc_service_config.json,gapic-config=${proto_path}/example_gapic.yaml,api-service-config=${proto_path}/example.yaml" \ + "transport=grpc,rest-numeric-enums,grpc-service-config=${proto_path}/example_grpc_service_config.json,gapic-config=${proto_path}/example_gapic.yaml,api-service-config=${proto_path}/example.yaml,repo=${repo}" \ "${gapic_opts}" } @@ -38,10 +39,11 @@ get_gapic_opts_without_rest_test() { local proto_path="${script_dir}/resources/gapic_options" local transport="grpc" local rest_numeric_enums="false" + local repo="googleapis/google-cloud-java" local gapic_opts - gapic_opts="$(get_gapic_opts "${transport}" "${rest_numeric_enums}" "" "" "")" + gapic_opts="$(get_gapic_opts "${transport}" "${rest_numeric_enums}" "" "" "" "${repo}")" assertEquals \ - "transport=grpc,,grpc-service-config=${proto_path}/example_grpc_service_config.json,gapic-config=${proto_path}/example_gapic.yaml,api-service-config=${proto_path}/example.yaml" \ + "transport=grpc,,grpc-service-config=${proto_path}/example_grpc_service_config.json,gapic-config=${proto_path}/example_gapic.yaml,api-service-config=${proto_path}/example.yaml,repo=${repo}" \ "$gapic_opts" } @@ -49,10 +51,11 @@ get_gapic_opts_with_non_default_test() { local proto_path="${script_dir}/resources/gapic_options" local transport="grpc" local rest_numeric_enums="false" + local repo="googleapis/google-cloud-java" local gapic_opts - gapic_opts="$(get_gapic_opts "${transport}" "${rest_numeric_enums}" "${proto_path}/example_gapic.yaml" "${proto_path}/example_grpc_service_config.json" "${proto_path}/example.yaml")" + gapic_opts="$(get_gapic_opts "${transport}" "${rest_numeric_enums}" "${proto_path}/example_gapic.yaml" "${proto_path}/example_grpc_service_config.json" "${proto_path}/example.yaml" "${repo}")" assertEquals \ - "transport=grpc,,grpc-service-config=${proto_path}/example_grpc_service_config.json,gapic-config=${proto_path}/example_gapic.yaml,api-service-config=${proto_path}/example.yaml" \ + "transport=grpc,,grpc-service-config=${proto_path}/example_grpc_service_config.json,gapic-config=${proto_path}/example_gapic.yaml,api-service-config=${proto_path}/example.yaml,repo=${repo}" \ "$gapic_opts" } diff --git a/hermetic_build/library_generation/tests/utilities_unit_tests.py b/hermetic_build/library_generation/tests/utilities_unit_tests.py index a6796b706d..d5a70054b1 100644 --- a/hermetic_build/library_generation/tests/utilities_unit_tests.py +++ b/hermetic_build/library_generation/tests/utilities_unit_tests.py @@ -178,6 +178,24 @@ def test_eprint_valid_input_succeeds(self): # print() appends a `\n` each time it's called self.assertEqual(test_input + "\n", result) + def test_get_library_repository_with_common_protos_returns_sdk_platform_java(self): + config = self.__get_a_gen_config(3) + library = common_protos + result = util.get_library_repository(config, library) + self.assertEqual("googleapis/sdk-platform-java", result) + + def test_get_library_repository_with_monorepo_returns_google_cloud_java(self): + config = self.__get_a_gen_config(2) + library = library_1 + result = util.get_library_repository(config, library) + self.assertEqual("googleapis/google-cloud-java", result) + + def test_get_library_repository_with_split_repo_returns_library_repo(self): + config = self.__get_a_gen_config(1) + library = library_1 + result = util.get_library_repository(config, library) + self.assertEqual("googleapis/java-bare-metal-solution", result) + def test_generate_postprocessing_prerequisite_files_non_monorepo_success(self): library_path = self.__setup_postprocessing_prerequisite_files( combination=1, library_type="GAPIC_COMBO" diff --git a/hermetic_build/library_generation/utils/utilities.py b/hermetic_build/library_generation/utils/utilities.py index ec5c03d069..d1d44e21bf 100755 --- a/hermetic_build/library_generation/utils/utilities.py +++ b/hermetic_build/library_generation/utils/utilities.py @@ -170,6 +170,24 @@ def prepare_repo( ) +def get_library_repository( + config: GenerationConfig, library: LibraryConfig, language: str = "java" +): + """ + Obtains the repository identifier (e.g. googleapis/java-bigtable) depending on + whether it's a monorepo (google-cloud-java or sdk-platform-java if has common-protos) or not. + + :return: string representing the repository + """ + if config.contains_common_protos(): + repo = SDK_PLATFORM_JAVA + elif config.is_monorepo(): + repo = "googleapis/google-cloud-java" + else: + repo = f"googleapis/{language}-{library.get_library_name()}" + return repo + + def generate_postprocessing_prerequisite_files( config: GenerationConfig, library: LibraryConfig, @@ -191,14 +209,9 @@ def generate_postprocessing_prerequisite_files( :param language: programming language of the library :return: None """ + repo = get_library_repository(config, library) library_name = library.get_library_name() artifact_id = library.get_artifact_id() - if config.contains_common_protos(): - repo = SDK_PLATFORM_JAVA - elif config.is_monorepo(): - repo = "googleapis/google-cloud-java" - else: - repo = f"googleapis/{language}-{library_name}" api_id = ( library.api_id if library.api_id else f"{library.api_shortname}.googleapis.com" ) diff --git a/hermetic_build/library_generation/utils/utilities.sh b/hermetic_build/library_generation/utils/utilities.sh index 863834f508..55e89098a7 100755 --- a/hermetic_build/library_generation/utils/utilities.sh +++ b/hermetic_build/library_generation/utils/utilities.sh @@ -70,6 +70,7 @@ get_gapic_opts() { local gapic_yaml=$3 local service_config=$4 local service_yaml=$5 + local repo=$6 if [ "${rest_numeric_enums}" == "true" ]; then rest_numeric_enums="rest-numeric-enums" else @@ -88,7 +89,7 @@ get_gapic_opts() { if [[ "${service_yaml}" == "" ]]; then service_yaml=$(find "${proto_path}" -maxdepth 1 -type f \( -name "*.yaml" ! -name "*gapic*.yaml" \)) fi - echo "transport=${transport},${rest_numeric_enums},grpc-service-config=${service_config},gapic-config=${gapic_yaml},api-service-config=${service_yaml}" + echo "transport=${transport},${rest_numeric_enums},grpc-service-config=${service_config},gapic-config=${gapic_yaml},api-service-config=${service_yaml},repo=${repo}" } remove_grpc_version() { diff --git a/java-showcase/gapic-showcase/src/main/resources/gapic.properties b/java-showcase/gapic-showcase/src/main/resources/gapic.properties new file mode 100644 index 0000000000..ac1d20a460 --- /dev/null +++ b/java-showcase/gapic-showcase/src/main/resources/gapic.properties @@ -0,0 +1 @@ +repo=googleapis/sdk-platform-java diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java new file mode 100644 index 0000000000..09ee14412b --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -0,0 +1,364 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.showcase.v1beta1.it; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.rpc.StatusCode; +import com.google.api.gax.tracing.OpenTelemetryTracingRecorder; +import com.google.api.gax.tracing.OpenTelemetryTracingTracer; +import com.google.api.gax.tracing.OpenTelemetryTracingTracerFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.rpc.Status; +import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.EchoRequest; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ITOtelTracing { + private static final String SHOWCASE_SERVER_PORT = "7469"; + private static final String SHOWCASE_SERVER_ADDRESS = "localhost"; + private static final String SHOWCASE_CLIENT_REPO = "googleapis/sdk-platform-java"; + + private InMemorySpanExporter spanExporter; + private OpenTelemetrySdk openTelemetrySdk; + + @BeforeEach + void setup() { + spanExporter = InMemorySpanExporter.create(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + + openTelemetrySdk = + OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal(); + } + + @AfterEach + void tearDown() { + if (openTelemetrySdk != null) { + openTelemetrySdk.close(); + } + GlobalOpenTelemetry.resetForTest(); + } + + @Test + void testTracing_successfulEcho_grpc() throws Exception { + OpenTelemetryTracingTracerFactory tracingFactory = + new OpenTelemetryTracingTracerFactory(new OpenTelemetryTracingRecorder(openTelemetrySdk)); + + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientOpentelemetry(tracingFactory)) { + + client.echo(EchoRequest.newBuilder().setContent("tracing-test").build()); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + SpanData attemptSpan = + spans.stream() + .filter(span -> span.getName().equals("Echo/Echo/attempt")) + .findFirst() + .orElseThrow(() -> new AssertionError("Attempt span 'Echo/Echo/attempt' not found")); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(OpenTelemetryTracingTracer.LANGUAGE_ATTRIBUTE))) + .isEqualTo(OpenTelemetryTracingTracer.DEFAULT_LANGUAGE); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(OpenTelemetryTracingTracer.PORT_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_PORT); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(OpenTelemetryTracingTracer.RPC_SYSTEM_ATTRIBUTE))) + .isEqualTo("grpc"); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(OpenTelemetryTracingTracer.SERVER_ADDRESS_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_ADDRESS); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(OpenTelemetryTracingTracer.REPO_ATTRIBUTE))) + .isEqualTo(SHOWCASE_CLIENT_REPO); + } + } + + @Test + void testTracing_successfulEcho_httpjson() throws Exception { + OpenTelemetryTracingTracerFactory tracingFactory = + new OpenTelemetryTracingTracerFactory(new OpenTelemetryTracingRecorder(openTelemetrySdk)); + + try (EchoClient client = + TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracingFactory)) { + + client.echo(EchoRequest.newBuilder().setContent("tracing-test").build()); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + SpanData attemptSpan = + spans.stream() + .filter(span -> span.getName().equals("google.showcase.v1beta1/Echo/Echo/attempt")) + .findFirst() + .orElseThrow(() -> new AssertionError("Attempt span 'Echo/Echo/attempt' not found")); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(OpenTelemetryTracingTracer.RPC_SYSTEM_ATTRIBUTE))) + .isEqualTo("http"); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(OpenTelemetryTracingTracer.SERVER_ADDRESS_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_ADDRESS); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(OpenTelemetryTracingTracer.REPO_ATTRIBUTE))) + .isEqualTo(SHOWCASE_CLIENT_REPO); + } + } + + @Test + void testTracing_errorRecording() throws Exception { + List errorCodes = + ImmutableList.of( + StatusCode.Code.UNAVAILABLE, + StatusCode.Code.INVALID_ARGUMENT, + StatusCode.Code.NOT_FOUND, + StatusCode.Code.DEADLINE_EXCEEDED, + StatusCode.Code.PERMISSION_DENIED); + + OpenTelemetryTracingTracerFactory tracingFactory = + new OpenTelemetryTracingTracerFactory(new OpenTelemetryTracingRecorder(openTelemetrySdk)); + + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientOpentelemetry(tracingFactory)) { + + for (StatusCode.Code code : errorCodes) { + spanExporter.reset(); + Assertions.assertThrows( + Exception.class, + () -> + client.echo( + EchoRequest.newBuilder() + .setContent("error-test-" + code) + .setError(Status.newBuilder().setCode(code.ordinal()).build()) + .build())); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + SpanData attemptSpan = + spans.stream() + .filter(span -> span.getName().equals("Echo/Echo/attempt")) + .findFirst() + .orElseThrow( + () -> + new AssertionError( + "Attempt span 'Echo/Echo/attempt' not found for code: " + code)); + + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(OpenTelemetryTracingTracer.ERROR_TYPE_ATTRIBUTE))) + .isEqualTo(code.toString()); + assertThat(attemptSpan.getStatus().getStatusCode()) + .isEqualTo(io.opentelemetry.api.trace.StatusCode.ERROR); + } + } + } + + @Test + void testTracing_resendCount_grpc() throws Exception { + OpenTelemetryTracingTracerFactory tracingFactory = + new OpenTelemetryTracingTracerFactory(new OpenTelemetryTracingRecorder(openTelemetrySdk)); + + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientOpentelemetry(tracingFactory)) { + + // Simulate UNAVAILABLE to trigger retries + Assertions.assertThrows( + Exception.class, + () -> + client.echo( + EchoRequest.newBuilder() + .setContent("resend-test-grpc") + .setError( + Status.newBuilder() + .setCode(StatusCode.Code.UNAVAILABLE.ordinal()) + .build()) + .build())); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + // Verify that subsequent attempts have gcp.grpc.resend_count + SpanData secondAttempt = + spans.stream() + .filter( + span -> + span.getName().equals("Echo/Echo/attempt") + && Objects.equals( + span.getAttributes() + .get( + AttributeKey.stringKey( + OpenTelemetryTracingTracer.GRPC_RESEND_COUNT_ATTRIBUTE)), + "1")) + .findFirst() + .orElseThrow(() -> new AssertionError("Second attempt span not found")); + + assertThat(secondAttempt.getAttributes().get(AttributeKey.stringKey("gcp.grpc.resend_count"))) + .isEqualTo("1"); + } + } + + @Test + void testTracing_resendCount_httpjson() throws Exception { + OpenTelemetryTracingTracerFactory tracingFactory = + new OpenTelemetryTracingTracerFactory(new OpenTelemetryTracingRecorder(openTelemetrySdk)); + + try (EchoClient client = + TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracingFactory)) { + + // Simulate UNAVAILABLE to trigger retries + Assertions.assertThrows( + Exception.class, + () -> + client.echo( + EchoRequest.newBuilder() + .setContent("resend-test-http") + .setError( + Status.newBuilder() + .setCode(StatusCode.Code.UNAVAILABLE.ordinal()) + .build()) + .build())); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + // Verify that subsequent attempts have http.request.resend_count + SpanData secondAttempt = + spans.stream() + .filter( + span -> + span.getName().equals("google.showcase.v1beta1/Echo/Echo/attempt") + && Objects.equals( + span.getAttributes() + .get( + AttributeKey.stringKey( + OpenTelemetryTracingTracer.HTTP_RESEND_COUNT_ATTRIBUTE)), + "1")) + .findFirst() + .orElseThrow(() -> new AssertionError("Second attempt span not found")); + + assertThat( + secondAttempt + .getAttributes() + .get(AttributeKey.stringKey("http.request.resend_count"))) + .isEqualTo("1"); + } + } + + @Test + void testTracing_withCustomAttributes() throws Exception { + Map opAttributes = ImmutableMap.of("op-key", "op-value"); + Map atAttributes = ImmutableMap.of("at-key", "at-value"); + OpenTelemetryTracingTracerFactory tracingFactory = + new OpenTelemetryTracingTracerFactory( + new OpenTelemetryTracingRecorder(openTelemetrySdk), opAttributes, atAttributes); + + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientOpentelemetry(tracingFactory)) { + + client.echo(EchoRequest.newBuilder().setContent("attr-test").build()); + + List spans = spanExporter.getFinishedSpanItems(); + + SpanData attemptSpan = + spans.stream() + .filter(span -> span.getName().equals("Echo/Echo/attempt")) + .findFirst() + .orElseThrow(() -> new AssertionError("Attempt span 'Echo/Echo/attempt' not found")); + assertThat(attemptSpan.getAttributes().get(AttributeKey.stringKey("at-key"))) + .isEqualTo("at-value"); + } + } + + @Test + void testTracing_customStubSettings_overridesServiceName() throws Exception { + String customServiceName = "showcase"; + OpenTelemetryTracingTracerFactory tracingFactory = + new OpenTelemetryTracingTracerFactory( + new OpenTelemetryTracingRecorder(openTelemetrySdk), + ImmutableMap.of(), + ImmutableMap.of("gcp.client.service", customServiceName)); + + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientWithCustomServiceName( + tracingFactory, customServiceName)) { + + client.echo(EchoRequest.newBuilder().setContent("content").build()); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + SpanData attemptSpan = + spans.stream() + .filter(span -> span.getName().equals("Echo/Echo/attempt")) + .findFirst() + .orElseThrow(() -> new AssertionError("Attempt span 'Echo/Echo/attempt' not found")); + assertThat(attemptSpan.getAttributes().get(AttributeKey.stringKey("gcp.client.service"))) + .isEqualTo(customServiceName); + } + } +} diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java index a3db1a00e3..1238899033 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java @@ -38,6 +38,7 @@ import com.google.showcase.v1beta1.stub.EchoStubSettings; import io.grpc.ClientInterceptor; import io.grpc.ManagedChannelBuilder; +import java.io.IOException; import java.util.List; import java.util.Set; @@ -290,11 +291,7 @@ public static ComplianceClient createHttpJsonComplianceClient( public static EchoClient createGrpcEchoClientOpentelemetry(ApiTracerFactory metricsTracerFactory) throws Exception { - return createGrpcEchoClientOpentelemetry( - metricsTracerFactory, - EchoSettings.defaultGrpcTransportProviderBuilder() - .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) - .build()); + return createGrpcEchoClientWithCustomServiceName(metricsTracerFactory, ""); } public static EchoClient createGrpcEchoClientOpentelemetry( @@ -340,4 +337,39 @@ public static EchoClient createHttpJsonEchoClientOpentelemetry( return EchoClient.create(stub); } + + public static EchoClient createGrpcEchoClientWithCustomServiceName( + ApiTracerFactory metricsTracerFactory, String customServiceName) throws Exception { + EchoStubSettings.Builder grpcEchoSettingsBuilder = + new EchoStubSettings.Builder() { + @Override + public EchoStubSettings build() throws IOException { + return new EchoStubSettings(this) { + /** + * The service name is normally inferred from the default host if it follows the + * format of `xxx.googleapis.com`. Since Showcase uses `localhost`, the service name + * is not automatically inferred. We override it here to ensure it's available for + * tracing and metrics. + */ + @Override + public String getServiceName() { + return customServiceName; + } + }; + } + }; + + grpcEchoSettingsBuilder.setCredentialsProvider(NoCredentialsProvider.create()); + grpcEchoSettingsBuilder.setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()); + grpcEchoSettingsBuilder.setEndpoint(DEFAULT_GRPC_ENDPOINT); + grpcEchoSettingsBuilder.setTracerFactory(metricsTracerFactory); + + EchoStubSettings echoStubSettings = grpcEchoSettingsBuilder.build(); + EchoStub stub = echoStubSettings.createStub(); + + return EchoClient.create(stub); + } }