diff --git a/.cloudbuild/library_generation/library_generation.Dockerfile b/.cloudbuild/library_generation/library_generation.Dockerfile index f0ec060357..eb9d944738 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..a6e86c5698 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,11 +95,14 @@ static GapicMetadata defaultGapicMetadata() { public abstract Transport transport(); + public abstract Optional repo(); + public static Builder builder() { return new AutoValue_GapicContext.Builder() .setMixinServices(Collections.emptyList()) .setGapicMetadataEnabled(false) - .setRestNumericEnumsEnabled(false); + .setRestNumericEnumsEnabled(false) + .setRepo(Optional.empty()); } @AutoValue.Builder @@ -130,6 +133,8 @@ public Builder setHelperResourceNames(Set helperResourceNames) { public abstract Builder setTransport(Transport transport); + public abstract Builder setRepo(Optional 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..43ef63a63c 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 repoOpt = 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(repoOpt) .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..9081c56b1f 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; @@ -25,6 +26,7 @@ import java.util.Collections; import java.util.Enumeration; import java.util.List; +import java.util.Optional; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; @@ -144,4 +146,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(Optional.of(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/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..e782fdd926 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 @@ -41,6 +41,7 @@ import com.google.api.gax.core.ExecutorAsBackgroundResource; import com.google.api.gax.core.ExecutorProvider; import com.google.api.gax.rpc.internal.QuotaProjectIdHidingCredentials; +import com.google.api.gax.tracing.ApiTracerContext; import com.google.api.gax.tracing.ApiTracerFactory; import com.google.api.gax.tracing.BaseApiTracerFactory; import com.google.auth.ApiKeyCredentials; @@ -269,6 +270,11 @@ public static ClientContext create(StubSettings settings) throws IOException { if (watchdogProvider != null && watchdogProvider.shouldAutoClose()) { backgroundResources.add(watchdog); } + ApiTracerContext apiTracerContext = + ApiTracerContext.newBuilder() + .setServerAddress(endpointContext.resolvedServerAddress()) + .build(); + ApiTracerFactory apiTracerFactory = settings.getTracerFactory().withContext(apiTracerContext); return newBuilder() .setBackgroundResources(backgroundResources.build()) @@ -284,7 +290,7 @@ public static ClientContext create(StubSettings settings) throws IOException { .setQuotaProjectId(settings.getQuotaProjectId()) .setStreamWatchdog(watchdog) .setStreamWatchdogCheckIntervalDuration(settings.getStreamWatchdogCheckIntervalDuration()) - .setTracerFactory(settings.getTracerFactory()) + .setTracerFactory(apiTracerFactory) .setEndpointContext(endpointContext) .build(); } 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..84111dd620 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 @@ -40,6 +40,7 @@ import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.common.net.HostAndPort; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -133,6 +134,8 @@ public static EndpointContext getDefaultInstance() { public abstract String resolvedEndpoint(); + public abstract String resolvedServerAddress(); + public abstract Builder toBuilder(); public static Builder newBuilder() { @@ -228,6 +231,8 @@ public abstract static class Builder { public abstract Builder setResolvedEndpoint(String resolvedEndpoint); + public abstract Builder setResolvedServerAddress(String serverAddress); + public abstract Builder setResolvedUniverseDomain(String resolvedUniverseDomain); abstract Builder setUseS2A(boolean useS2A); @@ -382,6 +387,23 @@ boolean shouldUseS2A() { return mtlsEndpoint().contains(Credentials.GOOGLE_DEFAULT_UNIVERSE); } + private String parseServerAddress(String endpoint) { + if (Strings.isNullOrEmpty(endpoint)) { + return endpoint; + } + String hostPort = endpoint; + if (hostPort.contains("://")) { + // Strip the scheme if present. HostAndPort doesn't support schemes. + hostPort = hostPort.substring(hostPort.indexOf("://") + 3); + } + try { + return HostAndPort.fromString(hostPort).getHost(); + } catch (IllegalArgumentException e) { + // Fallback for cases HostAndPort can't handle. + return hostPort; + } + } + // Default to port 443 for HTTPS. Using HTTP requires explicitly setting the endpoint private String buildEndpointTemplate(String serviceName, String resolvedUniverseDomain) { return serviceName + "." + resolvedUniverseDomain + ":443"; @@ -416,7 +438,9 @@ String mtlsEndpointResolver( 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)); setUseS2A(shouldUseS2A()); return autoBuild(); } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java new file mode 100644 index 0000000000..b35385f286 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracerContext.java @@ -0,0 +1,100 @@ +/* + * 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.InternalApi; +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * A context object that contains information used to infer attributes that are common for all + * {@link ApiTracer}s. + * + *

For internal use only. + */ +@InternalApi +@AutoValue +public abstract class ApiTracerContext { + private static final Logger LOGGER = Logger.getLogger(ApiTracerContext.class.getName()); + private static final String GAPIC_PROPERTIES_FILE = "/gapic.properties"; + private static final String REPO_KEY = "repo"; + + @Nullable + public abstract String getServerAddress(); + + @Nullable + public abstract String getRepo(); + + public static Builder newBuilder() { + return newBuilder(ApiTracerContext.class.getResourceAsStream(GAPIC_PROPERTIES_FILE)); + } + + @VisibleForTesting + static Builder newBuilder(@Nullable InputStream inputStream) { + Builder builder = new AutoValue_ApiTracerContext.Builder(); + loadRepoFromProperties(builder, inputStream); + return builder; + } + + private static void loadRepoFromProperties(Builder builder, @Nullable InputStream is) { + if (is == null) { + return; + } + try { + Properties properties = new Properties(); + properties.load(is); + builder.setRepo(properties.getProperty(REPO_KEY)); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not load gapic.properties", e); + } finally { + try { + is.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not close gapic.properties stream", e); + } + } + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setServerAddress(String serverAddress); + + public abstract Builder setRepo(String repo); + + public abstract ApiTracerContext build(); + } +} 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..07a0fcf12d 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 @@ -61,4 +61,15 @@ 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 use the provided context to infer attributes + * for all tracers created by the factory. + * + * @param context an {@link ApiTracerContext} object containing information to construct + * attributes + */ + default ApiTracerFactory withContext(ApiTracerContext context) { + return this; + } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricAttributes.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricAttributes.java new file mode 100644 index 0000000000..a060ba4a88 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricAttributes.java @@ -0,0 +1,64 @@ +/* + * 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.InternalApi; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class for providing common attributes used in app-centric observability. + * + *

This class extracts information from {@link ApiTracerContext} and maps it to standardized + * attribute keys that are expected by {@link ApiTracerFactory} implementations that conform to + * app-centric observability + * + *

For internal use only. + */ +@InternalApi +public class AppCentricAttributes { + /** The address of the server being called (e.g., "pubsub.googleapis.com"). */ + public static final String SERVER_ADDRESS_ATTRIBUTE = "server.address"; + + /** + * Extracts attempt-level attributes from the provided {@link ApiTracerContext}. + * + * @param context the context containing information about the current API call + * @return a map of attributes to be included in attempt-level spans + */ + public static Map getAttemptAttributes(ApiTracerContext context) { + Map attributes = new HashMap<>(); + if (context.getServerAddress() != null) { + attributes.put(SERVER_ADDRESS_ATTRIBUTE, context.getServerAddress()); + } + return attributes; + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracer.java new file mode 100644 index 0000000000..e3b3686874 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracer.java @@ -0,0 +1,91 @@ +/* + * 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; + +/** + * An implementation of {@link ApiTracer} that uses a {@link TraceRecorder} to record traces. This + * implementation is agnostic to the specific {@link TraceRecorder} in order to allow extensions + * that interact with other backends. + */ +@BetaApi +@InternalApi +public class AppCentricTracer implements ApiTracer { + public static final String LANGUAGE_ATTRIBUTE = "gcp.client.language"; + public static final String REPO_ATTRIBUTE = "gcp.client.repo"; + + public static final String DEFAULT_LANGUAGE = "Java"; + + private final TraceRecorder recorder; + private final Map attemptAttributes; + private final String attemptSpanName; + private TraceRecorder.TraceSpan attemptHandle; + + /** + * Creates a new instance of {@code AppCentricTracer}. + * + * @param recorder the {@link TraceRecorder} to use for recording spans + * @param attemptSpanName the name of the individual attempt spans + * @param attemptAttributes attributes to be added to each attempt span + */ + public AppCentricTracer( + TraceRecorder recorder, String attemptSpanName, Map attemptAttributes) { + this.recorder = recorder; + this.attemptSpanName = attemptSpanName; + this.attemptAttributes = new HashMap<>(attemptAttributes); + this.attemptAttributes.put(LANGUAGE_ATTRIBUTE, DEFAULT_LANGUAGE); + + // Start the long-lived operation span. + } + + @Override + public void attemptStarted(Object request, int attemptNumber) { + Map attemptAttributes = new HashMap<>(this.attemptAttributes); + // Start the specific attempt span with the operation span as parent + this.attemptHandle = recorder.createSpan(attemptSpanName, attemptAttributes); + } + + @Override + public void attemptSucceeded() { + endAttempt(); + } + + private void endAttempt() { + if (attemptHandle != null) { + attemptHandle.end(); + attemptHandle = null; + } + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracerFactory.java new file mode 100644 index 0000000000..0c60625a5b --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentricTracerFactory.java @@ -0,0 +1,99 @@ +/* + * 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.annotations.VisibleForTesting; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link ApiTracerFactory} to build instances of {@link AppCentricTracer}. + * + *

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

This class is expected to be initialized once during client initialization. + */ +@BetaApi +@InternalApi +public class AppCentricTracerFactory implements ApiTracerFactory { + private final TraceRecorder traceRecorder; + + /** Mapping of client attributes that are set for every AppCentricTracer at operation level */ + private final Map operationAttributes; + + /** Mapping of client attributes that are set for every AppCentricTracer at attempt level */ + private final Map attemptAttributes; + + /** Creates a AppCentricTracerFactory */ + public AppCentricTracerFactory(TraceRecorder traceRecorder) { + this(traceRecorder, new HashMap<>(), new HashMap<>()); + } + + /** + * Pass in a Map of client level attributes which will be added to every single AppCentricTracer + * created from the ApiTracerFactory. This is package private since span attributes are determined + * internally. + */ + @VisibleForTesting + AppCentricTracerFactory( + TraceRecorder traceRecorder, + Map operationAttributes, + Map attemptAttributes) { + this.traceRecorder = traceRecorder; + + this.operationAttributes = new HashMap<>(operationAttributes); + this.attemptAttributes = new HashMap<>(attemptAttributes); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + // TODO(diegomarquezp): this is a placeholder for span names and will be adjusted as the + // feature is developed. + String attemptSpanName = spanName.getClientName() + "/" + spanName.getMethodName() + "/attempt"; + + AppCentricTracer appCentricTracer = + new AppCentricTracer(traceRecorder, attemptSpanName, this.attemptAttributes); + return appCentricTracer; + } + + @Override + public ApiTracerFactory withContext(ApiTracerContext context) { + Map newAttemptAttributes = new HashMap<>(this.attemptAttributes); + newAttemptAttributes.putAll(AppCentricAttributes.getAttemptAttributes(context)); + if (context.getRepo() != null) { + newAttemptAttributes.put(AppCentricTracer.REPO_ATTRIBUTE, context.getRepo()); + } + return new AppCentricTracerFactory(traceRecorder, operationAttributes, newAttemptAttributes); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceRecorder.java new file mode 100644 index 0000000000..8e5202e3d9 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceRecorder.java @@ -0,0 +1,83 @@ +/* + * 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.Tracer; +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 OpenTelemetryTraceRecorder implements TraceRecorder { + private final Tracer tracer; + + public OpenTelemetryTraceRecorder(OpenTelemetry openTelemetry) { + this.tracer = openTelemetry.getTracer("gax-java"); + } + + @Override + public TraceSpan createSpan(String name, Map attributes) { + SpanBuilder spanBuilder = tracer.spanBuilder(name); + + // Attempt spans are of the CLIENT kind + spanBuilder.setSpanKind(SpanKind.CLIENT); + + if (attributes != null) { + attributes.forEach((k, v) -> spanBuilder.setAttribute(k, v)); + } + + Span span = spanBuilder.startSpan(); + + return new OtelTraceSpan(span); + } + + private static class OtelTraceSpan implements TraceSpan { + private final Span span; + + private OtelTraceSpan(Span span) { + this.span = span; + } + + @Override + public void end() { + span.end(); + } + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceRecorder.java new file mode 100644 index 0000000000..64f063fe62 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceRecorder.java @@ -0,0 +1,50 @@ +/* + * 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 TraceRecorder per client. + */ +@BetaApi +@InternalApi +public interface TraceRecorder { + /** Starts a span and returns a handle to manage its lifecycle. */ + TraceSpan createSpan(String name, Map attributes); + + interface TraceSpan { + void end(); + } +} 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..c1bcc50512 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,49 @@ 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.resolvedServerAddress()).isEqualTo("localhost"); + + endpoint = "localhost:7469"; + endpointContext = + defaultEndpointContextBuilder + .setClientSettingsEndpoint(endpoint) + .setTransportChannelProviderEndpoint(null) + .build(); + Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("localhost"); + + endpoint = "test.googleapis.com:443"; + endpointContext = + defaultEndpointContextBuilder + .setClientSettingsEndpoint(endpoint) + .setTransportChannelProviderEndpoint(null) + .build(); + Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("test.googleapis.com"); + + // IPv6 literal with port + endpoint = "[2001:db8::1]:443"; + endpointContext = + defaultEndpointContextBuilder + .setClientSettingsEndpoint(endpoint) + .setTransportChannelProviderEndpoint(null) + .build(); + Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("2001:db8::1"); + + // Bare IPv6 literal (no port) + endpoint = "2001:db8::1"; + endpointContext = + defaultEndpointContextBuilder + .setClientSettingsEndpoint(endpoint) + .setTransportChannelProviderEndpoint(null) + .build(); + Truth.assertThat(endpointContext.resolvedServerAddress()).isEqualTo("2001:db8::1"); + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/ApiTracerContextTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/ApiTracerContextTest.java new file mode 100644 index 0000000000..8d704643cb --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/ApiTracerContextTest.java @@ -0,0 +1,92 @@ +/* + * 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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class ApiTracerContextTest { + + @Test + void testNewBuilderWithNoPropertiesFile() { + ApiTracerContext context = + ApiTracerContext.newBuilder(null).setServerAddress("test-address").build(); + + assertThat(context.getServerAddress()).isEqualTo("test-address"); + assertThat(context.getRepo()).isNull(); + } + + @Test + void testNewBuilderWithPropertiesFile() { + String propertiesContent = "repo=test-repo"; + InputStream inputStream = + new ByteArrayInputStream(propertiesContent.getBytes(StandardCharsets.UTF_8)); + + ApiTracerContext context = + ApiTracerContext.newBuilder(inputStream).setServerAddress("test-address").build(); + + assertThat(context.getServerAddress()).isEqualTo("test-address"); + assertThat(context.getRepo()).isEqualTo("test-repo"); + } + + @Test + void testNewBuilderWithPropertiesFileAndNoRepoKey() { + String propertiesContent = "somekey=somevalue"; + InputStream inputStream = + new ByteArrayInputStream(propertiesContent.getBytes(StandardCharsets.UTF_8)); + + ApiTracerContext context = + ApiTracerContext.newBuilder(inputStream).setServerAddress("test-address").build(); + + assertThat(context.getServerAddress()).isEqualTo("test-address"); + assertThat(context.getRepo()).isNull(); + } + + @Test + void testNewBuilderWithPropertiesFileLoadingError() throws IOException { + InputStream mockInputStream = Mockito.mock(InputStream.class); + Mockito.doThrow(new IOException("Test IO Exception")) + .when(mockInputStream) + .read(Mockito.any(byte[].class)); + + ApiTracerContext context = + ApiTracerContext.newBuilder(mockInputStream).setServerAddress("test-address").build(); + + assertThat(context.getServerAddress()).isEqualTo("test-address"); + assertThat(context.getRepo()).isNull(); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricAttributesTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricAttributesTest.java new file mode 100644 index 0000000000..8256101b57 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricAttributesTest.java @@ -0,0 +1,60 @@ +/* + * 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 java.util.Map; +import org.junit.jupiter.api.Test; + +class AppCentricAttributesTest { + + @Test + void testGetAttemptAttributes_serverAddress() { + ApiTracerContext context = + ApiTracerContext.newBuilder().setServerAddress("test-address").build(); + + Map attributes = AppCentricAttributes.getAttemptAttributes(context); + + assertThat(attributes).hasSize(1); + assertThat(attributes) + .containsEntry(AppCentricAttributes.SERVER_ADDRESS_ATTRIBUTE, "test-address"); + } + + @Test + void testGetAttemptAttributes_nonePresent() { + ApiTracerContext context = ApiTracerContext.newBuilder().build(); + + Map attributes = AppCentricAttributes.getAttemptAttributes(context); + + assertThat(attributes).isEmpty(); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerFactoryTest.java new file mode 100644 index 0000000000..1997aae2a4 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerFactoryTest.java @@ -0,0 +1,134 @@ +/* + * 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.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 AppCentricTracerFactoryTest { + + @Test + void testNewTracer_createsOpenTelemetryTracingTracer() { + TraceRecorder recorder = mock(TraceRecorder.class); + when(recorder.createSpan(anyString(), anyMap())) + .thenReturn(mock(TraceRecorder.TraceSpan.class)); + + AppCentricTracerFactory factory = new AppCentricTracerFactory(recorder); + ApiTracer tracer = + factory.newTracer( + null, SpanName.of("service", "method"), ApiTracerFactory.OperationType.Unary); + assertThat(tracer).isInstanceOf(AppCentricTracer.class); + } + + @Test + void testNewTracer_addsAttributes() { + TraceRecorder recorder = mock(TraceRecorder.class); + TraceRecorder.TraceSpan attemptHandle = mock(TraceRecorder.TraceSpan.class); + when(recorder.createSpan(anyString(), anyMap())).thenReturn(attemptHandle); + + AppCentricTracerFactory factory = + new AppCentricTracerFactory( + 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()).createSpan(anyString(), attributesCaptor.capture()); + + Map attemptAttributes = attributesCaptor.getValue(); + assertThat(attemptAttributes).containsEntry("server.port", "443"); + } + + @Test + void testWithContext_addsInferredAttributes() { + TraceRecorder recorder = mock(TraceRecorder.class); + TraceRecorder.TraceSpan attemptHandle = mock(TraceRecorder.TraceSpan.class); + when(recorder.createSpan(anyString(), anyMap())).thenReturn(attemptHandle); + + ApiTracerContext context = + ApiTracerContext.newBuilder().setServerAddress("example.com").setRepo("my-repo").build(); + + AppCentricTracerFactory factory = new AppCentricTracerFactory(recorder); + ApiTracerFactory factoryWithContext = factory.withContext(context); + + ApiTracer tracer = + factoryWithContext.newTracer( + null, SpanName.of("service", "method"), ApiTracerFactory.OperationType.Unary); + + tracer.attemptStarted(null, 1); + + ArgumentCaptor> attributesCaptor = ArgumentCaptor.forClass(Map.class); + verify(recorder, atLeastOnce()).createSpan(anyString(), attributesCaptor.capture()); + + Map attemptAttributes = attributesCaptor.getValue(); + assertThat(attemptAttributes) + .containsEntry(AppCentricAttributes.SERVER_ADDRESS_ATTRIBUTE, "example.com"); + assertThat(attemptAttributes).containsEntry(AppCentricTracer.REPO_ATTRIBUTE, "my-repo"); + } + + @Test + void testWithContext_noEndpointContext_doesNotAddAttributes() { + TraceRecorder recorder = mock(TraceRecorder.class); + TraceRecorder.TraceSpan attemptHandle = mock(TraceRecorder.TraceSpan.class); + when(recorder.createSpan(anyString(), anyMap())).thenReturn(attemptHandle); + + ApiTracerContext context = ApiTracerContext.newBuilder(null).build(); + + AppCentricTracerFactory factory = new AppCentricTracerFactory(recorder); + ApiTracerFactory factoryWithContext = factory.withContext(context); + + ApiTracer tracer = + factoryWithContext.newTracer( + null, SpanName.of("service", "method"), ApiTracerFactory.OperationType.Unary); + + tracer.attemptStarted(null, 1); + + ArgumentCaptor> attributesCaptor = ArgumentCaptor.forClass(Map.class); + verify(recorder, atLeastOnce()).createSpan(anyString(), attributesCaptor.capture()); + + Map attemptAttributes = attributesCaptor.getValue(); + assertThat(attemptAttributes).doesNotContainKey(AppCentricAttributes.SERVER_ADDRESS_ATTRIBUTE); + assertThat(attemptAttributes).doesNotContainKey(AppCentricTracer.REPO_ATTRIBUTE); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerTest.java new file mode 100644 index 0000000000..dfffcc495b --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/AppCentricTracerTest.java @@ -0,0 +1,100 @@ +/* + * 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 java.util.HashMap; +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 AppCentricTracerTest { + @Mock private TraceRecorder recorder; + @Mock private TraceRecorder.TraceSpan attemptHandle; + private AppCentricTracer tracer; + private static final String ATTEMPT_SPAN_NAME = "Service/Method/attempt"; + + @BeforeEach + void setUp() { + tracer = new AppCentricTracer(recorder, ATTEMPT_SPAN_NAME, new HashMap<>()); + } + + @Test + void testAttemptLifecycle_startsAndEndsAttemptSpan() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + tracer.attemptStarted(new Object(), 1); + tracer.attemptSucceeded(); + + verify(attemptHandle).end(); + } + + @Test + void testAttemptStarted_includesLanguageAttribute() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + ArgumentCaptor> attributesCaptor = ArgumentCaptor.forClass(Map.class); + verify(recorder).createSpan(eq(ATTEMPT_SPAN_NAME), attributesCaptor.capture()); + + assertThat(attributesCaptor.getValue()) + .containsEntry(AppCentricTracer.LANGUAGE_ATTRIBUTE, AppCentricTracer.DEFAULT_LANGUAGE); + } + + @Test + void testAttemptStarted_includesRepoAttribute() { + Map attemptAttributes = new HashMap<>(); + attemptAttributes.put(AppCentricTracer.REPO_ATTRIBUTE, "test-repo"); + + tracer = new AppCentricTracer(recorder, ATTEMPT_SPAN_NAME, attemptAttributes); + + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + ArgumentCaptor> attributesCaptor = ArgumentCaptor.forClass(Map.class); + verify(recorder).createSpan(eq(ATTEMPT_SPAN_NAME), attributesCaptor.capture()); + + assertThat(attributesCaptor.getValue()) + .containsEntry(AppCentricTracer.REPO_ATTRIBUTE, "test-repo"); + assertThat(attributesCaptor.getValue()) + .containsEntry(AppCentricTracer.LANGUAGE_ATTRIBUTE, AppCentricTracer.DEFAULT_LANGUAGE); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTraceRecorderTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTraceRecorderTest.java new file mode 100644 index 0000000000..44e8b84433 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTraceRecorderTest.java @@ -0,0 +1,105 @@ +/* + * 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.Tracer; +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 OpenTelemetryTraceRecorderTest { + @Mock private OpenTelemetry openTelemetry; + @Mock private Tracer tracer; + @Mock private SpanBuilder spanBuilder; + @Mock private Span span; + + private OpenTelemetryTraceRecorder recorder; + + @BeforeEach + void setUp() { + when(openTelemetry.getTracer(anyString())).thenReturn(tracer); + recorder = new OpenTelemetryTraceRecorder(openTelemetry); + } + + @Test + void testCreateSpan_operation_isInternal() { + String spanName = "operation-span"; + when(tracer.spanBuilder(spanName)).thenReturn(spanBuilder); + when(spanBuilder.setSpanKind(SpanKind.CLIENT)).thenReturn(spanBuilder); + when(spanBuilder.startSpan()).thenReturn(span); + + recorder.createSpan(spanName, null); + + verify(spanBuilder).setSpanKind(SpanKind.CLIENT); + } + + @Test + void testCreateSpan_attempt_isClient() { + String spanName = "attempt-span"; + + when(tracer.spanBuilder(spanName)).thenReturn(spanBuilder); + when(spanBuilder.setSpanKind(SpanKind.CLIENT)).thenReturn(spanBuilder); + when(spanBuilder.startSpan()).thenReturn(span); + + recorder.createSpan(spanName, null); + + verify(spanBuilder).setSpanKind(SpanKind.CLIENT); + } + + @Test + void testCreateSpan_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); + + TraceRecorder.TraceSpan handle = recorder.createSpan(spanName, attributes); + handle.end(); + + verify(span).end(); + } +} 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..579e329d1a 100755 --- a/hermetic_build/library_generation/generate_library.sh +++ b/hermetic_build/library_generation/generate_library.sh @@ -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..695b8c8a7b --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -0,0 +1,156 @@ +/* + * 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.tracing.AppCentricAttributes; +import com.google.api.gax.tracing.AppCentricTracer; +import com.google.api.gax.tracing.AppCentricTracerFactory; +import com.google.api.gax.tracing.OpenTelemetryTraceRecorder; +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.api.trace.SpanKind; +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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ITOtelTracing { + private static final String SHOWCASE_SERVER_ADDRESS = "localhost"; + private static final String SHOWCASE_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 { + AppCentricTracerFactory tracingFactory = + new AppCentricTracerFactory(new OpenTelemetryTraceRecorder(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.getKind()).isEqualTo(SpanKind.CLIENT); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(AppCentricTracer.LANGUAGE_ATTRIBUTE))) + .isEqualTo(AppCentricTracer.DEFAULT_LANGUAGE); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(AppCentricAttributes.SERVER_ADDRESS_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_ADDRESS); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(AppCentricTracer.REPO_ATTRIBUTE))) + .isEqualTo(SHOWCASE_REPO); + } + } + + @Test + void testTracing_successfulEcho_httpjson() throws Exception { + AppCentricTracerFactory tracingFactory = + new AppCentricTracerFactory(new OpenTelemetryTraceRecorder(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.getKind()).isEqualTo(SpanKind.CLIENT); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(AppCentricTracer.LANGUAGE_ATTRIBUTE))) + .isEqualTo(AppCentricTracer.DEFAULT_LANGUAGE); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(AppCentricAttributes.SERVER_ADDRESS_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_ADDRESS); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(AppCentricTracer.REPO_ATTRIBUTE))) + .isEqualTo(SHOWCASE_REPO); + } + } +}