diff --git a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubSettingsClassComposer.java b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubSettingsClassComposer.java index 45c5d2193e..ed2f2c74d5 100644 --- a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubSettingsClassComposer.java +++ b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubSettingsClassComposer.java @@ -209,6 +209,7 @@ public GapicClass generate(GapicContext context, Service service) { service, serviceConfig, methodSettingsMemberVarExprs, messageTypes, typeStore)) .setMethods( createClassMethods( + context, service, methodSettingsMemberVarExprs, deprecatedSettingVarNames, @@ -1033,6 +1034,7 @@ private static Expr createPagedListResponseFactoryAssignExpr( } private List createClassMethods( + GapicContext context, Service service, Map methodSettingsMemberVarExprs, Set deprecatedSettingVarNames, @@ -1043,7 +1045,7 @@ private List createClassMethods( createMethodSettingsGetterMethods( methodSettingsMemberVarExprs, deprecatedSettingVarNames, internalSettingVarNames)); javaMethods.add(createCreateStubMethod(service, typeStore)); - javaMethods.addAll(createDefaultHelperAndGetterMethods(service, typeStore)); + javaMethods.addAll(createDefaultHelperAndGetterMethods(context, service, typeStore)); javaMethods.addAll( createNewBuilderMethods( service, @@ -1185,7 +1187,7 @@ private MethodDefinition createCreateStubMethod(Service service, TypeStore typeS } private List createDefaultHelperAndGetterMethods( - Service service, TypeStore typeStore) { + GapicContext context, Service service, TypeStore typeStore) { List javaMethods = new ArrayList<>(); TypeNode returnType; @@ -1206,6 +1208,21 @@ private List createDefaultHelperAndGetterMethods( .build()); } + // Create the getArtifactName method. + if (context.artifactName().isPresent()) { + returnType = TypeNode.STRING; + javaMethods.add( + MethodDefinition.builder() + .setIsOverride(true) + .setScope(ScopeNode.PROTECTED) + .setIsStatic(false) + .setReturnType(returnType) + .setName("getArtifactName") + .setReturnExpr( + ValueExpr.withValue(StringObjectValue.withValue(context.artifactName().get()))) + .build()); + } + // Create the defaultExecutorProviderBuilder method. returnType = TypeNode.withReference( 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..87958cc9a8 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 @@ -60,6 +60,8 @@ public abstract class GapicContext { public abstract boolean restNumericEnumsEnabled(); + public abstract Optional artifactName(); + public GapicMetadata gapicMetadata() { return gapicMetadata; } @@ -130,6 +132,8 @@ public Builder setHelperResourceNames(Set helperResourceNames) { public abstract Builder setTransport(Transport transport); + public abstract Builder setArtifactName(Optional artifactName); + 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..01accc176d 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 artifactNameOpt = PluginArgumentParser.parseArtifactName(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) + .setArtifactName(artifactNameOpt) .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..33499eb76b 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 @@ -26,6 +26,7 @@ public class PluginArgumentParser { private static final String EQUALS = "="; // Synced to rules_java_gapic/java_gapic.bzl. + @VisibleForTesting static final String KEY_ARTIFACT_NAME = "artifact-name"; @VisibleForTesting static final String KEY_GRPC_SERVICE_CONFIG = "grpc-service-config"; @VisibleForTesting static final String KEY_GAPIC_CONFIG = "gapic-config"; @VisibleForTesting static final String KEY_METADATA = "metadata"; @@ -53,6 +54,10 @@ static Optional parseTransport(CodeGeneratorRequest request) { return parseConfigArgument(request.getParameter(), KEY_TRANSPORT); } + static Optional parseArtifactName(CodeGeneratorRequest request) { + return parseConfigArgument(request.getParameter(), KEY_ARTIFACT_NAME); + } + static boolean hasMetadataFlag(CodeGeneratorRequest request) { return hasFlag(request.getParameter(), KEY_METADATA); } 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..37877685f8 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 @@ -270,6 +270,13 @@ public static ClientContext create(StubSettings settings) throws IOException { backgroundResources.add(watchdog); } + ApiTracerFactory tracerFactory = settings.getTracerFactory(); + if (!Strings.isNullOrEmpty(settings.getArtifactName())) { + tracerFactory = + tracerFactory.withAttributes( + ImmutableMap.of("gcp.client.artifact", settings.getArtifactName())); + } + return newBuilder() .setBackgroundResources(backgroundResources.build()) .setExecutor(backgroundExecutor) @@ -284,7 +291,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/StubSettings.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/StubSettings.java index f97f808ca7..2a79cb3d02 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 @@ -176,6 +176,16 @@ protected String getServiceName() { return ""; } + /** + * Marked with Internal Api and meant to be overridden by the generated subclasses. This getter is + * used to set the artifactName to the ClientContext. The value in generated StubSettings + * subclasses comes from the build configuration. + */ + @InternalApi + protected String getArtifactName() { + return ""; + } + /** * @return the fully resolved universe domain used by the client */ 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..782c3e285b 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,14 @@ 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 attributes the attributes to add to all tracers + */ + default ApiTracerFactory withAttributes(Map attributes) { + return this; + } } 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..747eb0eb28 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingRecorder.java @@ -0,0 +1,96 @@ +/* + * 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.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 + @SuppressWarnings("MustBeClosedChecker") // Scope is closed later in the lifecycle + public SpanHandle startSpan(String name, Map attributes) { + 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)); + } + + Span span = spanBuilder.startSpan(); + // makeCurrent() puts this span into the thread-local storage + Scope scope = span.makeCurrent(); + + return new OtelSpanHandle(span, scope); + } + + private static class OtelSpanHandle implements SpanHandle { + private final Span span; + private final Scope scope; + + private OtelSpanHandle(Span span, Scope scope) { + this.span = span; + this.scope = scope; + } + + @Override + public void end() { + scope.close(); // Remove from thread-local storage + span.end(); + } + + @Override + public void recordError(Throwable error) { + span.recordException(error); + span.setStatus(StatusCode.ERROR); + } + } +} 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..959568596b --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracer.java @@ -0,0 +1,82 @@ +/* + * 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 implements ApiTracer { + private final TracingRecorder recorder; + private final Map attributes; + private TracingRecorder.SpanHandle operationHandle; + private TracingRecorder.SpanHandle attemptHandle; + + public OpenTelemetryTracingTracer(TracingRecorder recorder, String methodName) { + this.recorder = recorder; + this.attributes = new HashMap<>(); + this.attributes.put("method", methodName); + + // Start the long-lived operation span + this.operationHandle = recorder.startSpan(methodName + "/operation", attributes); + } + + @Override + public void attemptStarted(Object request, int attemptNumber) { + // Start the specific attempt span + this.attemptHandle = recorder.startSpan(this.attributes.get("method") + "/attempt", attributes); + } + + @Override + public void attemptSucceeded() { + if (attemptHandle != null) { + attemptHandle.end(); + } + } + + @Override + public void operationSucceeded() { + operationHandle.end(); + } + + @Override + public void operationFailed(Throwable error) { + operationHandle.recordError(error); + operationHandle.end(); + } + + public void addAttributes(Map attributes) { + this.attributes.putAll(attributes); + } +} 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..0457e44779 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerFactory.java @@ -0,0 +1,85 @@ +/* + * 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.util.HashMap; +import java.util.Map; + +/** + * 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 final TracingRecorder tracingRecorder; + + /** Mapping of client attributes that are set for every TracingTracer */ + private final Map attributes; + + /** Creates a TracingTracerFactory with no additional client level attributes. */ + public OpenTelemetryTracingTracerFactory(TracingRecorder tracingRecorder) { + this(tracingRecorder, 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 attributes) { + this.tracingRecorder = tracingRecorder; + this.attributes = ImmutableMap.copyOf(attributes); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + OpenTelemetryTracingTracer tracingTracer = + new OpenTelemetryTracingTracer(tracingRecorder, spanName.getMethodName()); + tracingTracer.addAttributes(attributes); + return tracingTracer; + } + + @Override + public ApiTracerFactory withAttributes(Map attributes) { + Map newAttributes = new HashMap<>(this.attributes); + newAttributes.putAll(attributes); + return new OpenTelemetryTracingTracerFactory(tracingRecorder, newAttributes); + } +} 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..804900b938 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracingRecorder.java @@ -0,0 +1,54 @@ +/* + * 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); + + interface SpanHandle { + void end(); + + void recordError(Throwable error); + } +} diff --git a/hermetic_build/library_generation/generate_composed_library.py b/hermetic_build/library_generation/generate_composed_library.py index ea595a3f98..f6e0d49692 100755 --- a/hermetic_build/library_generation/generate_composed_library.py +++ b/hermetic_build/library_generation/generate_composed_library.py @@ -44,7 +44,7 @@ def generate_composed_library( config: GenerationConfig, library_path: str, - library: LibraryConfig, + library_config: LibraryConfig, repo_config: RepoConfig, ) -> None: """ @@ -53,7 +53,7 @@ def generate_composed_library( :param config: a GenerationConfig object representing a parsed configuration yaml :param library_path: the path to which the generated file goes - :param library: a LibraryConfig object contained inside config, passed here + :param library_config: a LibraryConfig object contained inside config, passed here for convenience and to prevent all libraries to be processed :param repo_config: :return None @@ -61,7 +61,7 @@ def generate_composed_library( output_folder = repo_config.output_folder owlbot_cli_source_folder = util.sh_util("mktemp -d") os.makedirs(f"{library_path}", exist_ok=True) - for gapic in library.get_sorted_gapic_configs(): + for gapic in library_config.get_sorted_gapic_configs(): build_file_folder = Path(f"{output_folder}/{gapic.proto_path}").resolve() print(f"build_file_folder: {build_file_folder}") gapic_inputs = parse_build_file(build_file_folder, gapic.proto_path) @@ -73,16 +73,17 @@ def generate_composed_library( # generating postprocessing files such as README. util.generate_postprocessing_prerequisite_files( config=config, - library=library, + library=library_config, proto_path=util.remove_version_from(gapic.proto_path), library_path=library_path, - transport=library.get_transport(gapic_inputs), + transport=library_config.get_transport(gapic_inputs), ) temp_destination_path = f"java-{gapic.proto_path.replace('/','-')}" - effective_arguments = __construct_effective_arg( + effective_arguments = __construct_effective_args( base_arguments=[], gapic=gapic, gapic_inputs=gapic_inputs, + library_config=library_config, temp_destination_path=temp_destination_path, ) print("arguments: ") @@ -101,7 +102,7 @@ def generate_composed_library( ) library_version = repo_config.get_library_version( - artifact_id=library.get_artifact_id() + artifact_id=library_config.get_artifact_id() ) # call postprocess library util.run_process_and_print_output( @@ -119,10 +120,11 @@ def generate_composed_library( ) -def __construct_effective_arg( +def __construct_effective_args( base_arguments: List[str], gapic: GapicConfig, gapic_inputs: GapicInputs, + library_config: LibraryConfig, temp_destination_path: str, ) -> List[str]: """ @@ -153,6 +155,8 @@ def __construct_effective_arg( gapic_inputs.service_yaml, "--include_samples", gapic_inputs.include_samples, + "--artifact_name", + library_config.get_maven_coordinate(), ] 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..9833476fb0 100755 --- a/hermetic_build/library_generation/generate_library.sh +++ b/hermetic_build/library_generation/generate_library.sh @@ -49,6 +49,9 @@ case $key in --os_architecture) os_architecture="$2" shift + --artifact_name) + artifact_name="$2" + shift ;; *) echo "Invalid option: [$1]" @@ -179,7 +182,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}" "${artifact_name}")" \ ${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/generate_repo.py b/hermetic_build/library_generation/generate_repo.py index 634ddffd38..1beed6e435 100755 --- a/hermetic_build/library_generation/generate_repo.py +++ b/hermetic_build/library_generation/generate_repo.py @@ -62,7 +62,7 @@ def generate_from_yaml( generate_composed_library( config=config, library_path=library_path, - library=library, + library_config=library, repo_config=repo_config, ) 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..b4f8959298 100755 --- a/hermetic_build/library_generation/tests/generate_library_unit_tests.sh +++ b/hermetic_build/library_generation/tests/generate_library_unit_tests.sh @@ -56,6 +56,18 @@ get_gapic_opts_with_non_default_test() { "$gapic_opts" } +get_gapic_opts_with_artifact_name_test() { + local proto_path="${script_dir}/resources/gapic_options" + local transport="grpc" + local rest_numeric_enums="false" + local artifact_name="com.google.cloud:google-cloud-library" + local gapic_opts + gapic_opts="$(get_gapic_opts "${transport}" "${rest_numeric_enums}" "" "" "" "${artifact_name}")" + 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,artifact-name=${artifact_name}" \ + "$gapic_opts" +} + remove_grpc_version_test() { local destination_path="${script_dir}/resources/gapic_options" cp "${destination_path}/QueryServiceGrpc_copy.java" "${destination_path}/QueryServiceGrpc.java" @@ -214,6 +226,7 @@ test_list=( get_gapic_opts_with_rest_test get_gapic_opts_without_rest_test get_gapic_opts_with_non_default_test + get_gapic_opts_with_artifact_name_test remove_grpc_version_test download_protoc_succeed_with_valid_version_linux_test download_protoc_succeed_with_valid_version_macos_test diff --git a/hermetic_build/library_generation/utils/utilities.sh b/hermetic_build/library_generation/utils/utilities.sh index 863834f508..f1ca1c3cb8 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 artifact_name=$6 if [ "${rest_numeric_enums}" == "true" ]; then rest_numeric_enums="rest-numeric-enums" else @@ -88,7 +89,10 @@ 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}" + if [[ -n "${artifact_name}" ]]; then + artifact_name_opt=",artifact-name=${artifact_name}" + fi + echo "transport=${transport},${rest_numeric_enums},grpc-service-config=${service_config},gapic-config=${gapic_yaml},api-service-config=${service_yaml}${artifact_name_opt}" } remove_grpc_version() { 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..b0bd186614 --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -0,0 +1,95 @@ +/* + * 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.OpenTelemetryTracingRecorder; +import com.google.api.gax.tracing.OpenTelemetryTracingTracerFactory; +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.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 SERVICE_NAME = "ShowcaseTracingTest"; + 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_recorded() 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(); + boolean foundLowLevelSpan = + spans.stream() + .anyMatch(span -> span.getName().equals(SERVICE_NAME + "/low-level-network-span")); + assertThat(foundLowLevelSpan).isTrue(); + } + } +} diff --git a/rules_java_gapic/java_gapic.bzl b/rules_java_gapic/java_gapic.bzl index fdab81e7af..edd81e9bbe 100644 --- a/rules_java_gapic/java_gapic.bzl +++ b/rules_java_gapic/java_gapic.bzl @@ -190,6 +190,7 @@ def _java_gapic_srcjar( # possible values are: "grpc", "rest", "grpc+rest" transport, rest_numeric_enums, + artifact_name, # Can be used to provide a java_library with a customized generator, # like the one which dumps descriptor to a file for future debugging. java_generator_name = "java_gapic", @@ -211,6 +212,9 @@ def _java_gapic_srcjar( opt_args = [] + if artifact_name: + opt_args.append("artifact-name=%s" % artifact_name) + if transport: opt_args.append("transport=%s" % transport) @@ -245,6 +249,7 @@ def java_gapic_library( # possible values are: "grpc", "rest", "grpc+rest" transport = None, rest_numeric_enums = False, + artifact_name = None, **kwargs): srcjar_name = name + "_srcjar" raw_srcjar_name = srcjar_name + "_raw" @@ -258,6 +263,7 @@ def java_gapic_library( transport = transport, rest_numeric_enums = rest_numeric_enums, java_generator_name = "java_gapic", + artifact_name = artifact_name, **kwargs ) diff --git a/test/integration/BUILD.bazel b/test/integration/BUILD.bazel index 4a3409aa0e..9823086bab 100644 --- a/test/integration/BUILD.bazel +++ b/test/integration/BUILD.bazel @@ -244,6 +244,7 @@ java_gapic_library( grpc_service_config = "@com_google_googleapis//google/pubsub/v1:pubsub_grpc_service_config.json", # For the IAM mixin. service_yaml = "pubsub_v1.yaml", + artifact_name = "com.google.cloud:google-cloud-pubsub", test_deps = [ "@com_google_googleapis//google/pubsub/v1:pubsub_java_grpc", "@com_google_googleapis//google/iam/v1:iam_java_grpc", diff --git a/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/PublisherStubSettings.java b/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/PublisherStubSettings.java index 3d7bf321ec..dda6f3af61 100644 --- a/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/PublisherStubSettings.java +++ b/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/PublisherStubSettings.java @@ -485,6 +485,11 @@ public String getServiceName() { return "pubsub"; } + @Override + protected String getArtifactName() { + return "com.google.cloud:google-cloud-pubsub"; + } + /** Returns a builder for the default ExecutorProvider for this service. */ public static InstantiatingExecutorProvider.Builder defaultExecutorProviderBuilder() { return InstantiatingExecutorProvider.newBuilder(); diff --git a/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/SchemaServiceStubSettings.java b/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/SchemaServiceStubSettings.java index 630d6ae302..c07879b280 100644 --- a/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/SchemaServiceStubSettings.java +++ b/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/SchemaServiceStubSettings.java @@ -346,6 +346,11 @@ public String getServiceName() { return "pubsub"; } + @Override + protected String getArtifactName() { + return "com.google.cloud:google-cloud-pubsub"; + } + /** Returns a builder for the default ExecutorProvider for this service. */ public static InstantiatingExecutorProvider.Builder defaultExecutorProviderBuilder() { return InstantiatingExecutorProvider.newBuilder(); diff --git a/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/SubscriberStubSettings.java b/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/SubscriberStubSettings.java index a02a1ae1d6..35169236a9 100644 --- a/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/SubscriberStubSettings.java +++ b/test/integration/goldens/pubsub/src/com/google/cloud/pubsub/v1/stub/SubscriberStubSettings.java @@ -392,6 +392,11 @@ public String getServiceName() { return "pubsub"; } + @Override + protected String getArtifactName() { + return "com.google.cloud:google-cloud-pubsub"; + } + /** Returns a builder for the default ExecutorProvider for this service. */ public static InstantiatingExecutorProvider.Builder defaultExecutorProviderBuilder() { return InstantiatingExecutorProvider.newBuilder();